scratch-blocks/core/field_textinput.js

415 lines
14 KiB
JavaScript
Raw Normal View History

/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 Google Inc.
2014-10-07 13:09:55 -07:00
* 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.BlockSvg.render');
goog.require('Blockly.Colours');
goog.require('Blockly.Field');
goog.require('Blockly.Msg');
goog.require('Blockly.utils');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.userAgent');
/**
* Class for an editable text field.
* @param {string} text The initial content of the field.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns either the accepted text, a replacement
* text, or null to abort the change.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldTextInput = function(text, opt_validator) {
Blockly.FieldTextInput.superClass_.constructor.call(this, text,
opt_validator);
};
goog.inherits(Blockly.FieldTextInput, Blockly.Field);
/**
* Length of animations in seconds.
*/
Blockly.FieldTextInput.ANIMATION_TIME = 0.25;
/**
* Padding to use for text measurement for the field during editing, in px.
*/
Blockly.FieldTextInput.TEXT_MEASURE_PADDING_MAGIC = 45;
/**
* 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;
/**
2014-09-08 14:26:52 -07:00
* Close the input widget if this input is being deleted.
*/
Blockly.FieldTextInput.prototype.dispose = function() {
2014-09-08 14:26:52 -07:00
Blockly.WidgetDiv.hideIfOwner(this);
Blockly.FieldTextInput.superClass_.dispose.call(this);
};
/**
* Set the text in this field.
* @param {?string} text New text.
* @override
*/
Blockly.FieldTextInput.prototype.setValue = function(text) {
if (text === null) {
2016-01-15 15:36:06 -08:00
return; // No change if null.
}
if (this.sourceBlock_ && this.validator_) {
var validated = this.validator_(text);
// If the new text is invalid, validation returns null.
// In this case we still want to display the illegal result.
if (validated !== null && validated !== undefined) {
text = validated;
}
}
Blockly.Field.prototype.setValue.call(this, text);
};
/**
* 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.
2014-09-08 14:26:52 -07:00
* @param {boolean=} opt_quietInput True if editor should be created without
* focus. Defaults to false.
* @param {boolean=} opt_readOnly True if editor should be created with HTML
* input set to read-only, to prevent virtual keyboards.
* @private
*/
Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput, opt_readOnly) {
this.workspace_ = this.sourceBlock_.workspace;
2014-09-08 14:26:52 -07:00
var quietInput = opt_quietInput || false;
var readOnly = opt_readOnly || false;
Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL,
this.widgetDispose_(), this.widgetDisposeAnimationFinished_(),
Blockly.FieldTextInput.ANIMATION_TIME);
var div = Blockly.WidgetDiv.DIV;
// Apply text-input-specific fixed CSS
div.className += ' fieldTextInput';
// Create the input.
var htmlInput = goog.dom.createDom('input', 'blocklyHtmlInput');
htmlInput.setAttribute('spellcheck', this.spellcheck_);
if (readOnly) {
htmlInput.setAttribute('readonly', 'true');
}
/** @type {!HTMLInputElement} */
Blockly.FieldTextInput.htmlInput_ = htmlInput;
div.appendChild(htmlInput);
htmlInput.value = htmlInput.defaultValue = this.text_;
htmlInput.oldValue_ = null;
this.validate_();
this.resizeEditor_();
2014-09-08 14:26:52 -07:00
if (!quietInput) {
htmlInput.focus();
htmlInput.select();
// For iOS only
htmlInput.setSelectionRange(0, 99999);
2014-09-08 14:26:52 -07:00
}
// Bind to keydown -- trap Enter without IME and Esc to hide.
htmlInput.onKeyDownWrapper_ =
Blockly.bindEvent_(htmlInput, 'keydown', this, this.onHtmlInputKeyDown_);
// Bind to keyup -- trap Enter; resize after every keystroke.
htmlInput.onKeyUpWrapper_ =
Blockly.bindEvent_(htmlInput, 'keyup', this, this.onHtmlInputChange_);
// Bind to keyPress -- repeatedly resize when holding down a key.
htmlInput.onKeyPressWrapper_ =
Blockly.bindEvent_(htmlInput, 'keypress', this, this.onHtmlInputChange_);
htmlInput.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this);
this.workspace_.addChangeListener(htmlInput.onWorkspaceChangeWrapper_);
// Add animation transition properties
var transitionProperties = 'box-shadow ' + Blockly.FieldTextInput.ANIMATION_TIME + 's';
if (Blockly.BlockSvg.FIELD_TEXTINPUT_ANIMATE_POSITIONING) {
div.style.transition += ',padding ' + Blockly.FieldTextInput.ANIMATION_TIME + 's,' +
'width ' + Blockly.FieldTextInput.ANIMATION_TIME + 's,' +
'height ' + Blockly.FieldTextInput.ANIMATION_TIME + 's,' +
'margin-left ' + Blockly.FieldTextInput.ANIMATION_TIME + 's';
}
div.style.transition = transitionProperties;
htmlInput.style.transition = 'font-size ' + Blockly.FieldTextInput.ANIMATION_TIME + 's';
// The animated properties themselves
htmlInput.style.fontSize = Blockly.BlockSvg.FIELD_TEXTINPUT_FONTSIZE_FINAL + 'pt';
div.style.boxShadow = '0px 0px 0px 4px ' + Blockly.Colours.fieldShadow;
};
/**
* Handle key down to the editor.
* @param {!Event} e Keyboard event.
* @private
*/
Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) {
var htmlInput = Blockly.FieldTextInput.htmlInput_;
2015-10-14 16:23:23 -07:00
var tabKey = 9, enterKey = 13, escKey = 27;
if (e.keyCode == enterKey) {
Blockly.WidgetDiv.hide();
} else if (e.keyCode == escKey) {
htmlInput.value = htmlInput.defaultValue;
Blockly.WidgetDiv.hide();
2015-10-14 16:23:23 -07:00
} 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 htmlInput = Blockly.FieldTextInput.htmlInput_;
2016-01-15 15:36:06 -08:00
// Update source block.
var text = htmlInput.value;
if (text !== htmlInput.oldValue_) {
htmlInput.oldValue_ = text;
this.setValue(text);
this.validate_();
} else if (goog.userAgent.WEBKIT) {
// Cursor key. Render the source block to show the caret moving.
// Chrome only (version 26, OS X).
this.sourceBlock_.render();
}
this.resizeEditor_();
};
/**
* Check to see if the contents of the editor validates.
* Style the editor accordingly.
* @private
*/
Blockly.FieldTextInput.prototype.validate_ = function() {
var valid = true;
goog.asserts.assertObject(Blockly.FieldTextInput.htmlInput_);
var htmlInput = Blockly.FieldTextInput.htmlInput_;
if (this.sourceBlock_ && this.validator_) {
valid = this.validator_(htmlInput.value);
}
if (valid === null) {
Blockly.addClass_(htmlInput, 'blocklyInvalidInput');
} else {
Blockly.removeClass_(htmlInput, 'blocklyInvalidInput');
}
};
/**
* Resize the editor and the underlying block to fit the text.
* @private
*/
Blockly.FieldTextInput.prototype.resizeEditor_ = function() {
var scale = this.sourceBlock_.workspace.scale;
var div = Blockly.WidgetDiv.DIV;
var width;
if (Blockly.BlockSvg.FIELD_TEXTINPUT_EXPAND_PAST_TRUNCATION) {
// Resize the box based on the measured width of the text, pre-truncation
var textWidth = Blockly.measureText(
Blockly.FieldTextInput.htmlInput_.style.fontSize,
Blockly.FieldTextInput.htmlInput_.style.fontFamily,
Blockly.FieldTextInput.htmlInput_.style.fontWeight,
Blockly.FieldTextInput.htmlInput_.value
);
// Size drawn in the canvas needs padding and scaling
textWidth += Blockly.FieldTextInput.TEXT_MEASURE_PADDING_MAGIC;
textWidth *= scale;
width = textWidth;
} else {
// Set width to (truncated) block size.
width = this.sourceBlock_.getHeightWidth().width * scale;
}
// The width must be at least FIELD_WIDTH and at most FIELD_WIDTH_MAX_EDIT
width = Math.max(width, Blockly.BlockSvg.FIELD_WIDTH_MIN_EDIT * scale);
width = Math.min(width, Blockly.BlockSvg.FIELD_WIDTH_MAX_EDIT * scale);
// Add 1px to width and height to account for border (pre-scale)
div.style.width = (width / scale + 1) + 'px';
div.style.height = (Blockly.BlockSvg.FIELD_HEIGHT_MAX_EDIT + 1) + 'px';
div.style.transform = 'scale(' + scale + ')';
// Use margin-left to animate repositioning of the box (value is unscaled).
// This is the difference between the default position and the positioning
// after growing the box.
var fieldWidth = this.sourceBlock_.getHeightWidth().width;
var initialWidth = fieldWidth * scale;
var finalWidth = width;
div.style.marginLeft = -0.5 * (finalWidth - initialWidth) + 'px';
// Add 0.5px to account for slight difference between SVG and CSS border
var borderRadius = this.getBorderRadius() + 0.5;
div.style.borderRadius = borderRadius + 'px';
// Pull stroke colour from the existing shadow block
var strokeColour = this.sourceBlock_.getColourTertiary();
div.style.borderColor = strokeColour;
2015-04-28 13:51:25 -07:00
var xy = this.getAbsoluteXY_();
// Account for border width, post-scale
xy.x -= scale / 2;
xy.y -= scale / 2;
2013-12-20 16:25:26 -08:00
// In RTL mode block fields and LTR input fields the left edge moves,
// whereas the right edge is fixed. Reposition the editor.
2015-04-28 13:51:25 -07:00
if (this.sourceBlock_.RTL) {
2016-04-05 19:41:04 -04:00
xy.x += width;
xy.x -= div.offsetWidth * scale;
}
// Shift by a few pixels to line up exactly.
xy.y += 1 * scale;
2015-08-21 14:13:07 -07:00
if (goog.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 += 2 * scale;
xy.y += 1 * scale;
2015-08-21 14:13:07 -07:00
}
if (goog.userAgent.WEBKIT) {
xy.y -= 1 * scale;
}
2016-04-05 19:43:41 -04:00
// Finally, set the actual style
div.style.left = xy.x + 'px';
div.style.top = xy.y + 'px';
};
/**
* Border radius for drawing this field, called when rendering the owning shadow block.
* @return {Number} Border radius in px.
*/
Blockly.FieldTextInput.prototype.getBorderRadius = function() {
return Blockly.BlockSvg.TEXT_FIELD_CORNER_RADIUS;
};
/**
* Close the editor, save the results, and start animating the disposal of elements.
* @return {!Function} Closure to call on destruction of the WidgetDiv.
* @private
*/
2014-09-08 14:26:52 -07:00
Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
var thisField = this;
return function() {
var div = Blockly.WidgetDiv.DIV;
var htmlInput = Blockly.FieldTextInput.htmlInput_;
// Save the edit (if it validates).
2014-09-08 14:26:52 -07:00
var text = htmlInput.value;
if (thisField.sourceBlock_ && thisField.validator_) {
var text1 = thisField.validator_(text);
2015-06-11 18:06:44 -07:00
if (text1 === null) {
// Invalid edit.
text = htmlInput.defaultValue;
2015-06-11 18:06:44 -07:00
} else if (text1 !== undefined) {
// Validation function has changed the text.
2015-06-11 18:06:44 -07:00
text = text1;
}
}
thisField.setValue(text);
2014-09-08 14:26:52 -07:00
thisField.sourceBlock_.rendered && thisField.sourceBlock_.render();
Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_);
Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);
thisField.workspace_.removeChangeListener(
htmlInput.onWorkspaceChangeWrapper_);
// Animation of disposal
htmlInput.style.fontSize = Blockly.BlockSvg.FIELD_TEXTINPUT_FONTSIZE_INITIAL + 'pt';
div.style.boxShadow = '';
// Resize to actual size of final source block.
if (thisField.sourceBlock_) {
var size = thisField.sourceBlock_.getHeightWidth();
div.style.width = (size.width + 1) + 'px';
div.style.height = (size.height + 1) + 'px';
}
div.style.marginLeft = 0;
};
};
/**
* Final disposal of the text field's elements and properties.
* @return {!Function} Closure to call on finish animation of the WidgetDiv.
* @private
*/
Blockly.FieldTextInput.prototype.widgetDisposeAnimationFinished_ = function() {
return function() {
2015-08-19 17:21:05 -07:00
// Delete style properties.
var style = Blockly.WidgetDiv.DIV.style;
style.width = 'auto';
style.height = 'auto';
style.fontSize = '';
// Reset class
Blockly.WidgetDiv.DIV.className = 'blocklyWidgetDiv';
// Remove all styles
Blockly.WidgetDiv.DIV.removeAttribute('style');
Blockly.FieldTextInput.htmlInput_.style.transition = '';
Blockly.FieldTextInput.htmlInput_ = null;
};
};
/**
* 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.
*/
Blockly.FieldTextInput.numberValidator = function(text) {
2015-01-22 15:58:10 -08:00
if (text === null) {
2014-12-07 19:19:35 -06:00
return null;
}
text = String(text);
// TODO: Handle cases like 'ten', '1.203,14', etc.
// 'O' is sometimes mistaken for '0' by inexperienced users.
text = text.replace(/O/ig, '0');
// Strip out thousands separators.
text = text.replace(/,/g, '');
var n = parseFloat(text || 0);
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.
*/
Blockly.FieldTextInput.nonnegativeIntegerValidator = function(text) {
var n = Blockly.FieldTextInput.numberValidator(text);
if (n) {
n = String(Math.max(0, Math.floor(n)));
}
return n;
};