diff --git a/blocks/lists.js b/blocks/lists.js index 4e9d3b5e2..6bfa42139 100644 --- a/blocks/lists.js +++ b/blocks/lists.js @@ -831,7 +831,6 @@ Blockly.Blocks['lists_split'] = { updateType_: function(newMode) { var mode = this.getFieldValue('MODE'); if (mode != newMode) { - this.setFieldValue(newMode, 'MODE'); var inputConnection = this.getInput('INPUT').connection; inputConnection.setShadowDom(null); var inputBlock = inputConnection.targetBlock(); diff --git a/blocks/text.js b/blocks/text.js index b98bd54be..244889456 100644 --- a/blocks/text.js +++ b/blocks/text.js @@ -864,11 +864,7 @@ Blockly.Constants.Text.TEXT_CHARAT_EXTENSION = function() { if (newAt != this.isAt_) { var block = this.sourceBlock_; block.updateAt_(newAt); - // This menu has been destroyed and replaced. Update the replacement. - block.setFieldValue(value, 'WHERE'); - return null; } - return undefined; }); this.updateAt_(true); // Assign 'this' to a variable for use in the tooltip closure below. diff --git a/core/field.js b/core/field.js index 6ce1aa79e..f103813fe 100644 --- a/core/field.js +++ b/core/field.js @@ -39,14 +39,15 @@ goog.require('goog.style'); /** * Abstract class for an editable field. - * @param {string} text The initial content of the field. - * @param {function(string):(string|null|undefined)=} opt_validator An optional - * function that is called to validate user input. See setValidator(). + * @param {*} value The initial value of the field. + * @param {Function=} opt_validator A function that is called to validate + * changes to the field's value. Takes in a value & returns a validated + * value, or null to abort the change. * @constructor */ -Blockly.Field = function(text, opt_validator) { +Blockly.Field = function(value, opt_validator) { this.size_ = new goog.math.Size(0, Blockly.BlockSvg.MIN_BLOCK_Y); - this.setValue(text); + this.setValue(value); this.setValidator(opt_validator); }; @@ -123,6 +124,14 @@ Blockly.Field.prototype.name = undefined; */ Blockly.Field.prototype.maxDisplayLength = 50; +/** + * Get the current value of the field. + * @return {*} Current value. + */ +Blockly.Field.prototype.getValue = function() { + return this.value_; +}; + /** * Visible text to display. * @type {string} @@ -233,10 +242,11 @@ Blockly.Field.prototype.initView = function() { 'y': 0, 'height': 16 }, this.fieldGroup_); - /** @type {!Element} */ this.textElement_ = Blockly.utils.createSvgElement('text', {'class': 'blocklyText', 'y': this.size_.height - 12.5}, this.fieldGroup_); + var textNode = document.createTextNode(''); + this.textElement_.appendChild(textNode); this.updateEditable(); @@ -375,14 +385,15 @@ Blockly.Field.prototype.setVisible = function(visible) { * Sets a new validation function for editable fields, or clears a previously * set validator. * - * The validator function takes in the text form of the users input, and - * optionally returns the accepted field text. Alternatively, if the function - * returns null, the field value change aborts. If the function does not return - * anything (or returns undefined), the input value is accepted as valid. This - * is a shorthand for fields using the validator function call as a field-level - * change event notification. + * The validator function takes in the new field value, and returns + * validated value. The validated value could be the input value, a modified + * version of the input value, or null to abort the change. * - * @param {?function(string):(string|null|undefined)} handler The validator + * If the function does not return anything (or returns undefined) the new + * value is accepted as valid. This is to allow for fields using the + * validated founction as a field-level change event notification. + * + * @param {Function=} handler The validator * function or null to clear a previous validator. */ Blockly.Field.prototype.setValidator = function(handler) { @@ -401,6 +412,8 @@ Blockly.Field.prototype.getValidator = function() { * Validates a change. Does nothing. Subclasses may override this. * @param {string} text The user's text. * @return {string} No change needed. + * @deprecated May 2019. Override doClassValidation and other relevant 'do' + * functions instead. */ Blockly.Field.prototype.classValidator = function(text) { return text; @@ -411,6 +424,7 @@ Blockly.Field.prototype.classValidator = function(text) { * function for the field's class and its parents. * @param {string} text Proposed text. * @return {?string} Revised text, or null if invalid. + * @deprecated May 2019. setValue now contains all relevant logic. */ Blockly.Field.prototype.callValidator = function(text) { var classResult = this.classValidator(text); @@ -452,15 +466,15 @@ Blockly.Field.prototype.updateColour = function() { }; /** - * Draws the border with the correct width. - * Saves the computed width in a property. + * Used by getSize() to move/resize any dom elements, and get the new size. + * + * All rendering that has an effect on the size/shape of the block should be + * done here, and should be triggered by getSize(). * @protected */ Blockly.Field.prototype.render_ = function() { - // Replace the text. this.textElement_.textContent = this.getDisplayText_(); this.updateWidth(); - this.isDirty_ = false; }; /** @@ -547,6 +561,7 @@ Blockly.Field.stopCache = function() { Blockly.Field.prototype.getSize = function() { if (this.isDirty_) { this.render_(); + this.isDirty_ = false; } else if (this.visible_ && this.size_.width == 0) { // If the field is not visible the width will be 0 as well, one of the // problems with the old system. @@ -644,39 +659,106 @@ Blockly.Field.prototype.forceRerender = function() { }; /** - * By default there is no difference between the human-readable text and - * the language-neutral values. Subclasses (such as dropdown) may define this. - * @return {string} Current value. - */ -Blockly.Field.prototype.getValue = function() { - return this.getText(); -}; - -/** - * By default there is no difference between the human-readable text and - * the language-neutral values. Subclasses (such as dropdown) may define this. - * @param {string} newValue New value. + * Used to change the value of the field. Handles validation and events. + * Subclasses should override doClassValidation_ and doValueUpdate_ rather + * than this method. + * @param {*} newValue New value. */ Blockly.Field.prototype.setValue = function(newValue) { + var doLogging = false; if (newValue === null) { - // No change if null. + doLogging && console.log('null, return'); + // Not a valid value to check. return; } - // Validate input. - var validated = this.callValidator(newValue); - if (validated !== null) { - newValue = validated; + + newValue = this.doClassValidation_(newValue); + if (newValue === null) { + doLogging && console.log('invalid, return'); + this.doValueInvalid_(); + if (this.isDirty_) { + this.forceRerender(); + } + return; + } + + var localValidator = this.getValidator(); + if (localValidator) { + var validatedValue = localValidator.call(this, newValue); + // Sometimes local validators are used as change listeners (bad!) which + // means they might return undefined accidentally, so we'll just ignore that. + if (validatedValue !== undefined) { + newValue = validatedValue; + } + if (newValue === null) { + doLogging && console.log('invalid, return'); + this.doValueInvalid_(); + if (this.isDirty_) { + this.forceRerender(); + } + return; + } } - // Check for change. var oldValue = this.getValue(); - if (oldValue == newValue) { + if (oldValue === newValue) { + doLogging && console.log('same, return'); + // No change. return; } + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { Blockly.Events.fire(new Blockly.Events.BlockChange( this.sourceBlock_, 'field', this.name, oldValue, newValue)); } - this.setText(newValue); + this.doValueUpdate_(newValue); + if (this.isDirty_) { + this.forceRerender(); + } + doLogging && console.log(this.value_); +}; + +/** + * A generic value possessed by the field. + * Should generally be non-null, only null when the field is created. + * @type {*} + * @protected + */ +Blockly.Field.prototype.value_ = null; + +/** + * Used to validate a value. Returns input by default. Can be overridden by + * subclasses, see FieldDropdown. + * @param {*} newValue The value to be validated. + * @return {*} The validated value, same as input by default. + * @protected + */ +Blockly.Field.prototype.doClassValidation_ = function(newValue) { + // For backwards compatibility. + newValue = this.classValidator(newValue); + return newValue; +}; + +/** + * Used to update the value of a field. Can be overridden by subclasses to do + * custom storage of values/updating of external things. + * @param {*} newValue The value to be saved. + * @protected + */ +Blockly.Field.prototype.doValueUpdate_ = function(newValue) { + this.value_ = newValue; + this.isDirty_ = true; + // For backwards compatibility. + this.text_ = String(newValue); +}; + +/** + * Used to notify the field an invalid value was input. Can be overiden by + * subclasses, see FieldTextInput. + * No-op by default. + * @protected + */ +Blockly.Field.prototype.doValueInvalid_ = function() { + // NOP }; /** diff --git a/core/field_angle.js b/core/field_angle.js index 2c29395e4..cd1138aec 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -34,21 +34,19 @@ goog.require('Blockly.utils'); /** * Class for an editable angle field. - * @param {(string|number)=} opt_value The initial content of the field. The - * value should cast to a number, and if it does not, '0' will be used. - * @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 the accepted text or null to abort - * the change. + * @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. * @extends {Blockly.FieldTextInput} * @constructor */ Blockly.FieldAngle = function(opt_value, opt_validator) { - // Add degree symbol: '360°' (LTR) or '°360' (RTL) - this.symbol_ = Blockly.utils.createSvgElement('tspan', {}, null); - this.symbol_.appendChild(document.createTextNode('\u00B0')); - - opt_value = (opt_value && !isNaN(opt_value)) ? String(opt_value) : '0'; + opt_value = this.doClassValidation_(opt_value); + if (opt_value === null) { + opt_value = 0; + } Blockly.FieldAngle.superClass_.constructor.call( this, opt_value, opt_validator); }; @@ -120,18 +118,25 @@ Blockly.FieldAngle.WRAP = 360; Blockly.FieldAngle.RADIUS = Blockly.FieldAngle.HALF - 1; /** - * Adds degree symbol and recalculates width. - * Saves the computed width in a property. + * Create the block UI for this field. + * @package + */ +Blockly.FieldAngle.prototype.initView = function() { + Blockly.FieldAngle.superClass_.initView.call(this); + // Add the degree symbol to the left of the number, even in RTL (issue #2380) + this.symbol_ = Blockly.utils.createSvgElement('tspan', {}, null); + this.symbol_.appendChild(document.createTextNode('\u00B0')); +}; + +/** + * Updates the graph when the field rerenders. * @private */ Blockly.FieldAngle.prototype.render_ = function() { - // Update textElement. this.textElement_.textContent = this.getDisplayText_(); - - // Insert degree symbol. - // Degree symbol should be left of number, even in RTL (issue #2380). this.textElement_.appendChild(this.symbol_); this.updateWidth(); + this.updateGraph_(); }; /** @@ -208,7 +213,6 @@ Blockly.FieldAngle.prototype.showEditor_ = function() { }, svg); } - var border = this.sourceBlock_.getColourBorder(); border = border.colourBorder == null ? border.colourLight : border.colourBorder; @@ -244,6 +248,7 @@ Blockly.FieldAngle.prototype.hide_ = function() { * @param {!Event} e Mouse move event. */ Blockly.FieldAngle.prototype.onMouseMove = function(e) { + // Calculate angle. var bBox = this.gauge_.ownerSVGElement.getBoundingClientRect(); var dx = e.clientX - bBox.left - Blockly.FieldAngle.HALF; var dy = e.clientY - bBox.top - Blockly.FieldAngle.HALF; @@ -268,24 +273,16 @@ Blockly.FieldAngle.prototype.onMouseMove = function(e) { angle = Math.round(angle / Blockly.FieldAngle.ROUND) * Blockly.FieldAngle.ROUND; } - angle = this.callValidator(angle); - Blockly.FieldTextInput.htmlInput_.value = angle; - this.setValue(angle); - this.validate_(); - this.resizeEditor_(); -}; -/** - * Insert a degree symbol. - * @param {?string} text New text. - */ -Blockly.FieldAngle.prototype.setText = function(text) { - Blockly.FieldAngle.superClass_.setText.call(this, text); - if (!this.textElement_) { - // Not rendered yet. - return; + // Update value. + var angleString = String(angle); + if (angleString != this.text_) { + Blockly.FieldTextInput.htmlInput_.value = angle; + this.setValue(angle); + // Always render the input angle. + this.text_ = angleString; + this.forceRerender(); } - this.updateGraph_(); }; /** @@ -296,6 +293,7 @@ Blockly.FieldAngle.prototype.updateGraph_ = function() { if (!this.gauge_) { return; } + // Always display the input (i.e. getText) even if it is invalid. var angleDegrees = Number(this.getText()) + Blockly.FieldAngle.OFFSET; var angleRadians = Blockly.utils.toRadians(angleDegrees); var path = ['M ', Blockly.FieldAngle.HALF, ',', Blockly.FieldAngle.HALF]; @@ -326,18 +324,16 @@ Blockly.FieldAngle.prototype.updateGraph_ = function() { }; /** - * Ensure that only an angle may be entered. - * @param {string} text The user's text. - * @return {?string} A string representing a valid angle, or null if invalid. + * Ensure that the input value is a valid angle. + * @param {string|number=} newValue The input value. + * @return {?number} A valid angle, or null if invalid. + * @protected */ -Blockly.FieldAngle.prototype.classValidator = function(text) { - if (text === null) { - return null; - } - var n = parseFloat(text || 0); - if (isNaN(n)) { +Blockly.FieldAngle.prototype.doClassValidation_ = function(newValue) { + if (isNaN(newValue)) { return null; } + var n = parseFloat(newValue || 0); n = n % 360; if (n < 0) { n += 360; @@ -345,7 +341,7 @@ Blockly.FieldAngle.prototype.classValidator = function(text) { if (n > Blockly.FieldAngle.WRAP) { n -= 360; } - return String(n); + return n; }; Blockly.Field.register('field_angle', Blockly.FieldAngle); diff --git a/core/field_checkbox.js b/core/field_checkbox.js index f096b1e9a..67d08237e 100644 --- a/core/field_checkbox.js +++ b/core/field_checkbox.js @@ -32,19 +32,22 @@ goog.require('Blockly.utils'); /** * Class for a checkbox field. - * @param {string=} opt_state The initial state of the field ('TRUE' or - * 'FALSE'), defaults to 'FALSE'. - * @param {Function=} opt_validator A function that is executed when a new - * option is selected. Its sole argument is the new checkbox state. If - * it returns a value, this becomes the new checkbox state, unless the - * value is null, in which case the change is aborted. + * @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. * @extends {Blockly.Field} * @constructor */ -Blockly.FieldCheckbox = function(opt_state, opt_validator) { - Blockly.FieldCheckbox.superClass_.constructor.call(this, '', opt_validator); - // Set the initial state. - this.setValue(opt_state); +Blockly.FieldCheckbox = function(opt_value, opt_validator) { + opt_value = this.doClassValidation_(opt_value); + if (opt_value === null) { + opt_value = 'FALSE'; + } + Blockly.FieldCheckbox.superClass_.constructor.call(this, opt_value, opt_validator); + this.size_.width = Blockly.FieldCheckbox.WIDTH; }; goog.inherits(Blockly.FieldCheckbox, Blockly.Field); @@ -56,9 +59,23 @@ goog.inherits(Blockly.FieldCheckbox, Blockly.Field); * @nocollapse */ Blockly.FieldCheckbox.fromJson = function(options) { - return new Blockly.FieldCheckbox(options['checked'] ? 'TRUE' : 'FALSE'); + return new Blockly.FieldCheckbox(options['checked']); }; +/** + * The width of a checkbox field. + * @type {number} + * @const + */ +Blockly.FieldCheckbox.WIDTH = 5; + +/** + * Character for the checkmark. + * @type {string} + * @const + */ +Blockly.FieldCheckbox.CHECK_CHAR = '\u2713'; + /** * Serializable fields are saved by the XML renderer, non-serializable fields * are not. Editable fields should also be serializable. @@ -67,11 +84,6 @@ Blockly.FieldCheckbox.fromJson = function(options) { */ Blockly.FieldCheckbox.prototype.SERIALIZABLE = true; -/** - * Character for the checkmark. - */ -Blockly.FieldCheckbox.CHECK_CHAR = '\u2713'; - /** * Mouse cursor style when over the hotspot that initiates editability. */ @@ -91,49 +103,98 @@ Blockly.FieldCheckbox.prototype.initView = function() { this.fieldGroup_); var textNode = document.createTextNode(Blockly.FieldCheckbox.CHECK_CHAR); this.checkElement_.appendChild(textNode); - this.checkElement_.style.display = this.state_ ? 'block' : 'none'; -}; + this.checkElement_.style.display = this.value_ ? 'block' : 'none'; -/** - * Return 'TRUE' if the checkbox is checked, 'FALSE' otherwise. - * @return {string} Current state. - */ -Blockly.FieldCheckbox.prototype.getValue = function() { - return String(this.state_).toUpperCase(); -}; - -/** - * Set the checkbox to be checked if newBool is 'TRUE' or true, - * unchecks otherwise. - * @param {string|boolean} newBool New state. - */ -Blockly.FieldCheckbox.prototype.setValue = function(newBool) { - var newState = (typeof newBool == 'string') ? - (newBool.toUpperCase() == 'TRUE') : !!newBool; - if (this.state_ !== newState) { - if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, this.state_, newState)); - } - this.state_ = newState; - if (this.checkElement_) { - this.checkElement_.style.display = newState ? 'block' : 'none'; - } + if (this.borderRect_) { + this.borderRect_.setAttribute('width', + this.size_.width + Blockly.BlockSvg.SEP_SPACE_X); } }; /** - * Toggle the state of the checkbox. + * Checkboxes have a constant width. * @private */ +Blockly.FieldCheckbox.prototype.render_ = function() { + this.size_.width = Blockly.FieldCheckbox.WIDTH; +}; + +/** + * Toggle the state of the checkbox on click. + * @protected + */ Blockly.FieldCheckbox.prototype.showEditor_ = function() { - var newState = !this.state_; - if (this.sourceBlock_) { - // Call any validation function, and allow it to override. - newState = this.callValidator(newState); + this.setValue(!this.value_); +}; + +/** + * Ensure that the input value is valid ('TRUE' or 'FALSE'). + * @param {string|boolean=} newValue The input value. + * @return {?string} A valid value ('TRUE' or 'FALSE), or null if invalid. + * @protected + */ +Blockly.FieldCheckbox.prototype.doClassValidation_ = function(newValue) { + if (newValue === true || newValue === 'TRUE') { + return 'TRUE'; } - if (newState !== null) { - this.setValue(String(newState).toUpperCase()); + if (newValue === false || newValue === 'FALSE') { + return 'FALSE'; + } + return null; +}; + +/** + * Update the value of the field, and update the checkElement. + * @param {string} newValue The new value ('TRUE' or 'FALSE') of the field. + * @protected + */ +Blockly.FieldCheckbox.prototype.doValueUpdate_ = function(newValue) { + this.value_ = this.convertValueToBool_(newValue); + // Update visual. + if (this.checkElement_) { + this.checkElement_.style.display = this.value_ ? 'block' : 'none'; + } +}; + +/** + * Get the value of this field, either 'TRUE' or 'FALSE'. + * @return {string} The value of this field. + */ +Blockly.FieldCheckbox.prototype.getValue = function() { + return this.value_ ? 'TRUE' : 'FALSE'; +}; + +/** + * Get the boolean value of this field. + * @return {string} The boolean value of this field. + */ +Blockly.FieldCheckbox.prototype.getValueBoolean = function() { + return 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'). + */ +Blockly.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 + */ +Blockly.FieldCheckbox.prototype.convertValueToBool_ = function(value) { + if (typeof value == 'string') { + return value == 'TRUE'; + } else { + return !!value; } }; diff --git a/core/field_colour.js b/core/field_colour.js index d0796acf0..0e0c830df 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -34,21 +34,21 @@ goog.require('goog.math.Size'); /** * Class for a colour input field. - * @param {string=} opt_colour The initial colour in '#rrggbb' format, defaults - * to the first value in the default colour array. - * @param {Function=} opt_validator A function that is executed when a new - * colour is selected. Its sole argument is the new colour value. Its - * return value becomes the selected colour, unless it is undefined, in - * which case the new colour stands, or it is null, in which case the change - * is aborted. + * @param {string=} opt_value The initial value of the field. Should be in + * '#rrggbb' format. Defaults to the first value in the default colour array. + * @param {Function=} opt_validator A function that is called to validate + * changes to the field's value. Takes in a colour string & returns a + * validated colour string ('#rrggbb' format), or null to abort the change. * @extends {Blockly.Field} * @constructor */ -Blockly.FieldColour = function(opt_colour, opt_validator) { - opt_colour = opt_colour || Blockly.FieldColour.COLOURS[0]; - Blockly.FieldColour.superClass_.constructor - .call(this, opt_colour, opt_validator); - this.setText(Blockly.Field.NBSP + Blockly.Field.NBSP + Blockly.Field.NBSP); +Blockly.FieldColour = function(opt_value, opt_validator) { + opt_value = this.doClassValidation_(opt_value); + if (opt_value === null) { + opt_value = Blockly.FieldColour.COLOURS[0]; + } + Blockly.FieldColour.superClass_.constructor.call( + this, opt_value, opt_validator); }; goog.inherits(Blockly.FieldColour, Blockly.Field); @@ -63,14 +63,6 @@ Blockly.FieldColour.fromJson = function(options) { return new Blockly.FieldColour(options['colour']); }; -/** - * Serializable fields are saved by the XML renderer, non-serializable fields - * are not. Editable fields should also be serializable. - * @type {boolean} - * @const - */ -Blockly.FieldColour.prototype.SERIALIZABLE = true; - /** * Default width of a colour field. * @type {number} @@ -87,6 +79,20 @@ Blockly.FieldColour.DEFAULT_WIDTH = 16; */ Blockly.FieldColour.DEFAULT_HEIGHT = 12; +/** + * Regex that defines the form of a colour string. + * @type {RegExp} + */ +Blockly.FieldColour.COLOUR_REGEX = new RegExp('#[0-9a-fA-F]{6}'); + +/** + * Serializable fields are saved by the XML renderer, non-serializable fields + * are not. Editable fields should also be serializable. + * @type {boolean} + * @const + */ +Blockly.FieldColour.prototype.SERIALIZABLE = true; + /** * Array of colours used by this field. If null, use the global list. * @type {Array.} @@ -137,7 +143,7 @@ Blockly.FieldColour.prototype.initView = function() { this.borderRect_.style['fillOpacity'] = 1; this.borderRect_.setAttribute('width', this.size_.width + Blockly.BlockSvg.SEP_SPACE_X); - this.setValue(this.getValue()); + this.borderRect_.style.fill = this.value_; }; /** @@ -154,42 +160,44 @@ Blockly.FieldColour.prototype.dispose = function() { }; /** - * Colour fields are fixed with, no need to update. + * Render the colour field. + * @private */ -Blockly.FieldColour.prototype.updateWidth = function() { - // NOP +Blockly.FieldColour.prototype.render_ = function() { + this.size_.width = Blockly.FieldColour.DEFAULT_WIDTH; }; /** - * Return the current colour. - * @return {string} Current colour in '#rrggbb' format. + * Ensure that the input value is a valid colour. + * @param {string=} newValue The input value. + * @return {?string} A valid colour, or null if invalid. + * @protected */ -Blockly.FieldColour.prototype.getValue = function() { - return this.colour_; -}; - -/** - * Set the colour. - * @param {string} colour The new colour in '#rrggbb' format. - */ -Blockly.FieldColour.prototype.setValue = function(colour) { - if (this.sourceBlock_ && Blockly.Events.isEnabled() && - this.colour_ != colour) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, this.colour_, colour)); +Blockly.FieldColour.prototype.doClassValidation_ = function(newValue) { + if (Blockly.FieldColour.COLOUR_REGEX.test(newValue)) { + return newValue.toLowerCase(); } - this.colour_ = colour; + return null; +}; + +/** + * Update the value of this colour field, and update the displayed colour. + * @param {string} newValue The new colour in '#rrggbb' format. + * @protected + */ +Blockly.FieldColour.prototype.doValueUpdate_ = function(newValue) { + this.value_ = newValue; if (this.borderRect_) { - this.borderRect_.style.fill = colour; + this.borderRect_.style.fill = newValue; } }; /** - * Get the text from this field. Used when the block is collapsed. - * @return {string} Current text. + * Get the text for this field. Used when the block is collapsed. + * @return {string} Text representing the value of this field. */ Blockly.FieldColour.prototype.getText = function() { - var colour = this.colour_; + var colour = this.value_; // Try to use #rgb format if possible, rather than #rrggbb. var m = colour.match(/^#(.)\1(.)\2(.)\3$/); if (m) { diff --git a/core/field_date.js b/core/field_date.js index 7cbe31a16..c71d0d743 100644 --- a/core/field_date.js +++ b/core/field_date.js @@ -40,21 +40,20 @@ goog.require('goog.ui.DatePicker'); /** * Class for a date input field. - * @param {string=} opt_date The initial date, defaults to the current day. - * @param {Function=} opt_validator A function that is executed when a new - * date is selected. Its sole argument is the new date value. Its - * return value becomes the selected date, unless it is undefined, in - * which case the new date stands, or it is null, in which case the change - * is aborted. + * @param {string=} opt_value The initial value of the field. Should be in + * 'YYYY-MM-DD' format. Defaults to the current date. + * @param {Function=} opt_validator A function that is called to validate + * changes to the field's value. Takes in a date string & returns a + * validated date string ('YYYY-MM-DD' format), or null to abort the change. * @extends {Blockly.Field} * @constructor */ -Blockly.FieldDate = function(opt_date, opt_validator) { - if (!opt_date) { - opt_date = new goog.date.Date().toIsoString(true); +Blockly.FieldDate = function(opt_value, opt_validator) { + opt_value = this.doClassValidation_(opt_value); + if (!opt_value) { + opt_value = new goog.date.Date().toIsoString(true); } - Blockly.FieldDate.superClass_.constructor.call(this, opt_date, opt_validator); - this.setValue(opt_date); + Blockly.FieldDate.superClass_.constructor.call(this, opt_value, opt_validator); }; goog.inherits(Blockly.FieldDate, Blockly.Field); @@ -91,28 +90,21 @@ Blockly.FieldDate.prototype.dispose = function() { }; /** - * Return the current date. - * @return {string} Current date. + * Ensure that the input value is a valid date. + * @param {string=} newValue The input value. + * @return {?string} A valid date, or null if invalid. + * @protected */ -Blockly.FieldDate.prototype.getValue = function() { - return this.date_; -}; - -/** - * Set the date. - * @param {string} date The new date. - */ -Blockly.FieldDate.prototype.setValue = function(date) { - if (this.sourceBlock_) { - var validated = this.callValidator(date); - // If the new date is invalid, validation returns null. - // In this case we still want to display the illegal result. - if (validated !== null) { - date = validated; - } +Blockly.FieldDate.prototype.doClassValidation_ = function(newValue) { + if (!newValue) { + return null; } - this.date_ = date; - Blockly.Field.prototype.setText.call(this, date); + // Check if the new value is parsable or not. + var date = goog.date.Date.fromIsoString(newValue); + if (!date || date.toIsoString(true) != newValue) { + return null; + } + return newValue; }; /** @@ -142,10 +134,6 @@ Blockly.FieldDate.prototype.showEditor_ = function() { function(event) { var date = event.date ? event.date.toIsoString(true) : ''; Blockly.WidgetDiv.hide(); - if (thisField.sourceBlock_) { - // Call any validation function, and allow it to override. - date = thisField.callValidator(date); - } thisField.setValue(date); }); }; diff --git a/core/field_dropdown.js b/core/field_dropdown.js index cbfded728..821ad3b68 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -42,11 +42,10 @@ goog.require('goog.ui.MenuItem'); * Class for an editable dropdown field. * @param {(!Array.|!Function)} menuGenerator An array of options * for a dropdown list, or a function which generates these options. - * @param {Function=} opt_validator A function that is executed when a new - * option is selected, with the newly selected value as its sole argument. - * If it returns a value, that value (which must be one of the options) will - * become selected in place of the newly selected option, unless the return - * value is null, in which case the change is aborted. + * @param {Function=} opt_validator A function that is called to validate + * changes to the field's value. Takes in a language-neutral dropdown + * option & returns a validated language-neutral dropdown option, or null to + * abort the change. * @extends {Blockly.Field} * @constructor */ @@ -105,13 +104,6 @@ Blockly.FieldDropdown.ARROW_CHAR = */ Blockly.FieldDropdown.prototype.CURSOR = 'default'; -/** - * Language-neutral currently selected string or image object. - * @type {string|!Object} - * @protected - */ -Blockly.FieldDropdown.prototype.value_ = ''; - /** * SVG image element if currently selected option is an image, or null. * @type {SVGElement} @@ -276,14 +268,7 @@ Blockly.FieldDropdown.prototype.getAnchorDimensions_ = function() { * @param {!goog.ui.MenuItem} menuItem The MenuItem selected within menu. */ Blockly.FieldDropdown.prototype.onItemSelected = function(menu, menuItem) { - var value = menuItem.getValue(); - if (this.sourceBlock_) { - // Call any validation function, and allow it to override. - value = this.callValidator(value); - } - if (value !== null) { - this.setValue(value); - } + this.setValue(menuItem.getValue()); }; /** @@ -386,32 +371,43 @@ Blockly.FieldDropdown.prototype.getOptions = function() { }; /** - * Get the language-neutral value from this dropdown menu. - * @return {string} Current text. + * Ensure that the input value is a valid language-neutral option. + * @param {string=} newValue The input value. + * @return {?string} A valid language-neutral option, or null if invalid. + * @protected */ -Blockly.FieldDropdown.prototype.getValue = function() { - return this.value_; +Blockly.FieldDropdown.prototype.doClassValidation_ = function(newValue) { + var isValueValid = false; + var options = this.getOptions(); + for (var i = 0, option; option = options[i]; i++) { + // Options are tuples of human-readable text and language-neutral values. + if (option[1] == newValue) { + isValueValid = true; + break; + } + } + if (!isValueValid) { + if (this.sourceBlock_) { + console.warn('Cannot set the dropdown\'s value to an unavailable option.' + + ' Block type: ' + this.sourceBlock_.type + ', Field name: ' + this.name + + ', Value: ' + newValue); + } + return null; + } + return newValue; }; /** - * Set the language-neutral value for this dropdown menu. - * @param {string} newValue New value to set. + * Update the value of this dropdown field. + * @param {string} newValue The new language-enutral value. + * @protected */ -Blockly.FieldDropdown.prototype.setValue = function(newValue) { - if (newValue === null || (newValue === this.value_ && this.text_)) { - return; // No change if null and text_ was initialized. - } - if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, this.value_, newValue)); - } - this.value_ = newValue; - // Look up and display the human-readable text. +Blockly.FieldDropdown.prototype.doValueUpdate_ = function(newValue) { + Blockly.FieldDropdown.superClass_.doValueUpdate_.call(this, newValue); var options = this.getOptions(); - for (var i = 0; i < options.length; i++) { - // Options are tuples of human-readable text and language-neutral values. - if (options[i][1] == newValue) { - var content = options[i][0]; + for (var i = 0, option; option = options[i]; i++) { + if (option[1] == this.value_) { + var content = option[0]; if (typeof content == 'object') { this.imageJson_ = content; this.text_ = content.alt; @@ -419,15 +415,8 @@ Blockly.FieldDropdown.prototype.setValue = function(newValue) { this.imageJson_ = null; this.text_ = content; } - // Always rerender if either the value or the text has changed. - this.forceRerender(); - return; } } - // Value not found. Add it, maybe it will become valid once set - // (like variable names). - this.text_ = newValue; - this.forceRerender(); }; /** diff --git a/core/field_image.js b/core/field_image.js index dda922364..532543c65 100644 --- a/core/field_image.js +++ b/core/field_image.js @@ -34,7 +34,7 @@ goog.require('goog.math.Size'); /** * Class for an image on a block. - * @param {string=} src The URL of the image, defaults to an empty string. + * @param {string=} src The URL of the image. Defaults to an empty string. * @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. @@ -66,8 +66,8 @@ Blockly.FieldImage = function(src, width, height, this.flipRtl_ = opt_flipRtl; this.tooltip_ = ''; + this.text_ = opt_alt || ''; this.setValue(src || ''); - this.setText(opt_alt || ''); if (typeof opt_onClick == 'function') { this.clickHandler_ = opt_onClick; @@ -111,11 +111,12 @@ Blockly.FieldImage.prototype.initView = function() { 'image', { 'height': this.height_ + 'px', - 'width': this.width_ + 'px' + 'width': this.width_ + 'px', + 'alt': this.text_ }, this.fieldGroup_); - this.setValue(this.src_); - this.setText(this.text_); + this.imageElement_.setAttributeNS('http://www.w3.org/1999/xlink', + 'xlink:href', this.value_); this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_); if (this.tooltip_) { @@ -166,28 +167,28 @@ Blockly.FieldImage.prototype.setTooltip = function(newTip) { }; /** - * Get the source URL of this image. - * @return {string} Current text. - * @override + * Ensure that the input value (the source URL) is a string. + * @param {string=} newValue The input value + * @return {?string} A string, or null if invalid. + * @protected */ -Blockly.FieldImage.prototype.getValue = function() { - return this.src_; +Blockly.FieldImage.prototype.doClassValidation_ = function(newValue) { + if (typeof newValue != 'string') { + return null; + } + return newValue; }; /** - * Set the source URL of this image. - * @param {?string} src New source. - * @override + * Update the value of this image field, and update the displayed image. + * @param {string} newValue The new image src. + * @protected */ -Blockly.FieldImage.prototype.setValue = function(src) { - if (src === null) { - // No change if null. - return; - } - this.src_ = src; +Blockly.FieldImage.prototype.doValueUpdate_ = function(newValue) { + this.value_ = newValue; if (this.imageElement_) { this.imageElement_.setAttributeNS('http://www.w3.org/1999/xlink', - 'xlink:href', src || ''); + 'xlink:href', this.value_ || ''); } }; @@ -220,22 +221,8 @@ Blockly.FieldImage.prototype.setText = function(alt) { * @private */ Blockly.FieldImage.prototype.render_ = function() { - // NOP -}; - -/** - * Images are fixed width, no need to render even if forced. - */ -Blockly.FieldImage.prototype.forceRerender = function() { - // NOP -}; - -/** - * Images are fixed width, no need to update. - * @private - */ -Blockly.FieldImage.prototype.updateWidth = function() { - // NOP + this.size_.width = this.width_; + this.size_.height = this.height_ + 2 * Blockly.BlockSvg.INLINE_PADDING_Y; }; /** diff --git a/core/field_label.js b/core/field_label.js index 2db51c03a..f287964e0 100644 --- a/core/field_label.js +++ b/core/field_label.js @@ -36,19 +36,20 @@ goog.require('goog.math.Size'); /** * Class for a non-editable, non-serializable text field. - * @param {string=} text The initial content of the field, defaults to an - * empty string. + * @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. * @extends {Blockly.Field} * @constructor */ -Blockly.FieldLabel = function(text, opt_class) { +Blockly.FieldLabel = function(opt_value, opt_class) { this.size_ = new goog.math.Size(0, 17.5); this.class_ = opt_class; - if (text === null || text === undefined) { - text = ''; + opt_value = this.doClassValidation_(opt_value); + if (opt_value === null) { + opt_value = ''; } - this.setValue(String(text)); + this.setValue(opt_value); this.tooltip_ = ''; }; goog.inherits(Blockly.FieldLabel, Blockly.Field); @@ -84,6 +85,8 @@ Blockly.FieldLabel.prototype.initView = function() { 'class': 'blocklyText', 'y': this.size_.height - 5 }, this.fieldGroup_); + var textNode = document.createTextNode(''); + this.textElement_.appendChild(textNode); if (this.class_) { Blockly.utils.addClass(this.textElement_, this.class_); } @@ -107,6 +110,19 @@ Blockly.FieldLabel.prototype.dispose = function() { } }; +/** + * Ensure that the input value casts to a valid string. + * @param {string=} newValue The input value. + * @return {?string} A valid string, or null if invalid. + * @protected + */ +Blockly.FieldLabel.prototype.doClassValidation_ = function(newValue) { + if (newValue === null || newValue === undefined) { + return null; + } + return String(newValue); +}; + /** * Change the tooltip text for this field. * @param {string|!Element} newTip Text for tooltip or a parent element to diff --git a/core/field_label_serializable.js b/core/field_label_serializable.js index b8f579f7f..833a4c61d 100644 --- a/core/field_label_serializable.js +++ b/core/field_label_serializable.js @@ -33,14 +33,15 @@ goog.require('Blockly.utils'); /** * Class for a non-editable, serializable text field. - * @param {!string} text The initial content of the field. + * @param {*} 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. * @extends {Blockly.FieldLabel} * @constructor * */ -Blockly.FieldLabelSerializable = function(text, opt_class) { - Blockly.FieldLabelSerializable.superClass_.constructor.call(this, text, +Blockly.FieldLabelSerializable = function(opt_value, opt_class) { + Blockly.FieldLabelSerializable.superClass_.constructor.call(this, opt_value, opt_class); }; goog.inherits(Blockly.FieldLabelSerializable, Blockly.FieldLabel); diff --git a/core/field_number.js b/core/field_number.js index 10e0efdf0..636621aca 100644 --- a/core/field_number.js +++ b/core/field_number.js @@ -31,22 +31,23 @@ goog.require('Blockly.FieldTextInput'); /** * Class for an editable number field. - * @param {(string|number)=} opt_value The initial content of the field. - * The value should cast to a number, and if it does not, '0' will be used. + * @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 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. - * @extends {Blockly.FieldTextInput} + * @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. * @constructor */ Blockly.FieldNumber = function(opt_value, opt_min, opt_max, opt_precision, opt_validator) { this.setConstraints(opt_min, opt_max, opt_precision); - opt_value = (opt_value && !isNaN(opt_value)) ? String(opt_value) : '0'; + opt_value = this.doClassValidation_(opt_value); + if (opt_value === null) { + opt_value = 0; + } Blockly.FieldNumber.superClass_.constructor.call( this, opt_value, opt_validator); }; @@ -98,21 +99,26 @@ Blockly.FieldNumber.prototype.setConstraints = function(min, max, precision) { }; /** - * 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. + * Ensure that the input value is a valid number (must fulfill the + * constraints placed on the field). + * @param {string|number=} newValue The input value. + * @return {?number} A valid number, or null if invalid. + * @protected */ -Blockly.FieldNumber.prototype.classValidator = function(text) { - if (text === null) { +Blockly.FieldNumber.prototype.doClassValidation_ = function(newValue) { + if (newValue === null || newValue === undefined) { return null; } - text = String(text); + // Clean up text. + newValue = String(newValue); // TODO: Handle cases like 'ten', '1.203,14', etc. // 'O' is sometimes mistaken for '0' by inexperienced users. - text = text.replace(/O/ig, '0'); + newValue = newValue.replace(/O/ig, '0'); // Strip out thousands separators. - text = text.replace(/,/g, ''); - var n = parseFloat(text || 0); + newValue = newValue.replace(/,/g, ''); + + // Clean up number. + var n = parseFloat(newValue || 0); if (isNaN(n)) { // Invalid number. return null; @@ -123,8 +129,10 @@ Blockly.FieldNumber.prototype.classValidator = function(text) { if (this.precision_ && isFinite(n)) { n = Math.round(n / this.precision_) * this.precision_; } - return (this.fractionalDigits_ == -1) ? String(n) : - n.toFixed(this.fractionalDigits_); + // Clean up floating point errors. + n = (this.fractionalDigits_ == -1) ? n : + Number(n.toFixed(this.fractionalDigits_)); + return n; }; Blockly.Field.register('field_number', Blockly.FieldNumber); diff --git a/core/field_textinput.js b/core/field_textinput.js index 088a28109..717f9113e 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -37,20 +37,20 @@ goog.require('goog.math.Coordinate'); /** * Class for an editable text field. - * @param {string=} text The initial content of the field, defaults to an - * empty string. - * @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 {string=} opt_value The initial value of the field. Should cast to a + * string. Defaults to an empty string if null or undefined. + * @param {Function=} opt_validator A function that is called to validate + * changes to the field's value. Takes in a string & returns a validated + * string, or null to abort the change. * @extends {Blockly.Field} * @constructor */ -Blockly.FieldTextInput = function(text, opt_validator) { - if (text === null || text === undefined) { - text = ''; +Blockly.FieldTextInput = function(opt_value, opt_validator) { + opt_value = this.doClassValidation_(opt_value); + if (opt_value === null) { + opt_value = ''; } - Blockly.FieldTextInput.superClass_.constructor.call(this, String(text), + Blockly.FieldTextInput.superClass_.constructor.call(this, opt_value, opt_validator); }; goog.inherits(Blockly.FieldTextInput, Blockly.Field); @@ -114,43 +114,75 @@ Blockly.FieldTextInput.prototype.dispose = function() { }; /** - * Set the value of this field. - * @param {?string} newValue New value. - * @override + * Ensure that the input value casts to a valid string. + * @param {string=} newValue The input value. + * @return {?string} A valid string, or null if invalid. + * @protected */ -Blockly.FieldTextInput.prototype.setValue = function(newValue) { - if (newValue !== null) { // No change if null. - if (this.sourceBlock_) { - var validated = this.callValidator(newValue); - // If the new value is invalid, validation returns null. - // In this case we still want to display the illegal result. - if (validated !== null) { - newValue = validated; +Blockly.FieldTextInput.prototype.doClassValidation_ = function(newValue) { + if (newValue === null || newValue === undefined) { + return null; + } + return String(newValue); +}; + +/** + * Called by setValue if the text input is not valid. If the field is + * currently being edited it reverts value of the field to the previous + * value while allowing the display text to be handled by the htmlInput_. + * @protected + */ +Blockly.FieldTextInput.prototype.doValueInvalid_ = function() { + if (this.isBeingEdited_) { + this.isTextValid_ = false; + var htmlInput = Blockly.FieldTextInput.htmlInput_; + if (htmlInput) { + var oldValue = this.value_; + // Revert value when the text becomes invalid. + this.value_ = htmlInput.untypedDefaultValue_; + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.BlockChange( + this.sourceBlock_, 'field', this.name, oldValue, this.value_)); } } - Blockly.Field.prototype.setValue.call(this, newValue); } }; /** - * Set the text in this field and fire a change event. - * @param {*} newText New text. + * Called by setValue if the text input is valid. Updates the value of the + * field, and updates the text of the field if it is not currently being + * edited (i.e. handled by the htmlInput_). + * @param {!string} newValue The new validated value of the field. + * @protected */ -Blockly.FieldTextInput.prototype.setText = function(newText) { - if (newText === null) { - // No change if null. - return; +Blockly.FieldTextInput.prototype.doValueUpdate_ = function(newValue) { + this.isTextValid_ = true; + this.value_ = newValue; + if (!this.isBeingEdited_) { + // This should only occur if setValue is triggered programmatically. + this.text_ = String(newValue); + this.isDirty_ = true; } - newText = String(newText); - if (newText === this.text_) { - // No change. - return; +}; + +/** + * Updates the colour of the htmlInput given the current validity of the + * field's value. + * @protected + */ +Blockly.FieldTextInput.prototype.render_ = function() { + Blockly.FieldTextInput.superClass_.render_.call(this); + // This logic is done in render_ rather than doValueInvalid_ or + // doValueUpdate_ so that the code is more centralized. + if (this.isBeingEdited_) { + var htmlInput = Blockly.FieldTextInput.htmlInput_; + this.resizeEditor_(); + if (!this.isTextValid_) { + Blockly.utils.addClass(htmlInput, 'blocklyInvalidInput'); + } else { + Blockly.utils.removeClass(htmlInput, 'blocklyInvalidInput'); + } } - if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, this.text_, newText)); - } - Blockly.Field.prototype.setText.call(this, newText); }; /** @@ -188,9 +220,6 @@ Blockly.FieldTextInput.prototype.showPromptEditor_ = function() { var fieldText = this; Blockly.prompt(Blockly.Msg['CHANGE_VALUE_TITLE'], this.text_, function(newValue) { - if (fieldText.sourceBlock_) { - newValue = fieldText.callValidator(newValue); - } fieldText.setValue(newValue); }); }; @@ -202,8 +231,13 @@ Blockly.FieldTextInput.prototype.showPromptEditor_ = function() { * @private */ Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) { + // Start editing. + this.isBeingEdited_ = true; + + // Show the div. Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_()); var div = Blockly.WidgetDiv.DIV; + // Create the input. var htmlInput = document.createElement('input'); htmlInput.className = 'blocklyHtmlInput'; @@ -212,20 +246,25 @@ Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) { (Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt'; div.style.fontSize = fontSize; htmlInput.style.fontSize = fontSize; - - Blockly.FieldTextInput.htmlInput_ = htmlInput; div.appendChild(htmlInput); - htmlInput.value = htmlInput.defaultValue = this.text_; + // Assign the current value to the input & resize. + htmlInput.value = htmlInput.defaultValue = this.value_; + htmlInput.untypedDefaultValue_ = this.value_; htmlInput.oldValue_ = null; - this.validate_(); this.resizeEditor_(); + + // Give it focus. if (!quietInput) { htmlInput.focus(); htmlInput.select(); } + // Bind events. this.bindEvents_(htmlInput); + + // Save it. + Blockly.FieldTextInput.htmlInput_ = htmlInput; }; /** @@ -239,7 +278,7 @@ Blockly.FieldTextInput.prototype.bindEvents_ = function(htmlInput) { htmlInput.onKeyDownWrapper_ = Blockly.bindEventWithChecks_( htmlInput, 'keydown', this, this.onHtmlInputKeyDown_); - // Bind to keyup -- trap Enter; resize after every keystroke. + // Bind to keyup -- resize after every keystroke. htmlInput.onKeyUpWrapper_ = Blockly.bindEventWithChecks_( htmlInput, 'keyup', this, this.onHtmlInputChange_); @@ -294,7 +333,6 @@ Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) { */ Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(_e) { var htmlInput = Blockly.FieldTextInput.htmlInput_; - // Update source block. var text = htmlInput.value; if (text !== htmlInput.oldValue_) { htmlInput.oldValue_ = text; @@ -304,40 +342,15 @@ Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(_e) { // broader fix for all field types. Blockly.Events.setGroup(true); this.setValue(text); + // Always render the input text. + this.text_ = Blockly.FieldTextInput.htmlInput_.value; + this.forceRerender(); Blockly.Events.setGroup(false); - this.validate_(); - } else if (Blockly.userAgent.WEBKIT) { - // Cursor key. Render the source block to show the caret moving. - // Chrome only (version 26, OS X). - this.sourceBlock_.render(); - } - this.resizeEditor_(); - Blockly.svgResize(this.sourceBlock_.workspace); -}; - -/** - * Check to see if the contents of the editor validates. - * Style the editor accordingly. - * @protected - */ -Blockly.FieldTextInput.prototype.validate_ = function() { - var valid = true; - if (!Blockly.FieldTextInput.htmlInput_) { - throw Error('htmlInput not defined'); - } - var htmlInput = Blockly.FieldTextInput.htmlInput_; - if (this.sourceBlock_) { - valid = this.callValidator(htmlInput.value); - } - if (valid === null) { - Blockly.utils.addClass(htmlInput, 'blocklyInvalidInput'); - } else { - Blockly.utils.removeClass(htmlInput, 'blocklyInvalidInput'); } }; /** - * Resize the editor and the underlying block to fit the text. + * Resize the editor to fit the text. * @protected */ Blockly.FieldTextInput.prototype.resizeEditor_ = function() { @@ -376,12 +389,30 @@ Blockly.FieldTextInput.prototype.widgetDispose_ = function() { var thisField = this; return function() { var htmlInput = Blockly.FieldTextInput.htmlInput_; - // Save the edit (if it validates). - thisField.maybeSaveEdit_(); + // Finalize value. + thisField.isBeingEdited_ = false; + // No need to call setValue because if the widget is being closed the + // latest input text has already been validated. + if (thisField.value_ != thisField.text_) { + // At the end of an edit the text should be the same as the value. It + // may not be if the input text is different than the validated text. + // We should fix that. + thisField.text_ = String(thisField.value_); + thisField.isTextValid_ = true; + thisField.forceRerender(); + } + // Otherwise don't rerender. + + // Call onFinishEditing + // TODO: Get rid of this or make it less of a hack. + if (thisField.onFinishEditing_) { + thisField.onFinishEditing_(thisField.value_); + } + + // Remove htmlInput. thisField.unbindEvents_(htmlInput); Blockly.FieldTextInput.htmlInput_ = null; - Blockly.Events.setGroup(false); // Delete style properties. var style = Blockly.WidgetDiv.DIV.style; @@ -391,36 +422,11 @@ Blockly.FieldTextInput.prototype.widgetDispose_ = function() { }; }; -/** - * Attempt to save the text field changes when the user input loses focus. - * If the value is not valid, revert to the default value. - * @private - */ -Blockly.FieldTextInput.prototype.maybeSaveEdit_ = function() { - var htmlInput = Blockly.FieldTextInput.htmlInput_; - // Save the edit (if it validates). - var text = htmlInput.value; - if (this.sourceBlock_) { - var text1 = this.callValidator(text); - if (text1 === null) { - // Invalid edit. - text = htmlInput.defaultValue; - } else { - // Validation function has changed the text. - text = text1; - if (this.onFinishEditing_) { - this.onFinishEditing_(text); - } - } - } - this.setText(text); - this.sourceBlock_.rendered && this.sourceBlock_.render(); -}; - /** * Ensure that only a number may be entered. * @param {string} text The user's text. * @return {?string} A string representing a valid number, or null if invalid. + * @deprecated */ Blockly.FieldTextInput.numberValidator = function(text) { console.warn('Blockly.FieldTextInput.numberValidator is deprecated. ' + @@ -442,6 +448,7 @@ Blockly.FieldTextInput.numberValidator = function(text) { * Ensure that only a nonnegative integer may be entered. * @param {string} text The user's text. * @return {?string} A string representing a valid int, or null if invalid. + * @deprecated */ Blockly.FieldTextInput.nonnegativeIntegerValidator = function(text) { var n = Blockly.FieldTextInput.numberValidator(text); diff --git a/core/field_variable.js b/core/field_variable.js index 6c5561a7f..5b2fc7d2b 100644 --- a/core/field_variable.js +++ b/core/field_variable.js @@ -39,8 +39,9 @@ goog.require('goog.math.Size'); * 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 executed when a new - * option is selected. Its sole argument is the new option value. + * @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.=} 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 @@ -78,6 +79,13 @@ Blockly.FieldVariable.fromJson = function(options) { return new Blockly.FieldVariable(varname, null, variableTypes, defaultType); }; +/** + * The workspace that this variable field belongs to. + * @type {?Blockly.Workspace} + * @private + */ +Blockly.FieldVariable.prototype.workspace_ = null; + /** * Serializable fields are saved by the XML renderer, non-serializable fields * are not. Editable fields should also be serializable. @@ -96,28 +104,28 @@ Blockly.FieldVariable.prototype.initModel = function() { if (this.variable_) { return; // Initialization already happened. } - this.workspace_ = this.sourceBlock_.workspace; var variable = Blockly.Variables.getOrCreateVariablePackage( this.workspace_, null, this.defaultVariableName, this.defaultType_); // Don't fire a change event for this setValue. It would have null as the // old value, which is not valid. Blockly.Events.disable(); - try { - this.setValue(variable.getId()); - } finally { - Blockly.Events.enable(); - } + this.setValue(variable.getId()); + Blockly.Events.enable(); }; +/** + * Initialize this field based on the given XML. + * @param {!Element} fieldElement The element containing information about the + * variable field's state. + */ Blockly.FieldVariable.prototype.fromXml = function(fieldElement) { var id = fieldElement.getAttribute('id'); var variableName = fieldElement.textContent; var variableType = fieldElement.getAttribute('variabletype') || ''; var variable = Blockly.Variables.getOrCreateVariablePackage( - this.workspace_ || this.sourceBlock_.workspace, id, - variableName, variableType); + this.workspace_, id, variableName, variableType); // This should never happen :) if (variableType != null && variableType !== variable.type) { @@ -130,6 +138,12 @@ Blockly.FieldVariable.prototype.fromXml = function(fieldElement) { 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. + */ Blockly.FieldVariable.prototype.toXml = function(fieldElement) { // Make sure the variable is initialized. this.initModel(); @@ -160,6 +174,7 @@ Blockly.FieldVariable.prototype.setSourceBlock = function(block) { throw Error('Variable fields are not allowed to exist on shadow blocks.'); } Blockly.FieldVariable.superClass_.setSourceBlock.call(this, block); + this.workspace_ = block.workspace; }; /** @@ -192,30 +207,57 @@ Blockly.FieldVariable.prototype.getVariable = function() { }; /** - * Set the variable ID. - * @param {string} id New variable ID, which must reference an existing - * 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. */ -Blockly.FieldVariable.prototype.setValue = function(id) { - var workspace = this.sourceBlock_.workspace; - var variable = Blockly.Variables.getVariable(workspace, id); - - if (!variable) { - throw Error('Variable id doesn\'t point to a real variable! ID was ' + id); +Blockly.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_; } - // Type checks! + return null; +}; + +/** + * Ensure that the id belongs to a valid variable of an allowed type. + * @param {string} newId The id of the new variable to set. + * @return {?string} The validated id, or null if invalid. + * @protected + */ +Blockly.FieldVariable.prototype.doClassValidation_ = function(newId) { + var variable = Blockly.Variables.getVariable(this.workspace_, newId); + if (!variable) { + console.warn('Variable id doesn\'t point to a real variable! ' + + 'ID was ' + newId); + return null; + } + // Type Checks. var type = variable.type; if (!this.typeIsAllowed_(type)) { - throw Error('Variable type doesn\'t match this field! Type was ' + type); + console.warn('Variable type doesn\'t match this field! Type was ' + type); + return null; } - if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - var oldValue = this.variable_ ? this.variable_.getId() : null; - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, oldValue, id)); - } - this.variable_ = variable; - this.value_ = id; - this.setText(variable.name); + 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 {string} newId The id of the new variable. + * @protected + */ +Blockly.FieldVariable.prototype.doValueUpdate_ = function(newId) { + this.variable_ = Blockly.Variables.getVariable(this.workspace_, newId); + this.value_ = newId; + this.text_ = (this.variable_.name); + this.isDirty_ = true; }; /** @@ -248,9 +290,8 @@ Blockly.FieldVariable.prototype.getVariableTypes_ = function() { var variableTypes = this.variableTypes; if (variableTypes === null) { // If variableTypes is null, return all variable types. - if (this.sourceBlock_) { - var workspace = this.sourceBlock_.workspace; - return workspace.getVariableTypes(); + if (this.workspace_) { + return this.workspace_.getVariableTypes(); } } variableTypes = variableTypes || ['']; @@ -315,18 +356,14 @@ Blockly.FieldVariable.dropdownCreate = function() { ' variable selected.'); } var name = this.getText(); - var workspace = null; - if (this.sourceBlock_) { - workspace = this.sourceBlock_.workspace; - } var variableModelList = []; - if (workspace) { + if (this.workspace_) { var 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 (var i = 0; i < variableTypes.length; i++) { var variableType = variableTypes[i]; - var variables = workspace.getVariablesOfType(variableType); + var variables = this.workspace_.getVariablesOfType(variableType); variableModelList = variableModelList.concat(variables); } } @@ -359,20 +396,19 @@ Blockly.FieldVariable.dropdownCreate = function() { */ Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) { var id = menuItem.getValue(); - if (this.sourceBlock_ && this.sourceBlock_.workspace) { - var workspace = this.sourceBlock_.workspace; + // Handle special cases. + if (this.workspace_) { if (id == Blockly.RENAME_VARIABLE_ID) { // Rename variable. - Blockly.Variables.renameVariable(workspace, this.variable_); + Blockly.Variables.renameVariable(this.workspace_, this.variable_); return; } else if (id == Blockly.DELETE_VARIABLE_ID) { // Delete variable. - workspace.deleteVariableById(this.variable_.getId()); + this.workspace_.deleteVariableById(this.variable_.getId()); return; } - - // TODO (#1529): Call any validation function, and allow it to override. } + // Handle unspecial case. this.setValue(id); }; diff --git a/tests/blocks/test_blocks.js b/tests/blocks/test_blocks.js index 1d5b47b0f..cc7ca7124 100644 --- a/tests/blocks/test_blocks.js +++ b/tests/blocks/test_blocks.js @@ -263,6 +263,21 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT "tooltip": "", "helpUrl": "" }, + { + "type": "test_fields_image", + "message0": "image %1", + "args0": [ + { + "type": "field_image", + "name": "IMAGE", + "src": "https://blockly-demo.appspot.com/static/tests/media/a.png", + "width": 32, + "height": 32, + "alt": "A" + } + ], + "colour": 230 + }, { "type": "test_numbers_float", "message0": "float %1", @@ -670,6 +685,420 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT } ]); // END JSON EXTRACT (Do not delete this comment.) +Blockly.Blocks['test_validators_text_null'] = { + init: function() { + this.appendDummyInput() + .appendField("always null") + .appendField(new Blockly.FieldTextInput("default", this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('All input validates to null (invalid). The display' + + ' text will remain the input text, but the value should be the default' + + ' text. The input should be red after the first keystroke.'); + }, + + validate: function(newValue) { + return null; + } +}; +Blockly.Blocks['test_validators_text_A'] = { + init: function() { + this.appendDummyInput() + .appendField("remove \'a\'") + .appendField(new Blockly.FieldTextInput("default", this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('All \'a\' characters are removed from field value.' + + ' The display text will include invalid \'a\' characters while the' + + ' field is being edited, but the value will not.'); + }, + + validate: function(newValue) { + return newValue.replace(/\a/g, ''); + } +}; +Blockly.Blocks['test_validators_text_B'] = { + init: function() { + this.appendDummyInput() + .appendField("\'b\' -> null") + .appendField(new Blockly.FieldTextInput("default", this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('Upon detecting a \'b\' character the input will' + + ' validated to null (invalid). Upon removal it should revert to being' + + ' valid. The display text will remain the input text, but if the input' + + ' text is invalid the value should be the default text.'); + }, + + validate: function(newValue) { + if (newValue.indexOf('b') != -1) { + return null; + } + return newValue; + } +}; + +Blockly.Blocks['test_validators_angle_null'] = { + init: function() { + this.appendDummyInput() + .appendField("always null") + .appendField(new Blockly.FieldAngle(90, this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('All input validates to null (invalid). The field' + + ' will display the input while the field is being edited (this' + + ' includes the text and the graphic), but the value should be the' + + ' default value. The input should be red after the first' + + ' keystroke.'); + }, + + validate: function(newValue) { + return null; + } +}; +Blockly.Blocks['test_validators_angle_mult30_force'] = { + init: function() { + this.appendDummyInput() + .appendField("force mult of 30") + .appendField(new Blockly.FieldAngle(90, this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('The input value will be rounded to the nearest' + + ' multiple of 30. The field will display the input while the field is' + + ' being edited (this includes the text and the graphic), but the value' + + ' will be the validated (rounded) value. Note: If you want to do' + + ' rounding this is not the proper way, use the ROUND property of the' + + ' field angle instead.'); + }, + + validate: function(newValue) { + return Math.round(newValue / 30) * 30; + } +}; +Blockly.Blocks['test_validators_angle_mult30_null'] = { + init: function() { + this.appendDummyInput() + .appendField("not mult of 30 -> null") + .appendField(new Blockly.FieldAngle(90, this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('If the input value is not a multiple of 30, the' + + ' input will validated to null (invalid). The field will display the' + + ' input while the field is being edited (this includes the text and' + + ' the graphic), but if the input value is invalid the value should be' + + ' the default value.'); + }, + + validate: function(newValue) { + if (newValue % 30 != 0) { + return null; + } + return newValue; + } +}; + +Blockly.Blocks['test_validators_checkbox_null'] = { + init: function() { + this.appendDummyInput() + .appendField("always null") + .appendField(new Blockly.FieldCheckbox(true, this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('The new input always validates to null (invalid).' + + ' This means that the field value should not change.'); + }, + + validate: function(newValue) { + return null; + } +}; +Blockly.Blocks['test_validators_checkbox_match'] = { + init: function() { + this.appendDummyInput() + .appendField("force match") + .appendField(new Blockly.FieldCheckbox(true), "MATCH") + .appendField(new Blockly.FieldCheckbox(true, this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('The validator for this block only works on the' + + ' end-most checkbox. The validator will always return the value of the' + + ' start-most checkbox. Therefor they should always match.') + }, + + validate: function(newValue) { + return this.sourceBlock_.getFieldValue('MATCH'); + } +}; +Blockly.Blocks['test_validators_checkbox_not_match_null'] = { + init: function() { + this.appendDummyInput() + .appendField("not match -> null") + .appendField(new Blockly.FieldCheckbox(true), "MATCH") + .appendField(new Blockly.FieldCheckbox(true, this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('The validator for this block only works on the' + + ' end-most checkbox. If the new value does not match the value of the' + + ' start-most checkbox, it will return null (invalid), which means the' + + ' field value should not change. Therfore they should always match.'); + }, + + validate: function(newValue) { + if (this.sourceBlock_.getFieldValue('MATCH') != newValue) { + return null; + } + return newValue; + } +}; + +Blockly.Blocks['test_validators_colour_null'] = { + init: function() { + var colourField = new Blockly.FieldColour('#ff0000', this.validate); + colourField.setColours([ + '#ffffff', '#ffdcdc', '#ffb4b4','#ff8c8c','#ff6464','#ff3c3c','#ff1414', + '#00ffff', '#00dcdc', '#00b4b4','#008c8c','#006464','#003c3c','#001414']); + + this.appendDummyInput() + .appendField("always null") + .appendField(colourField, "INPUT"); + this.setColour(230); + this.setCommentText('All input validates to null (invalid). This means' + + ' the field value should not change.'); + }, + + validate: function(newValue) { + return null; + } +}; +Blockly.Blocks['test_validators_colour_force_red'] = { + init: function() { + var colourField = new Blockly.FieldColour('#ff0000', this.validate); + colourField.setColours([ + '#ffffff', '#ffdcdc', '#ffb4b4','#ff8c8c','#ff6464','#ff3c3c','#ff1414', + '#00ffff', '#00dcdc', '#00b4b4','#008c8c','#006464','#003c3c','#001414']); + + this.appendDummyInput() + .appendField("force full red") + .appendField(colourField, "INPUT"); + this.setColour(230); + this.setCommentText('The input will have its red value replaced with' + + ' full red.'); + }, + + validate: function(newValue) { + return '#ff' + newValue.substr(3, 4); + } +}; +Blockly.Blocks['test_validators_colour_red_null'] = { + init: function() { + var colourField = new Blockly.FieldColour('#ff0000', this.validate); + colourField.setColours([ + '#ffffff', '#ffdcdc', '#ffb4b4','#ff8c8c','#ff6464','#ff3c3c','#ff1414', + '#00ffff', '#00dcdc', '#00b4b4','#008c8c','#006464','#003c3c','#001414']); + + this.appendDummyInput() + .appendField("not red -> null") + .appendField(colourField, "INPUT"); + this.setColour(230); + this.setCommentText('If the input does not have full red, the input will' + + ' validate to null (invalid). Otherwise it will return the input value'); + }, + + validate: function(newValue) { + if (newValue.substr(1, 2) != 'ff') { + return null; + } + return newValue; + } +}; + +Blockly.Blocks['test_validators_date_null'] = { + init: function() { + this.appendDummyInput() + .appendField("always null") + .appendField(new Blockly.FieldDate("2020-02-20", this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('All input validates to null (invalid). This means' + + ' the field value should not change.'); + }, + + validate: function(newValue) { + return null; + } +}; +Blockly.Blocks['test_validators_date_force_20s'] = { + init: function() { + this.appendDummyInput() + .appendField("force day 20s") + .appendField(new Blockly.FieldDate("2020-02-20", this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('The input\'s date will change to always be in the' + + ' 20s.'); + }, + + validate: function(newValue) { + return newValue.substr(0, 8) + '2' + newValue.substr(9, 1); + } +}; +Blockly.Blocks['test_validators_date_20s_null'] = { + init: function() { + this.appendDummyInput() + .appendField("not 20s -> null") + .appendField(new Blockly.FieldDate("2020-02-20", this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('If the input is not in the 20s, the input will' + + ' validate to null (invalid). Otherwise it will return the input value.'); + }, + + validate: function(newValue) { + if (newValue.charAt(8) != '2') { + return null; + } + return newValue; + } +}; + +Blockly.Blocks['test_validators_dropdown_null'] = { + init: function() { + this.appendDummyInput() + .appendField("always null") + .appendField(new Blockly.FieldDropdown([ + ["1a","1A"], ["1b","1B"], ["1c","1C"], + ["2a","2A"], ["2b","2B"], ["2c","2C"]], this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('All input validates to null (invalid). This means' + + ' the field value should not change.'); + }, + + validate: function(newValue) { + return null; + } +}; +Blockly.Blocks['test_validators_dropdown_force_1s'] = { + init: function() { + this.appendDummyInput() + .appendField("force 1s") + .appendField(new Blockly.FieldDropdown([ + ["1a","1A"], ["1b","1B"], ["1c","1C"], + ["2a","2A"], ["2b","2B"], ["2c","2C"]], this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('The input\'s value will always change to start with' + + ' 1.'); + }, + + validate: function(newValue) { + return '1' + newValue.charAt(1); + } +}; +Blockly.Blocks['test_validators_dropdown_1s_null'] = { + init: function() { + this.appendDummyInput() + .appendField("not 1s -> null") + .appendField(new Blockly.FieldDropdown([ + ["1a","1A"], ["1b","1B"], ["1c","1C"], + ["2a","2A"], ["2b","2B"], ["2c","2C"]], this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('If the input does not start with 1, the input will' + + ' validate to null (invalid). Otherwise it will return the input value.'); + }, + + validate: function(newValue) { + if (newValue.charAt(0) != '1') { + return null; + } + return newValue; + } +}; + +Blockly.Blocks['test_validators_number_null'] = { + init: function() { + this.appendDummyInput() + .appendField("always null") + .appendField(new Blockly.FieldNumber(123, null, null, null, this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('All input validates to null (invalid). The field' + + ' will display the input while the field is being edited, but the value' + + ' should be the default value. The input should be red after the first' + + ' keystroke.'); + }, + + validate: function(newValue) { + return null; + } +}; +Blockly.Blocks['test_validators_number_mult10_force'] = { + init: function() { + this.appendDummyInput() + .appendField("force mult of 10") + .appendField(new Blockly.FieldNumber(123, null, null, null, this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('Theinput value will be rounded to the nearest' + + ' multiple of 10. The field will display the input while the field is' + + ' being edited, but the value should be the validated (rounded) value.' + + ' Note: If you want to do rounding this is not the proper way, use the' + + ' precision option of the number field constructor instead.'); + }, + + validate: function(newValue) { + return Math.round(newValue / 10) * 10; + } +}; +Blockly.Blocks['test_validators_number_mult10_null'] = { + init: function() { + this.appendDummyInput() + .appendField("not mult of 10 -> null") + .appendField(new Blockly.FieldNumber(123, null, null, null, this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('If the input value is not a multiple of 10, the' + + ' input will validate to null (invalid). The field will display the' + + ' input while the field is being edited, but if the input value is' + + ' invalid the value should be the default value.'); + }, + + validate: function(newValue) { + if (newValue % 10 != 0) { + return null; + } + return newValue; + } +}; + +Blockly.Blocks['test_validators_variable_null'] = { + init: function() { + this.appendDummyInput() + .appendField("always null") + .appendField(new Blockly.FieldVariable('1a', this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('All ids validate to null (invalid). This means' + + ' the variable should not change.'); + }, + + validate: function(newValue) { + return null; + } +}; +Blockly.Blocks['test_validators_variable_force_1s'] = { + init: function() { + this.appendDummyInput() + .appendField("force 1s") + .appendField(new Blockly.FieldVariable('1a', this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('The id will always change to start with 1.'); + }, + + validate: function(newValue) { + return '1' + newValue.charAt(1); + } +}; +Blockly.Blocks['test_validators_variable_1s_null'] = { + init: function() { + this.appendDummyInput() + .appendField("not 1s -> null") + .appendField(new Blockly.FieldVariable('1a', this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('If the id does not start with 1, the id will' + + ' validate to null (invalid). Otherwise it will return the id.'); + }, + + validate: function(newValue) { + if (newValue.charAt(0) != '1') { + return null; + } + return newValue; + } +}; + Blockly.Blocks['test_basic_empty_with_mutator'] = { init: function() { this.setMutator(new Blockly.Mutator(['math_number'])); diff --git a/tests/jsunit/field_angle_test.js b/tests/jsunit/field_angle_test.js deleted file mode 100644 index 66b6ab009..000000000 --- a/tests/jsunit/field_angle_test.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - /** - * @fileoverview Tests for Blockly.FieldAngle - * @author Anm@anm.me (Andrew n marshall) - */ -'use strict'; - -function test_fieldangle_constructor() { - assertEquals(new Blockly.FieldAngle().getValue(), '0'); - assertEquals(new Blockly.FieldAngle(null).getValue(), '0'); - assertEquals(new Blockly.FieldAngle(undefined).getValue(), '0'); - assertEquals(new Blockly.FieldAngle(1).getValue(), '1'); - assertEquals(new Blockly.FieldAngle(1.5).getValue(), '1.5'); - assertEquals(new Blockly.FieldAngle('2').getValue(), '2'); - assertEquals(new Blockly.FieldAngle('2.5').getValue(), '2.5'); - - // Bad values - assertEquals(new Blockly.FieldAngle('bad').getValue(), '0'); - assertEquals(new Blockly.FieldAngle(NaN).getValue(), '0'); -} - -function test_fieldangle_fromJson() { - assertEquals(Blockly.FieldAngle.fromJson({}).getValue(), '0'); - assertEquals(Blockly.FieldAngle.fromJson({ angle: 1 }).getValue(), '1'); -} diff --git a/tests/jsunit/field_number_test.js b/tests/jsunit/field_number_test.js deleted file mode 100644 index 507af7a4b..000000000 --- a/tests/jsunit/field_number_test.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - /** - * @fileoverview Tests for Blockly.FieldNumber - * @author Anm@anm.me (Andrew n marshall) - */ -'use strict'; - -function test_fieldnumber_constructor() { - // No arguments - var field = new Blockly.FieldNumber(); - assertEquals(field.getValue(), '0'); - assertEquals(field.min_, -Infinity); - assertEquals(field.max_, Infinity); - assertEquals(field.precision_, 0); - - // Numeric values - field = new Blockly.FieldNumber(1); - assertEquals(field.getValue(), '1'); - field = new Blockly.FieldNumber(1.5); - assertEquals(field.getValue(), '1.5'); - - // String value - field = new Blockly.FieldNumber('2'); - assertEquals(field.getValue(), '2'); - field = new Blockly.FieldNumber('2.5'); - assertEquals(field.getValue(), '2.5'); - - // All values - field = new Blockly.FieldNumber( - /* value */ 0, - /* min */ -128, - /* max */ 127, - /* precision */ 1); - assertEquals(field.getValue(), '0'); - assertEquals(field.min_, -128); - assertEquals(field.max_, 127); - assertEquals(field.precision_, 1); - - // Bad value defaults to '0' - field = new Blockly.FieldNumber('bad'); - assertEquals(field.getValue(), '0'); - field = new Blockly.FieldNumber(NaN); - assertEquals(field.getValue(), '0'); -} - -function test_fieldnumber_fromJson() { - assertEquals(Blockly.FieldNumber.fromJson({}).getValue(), '0'); - assertEquals(Blockly.FieldNumber.fromJson({ value: 1 }).getValue(), '1'); - - // All options - var field = Blockly.FieldNumber.fromJson({ - value: 0, - min: -128, - max: 127, - precision: 1 - }); - assertEquals(field.getValue(), '0'); - assertEquals(field.min_, -128); - assertEquals(field.max_, 127); - assertEquals(field.precision_, 1); -} diff --git a/tests/jsunit/field_variable_test.js b/tests/jsunit/field_variable_test.js deleted file mode 100644 index 3f8785cb9..000000000 --- a/tests/jsunit/field_variable_test.js +++ /dev/null @@ -1,237 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - /** - * @fileoverview Tests for Blockly.FieldVariable - * @author marisaleung@google.com (Marisa Leung) - */ -'use strict'; - -goog.require('goog.testing'); -goog.require('goog.testing.MockControl'); - -var workspace; -var mockControl_; - -function fieldVariableTestWithMocks_setUp() { - workspace = new Blockly.Workspace(); - mockControl_ = new goog.testing.MockControl(); -} - -function fieldVariableTestWithMocks_tearDown() { - mockControl_.$tearDown(); - workspace.dispose(); -} - -function fieldVariable_mockBlock(workspace) { - return {'workspace': workspace, 'isShadow': function(){return false;}}; -} - -function fieldVariable_createAndInitField(workspace) { - var fieldVariable = new Blockly.FieldVariable('name1'); - var mockBlock = fieldVariable_mockBlock(workspace); - fieldVariable.setSourceBlock(mockBlock); - // No view to initialize, but the model still needs work. - fieldVariable.initModel(); - return fieldVariable; -} - -function test_fieldVariable_Constructor() { - workspace = new Blockly.Workspace(); - var fieldVariable = new Blockly.FieldVariable('name1'); - // The field does not have a variable until after init() is called. - assertEquals('', fieldVariable.getText()); - workspace.dispose(); -} - -function test_fieldVariable_setValueMatchId() { - // Expect the fieldVariable value to be set to variable name - fieldVariableTestWithMocks_setUp(); - workspace.createVariable('name2', null, 'id2'); - - var fieldVariable = fieldVariable_createAndInitField(workspace); - - var oldId = fieldVariable.getValue(); - var event = new Blockly.Events.BlockChange( - fieldVariable.sourceBlock_, 'field', undefined, oldId, 'id2'); - setUpMockMethod(mockControl_, Blockly.Events, 'fire', [event], null); - - fieldVariable.setValue('id2'); - assertEquals('name2', fieldVariable.getText()); - assertEquals('id2', fieldVariable.getValue()); - fieldVariableTestWithMocks_tearDown(); -} - -function test_fieldVariable_setValueNoVariable() { - fieldVariableTestWithMocks_setUp(); - - var fieldVariable = fieldVariable_createAndInitField(workspace); - var mockBlock = fieldVariable.sourceBlock_; - mockBlock.isShadow = function() { - return false; - }; - - try { - fieldVariable.setValue('id1'); - // Calling setValue with a variable that doesn't exist throws an error. - fail(); - } catch (e) { - // expected - } finally { - fieldVariableTestWithMocks_tearDown(); - } -} - -function test_fieldVariable_dropdownCreateVariablesExist() { - // Expect that the dropdown options will contain the variables that exist. - workspace = new Blockly.Workspace(); - workspace.createVariable('name1', '', 'id1'); - workspace.createVariable('name2', '', 'id2'); - - var fieldVariable = fieldVariable_createAndInitField(workspace); - - var result_options = Blockly.FieldVariable.dropdownCreate.call( - fieldVariable); - - assertEquals(result_options.length, 3); - isEqualArrays(result_options[0], ['name1', 'id1']); - isEqualArrays(result_options[1], ['name2', 'id2']); - - workspace.dispose(); -} - -function test_fieldVariable_setValueNull() { - // This should no longer create a variable for the selected option. - fieldVariableTestWithMocks_setUp(); - setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['id1', null]); - - var fieldVariable = fieldVariable_createAndInitField(workspace); - try { - fieldVariable.setValue(null); - fail(); - } catch (e) { - // expected - } finally { - fieldVariableTestWithMocks_tearDown(); - } - -} - -function test_fieldVariable_getVariableTypes_undefinedVariableTypes() { - // Expect that since variableTypes is undefined, only type empty string - // will be returned (regardless of what types are available on the workspace). - workspace = new Blockly.Workspace(); - workspace.createVariable('name1', 'type1'); - workspace.createVariable('name2', 'type2'); - - var fieldVariable = new Blockly.FieldVariable('name1'); - var resultTypes = fieldVariable.getVariableTypes_(); - isEqualArrays(resultTypes, ['']); - workspace.dispose(); -} - -function test_fieldVariable_getVariableTypes_givenVariableTypes() { - // Expect that since variableTypes is defined, it will be the return value, - // regardless of what types are available on the workspace. - workspace = new Blockly.Workspace(); - workspace.createVariable('name1', 'type1'); - workspace.createVariable('name2', 'type2'); - - var fieldVariable = new Blockly.FieldVariable( - 'name1', null, ['type1', 'type2'], 'type1'); - var resultTypes = fieldVariable.getVariableTypes_(); - isEqualArrays(resultTypes, ['type1', 'type2']); - assertEquals('Default type was wrong', 'type1', fieldVariable.defaultType_); - workspace.dispose(); -} - -function test_fieldVariable_getVariableTypes_nullVariableTypes() { - // Expect all variable types to be returned. - // The variable does not need to be initialized to do this--it just needs a - // pointer to the workspace. - workspace = new Blockly.Workspace(); - workspace.createVariable('name1', 'type1'); - workspace.createVariable('name2', 'type2'); - - var fieldVariable = new Blockly.FieldVariable('name1'); - var mockBlock = fieldVariable_mockBlock(workspace); - fieldVariable.setSourceBlock(mockBlock); - fieldVariable.variableTypes = null; - - var resultTypes = fieldVariable.getVariableTypes_(); - // The empty string is always one of the options. - isEqualArrays(resultTypes, ['type1', 'type2', '']); - workspace.dispose(); -} - -function test_fieldVariable_getVariableTypes_emptyListVariableTypes() { - // Expect an error to be thrown. - workspace = new Blockly.Workspace(); - workspace.createVariable('name1', 'type1'); - workspace.createVariable('name2', 'type2'); - - var fieldVariable = new Blockly.FieldVariable('name1'); - var mockBlock = fieldVariable_mockBlock(workspace); - fieldVariable.setSourceBlock(mockBlock); - fieldVariable.variableTypes = []; - - try { - fieldVariable.getVariableTypes_(); - fail(); - } catch (e) { - //expected - } finally { - workspace.dispose(); - } -} - -function test_fieldVariable_defaultType_exists() { - var fieldVariable = new Blockly.FieldVariable(null, null, ['b'], 'b'); - assertEquals('The variable field\'s default type should be "b"', - 'b', fieldVariable.defaultType_); -} - -function test_fieldVariable_noDefaultType() { - var fieldVariable = new Blockly.FieldVariable(null); - assertEquals('The variable field\'s default type should be the empty string', - '', fieldVariable.defaultType_); - assertNull('The variable field\'s allowed types should be null', - fieldVariable.variableTypes); -} - -function test_fieldVariable_defaultTypeMismatch() { - try { - var fieldVariable = new Blockly.FieldVariable(null, null, ['a'], 'b'); - fail('Variable field creation should have failed due to an invalid ' + - 'default type'); - } catch (e) { - // expected - } -} - -function test_fieldVariable_defaultTypeMismatch_empty() { - try { - var fieldVariable = new Blockly.FieldVariable(null, null, ['a']); - fail('Variable field creation should have failed due to an invalid ' + - 'default type'); - } catch (e) { - // expected - } -} diff --git a/tests/jsunit/index.html b/tests/jsunit/index.html index 18ee34d32..ccc72d3a4 100644 --- a/tests/jsunit/index.html +++ b/tests/jsunit/index.html @@ -18,10 +18,7 @@ - - - diff --git a/tests/mocha/field_angle_test.js b/tests/mocha/field_angle_test.js index de468e48f..8059c01c8 100644 --- a/tests/mocha/field_angle_test.js +++ b/tests/mocha/field_angle_test.js @@ -127,7 +127,7 @@ suite ('Angle Fields', function() { this.angleField.setValue(undefined); assertValueDefault(this.angleField); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { this.angleField.setValue('bad'); assertValueDefault(this.angleField); }); @@ -164,15 +164,15 @@ suite ('Angle Fields', function() { this.angleField.setValue(null); assertValue(this.angleField, 1); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.angleField.setValue(undefined); assertValue(this.angleField, 1); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { this.angleField.setValue('bad'); assertValue(this.angleField, 1); }); - test.skip('NaN', function() { + test('NaN', function() { this.angleField.setValue(NaN); assertValue(this.angleField, 1); }); @@ -198,7 +198,7 @@ suite ('Angle Fields', function() { }); }); }); - suite.skip('Validators', function() { + suite('Validators', function() { setup(function() { this.angleField = new Blockly.FieldAngle(1); Blockly.FieldTextInput.htmlInput_ = Object.create(null); diff --git a/tests/mocha/field_checkbox_test.js b/tests/mocha/field_checkbox_test.js index 0c909df82..2ad0bb3a9 100644 --- a/tests/mocha/field_checkbox_test.js +++ b/tests/mocha/field_checkbox_test.js @@ -18,7 +18,7 @@ * limitations under the License. */ -suite.skip('Checkbox Fields', function() { +suite('Checkbox Fields', function() { function assertValue(checkboxField, expectedValue, expectedText) { var actualValue = checkboxField.getValue(); var actualText = checkboxField.getText(); diff --git a/tests/mocha/field_colour_test.js b/tests/mocha/field_colour_test.js index 2979bf0ca..507dd8364 100644 --- a/tests/mocha/field_colour_test.js +++ b/tests/mocha/field_colour_test.js @@ -57,11 +57,11 @@ suite ('Colour Fields', function() { var colourField = new Blockly.FieldColour(undefined); assertValueDefault(colourField); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { var colourField = new Blockly.FieldColour('bad'); assertValueDefault(colourField); }); - test.skip('#AAAAAA', function() { + test('#AAAAAA', function() { var colourField = new Blockly.FieldColour('#AAAAAA'); assertValue(colourField, '#aaaaaa', '#aaa'); }); @@ -69,7 +69,7 @@ suite ('Colour Fields', function() { var colourField = new Blockly.FieldColour('#aaaaaa'); assertValue(colourField, '#aaaaaa', '#aaa'); }); - test.skip('#AAAA00', function() { + test('#AAAA00', function() { var colourField = new Blockly.FieldColour('#AAAA00'); assertValue(colourField, '#aaaa00', '#aa0'); }); @@ -77,7 +77,7 @@ suite ('Colour Fields', function() { var colourField = new Blockly.FieldColour('#aaaa00'); assertValue(colourField, '#aaaa00', '#aa0'); }); - test.skip('#BCBCBC', function() { + test('#BCBCBC', function() { var colourField = new Blockly.FieldColour('#BCBCBC'); assertValue(colourField, '#bcbcbc', '#bcbcbc'); }); @@ -99,11 +99,11 @@ suite ('Colour Fields', function() { var colourField = new Blockly.FieldColour.fromJson({ colour:undefined }); assertValueDefault(colourField); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { var colourField = new Blockly.FieldColour.fromJson({ colour:'bad' }); assertValueDefault(colourField); }); - test.skip('#AAAAAA', function() { + test('#AAAAAA', function() { var colourField = Blockly.FieldColour.fromJson({ colour: '#AAAAAA' }); assertValue(colourField, '#aaaaaa', '#aaa'); }); @@ -111,7 +111,7 @@ suite ('Colour Fields', function() { var colourField = Blockly.FieldColour.fromJson({ colour: '#aaaaaa' }); assertValue(colourField, '#aaaaaa', '#aaa'); }); - test.skip('#AAAA00', function() { + test('#AAAA00', function() { var colourField = Blockly.FieldColour.fromJson({ colour: '#AAAA00' }); assertValue(colourField, '#aaaa00', '#aa0'); }); @@ -119,7 +119,7 @@ suite ('Colour Fields', function() { var colourField = Blockly.FieldColour.fromJson({ colour: '#aaaa00' }); assertValue(colourField, '#aaaa00', '#aa0'); }); - test.skip('#BCBCBC', function() { + test('#BCBCBC', function() { var colourField = Blockly.FieldColour.fromJson({ colour: '#BCBCBC' }); assertValue(colourField, '#bcbcbc', '#bcbcbc'); }); @@ -133,15 +133,15 @@ suite ('Colour Fields', function() { setup(function() { this.colourField = new Blockly.FieldColour(); }); - test.skip('Null', function() { + test('Null', function() { this.colourField.setValue(null); assertValueDefault(this.colourField); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.colourField.setValue(undefined); assertValueDefault(this.colourField); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { this.colourField.setValue('bad'); assertValueDefault(this.colourField); }); @@ -158,15 +158,15 @@ suite ('Colour Fields', function() { setup(function() { this.colourField = new Blockly.FieldColour('#aaaaaa'); }); - test.skip('Null', function() { + test('Null', function() { this.colourField.setValue(null); assertValue(this.colourField, '#aaaaaa', '#aaa'); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.colourField.setValue(undefined); assertValue(this.colourField, '#aaaaaa', '#aaa'); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { this.colourField.setValue('bad'); assertValue(this.colourField, '#aaaaaa', '#aaa'); }); @@ -180,7 +180,7 @@ suite ('Colour Fields', function() { }); }); }); - suite.skip('Validators', function() { + suite('Validators', function() { setup(function() { this.colourField = new Blockly.FieldColour('#aaaaaa'); }); diff --git a/tests/mocha/field_date_test.js b/tests/mocha/field_date_test.js index cc95b36ba..b22177d37 100644 --- a/tests/mocha/field_date_test.js +++ b/tests/mocha/field_date_test.js @@ -42,7 +42,7 @@ suite ('Date Fields', function() { var dateField = new Blockly.FieldDate(undefined); assertValueDefault(dateField); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { var dateField = new Blockly.FieldDate('bad'); assertValueDefault(dateField); }); @@ -50,11 +50,11 @@ suite ('Date Fields', function() { var dateField = new Blockly.FieldDate('2020-02-20'); assertValue(dateField, '2020-02-20'); }); - test.skip('Invalid Date - Month(2020-13-20)', function() { + test('Invalid Date - Month(2020-13-20)', function() { var dateField = new Blockly.FieldDate('2020-13-20'); assertValueDefault(dateField); }); - test.skip('Invalid Date - Day(2020-02-32)', function() { + test('Invalid Date - Day(2020-02-32)', function() { var dateField = new Blockly.FieldDate('2020-02-32'); assertValueDefault(dateField); }); @@ -72,7 +72,7 @@ suite ('Date Fields', function() { var dateField = Blockly.FieldDate.fromJson({ date: undefined }); assertValueDefault(dateField); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { var dateField = Blockly.FieldDate.fromJson({ date: 'bad' }); assertValueDefault(dateField); }); @@ -80,11 +80,11 @@ suite ('Date Fields', function() { var dateField = Blockly.FieldDate.fromJson({ date: '2020-02-20' }); assertValue(dateField, '2020-02-20'); }); - test.skip('Invalid Date - Month(2020-13-20)', function() { + test('Invalid Date - Month(2020-13-20)', function() { var dateField = Blockly.FieldDate.fromJson({ date: '2020-13-20' }); assertValueDefault(dateField); }); - test.skip('Invalid Date - Day(2020-02-32)', function() { + test('Invalid Date - Day(2020-02-32)', function() { var dateField = Blockly.FieldDate.fromJson({ date: '2020-02-32' }); assertValueDefault(dateField); }); @@ -94,23 +94,23 @@ suite ('Date Fields', function() { setup(function() { this.dateField = new Blockly.FieldDate(); }); - test.skip('Null', function() { + test('Null', function() { this.dateField.setValue(null); assertValueDefault(this.dateField); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.dateField.setValue(undefined); assertValueDefault(this.dateField); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { this.dateField.setValue('bad'); assertValueDefault(this.dateField); }); - test.skip('Invalid Date - Month(2020-13-20)', function() { + test('Invalid Date - Month(2020-13-20)', function() { this.dateField.setValue('2020-13-20'); assertValueDefault(this.dateField); }); - test.skip('Invalid Date - Day(2020-02-32)', function() { + test('Invalid Date - Day(2020-02-32)', function() { this.dateField.setValue('2020-02-32'); assertValueDefault(this.dateField); }); @@ -123,23 +123,23 @@ suite ('Date Fields', function() { setup(function() { this.dateField = new Blockly.FieldDate('2020-02-20'); }); - test.skip('Null', function() { + test('Null', function() { this.dateField.setValue(null); assertValue(this.dateField, '2020-02-20'); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.dateField.setValue(undefined); assertValue(this.dateField, '2020-02-20'); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { this.dateField.setValue('bad'); assertValue(this.dateField, '2020-02-20'); }); - test.skip('Invalid Date - Month(2020-13-20)', function() { + test('Invalid Date - Month(2020-13-20)', function() { this.dateField.setValue('2020-13-20'); assertValue(this.dateField, '2020-02-20'); }); - test.skip('Invalid Date - Day(2020-02-32)', function() { + test('Invalid Date - Day(2020-02-32)', function() { this.dateField.setValue('2020-02-32'); assertValue(this.dateField, '2020-02-20'); }); @@ -149,7 +149,7 @@ suite ('Date Fields', function() { }); }); }); - suite.skip('Validators', function() { + suite('Validators', function() { setup(function() { this.dateField = new Blockly.FieldDate('2020-02-20'); }); diff --git a/tests/mocha/field_dropdown_test.js b/tests/mocha/field_dropdown_test.js index 23c5f8016..f8c54d801 100644 --- a/tests/mocha/field_dropdown_test.js +++ b/tests/mocha/field_dropdown_test.js @@ -148,11 +148,11 @@ suite ('Dropdown Fields', function() { this.dropdownField.setValue(null); assertValue(this.dropdownField, 'A', 'a'); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.dropdownField.setValue(undefined); assertValue(this.dropdownField, 'A', 'a'); }); - test.skip('Invalid ID', function() { + test('Invalid ID', function() { this.dropdownField.setValue('bad'); assertValue(this.dropdownField, 'A', 'a'); }); @@ -161,7 +161,7 @@ suite ('Dropdown Fields', function() { assertValue(this.dropdownField, 'B', 'b'); }); }); - suite.skip('Validators', function() { + suite('Validators', function() { setup(function() { this.dropdownField = new Blockly.FieldDropdown([ ["1a","1A"], ["1b","1B"], ["1c","1C"], diff --git a/tests/mocha/field_image_test.js b/tests/mocha/field_image_test.js index eb968882c..ca39f0c22 100644 --- a/tests/mocha/field_image_test.js +++ b/tests/mocha/field_image_test.js @@ -149,7 +149,7 @@ suite ('Image Fields', function() { this.imageField.setValue(null); assertValue(this.imageField, 'src', 'alt'); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.imageField.setValue(undefined); assertValue(this.imageField, 'src', 'alt'); }); diff --git a/tests/mocha/field_label_serializable_test.js b/tests/mocha/field_label_serializable_test.js index e59fd329c..2b508dd63 100644 --- a/tests/mocha/field_label_serializable_test.js +++ b/tests/mocha/field_label_serializable_test.js @@ -108,7 +108,7 @@ suite ('Label Serializable Fields', function() { this.labelField.setValue(null); assertValueDefault(this.labelField); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.labelField.setValue(undefined); assertValueDefault(this.labelField); }); @@ -120,7 +120,7 @@ suite ('Label Serializable Fields', function() { this.labelField.setValue(1); assertValue(this.labelField, '1'); }); - test.skip('Number (Falsy)', function() { + test('Number (Falsy)', function() { this.labelField.setValue(0); assertValue(this.labelField, '0'); }); @@ -128,7 +128,7 @@ suite ('Label Serializable Fields', function() { this.labelField.setValue(true); assertValue(this.labelField, 'true'); }); - test.skip('Boolean False', function() { + test('Boolean False', function() { this.labelField.setValue(false); assertValue(this.labelField, 'false'); }); @@ -141,7 +141,7 @@ suite ('Label Serializable Fields', function() { this.labelField.setValue(null); assertValue(this.labelField, 'value'); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.labelField.setValue(undefined); assertValue(this.labelField, 'value'); }); diff --git a/tests/mocha/field_label_test.js b/tests/mocha/field_label_test.js index 1974da55e..1f9b8d0f6 100644 --- a/tests/mocha/field_label_test.js +++ b/tests/mocha/field_label_test.js @@ -105,7 +105,7 @@ suite ('Label Fields', function() { this.labelField.setValue(null); assertValueDefault(this.labelField); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.labelField.setValue(undefined); assertValueDefault(this.labelField); }); @@ -117,7 +117,7 @@ suite ('Label Fields', function() { this.labelField.setValue(1); assertValue(this.labelField, '1'); }); - test.skip('Number (Falsy)', function() { + test('Number (Falsy)', function() { this.labelField.setValue(0); assertValue(this.labelField, '0'); }); @@ -125,7 +125,7 @@ suite ('Label Fields', function() { this.labelField.setValue(true); assertValue(this.labelField, 'true'); }); - test.skip('Boolean False', function() { + test('Boolean False', function() { this.labelField.setValue(false); assertValue(this.labelField, 'false'); }); @@ -138,7 +138,7 @@ suite ('Label Fields', function() { this.labelField.setValue(null); assertValue(this.labelField, 'value'); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.labelField.setValue(undefined); assertValue(this.labelField, 'value'); }); diff --git a/tests/mocha/field_number_test.js b/tests/mocha/field_number_test.js index f0c1d1db0..3402c6766 100644 --- a/tests/mocha/field_number_test.js +++ b/tests/mocha/field_number_test.js @@ -136,15 +136,15 @@ suite ('Number Fields', function() { this.numberField.setValue(null); assertValueDefault(this.numberField); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.numberField.setValue(undefined); assertValueDefault(this.numberField); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { this.numberField.setValue('bad'); assertValueDefault(this.numberField); }); - test.skip('NaN', function() { + test('NaN', function() { this.numberField.setValue(NaN); assertValueDefault(this.numberField); }); @@ -173,15 +173,15 @@ suite ('Number Fields', function() { this.numberField.setValue(null); assertValue(this.numberField, 1); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.numberField.setValue(undefined); assertValue(this.numberField, 1); }); - test.skip('Non-Parsable String', function() { + test('Non-Parsable String', function() { this.numberField.setValue('bad'); assertValue(this.numberField, 1); }); - test.skip('NaN', function() { + test('NaN', function() { this.numberField.setValue(NaN); assertValue(this.numberField, 1); }); @@ -228,7 +228,7 @@ suite ('Number Fields', function() { numberField.setValue(123.456); assertValue(numberField, 123); }); - test.skip('1.5', function() { + test('1.5', function() { var numberField = new Blockly.FieldNumber .fromJson({ precision: 1.5 }); numberField.setValue(123.456); @@ -295,7 +295,7 @@ suite ('Number Fields', function() { }); }); }); - suite.skip('Validators', function() { + suite('Validators', function() { setup(function() { this.numberFieldField = new Blockly.FieldNumber(1); Blockly.FieldTextInput.htmlInput_ = Object.create(null); diff --git a/tests/mocha/field_textinput_test.js b/tests/mocha/field_textinput_test.js index 2a1b72b06..64e36d6ed 100644 --- a/tests/mocha/field_textinput_test.js +++ b/tests/mocha/field_textinput_test.js @@ -107,7 +107,7 @@ suite ('Text Input Fields', function() { this.textInputField.setValue(null); assertValueDefault(this.textInputField); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.textInputField.setValue(undefined); assertValueDefault(this.textInputField); }); @@ -119,7 +119,7 @@ suite ('Text Input Fields', function() { this.textInputField.setValue(1); assertValue(this.textInputField, '1'); }); - test.skip('Number (Falsy)', function() { + test('Number (Falsy)', function() { this.textInputField.setValue(0); assertValue(this.textInputField, '0'); }); @@ -127,7 +127,7 @@ suite ('Text Input Fields', function() { this.textInputField.setValue(true); assertValue(this.textInputField, 'true'); }); - test.skip('Boolean False', function() { + test('Boolean False', function() { this.textInputField.setValue(false); assertValue(this.textInputField, 'false'); }); @@ -140,7 +140,7 @@ suite ('Text Input Fields', function() { this.textInputField.setValue(null); assertValue(this.textInputField, 'value'); }); - test.skip('Undefined', function() { + test('Undefined', function() { this.textInputField.setValue(undefined); assertValue(this.textInputField, 'value'); }); @@ -166,7 +166,7 @@ suite ('Text Input Fields', function() { }); }); }); - suite.skip('Validators', function() { + suite('Validators', function() { setup(function() { this.textInputField = new Blockly.FieldTextInput('value'); Blockly.FieldTextInput.htmlInput_ = Object.create(null); diff --git a/tests/mocha/field_variable_test.js b/tests/mocha/field_variable_test.js index 8e7026eb6..1e9322895 100644 --- a/tests/mocha/field_variable_test.js +++ b/tests/mocha/field_variable_test.js @@ -127,13 +127,13 @@ suite('Variable Fields', function() { }); }); suite('setValue', function() { - test.skip('Null', function() { + test('Null', function() { var variableField = createAndInitFieldConstructor( this.workspace, 'name1'); variableField.setValue(null); assertValue(variableField, 'name1'); }); - test.skip('Undefined', function() { + test('Undefined', function() { var variableField = createAndInitFieldConstructor( this.workspace, 'name1'); variableField.setValue(undefined); @@ -152,14 +152,14 @@ suite('Variable Fields', function() { assertEquals('id2', variableField.getValue()); chai.assert.notEqual(oldId, variableField.getValue()); }); - test.skip('Variable Does not Exist', function() { + test('Variable Does not Exist', function() { var variableField = createAndInitFieldConstructor( this.workspace, 'name1'); variableField.setValue('id1'); assertValue(variableField, 'name1'); }); }); - suite.skip('Validators', function() { + suite('Validators', function() { setup(function() { this.workspace.createVariable('name1', null, 'id1'); this.workspace.createVariable('name2', null, 'id2'); diff --git a/tests/playground.html b/tests/playground.html index 72d5dbd5d..0a0220e13 100644 --- a/tests/playground.html +++ b/tests/playground.html @@ -144,6 +144,15 @@ function start() { } function addToolboxButtonCallbacks() { + var addAllBlocksToWorkspace = function(button) { + var workspace = button.getTargetWorkspace(); + var blocks = button.workspace_.getTopBlocks(); + for(var i = 0, block; block = blocks[i]; i++) { + var xml = Blockly.Xml.textToDom(''); + xml.appendChild(Blockly.Xml.blockToDom(block)); + Blockly.Xml.appendDomToWorkspace(xml, workspace); + } + }; var randomizeLabelText = function(button) { var blocks = button.targetWorkspace_ .getBlocksByType('test_fields_label_serializable'); @@ -177,13 +186,59 @@ function addToolboxButtonCallbacks() { block.setShadow(!block.isShadow()); } }; + var toggleCollapsed = function(button) { + var blocks = button.workspace_.getAllBlocks(); + for(var i = 0, block; block = blocks[i]; i++) { + block.setCollapsed(!block.isCollapsed()); + } + }; + var setInput = function(button) { + Blockly.prompt('Input text to set.', 'ab', function(input) { + var blocks = button.getTargetWorkspace().getAllBlocks(); + for(var i = 0, block; block = blocks[i]; i++) { + if (block.getField('INPUT')) { + block.setFieldValue(input, 'INPUT'); + } + } + }) + }; + var changeImage = function(button) { + var blocks = button.workspace_.getBlocksByType('test_fields_image'); + var possible = 'abcdefghijklm'; + var image = possible.charAt(Math.floor(Math.random() * possible.length)); + var src = 'https://blockly-demo.appspot.com/static/tests/media/' + + image + '.png'; + for (var i = 0, block; block = blocks[i]; i++) { + var imageField = block.getField('IMAGE'); + imageField.setValue(src); + imageField.setText(image); + } + }; + var addVariables = function(button) { + workspace.createVariable('1a', '', '1A'); + workspace.createVariable('1b', '', '1B'); + workspace.createVariable('1c', '', '1C'); + workspace.createVariable('2a', '', '2A'); + workspace.createVariable('2b', '', '2B'); + workspace.createVariable('2c', '', '2C'); + }; + workspace.registerButtonCallback( + 'addVariables', addVariables); + workspace.registerButtonCallback( + 'changeImage', changeImage); + workspace.registerButtonCallback( + 'addAllBlocksToWorkspace', addAllBlocksToWorkspace); + workspace.registerButtonCallback( + 'setInput', setInput); workspace.registerButtonCallback( 'setRandomStyle', setRandomStyle); workspace.registerButtonCallback( 'toggleEnabled', toggleEnabled); workspace.registerButtonCallback( 'toggleShadow', toggleShadow); + workspace.registerButtonCallback( + 'toggleCollapsed', toggleCollapsed); workspace.registerButtonCallback( 'randomizeLabelText', randomizeLabelText); workspace.registerButtonCallback( @@ -1236,11 +1291,15 @@ h1 { + + + + @@ -1248,7 +1307,11 @@ h1 { + + + + @@ -1298,6 +1361,69 @@ h1 { Zalgo in text field: B̛̻̦̬̘̰͎̥̈̔͊͞ͅl̡͖̫̺̬̖̣̳̃̀́͑͑̕͟͠͝o̢̹͙̮̫͔͋̉̊̑̿̽̚c̸̹̹̜͙̹̠͋̒͑̊̇͝k̡͉̫͇̖̳͖̊͒́̆̄̎̂̔̕͜͞l̰̙̞̳̩̠͖̯̀̆̈́̿̈̓͗y̨̡̟͇̮͈̬̙̲̏̅̀͘͠ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +