diff --git a/core/field_angle.js b/core/field_angle.js
index f2cfe4b5e..059c17f53 100644
--- a/core/field_angle.js
+++ b/core/field_angle.js
@@ -134,6 +134,7 @@ Blockly.FieldAngle.prototype.initView = function() {
/**
* Updates the graph when the field rerenders.
* @private
+ * @override
*/
Blockly.FieldAngle.prototype.render_ = function() {
Blockly.FieldAngle.superClass_.render_.call(this);
diff --git a/core/field_textinput.js b/core/field_textinput.js
index 95db6533a..374bfee03 100644
--- a/core/field_textinput.js
+++ b/core/field_textinput.js
@@ -56,12 +56,6 @@ Blockly.FieldTextInput = function(opt_value, opt_validator) {
}
Blockly.FieldTextInput.superClass_.constructor.call(this, opt_value,
opt_validator);
- /**
- * A cache of the last value in the html input.
- * @type {*}
- * @private
- */
- this.htmlInputValue_ = null;
};
goog.inherits(Blockly.FieldTextInput, Blockly.Field);
@@ -237,10 +231,10 @@ Blockly.FieldTextInput.prototype.showPromptEditor_ = function() {
* @private
*/
Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) {
- this.isBeingEdited_ = true;
Blockly.WidgetDiv.show(
this, this.sourceBlock_.RTL, this.widgetDispose_.bind(this));
this.htmlInput_ = this.widgetCreate_();
+ this.isBeingEdited_ = true;
if (!quietInput) {
this.htmlInput_.focus();
@@ -268,7 +262,7 @@ Blockly.FieldTextInput.prototype.widgetCreate_ = function() {
htmlInput.style.borderRadius = borderRadius;
div.appendChild(htmlInput);
- htmlInput.value = htmlInput.defaultValue = String(this.value_);
+ htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
htmlInput.untypedDefaultValue_ = this.value_;
htmlInput.oldValue_ = null;
if (Blockly.utils.userAgent.GECKO) {
@@ -292,19 +286,11 @@ Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
// Finalize value.
this.isBeingEdited_ = false;
this.isTextValid_ = true;
- // No need to call setValue because if the widget is being closed the
- // latest input text has already been validated.
- if (this.value_ != this.htmlInputValue_) {
- // 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.
- // There are two scenarios where that is the case:
- // - The text in the input was invalid.
- // - The text in the input is different to that returned by a validator.
- // Re-render to fix that.
- this.htmlInputValue_ = null;
- this.forceRerender();
- }
- // Otherwise don't rerender.
+
+ // Always re-render when the we close the editor as value
+ // set on the field's node may be inconsistent with the field's
+ // internal value.
+ this.forceRerender();
// Call onFinishEditing
// TODO: Get rid of this or make it less of a hack.
@@ -384,7 +370,9 @@ Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(_e) {
// moved up to the Field setValue method. This would create a
// broader fix for all field types.
Blockly.Events.setGroup(true);
- this.setEditorValue_(text);
+ var value = this.getValueFromEditorText_(text);
+ this.setValue(value);
+ this.forceRerender();
Blockly.Events.setGroup(false);
}
};
@@ -397,14 +385,15 @@ Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(_e) {
* @protected
*/
Blockly.FieldTextInput.prototype.setEditorValue_ = function(newValue) {
- this.setValue(newValue);
+ this.isDirty_ = true;
if (this.isBeingEdited_) {
- this.htmlInput_.value = newValue;
- // This cache is stored in order to determine if we must re-render when
- // disposing of the widget div.
- this.htmlInputValue_ = newValue;
- this.forceRerender();
+ // In the case this method is passed an invalid value, we still
+ // pass it through the transformation method `getEditorText` to deal
+ // with. Otherwise, the internal field's state will be inconsistent
+ // with what's shown to the user.
+ this.htmlInput_.value = this.getEditorText_(newValue);
}
+ this.setValue(newValue);
};
/**
@@ -460,7 +449,7 @@ Blockly.FieldTextInput.numberValidator = function(text) {
};
/**
- * Ensure that only a nonnegative integer may be entered.
+ * Ensure that only a non-negative integer may be entered.
* @param {string} text The user's text.
* @return {?string} A string representing a valid int, or null if invalid.
* @deprecated
@@ -474,7 +463,7 @@ Blockly.FieldTextInput.nonnegativeIntegerValidator = function(text) {
};
/**
- * Use the `getText_` developer hook to override the field's text represenation.
+ * Use the `getText_` developer hook to override the field's text representation.
* When we're currently editing, return the current html value instead.
* Otherwise, return null which tells the field to use the default behaviour
* (which is a string cast of the field's value).
@@ -490,4 +479,31 @@ Blockly.FieldTextInput.prototype.getText_ = function() {
return null;
};
+/**
+ * Transform the provided value into a text to show in the html input.
+ * Override this method if the field's html input representation is different
+ * than the field's value. This should be coupled with an override of
+ * `getValueFromEditorText_`.
+ * @param {*} value The value stored in this field.
+ * @returns {string} The text to show on the html input.
+ * @protected
+ */
+Blockly.FieldTextInput.prototype.getEditorText_ = function(value) {
+ return String(value);
+};
+
+/**
+ * Transform the text received from the html input into a value to store
+ * in this field.
+ * Override this method if the field's html input representation is different
+ * than the field's value. This should be coupled with an override of
+ * `getEditorText_`.
+ * @param {string} text Text received from the html input.
+ * @returns {*} The value to store.
+ * @protected
+ */
+Blockly.FieldTextInput.prototype.getValueFromEditorText_ = function(text) {
+ return text;
+};
+
Blockly.fieldRegistry.register('field_input', Blockly.FieldTextInput);
diff --git a/demos/custom-fields/icon.png b/demos/custom-fields/icon.png
index 3a7314ac9..e69de29bb 100644
Binary files a/demos/custom-fields/icon.png and b/demos/custom-fields/icon.png differ
diff --git a/demos/custom-fields/index.html b/demos/custom-fields/index.html
index b074fd71a..467b7cd7a 100644
--- a/demos/custom-fields/index.html
+++ b/demos/custom-fields/index.html
@@ -1,153 +1,54 @@
-
-
+
diff --git a/demos/custom-fields/pitch/blocks.js b/demos/custom-fields/pitch/blocks.js
new file mode 100644
index 000000000..bfa300424
--- /dev/null
+++ b/demos/custom-fields/pitch/blocks.js
@@ -0,0 +1,33 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2019 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 Pitch field demo blocks.
+ * @author samelh@gmail.com (Sam El-Husseini)
+ */
+
+Blockly.Blocks['test_pitch_field'] = {
+ init: function() {
+ this.appendDummyInput()
+ .appendField('pitch')
+ .appendField(new Blockly.FieldPitch('7'), 'PITCH');
+ this.setStyle('loop_blocks');
+ }
+};
diff --git a/demos/custom-fields/pitch/field_pitch.js b/demos/custom-fields/pitch/field_pitch.js
new file mode 100644
index 000000000..3a50237ac
--- /dev/null
+++ b/demos/custom-fields/pitch/field_pitch.js
@@ -0,0 +1,231 @@
+/**
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 Google Inc.
+ * https://github.com/google/blockly-games
+ *
+ * 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 Music pitch input field. Borrowed from Blockly Games.
+ * @author fraser@google.com (Neil Fraser)
+ * @author samelh@google.com (Sam El-Husseini)
+ */
+'use strict';
+
+goog.provide('Blockly.FieldPitch');
+
+goog.require('Blockly.FieldTextInput');
+goog.require('Blockly.utils.math');
+
+
+/**
+ * Class for an editable pitch field.
+ * @param {string} text The initial content of the field.
+ * @extends {Blockly.FieldTextInput}
+ * @constructor
+ */
+Blockly.FieldPitch = function(text) {
+ Blockly.FieldPitch.superClass_.constructor.call(this, text);
+};
+goog.inherits(Blockly.FieldPitch, Blockly.FieldTextInput);
+
+/**
+ * Construct a FieldPitch from a JSON arg object.
+ * @param {!Object} options A JSON object with options (pitch).
+ * @return {!Blockly.FieldPitch} The new field instance.
+ * @package
+ * @nocollapse
+ */
+Blockly.FieldPitch.fromJson = function(options) {
+ return new Blockly.FieldPitch(options['pitch']);
+};
+
+/**
+ * All notes available for the picker.
+ */
+Blockly.FieldPitch.NOTES = 'C3 D3 E3 F3 G3 A3 B3 C4 D4 E4 F4 G4 A4'.split(/ /);
+
+/**
+ * Show the inline free-text editor on top of the text and the note picker.
+ * @private
+ */
+Blockly.FieldPitch.prototype.showEditor_ = function() {
+ Blockly.FieldPitch.superClass_.showEditor_.call(this);
+
+ var div = Blockly.WidgetDiv.DIV;
+ if (!div.firstChild) {
+ // Mobile interface uses Blockly.prompt.
+ return;
+ }
+ // Build the DOM.
+ var editor = this.dropdownCreate_();
+ Blockly.DropDownDiv.getContentDiv().appendChild(editor);
+
+ var border = this.sourceBlock_.getColourBorder();
+ border = border.colourBorder || border.colourLight;
+ Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), border);
+
+ Blockly.DropDownDiv.showPositionedByField(
+ this, this.dropdownDispose_.bind(this));
+
+ // The note picker is different from other fields in that it updates on
+ // mousemove even if it's not in the middle of a drag. In future we may
+ // change this behaviour. For now, using bindEvent_ instead of
+ // bindEventWithChecks_ allows it to work without a mousedown/touchstart.
+ this.clickWrapper_ =
+ Blockly.bindEvent_(this.imageElement_, 'click', this,
+ this.hide_);
+ this.moveWrapper_ =
+ Blockly.bindEvent_(this.imageElement_, 'mousemove', this,
+ this.onMouseMove);
+
+ this.updateGraph_();
+};
+
+/**
+ * Create the pitch editor.
+ * @return {!Element} The newly created pitch picker.
+ * @private
+ */
+Blockly.FieldPitch.prototype.dropdownCreate_ = function() {
+ this.imageElement_ = document.createElement('div');
+ this.imageElement_.id = 'notePicker';
+
+ return this.imageElement_;
+};
+
+/**
+ * Dispose of events belonging to the pitch editor.
+ * @private
+ */
+Blockly.FieldPitch.prototype.dropdownDispose_ = function() {
+ Blockly.unbindEvent_(this.clickWrapper_);
+ Blockly.unbindEvent_(this.moveWrapper_);
+};
+
+/**
+ * Hide the editor.
+ * @private
+ */
+Blockly.FieldPitch.prototype.hide_ = function() {
+ Blockly.WidgetDiv.hide();
+ Blockly.DropDownDiv.hideWithoutAnimation();
+};
+
+/**
+ * Set the note to match the mouse's position.
+ * @param {!Event} e Mouse move event.
+ */
+Blockly.FieldPitch.prototype.onMouseMove = function(e) {
+ var bBox = this.imageElement_.getBoundingClientRect();
+ var dy = e.clientY - bBox.top;
+ var note = Blockly.utils.math.clamp(Math.round(13.5 - dy / 7.5), 0, 12);
+ this.imageElement_.style.backgroundPosition = (-note * 37) + 'px 0';
+ this.setEditorValue_(note);
+};
+
+/**
+ * Convert the machine-readable value (0-12) to human-readable
+ * text (C3-A4).
+ * @param {number|string} value The provided value.
+ * @return {string} The respective note.
+ */
+Blockly.FieldPitch.prototype.valueToNote = function(value) {
+ var note = Blockly.FieldPitch.NOTES[Number(value)];
+ return note;
+};
+
+/**
+ * Convert the human-readable text (C3-A4) to machine-readable
+ * value (0-12).
+ * @param {string} text The provided note.
+ * @return {number} The respective value.
+ */
+Blockly.FieldPitch.prototype.noteToValue = function(text) {
+ var i = Blockly.FieldPitch.NOTES.indexOf(text);
+ return i;
+};
+
+/**
+ * Get the text to be displayed on the field node.
+ * @return {?string} The HTML value if we're editing, otherwise null. Null means
+ * the super class will handle it, likely a string cast of value.
+ * @protected
+ */
+Blockly.FieldPitch.prototype.getText_ = function() {
+ if (this.isBeingEdited_) {
+ return Blockly.FieldPitch.superClass_.getText_.call(this);
+ }
+ return this.valueToNote(this.getValue()) || null;
+};
+
+/**
+ * Transform the provided value into a text to show in the HTML input.
+ * @param {*} value The value stored in this field.
+ * @returns {string} The text to show on the HTML input.
+ */
+Blockly.FieldPitch.prototype.getEditorText_ = function(value) {
+ return this.valueToNote(value);
+};
+
+/**
+ * Transform the text received from the HTML input (note) into a value
+ * to store in this field.
+ * @param {string} text Text received from the HTML input.
+ * @returns {*} The value to store.
+ */
+Blockly.FieldPitch.prototype.getValueFromEditorText_ = function(text) {
+ return this.noteToValue(text);
+};
+
+/**
+ * Updates the graph when the field rerenders.
+ * @private
+ * @override
+ */
+Blockly.FieldPitch.prototype.render_ = function() {
+ Blockly.FieldPitch.superClass_.render_.call(this);
+ this.updateGraph_();
+};
+
+/**
+ * Redraw the note picker with the current note.
+ * @private
+ */
+Blockly.FieldPitch.prototype.updateGraph_ = function() {
+ if (!this.imageElement_) {
+ return;
+ }
+ var i = this.getValue();
+ this.imageElement_.style.backgroundPosition = (-i * 37) + 'px 0';
+};
+
+/**
+ * Ensure that only a valid value may be entered.
+ * @param {*} opt_newValue The input value.
+ * @return {*} A valid value, or null if invalid.
+ */
+Blockly.FieldPitch.prototype.doClassValidation_ = function(opt_newValue) {
+ if (opt_newValue === null || opt_newValue === undefined) {
+ return null;
+ }
+ var note = this.valueToNote(opt_newValue);
+ if (note) {
+ return opt_newValue;
+ }
+ return null;
+};
+
+Blockly.fieldRegistry.register('field_pitch', Blockly.FieldPitch);
diff --git a/demos/custom-fields/pitch/index.html b/demos/custom-fields/pitch/index.html
new file mode 100644
index 000000000..4dd557747
--- /dev/null
+++ b/demos/custom-fields/pitch/index.html
@@ -0,0 +1,103 @@
+
+
+
+
+ Blockly Demo: Custom Pitch Field
+
+
+
+
+
+
+
+
This is a demo of creating custom block fields. In this case the field
+ is used to select a note pitch.
+
+
+
All of the custom field implementation is in
+ demos/custom-fields/pitch/field_pitch.js, including comments on each required
+ function.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/custom-fields/pitch/media/notes.png b/demos/custom-fields/pitch/media/notes.png
new file mode 100644
index 000000000..b9a57b59e
Binary files /dev/null and b/demos/custom-fields/pitch/media/notes.png differ
diff --git a/demos/custom-fields/pitch/pitch.css b/demos/custom-fields/pitch/pitch.css
new file mode 100644
index 000000000..249a3c605
--- /dev/null
+++ b/demos/custom-fields/pitch/pitch.css
@@ -0,0 +1,27 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2019 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.
+ */
+
+
+#notePicker {
+ background-image: url(media/notes.png);
+ border: 1px solid #ccc;
+ height: 109px;
+ width: 46px;
+}
\ No newline at end of file
diff --git a/demos/custom-fields/blocks.js b/demos/custom-fields/turtle/blocks.js
similarity index 100%
rename from demos/custom-fields/blocks.js
rename to demos/custom-fields/turtle/blocks.js
diff --git a/demos/custom-fields/field_turtle.js b/demos/custom-fields/turtle/field_turtle.js
similarity index 100%
rename from demos/custom-fields/field_turtle.js
rename to demos/custom-fields/turtle/field_turtle.js
diff --git a/demos/custom-fields/turtle/icon.png b/demos/custom-fields/turtle/icon.png
new file mode 100644
index 000000000..3a7314ac9
Binary files /dev/null and b/demos/custom-fields/turtle/icon.png differ
diff --git a/demos/custom-fields/turtle/index.html b/demos/custom-fields/turtle/index.html
new file mode 100644
index 000000000..bcfc8a18f
--- /dev/null
+++ b/demos/custom-fields/turtle/index.html
@@ -0,0 +1,155 @@
+
+
+
+
+ Blockly Demo: Custom Turtle Field
+
+
+
+
+
+
+
+