Files
blockly/core/field_multilineinput.js
Neil Fraser b46a4fe286 Bring our license format up to date (#3127)
* Google changed from an Inc to an LLC.

This happened back in 2017 but we didn’t notice.  Officially we should update files from Inc to LLC when they are changed as part of regular edits, but this is a nightmare to remember for the next decade.

* Remove project description/titles from licenses

This is no longer part of Google’s header requirements.  Our existing descriptions were useless (“Visual Blocks Editor”) or grossly obselete (“Visual Blocks Language”).

* License no longer requires URL.

* Fix license regexps.
2019-10-02 14:46:56 -07:00

293 lines
9.4 KiB
JavaScript

/**
* @license
* Copyright 2019 Google LLC
*
* 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 Area field.
* @author fraser@google.com (Neil Fraser)
* @author Andrew Mee
* @author acbart@udel.edu (Austin Cory Bart)
*/
'use strict';
goog.provide('Blockly.FieldMultilineInput');
goog.require('Blockly.Css');
goog.require('Blockly.DropDownDiv');
goog.require('Blockly.FieldTextInput');
goog.require('Blockly.utils');
goog.require('Blockly.utils.Coordinate');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.KeyCodes');
goog.require('Blockly.utils.object');
goog.require('Blockly.utils.userAgent');
/**
* Class for an editable text area field.
* @param {string=} opt_value The initial content of the field. Should cast to a
* string. Defaults to an empty string if null or undefined.
* @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 {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/text-input#creation}
* for a list of properties this parameter supports.
* @extends {Blockly.FieldTextInput}
* @constructor
*/
Blockly.FieldMultilineInput = function(opt_value, opt_validator, opt_config) {
// TODO: Once this field is documented the opt_config link should point to its
// creation documentation, rather than the text input field's.
if (opt_value == null) {
opt_value = '';
}
Blockly.FieldMultilineInput.superClass_.constructor.call(this,
opt_value, opt_validator, opt_config);
};
Blockly.utils.object.inherits(Blockly.FieldMultilineInput,
Blockly.FieldTextInput);
/**
* The default height of a single line of text.
* @type {number}
* @const
*/
Blockly.FieldMultilineInput.LINE_HEIGHT = 20;
/**
* Construct a FieldMultilineInput from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and spellcheck).
* @return {!Blockly.FieldMultilineInput} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldMultilineInput.fromJson = function(options) {
var text = Blockly.utils.replaceMessageReferences(options['text']);
return new Blockly.FieldMultilineInput(text, undefined, options);
};
/**
* Create the block UI for this field.
* @package
*/
Blockly.FieldMultilineInput.prototype.initView = function() {
this.createBorderRect_();
this.textGroup_ = Blockly.utils.dom.createSvgElement('g',
{
'class': 'blocklyEditableText',
}, this.fieldGroup_);
};
/**
* Get the text from this field as displayed on screen. May differ from getText
* due to ellipsis, and other formatting.
* @return {string} Currently displayed text.
* @private
*/
Blockly.FieldMultilineInput.prototype.getDisplayText_ = function() {
var value = this.value_;
if (!value) {
// Prevent the field from disappearing if empty.
return Blockly.Field.NBSP;
}
var lines = value.split('\n');
value = '';
for (var i = 0; i < lines.length; i++) {
var text = lines[i];
if (text.length > this.maxDisplayLength) {
// Truncate displayed string and add an ellipsis ('...').
text = text.substring(0, this.maxDisplayLength - 4) + '...';
}
// Replace whitespace with non-breaking spaces so the text doesn't collapse.
text = text.replace(/\s/g, Blockly.Field.NBSP);
value += text;
if (i !== lines.length - 1) {
value += '\n';
}
}
if (this.sourceBlock_.RTL) {
// The SVG is LTR, force value to be RTL.
value += '\u200F';
}
return value;
};
/**
* Updates the text of the textElement.
* @protected
*/
Blockly.FieldMultilineInput.prototype.render_ = function() {
// Remove all text group children.
var currentChild;
while (currentChild = this.textGroup_.firstChild) {
this.textGroup_.removeChild(currentChild);
}
// Add in text elements into the group.
var lines = this.getDisplayText_().split('\n');
var yOffset = Blockly.Field.Y_PADDING / 2;
var y = 0;
for (var i = 0; i < lines.length; i++) {
var span = Blockly.utils.dom.createSvgElement('text', {
'class': 'blocklyText blocklyMultilineText',
x: Blockly.Field.DEFAULT_TEXT_OFFSET,
y: y + yOffset,
dy: Blockly.FieldMultilineInput.LINE_HEIGHT / 2
}, this.textGroup_);
span.appendChild(document.createTextNode(lines[i]));
y += Blockly.FieldMultilineInput.LINE_HEIGHT;
}
this.updateSize_();
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.
setTimeout(this.resizeEditor_.bind(this), 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);
}
}
};
/**
* Updates the size of the field based on the text.
* @protected
*/
Blockly.FieldMultilineInput.prototype.updateSize_ = function() {
var nodes = this.textGroup_.childNodes;
var totalWidth = 0;
var totalHeight = 0;
for (var i = 0; i < nodes.length; i++) {
var tspan = nodes[i];
var textWidth = Blockly.utils.dom.getTextWidth(tspan);
if (textWidth > totalWidth) {
totalWidth = textWidth;
}
totalHeight += Blockly.FieldMultilineInput.LINE_HEIGHT;
}
if (this.borderRect_) {
totalWidth += Blockly.Field.X_PADDING;
this.borderRect_.setAttribute('width', totalWidth);
this.borderRect_.setAttribute('height', totalHeight);
}
this.size_.width = totalWidth;
this.size_.height = totalHeight;
};
/**
* Resize the editor to fit the text.
* @protected
*/
Blockly.FieldMultilineInput.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);
div.style.left = xy.x + 'px';
div.style.top = xy.y + 'px';
};
/**
* Create the text input editor widget.
* @return {!HTMLTextAreaElement} The newly created text input editor.
* @protected
*/
Blockly.FieldMultilineInput.prototype.widgetCreate_ = function() {
var div = Blockly.WidgetDiv.DIV;
var scale = this.workspace_.scale;
var htmlInput = /** @type {HTMLTextAreaElement} */ (document.createElement('textarea'));
htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
htmlInput.setAttribute('spellcheck', this.spellcheck_);
var fontSize = (Blockly.FieldTextInput.FONTSIZE * scale) + 'pt';
div.style.fontSize = fontSize;
htmlInput.style.fontSize = fontSize;
var borderRadius = (Blockly.FieldTextInput.BORDERRADIUS * scale) + 'px';
htmlInput.style.borderRadius = borderRadius;
var padding = Blockly.Field.DEFAULT_TEXT_OFFSET * scale;
htmlInput.style.paddingLeft = padding + 'px';
htmlInput.style.width = 'calc(100% - ' + padding + 'px)';
htmlInput.style.lineHeight =
(Blockly.FieldMultilineInput.LINE_HEIGHT * scale) + 'px';
div.appendChild(htmlInput);
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
htmlInput.untypedDefaultValue_ = this.value_;
htmlInput.oldValue_ = null;
if (Blockly.utils.userAgent.GECKO) {
// In FF, ensure the browser reflows before resizing to avoid issue #2777.
setTimeout(this.resizeEditor_.bind(this), 0);
} else {
this.resizeEditor_();
}
this.bindInputEvents_(htmlInput);
return htmlInput;
};
/**
* Handle key down to the editor. Override the text input definition of this
* so as to not close the editor when enter is typed in.
* @param {!Event} e Keyboard event.
* @protected
*/
Blockly.FieldMultilineInput.prototype.onHtmlInputKeyDown_ = function(e) {
if (e.keyCode !== Blockly.utils.KeyCodes.ENTER) {
Blockly.FieldMultilineInput.superClass_.onHtmlInputKeyDown_.call(this, e);
}
};
/**
* CSS for multiline field. See css.js for use.
*/
Blockly.Css.register([
/* eslint-disable indent */
'.blocklyHtmlTextAreaInput {',
'font-family: monospace;',
'resize: none;',
'overflow: hidden;',
'height: 100%;',
'text-align: left;',
'}'
/* eslint-enable indent */
]);
Blockly.fieldRegistry.register('field_multilinetext', Blockly.FieldMultilineInput);