2013-10-30 14:46:03 -07:00
|
|
|
/**
|
2014-01-28 03:00:09 -08:00
|
|
|
* @license
|
2013-10-30 14:46:03 -07:00
|
|
|
* Visual Blocks Editor
|
|
|
|
*
|
|
|
|
* Copyright 2012 Google Inc.
|
2014-10-07 13:09:55 -07:00
|
|
|
* https://developers.google.com/blockly/
|
2013-10-30 14:46:03 -07:00
|
|
|
*
|
|
|
|
* 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');
|
|
|
|
|
2016-02-24 10:19:59 -08:00
|
|
|
goog.require('Blockly.BlockSvg.render');
|
2013-10-30 14:46:03 -07:00
|
|
|
goog.require('Blockly.Field');
|
|
|
|
goog.require('Blockly.Msg');
|
|
|
|
goog.require('goog.asserts');
|
2015-02-06 15:27:25 -08:00
|
|
|
goog.require('goog.dom');
|
2013-10-30 14:46:03 -07:00
|
|
|
goog.require('goog.userAgent');
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Class for an editable text field.
|
|
|
|
* @param {string} text The initial content of the field.
|
2016-02-10 14:33:13 -08:00
|
|
|
* @param {Function=} opt_validator An optional function that is called
|
2013-10-30 14:46:03 -07:00
|
|
|
* to validate any constraints on what the user entered. Takes the new
|
2013-12-14 03:00:02 -08:00
|
|
|
* text as an argument and returns either the accepted text, a replacement
|
|
|
|
* text, or null to abort the change.
|
2013-10-30 14:46:03 -07:00
|
|
|
* @extends {Blockly.Field}
|
|
|
|
* @constructor
|
|
|
|
*/
|
2016-02-10 14:33:13 -08:00
|
|
|
Blockly.FieldTextInput = function(text, opt_validator) {
|
|
|
|
Blockly.FieldTextInput.superClass_.constructor.call(this, text,
|
|
|
|
opt_validator);
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
goog.inherits(Blockly.FieldTextInput, Blockly.Field);
|
|
|
|
|
2015-08-19 17:21:05 -07:00
|
|
|
/**
|
|
|
|
* Point size of text. Should match blocklyText's font-size in CSS.
|
|
|
|
*/
|
|
|
|
Blockly.FieldTextInput.FONTSIZE = 11;
|
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
/**
|
|
|
|
* Mouse cursor style when over the hotspot that initiates the editor.
|
|
|
|
*/
|
|
|
|
Blockly.FieldTextInput.prototype.CURSOR = 'text';
|
|
|
|
|
2015-02-25 13:35:37 -08:00
|
|
|
/**
|
|
|
|
* Allow browser to spellcheck this field.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.FieldTextInput.prototype.spellcheck_ = true;
|
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
/**
|
2014-09-08 14:26:52 -07:00
|
|
|
* Close the input widget if this input is being deleted.
|
2013-10-30 14:46:03 -07:00
|
|
|
*/
|
|
|
|
Blockly.FieldTextInput.prototype.dispose = function() {
|
2014-09-08 14:26:52 -07:00
|
|
|
Blockly.WidgetDiv.hideIfOwner(this);
|
2013-10-30 14:46:03 -07:00
|
|
|
Blockly.FieldTextInput.superClass_.dispose.call(this);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the text in this field.
|
|
|
|
* @param {?string} text New text.
|
|
|
|
* @override
|
|
|
|
*/
|
2016-01-20 19:11:03 -08:00
|
|
|
Blockly.FieldTextInput.prototype.setValue = function(text) {
|
2013-10-30 14:46:03 -07:00
|
|
|
if (text === null) {
|
2016-01-15 15:36:06 -08:00
|
|
|
return; // No change if null.
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
2016-02-10 14:33:13 -08:00
|
|
|
if (this.sourceBlock_ && this.validator_) {
|
|
|
|
var validated = this.validator_(text);
|
2013-10-30 14:46:03 -07:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
2016-01-20 19:11:03 -08:00
|
|
|
Blockly.Field.prototype.setValue.call(this, text);
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
|
2015-02-25 13:35:37 -08:00
|
|
|
/**
|
|
|
|
* Set whether this field is spellchecked by the browser.
|
|
|
|
* @param {boolean} check True if checked.
|
|
|
|
*/
|
|
|
|
Blockly.FieldTextInput.prototype.setSpellcheck = function(check) {
|
|
|
|
this.spellcheck_ = check;
|
|
|
|
};
|
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
/**
|
|
|
|
* 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.
|
2013-10-30 14:46:03 -07:00
|
|
|
* @private
|
|
|
|
*/
|
2014-09-08 14:26:52 -07:00
|
|
|
Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) {
|
2016-03-29 21:41:20 -07:00
|
|
|
this.workspace_ = this.sourceBlock_.workspace;
|
2014-09-08 14:26:52 -07:00
|
|
|
var quietInput = opt_quietInput || false;
|
|
|
|
if (!quietInput && (goog.userAgent.MOBILE || goog.userAgent.ANDROID ||
|
|
|
|
goog.userAgent.IPAD)) {
|
2013-10-30 14:46:03 -07:00
|
|
|
// Mobile browsers have issues with in-line textareas (focus & keyboards).
|
|
|
|
var newValue = window.prompt(Blockly.Msg.CHANGE_VALUE_TITLE, this.text_);
|
2016-02-10 14:33:13 -08:00
|
|
|
if (this.sourceBlock_ && this.validator_) {
|
|
|
|
var override = this.validator_(newValue);
|
2013-10-30 14:46:03 -07:00
|
|
|
if (override !== undefined) {
|
|
|
|
newValue = override;
|
|
|
|
}
|
|
|
|
}
|
2016-01-20 19:11:03 -08:00
|
|
|
this.setValue(newValue);
|
2013-10-30 14:46:03 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-04-28 13:51:25 -07:00
|
|
|
Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_());
|
2013-10-30 14:46:03 -07:00
|
|
|
var div = Blockly.WidgetDiv.DIV;
|
|
|
|
// Create the input.
|
|
|
|
var htmlInput = goog.dom.createDom('input', 'blocklyHtmlInput');
|
2015-02-25 13:35:37 -08:00
|
|
|
htmlInput.setAttribute('spellcheck', this.spellcheck_);
|
2016-03-29 21:41:20 -07:00
|
|
|
var fontSize =
|
2016-04-05 18:36:02 -04:00
|
|
|
(Blockly.FieldTextInput.FONTSIZE) + 'pt';
|
2015-08-19 17:21:05 -07:00
|
|
|
div.style.fontSize = fontSize;
|
|
|
|
htmlInput.style.fontSize = fontSize;
|
2015-08-10 13:46:16 -05:00
|
|
|
/** @type {!HTMLInputElement} */
|
2013-10-30 14:46:03 -07:00
|
|
|
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();
|
|
|
|
}
|
2013-10-30 14:46:03 -07:00
|
|
|
|
2015-02-17 23:20:36 +09: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.
|
2013-10-30 14:46:03 -07:00
|
|
|
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_);
|
2016-02-11 21:40:33 -08:00
|
|
|
htmlInput.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this);
|
2016-03-29 21:41:20 -07:00
|
|
|
this.workspace_.addChangeListener(htmlInput.onWorkspaceChangeWrapper_);
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2015-02-17 23:20:36 +09:00
|
|
|
* Handle key down to the editor.
|
2013-10-30 14:46:03 -07:00
|
|
|
* @param {!Event} e Keyboard event.
|
|
|
|
* @private
|
|
|
|
*/
|
2015-02-17 23:20:36 +09:00
|
|
|
Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) {
|
2013-10-30 14:46:03 -07:00
|
|
|
var htmlInput = Blockly.FieldTextInput.htmlInput_;
|
2015-10-14 16:23:23 -07:00
|
|
|
var tabKey = 9, enterKey = 13, escKey = 27;
|
2015-02-17 23:20:36 +09:00
|
|
|
if (e.keyCode == enterKey) {
|
2013-10-30 14:46:03 -07:00
|
|
|
Blockly.WidgetDiv.hide();
|
2015-02-17 23:20:36 +09:00
|
|
|
} else if (e.keyCode == escKey) {
|
2016-01-20 19:11:03 -08:00
|
|
|
htmlInput.value = htmlInput.defaultValue;
|
2013-10-30 14:46:03 -07:00
|
|
|
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();
|
2015-02-17 23:20:36 +09:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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();
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
2016-01-20 19:11:03 -08:00
|
|
|
this.resizeEditor_();
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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_);
|
2015-08-10 13:46:16 -05:00
|
|
|
var htmlInput = Blockly.FieldTextInput.htmlInput_;
|
2016-02-10 14:33:13 -08:00
|
|
|
if (this.sourceBlock_ && this.validator_) {
|
|
|
|
valid = this.validator_(htmlInput.value);
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
|
|
|
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() {
|
2016-04-05 18:36:02 -04:00
|
|
|
// Pull stroke colour from the existing SVG shadow block
|
|
|
|
// XXX: how to do this better...
|
|
|
|
var strokeColour = this.sourceBlock_.getSvgRoot().childNodes[0].getAttribute('stroke');
|
|
|
|
|
|
|
|
var scale = this.sourceBlock_.workspace.scale;
|
2013-10-30 14:46:03 -07:00
|
|
|
var div = Blockly.WidgetDiv.DIV;
|
2016-04-05 18:36:02 -04:00
|
|
|
var bBox = this.getScaledBBox_();
|
|
|
|
// Add 1px to width and height to account for border (pre-scale)
|
|
|
|
var width = Math.max(bBox.width, Blockly.BlockSvg.FIELD_WIDTH * scale);
|
|
|
|
div.style.width = (width / scale + 1) + 'px';
|
|
|
|
div.style.height = (bBox.height / scale + 1) + 'px';
|
|
|
|
div.style.transform = 'scale(' + scale + ')';
|
|
|
|
|
|
|
|
// This is the same check as in block_render.
|
|
|
|
// Possibly we should switch to a different check in the future.
|
|
|
|
// XXX: move this out
|
|
|
|
var borderRadius = Blockly.BlockSvg.TEXT_FIELD_CORNER_RADIUS;
|
|
|
|
if (this.sourceBlock_.type === 'math_number') {
|
|
|
|
borderRadius = Blockly.BlockSvg.NUMBER_FIELD_CORNER_RADIUS;
|
|
|
|
}
|
|
|
|
borderRadius += 0.5; // Add 0.5px to account for slight difference between SVG and CSS border
|
|
|
|
div.style.borderColor = strokeColour;
|
|
|
|
div.style.borderRadius = borderRadius + 'px';
|
|
|
|
|
2015-04-28 13:51:25 -07:00
|
|
|
var xy = this.getAbsoluteXY_();
|
2016-04-05 18:36:02 -04:00
|
|
|
|
|
|
|
// Account for border 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,
|
2013-10-30 14:46:03 -07:00
|
|
|
// whereas the right edge is fixed. Reposition the editor.
|
2015-04-28 13:51:25 -07:00
|
|
|
if (this.sourceBlock_.RTL) {
|
2016-04-05 18:36:02 -04:00
|
|
|
xy.x += bBox.width;
|
2013-10-30 14:46:03 -07:00
|
|
|
xy.x -= div.offsetWidth;
|
|
|
|
}
|
|
|
|
// Shift by a few pixels to line up exactly.
|
2016-04-05 18:36:02 -04:00
|
|
|
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.
|
2016-04-05 18:36:02 -04:00
|
|
|
xy.x += 2 * scale;
|
|
|
|
xy.y += 1 * scale;
|
2015-08-21 14:13:07 -07:00
|
|
|
}
|
2013-10-30 14:46:03 -07:00
|
|
|
if (goog.userAgent.WEBKIT) {
|
2016-04-05 18:36:02 -04:00
|
|
|
xy.y -= 1 * scale;
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
|
|
|
div.style.left = xy.x + 'px';
|
|
|
|
div.style.top = xy.y + 'px';
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Close the editor, save the results, and dispose of the editable
|
|
|
|
* text field's 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() {
|
2013-10-30 14:46:03 -07:00
|
|
|
var thisField = this;
|
|
|
|
return function() {
|
|
|
|
var htmlInput = Blockly.FieldTextInput.htmlInput_;
|
|
|
|
// Save the edit (if it validates).
|
2014-09-08 14:26:52 -07:00
|
|
|
var text = htmlInput.value;
|
2016-02-10 14:33:13 -08:00
|
|
|
if (thisField.sourceBlock_ && thisField.validator_) {
|
|
|
|
var text1 = thisField.validator_(text);
|
2015-06-11 18:06:44 -07:00
|
|
|
if (text1 === null) {
|
2013-10-30 14:46:03 -07:00
|
|
|
// Invalid edit.
|
|
|
|
text = htmlInput.defaultValue;
|
2015-06-11 18:06:44 -07:00
|
|
|
} else if (text1 !== undefined) {
|
2016-02-10 14:33:13 -08:00
|
|
|
// Validation function has changed the text.
|
2015-06-11 18:06:44 -07:00
|
|
|
text = text1;
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
|
|
|
}
|
2016-01-20 19:11:03 -08:00
|
|
|
thisField.setValue(text);
|
2014-09-08 14:26:52 -07:00
|
|
|
thisField.sourceBlock_.rendered && thisField.sourceBlock_.render();
|
2015-02-17 23:20:36 +09:00
|
|
|
Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_);
|
2013-10-30 14:46:03 -07:00
|
|
|
Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
|
|
|
|
Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);
|
2016-03-29 21:41:20 -07:00
|
|
|
thisField.workspace_.removeChangeListener(
|
2016-02-11 21:40:33 -08:00
|
|
|
htmlInput.onWorkspaceChangeWrapper_);
|
2013-10-30 14:46:03 -07:00
|
|
|
Blockly.FieldTextInput.htmlInput_ = null;
|
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 = '';
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
2015-06-08 15:54:18 -07:00
|
|
|
text = String(text);
|
2013-10-30 14:46:03 -07:00
|
|
|
// 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;
|
|
|
|
};
|