mirror of
https://github.com/google/blockly.git
synced 2026-01-05 08:00:09 +01:00
453 lines
14 KiB
JavaScript
453 lines
14 KiB
JavaScript
/**
|
|
* @license
|
|
* Visual Blocks Editor
|
|
*
|
|
* Copyright 2012 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 Text input field.
|
|
* @author fraser@google.com (Neil Fraser)
|
|
*/
|
|
'use strict';
|
|
|
|
goog.provide('Blockly.FieldTextInput');
|
|
|
|
goog.require('Blockly.Events');
|
|
goog.require('Blockly.Events.BlockChange');
|
|
goog.require('Blockly.Field');
|
|
goog.require('Blockly.Msg');
|
|
goog.require('Blockly.utils');
|
|
goog.require('Blockly.utils.aria');
|
|
goog.require('Blockly.utils.Coordinate');
|
|
goog.require('Blockly.utils.dom');
|
|
goog.require('Blockly.utils.fields');
|
|
goog.require('Blockly.utils.Size');
|
|
goog.require('Blockly.utils.userAgent');
|
|
|
|
|
|
/**
|
|
* Class for an editable text field.
|
|
* @param {string=} opt_value The initial value of the field. Should cast to a
|
|
* string. Defaults to an empty string if null or undefined.
|
|
* @param {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(opt_value, opt_validator) {
|
|
opt_value = this.doClassValidation_(opt_value);
|
|
if (opt_value === null) {
|
|
opt_value = '';
|
|
}
|
|
Blockly.FieldTextInput.superClass_.constructor.call(this, opt_value,
|
|
opt_validator);
|
|
};
|
|
goog.inherits(Blockly.FieldTextInput, Blockly.Field);
|
|
|
|
/**
|
|
* Construct a FieldTextInput from a JSON arg object,
|
|
* dereferencing any string table references.
|
|
* @param {!Object} options A JSON object with options (text, class, and
|
|
* spellcheck).
|
|
* @return {!Blockly.FieldTextInput} The new field instance.
|
|
* @package
|
|
* @nocollapse
|
|
*/
|
|
Blockly.FieldTextInput.fromJson = function(options) {
|
|
var text = Blockly.utils.replaceMessageReferences(options['text']);
|
|
var field = new Blockly.FieldTextInput(text);
|
|
if (typeof options['spellcheck'] === 'boolean') {
|
|
field.setSpellcheck(options['spellcheck']);
|
|
}
|
|
return field;
|
|
};
|
|
|
|
/**
|
|
* Serializable fields are saved by the XML renderer, non-serializable fields
|
|
* are not. Editable fields should also be serializable.
|
|
* @type {boolean}
|
|
* @const
|
|
*/
|
|
Blockly.FieldTextInput.prototype.SERIALIZABLE = true;
|
|
|
|
/**
|
|
* Point size of text. Should match blocklyText's font-size in CSS.
|
|
*/
|
|
Blockly.FieldTextInput.FONTSIZE = 11;
|
|
|
|
/**
|
|
* Pixel size of input border radius. Should match blocklyText's border-radius in CSS.
|
|
*/
|
|
Blockly.FieldTextInput.BORDERRADIUS = 4;
|
|
|
|
/**
|
|
* Mouse cursor style when over the hotspot that initiates the editor.
|
|
*/
|
|
Blockly.FieldTextInput.prototype.CURSOR = 'text';
|
|
|
|
/**
|
|
* Allow browser to spellcheck this field.
|
|
* @private
|
|
*/
|
|
Blockly.FieldTextInput.prototype.spellcheck_ = true;
|
|
|
|
/**
|
|
* Ensure that the input value casts to a valid string.
|
|
* @param {string=} opt_newValue The input value.
|
|
* @return {?string} A valid string, or null if invalid.
|
|
* @protected
|
|
*/
|
|
Blockly.FieldTextInput.prototype.doClassValidation_ = function(opt_newValue) {
|
|
if (opt_newValue === null || opt_newValue === undefined) {
|
|
return null;
|
|
}
|
|
return String(opt_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_.
|
|
* @param {*} _invalidValue The input value that was determined to be invalid.
|
|
* This is not used by the text input because its display value is stored on
|
|
* the htmlInput_.
|
|
* @protected
|
|
*/
|
|
Blockly.FieldTextInput.prototype.doValueInvalid_ = function(_invalidValue) {
|
|
if (this.isBeingEdited_) {
|
|
this.isTextValid_ = false;
|
|
var oldValue = this.value_;
|
|
// Revert value when the text becomes invalid.
|
|
this.value_ = this.htmlInput_.untypedDefaultValue_;
|
|
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
|
|
Blockly.Events.fire(new Blockly.Events.BlockChange(
|
|
this.sourceBlock_, 'field', this.name, oldValue, this.value_));
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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.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;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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_) {
|
|
if (this.sourceBlock_.RTL) {
|
|
// in RTL, we need to let the browser reflow before resizing
|
|
// in order to get the correct bounding box of the borderRect
|
|
// avoiding issue #2777.
|
|
var field = this;
|
|
setTimeout(function() {
|
|
field.resizeEditor_();
|
|
}, 0);
|
|
} else {
|
|
this.resizeEditor_();
|
|
}
|
|
if (!this.isTextValid_) {
|
|
Blockly.utils.dom.addClass(this.htmlInput_, 'blocklyInvalidInput');
|
|
Blockly.utils.aria.setState(this.htmlInput_, 'invalid', true);
|
|
} else {
|
|
Blockly.utils.dom.removeClass(this.htmlInput_, 'blocklyInvalidInput');
|
|
Blockly.utils.aria.setState(this.htmlInput_, 'invalid', false);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set whether this field is spellchecked by the browser.
|
|
* @param {boolean} check True if checked.
|
|
*/
|
|
Blockly.FieldTextInput.prototype.setSpellcheck = function(check) {
|
|
this.spellcheck_ = check;
|
|
};
|
|
|
|
/**
|
|
* Show the inline free-text editor on top of the text.
|
|
* @param {boolean=} opt_quietInput True if editor should be created without
|
|
* focus. Defaults to false.
|
|
* @protected
|
|
*/
|
|
Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) {
|
|
this.workspace_ = this.sourceBlock_.workspace;
|
|
var quietInput = opt_quietInput || false;
|
|
if (!quietInput && (Blockly.utils.userAgent.MOBILE ||
|
|
Blockly.utils.userAgent.ANDROID ||
|
|
Blockly.utils.userAgent.IPAD)) {
|
|
this.showPromptEditor_();
|
|
} else {
|
|
this.showInlineEditor_(quietInput);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create and show a text input editor that is a prompt (usually a popup).
|
|
* Mobile browsers have issues with in-line textareas (focus and keyboards).
|
|
* @private
|
|
*/
|
|
Blockly.FieldTextInput.prototype.showPromptEditor_ = function() {
|
|
var fieldText = this;
|
|
Blockly.prompt(Blockly.Msg['CHANGE_VALUE_TITLE'], this.text_,
|
|
function(newValue) {
|
|
fieldText.setValue(newValue);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Create and show a text input editor that sits directly over the text input.
|
|
* @param {boolean} quietInput True if editor should be created without
|
|
* focus.
|
|
* @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_();
|
|
|
|
if (!quietInput) {
|
|
this.htmlInput_.focus();
|
|
this.htmlInput_.select();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create the text input editor widget.
|
|
* @return {!HTMLInputElement} The newly created text input editor.
|
|
* @private
|
|
*/
|
|
Blockly.FieldTextInput.prototype.widgetCreate_ = function() {
|
|
var div = Blockly.WidgetDiv.DIV;
|
|
|
|
var htmlInput = document.createElement('input');
|
|
htmlInput.className = 'blocklyHtmlInput';
|
|
htmlInput.setAttribute('spellcheck', this.spellcheck_);
|
|
var fontSize =
|
|
(Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt';
|
|
div.style.fontSize = fontSize;
|
|
htmlInput.style.fontSize = fontSize;
|
|
var borderRadius =
|
|
(Blockly.FieldTextInput.BORDERRADIUS * this.workspace_.scale) + 'px';
|
|
htmlInput.style.borderRadius = borderRadius;
|
|
div.appendChild(htmlInput);
|
|
|
|
htmlInput.value = htmlInput.defaultValue = this.value_;
|
|
htmlInput.untypedDefaultValue_ = this.value_;
|
|
htmlInput.oldValue_ = null;
|
|
// Ensure the browser reflows before resizing to avoid issue #2777.
|
|
var field = this;
|
|
setTimeout(function() {
|
|
field.resizeEditor_();
|
|
}, 0);
|
|
|
|
this.bindInputEvents_(htmlInput);
|
|
|
|
return htmlInput;
|
|
};
|
|
|
|
/**
|
|
* Close the editor, save the results, and dispose any events bound to the
|
|
* text input's editor.
|
|
* @private
|
|
*/
|
|
Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
|
|
// Finalize value.
|
|
this.isBeingEdited_ = false;
|
|
// No need to call setValue because if the widget is being closed the
|
|
// latest input text has already been validated.
|
|
if (this.value_ !== this.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.
|
|
this.text_ = String(this.value_);
|
|
this.isTextValid_ = true;
|
|
this.forceRerender();
|
|
}
|
|
// Otherwise don't rerender.
|
|
|
|
// Call onFinishEditing
|
|
// TODO: Get rid of this or make it less of a hack.
|
|
if (this.onFinishEditing_) {
|
|
this.onFinishEditing_(this.value_);
|
|
}
|
|
|
|
// Remove htmlInput events.
|
|
this.unbindInputEvents_();
|
|
|
|
// Delete style properties.
|
|
var style = Blockly.WidgetDiv.DIV.style;
|
|
style.width = 'auto';
|
|
style.height = 'auto';
|
|
style.fontSize = '';
|
|
};
|
|
|
|
/**
|
|
* Bind handlers for user input on the text input field's editor.
|
|
* @param {!HTMLInputElement} htmlInput The htmlInput to which event
|
|
* handlers will be bound.
|
|
* @private
|
|
*/
|
|
Blockly.FieldTextInput.prototype.bindInputEvents_ = function(htmlInput) {
|
|
// Trap Enter without IME and Esc to hide.
|
|
this.onKeyDownWrapper_ =
|
|
Blockly.bindEventWithChecks_(
|
|
htmlInput, 'keydown', this, this.onHtmlInputKeyDown_);
|
|
// Resize after every input change.
|
|
this.onKeyInputWrapper_ =
|
|
Blockly.bindEventWithChecks_(
|
|
htmlInput, 'input', this, this.onHtmlInputChange_);
|
|
};
|
|
|
|
/**
|
|
* Unbind handlers for user input and workspace size changes.
|
|
* @private
|
|
*/
|
|
Blockly.FieldTextInput.prototype.unbindInputEvents_ = function() {
|
|
Blockly.unbindEvent_(this.onKeyDownWrapper_);
|
|
Blockly.unbindEvent_(this.onKeyInputWrapper_);
|
|
};
|
|
|
|
/**
|
|
* Handle key down to the editor.
|
|
* @param {!Event} e Keyboard event.
|
|
* @private
|
|
*/
|
|
Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) {
|
|
var tabKey = 9, enterKey = 13, escKey = 27;
|
|
if (e.keyCode == enterKey) {
|
|
Blockly.WidgetDiv.hide();
|
|
} else if (e.keyCode == escKey) {
|
|
this.htmlInput_.value = this.htmlInput_.defaultValue;
|
|
Blockly.WidgetDiv.hide();
|
|
} else if (e.keyCode == tabKey) {
|
|
Blockly.WidgetDiv.hide();
|
|
this.sourceBlock_.tab(this, !e.shiftKey);
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle a change to the editor.
|
|
* @param {!Event} _e Keyboard event.
|
|
* @private
|
|
*/
|
|
Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(_e) {
|
|
var text = this.htmlInput_.value;
|
|
if (text !== this.htmlInput_.oldValue_) {
|
|
this.htmlInput_.oldValue_ = text;
|
|
|
|
// TODO(#2169): Once issue is fixed the setGroup functionality could be
|
|
// moved up to the Field setValue method. This would create a
|
|
// broader fix for all field types.
|
|
Blockly.Events.setGroup(true);
|
|
this.setValue(text);
|
|
// Always render the input text.
|
|
this.text_ = this.htmlInput_.value;
|
|
this.forceRerender();
|
|
Blockly.Events.setGroup(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Resize the editor to fit the text.
|
|
* @protected
|
|
*/
|
|
Blockly.FieldTextInput.prototype.resizeEditor_ = function() {
|
|
var div = Blockly.WidgetDiv.DIV;
|
|
var bBox = this.getScaledBBox_();
|
|
div.style.width = bBox.right - bBox.left + 'px';
|
|
div.style.height = bBox.bottom - bBox.top + 'px';
|
|
|
|
// In RTL mode block fields and LTR input fields the left edge moves,
|
|
// whereas the right edge is fixed. Reposition the editor.
|
|
var x = this.sourceBlock_.RTL ? bBox.right - div.offsetWidth : bBox.left;
|
|
var xy = new Blockly.utils.Coordinate(x, bBox.top);
|
|
|
|
// Shift by a few pixels to line up exactly.
|
|
xy.y += 1;
|
|
if (Blockly.utils.userAgent.GECKO && Blockly.WidgetDiv.DIV.style.top) {
|
|
// Firefox mis-reports the location of the border by a pixel
|
|
// once the WidgetDiv is moved into position.
|
|
xy.x -= 1;
|
|
xy.y -= 1;
|
|
}
|
|
if (Blockly.utils.userAgent.WEBKIT) {
|
|
xy.y -= 3;
|
|
}
|
|
div.style.left = xy.x + 'px';
|
|
div.style.top = xy.y + 'px';
|
|
};
|
|
|
|
/**
|
|
* 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. ' +
|
|
'Use Blockly.FieldNumber instead.');
|
|
if (text === null) {
|
|
return null;
|
|
}
|
|
text = String(text);
|
|
// TODO: Handle cases like 'ten', '1.203,14', etc.
|
|
// 'O' is sometimes mistaken for '0' by inexperienced users.
|
|
text = text.replace(/O/ig, '0');
|
|
// Strip out thousands separators.
|
|
text = text.replace(/,/g, '');
|
|
var n = Number(text || 0);
|
|
return isNaN(n) ? null : String(n);
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
if (n) {
|
|
n = String(Math.max(0, Math.floor(n)));
|
|
}
|
|
return n;
|
|
};
|
|
|
|
Blockly.utils.fields.register('field_input', Blockly.FieldTextInput);
|