mirror of
https://github.com/google/blockly.git
synced 2026-01-04 23:50:12 +01:00
Our files are up to a decade old, and have churned so much, that the initial author of the file no longer has much meaning. Furthermore, this will encourage developers to post to the developer group, rather than emailing Googlers (usually me) directly.
450 lines
14 KiB
JavaScript
450 lines
14 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2019 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Text Area field.
|
|
*/
|
|
'use strict';
|
|
|
|
/**
|
|
* Text Area field.
|
|
* @class
|
|
*/
|
|
goog.module('Blockly.FieldMultilineInput');
|
|
|
|
const Css = goog.require('Blockly.Css');
|
|
const Field = goog.require('Blockly.Field');
|
|
const FieldTextInput = goog.require('Blockly.FieldTextInput');
|
|
const KeyCodes = goog.require('Blockly.utils.KeyCodes');
|
|
const Svg = goog.require('Blockly.utils.Svg');
|
|
const WidgetDiv = goog.require('Blockly.WidgetDiv');
|
|
const aria = goog.require('Blockly.utils.aria');
|
|
const dom = goog.require('Blockly.utils.dom');
|
|
const fieldRegistry = goog.require('Blockly.fieldRegistry');
|
|
const object = goog.require('Blockly.utils.object');
|
|
const userAgent = goog.require('Blockly.utils.userAgent');
|
|
const utils = goog.require('Blockly.utils');
|
|
|
|
|
|
/**
|
|
* 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/multiline-text-input#creation}
|
|
* for a list of properties this parameter supports.
|
|
* @extends {FieldTextInput}
|
|
* @constructor
|
|
* @alias Blockly.FieldMultilineInput
|
|
*/
|
|
const FieldMultilineInput = function(opt_value, opt_validator, opt_config) {
|
|
FieldMultilineInput.superClass_.constructor.call(
|
|
this, opt_value, opt_validator, opt_config);
|
|
|
|
/**
|
|
* The SVG group element that will contain a text element for each text row
|
|
* when initialized.
|
|
* @type {SVGGElement}
|
|
*/
|
|
this.textGroup_ = null;
|
|
|
|
/**
|
|
* Defines the maximum number of lines of field.
|
|
* If exceeded, scrolling functionality is enabled.
|
|
* @type {number}
|
|
* @protected
|
|
*/
|
|
this.maxLines_ = Infinity;
|
|
|
|
/**
|
|
* Whether Y overflow is currently occurring.
|
|
* @type {boolean}
|
|
* @protected
|
|
*/
|
|
this.isOverflowedY_ = false;
|
|
};
|
|
object.inherits(FieldMultilineInput, FieldTextInput);
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
FieldMultilineInput.prototype.configure_ = function(config) {
|
|
FieldMultilineInput.superClass_.configure_.call(this, config);
|
|
config.maxLines && this.setMaxLines(config.maxLines);
|
|
};
|
|
|
|
/**
|
|
* 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 {!FieldMultilineInput} The new field instance.
|
|
* @package
|
|
* @nocollapse
|
|
*/
|
|
FieldMultilineInput.fromJson = function(options) {
|
|
const text = utils.replaceMessageReferences(options['text']);
|
|
// `this` might be a subclass of FieldMultilineInput if that class doesn't
|
|
// override the static fromJson method.
|
|
return new this(text, undefined, options);
|
|
};
|
|
|
|
/**
|
|
* Serializes this field's value to XML. Should only be called by Blockly.Xml.
|
|
* @param {!Element} fieldElement The element to populate with info about the
|
|
* field's state.
|
|
* @return {!Element} The element containing info about the field's state.
|
|
* @package
|
|
*/
|
|
FieldMultilineInput.prototype.toXml = function(fieldElement) {
|
|
// Replace '\n' characters with HTML-escaped equivalent '
'. This is
|
|
// needed so the plain-text representation of the XML produced by
|
|
// `Blockly.Xml.domToText` will appear on a single line (this is a limitation
|
|
// of the plain-text format).
|
|
fieldElement.textContent = this.getValue().replace(/\n/g, ' ');
|
|
return fieldElement;
|
|
};
|
|
|
|
/**
|
|
* Sets the field's value based on the given XML element. Should only be
|
|
* called by Blockly.Xml.
|
|
* @param {!Element} fieldElement The element containing info about the
|
|
* field's state.
|
|
* @package
|
|
*/
|
|
FieldMultilineInput.prototype.fromXml = function(fieldElement) {
|
|
this.setValue(fieldElement.textContent.replace(/ /g, '\n'));
|
|
};
|
|
|
|
/**
|
|
* Saves this field's value.
|
|
* @return {*} The state of this field.
|
|
* @package
|
|
*/
|
|
FieldMultilineInput.prototype.saveState = function() {
|
|
const legacyState = this.saveLegacyState(FieldMultilineInput);
|
|
if (legacyState !== null) {
|
|
return legacyState;
|
|
}
|
|
return this.getValue();
|
|
};
|
|
|
|
/**
|
|
* Sets the field's value based on the given state.
|
|
* @param {*} state The state of the variable to assign to this variable field.
|
|
* @override
|
|
* @package
|
|
*/
|
|
FieldMultilineInput.prototype.loadState = function(state) {
|
|
if (this.loadLegacyState(Field, state)) {
|
|
return;
|
|
}
|
|
this.setValue(state);
|
|
};
|
|
|
|
/**
|
|
* Create the block UI for this field.
|
|
* @package
|
|
*/
|
|
FieldMultilineInput.prototype.initView = function() {
|
|
this.createBorderRect_();
|
|
this.textGroup_ = dom.createSvgElement(
|
|
Svg.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.
|
|
* @protected
|
|
* @override
|
|
*/
|
|
FieldMultilineInput.prototype.getDisplayText_ = function() {
|
|
let textLines = this.getText();
|
|
if (!textLines) {
|
|
// Prevent the field from disappearing if empty.
|
|
return Field.NBSP;
|
|
}
|
|
const lines = textLines.split('\n');
|
|
textLines = '';
|
|
const displayLinesNumber =
|
|
this.isOverflowedY_ ? this.maxLines_ : lines.length;
|
|
for (let i = 0; i < displayLinesNumber; i++) {
|
|
let text = lines[i];
|
|
if (text.length > this.maxDisplayLength) {
|
|
// Truncate displayed string and add an ellipsis ('...').
|
|
text = text.substring(0, this.maxDisplayLength - 4) + '...';
|
|
} else if (this.isOverflowedY_ && i === displayLinesNumber - 1) {
|
|
text = text.substring(0, text.length - 3) + '...';
|
|
}
|
|
// Replace whitespace with non-breaking spaces so the text doesn't collapse.
|
|
text = text.replace(/\s/g, Field.NBSP);
|
|
|
|
textLines += text;
|
|
if (i !== displayLinesNumber - 1) {
|
|
textLines += '\n';
|
|
}
|
|
}
|
|
if (this.sourceBlock_.RTL) {
|
|
// The SVG is LTR, force value to be RTL.
|
|
textLines += '\u200F';
|
|
}
|
|
return textLines;
|
|
};
|
|
|
|
/**
|
|
* 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_). Is being redefined here to update
|
|
* overflow state of the field.
|
|
* @param {*} newValue The value to be saved. The default validator guarantees
|
|
* that this is a string.
|
|
* @protected
|
|
*/
|
|
FieldMultilineInput.prototype.doValueUpdate_ = function(newValue) {
|
|
FieldMultilineInput.superClass_.doValueUpdate_.call(this, newValue);
|
|
this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_;
|
|
};
|
|
|
|
/**
|
|
* Updates the text of the textElement.
|
|
* @protected
|
|
*/
|
|
FieldMultilineInput.prototype.render_ = function() {
|
|
// Remove all text group children.
|
|
let currentChild;
|
|
while ((currentChild = this.textGroup_.firstChild)) {
|
|
this.textGroup_.removeChild(currentChild);
|
|
}
|
|
|
|
// Add in text elements into the group.
|
|
const lines = this.getDisplayText_().split('\n');
|
|
let y = 0;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
|
|
this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
|
|
const span = dom.createSvgElement(
|
|
Svg.TEXT, {
|
|
'class': 'blocklyText blocklyMultilineText',
|
|
x: this.getConstants().FIELD_BORDER_RECT_X_PADDING,
|
|
y: y + this.getConstants().FIELD_BORDER_RECT_Y_PADDING,
|
|
dy: this.getConstants().FIELD_TEXT_BASELINE
|
|
},
|
|
this.textGroup_);
|
|
span.appendChild(document.createTextNode(lines[i]));
|
|
y += lineHeight;
|
|
}
|
|
|
|
if (this.isBeingEdited_) {
|
|
var htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
|
|
if (this.isOverflowedY_) {
|
|
dom.addClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
|
|
} else {
|
|
dom.removeClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
|
|
}
|
|
}
|
|
|
|
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_();
|
|
}
|
|
var htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
|
|
if (!this.isTextValid_) {
|
|
dom.addClass(htmlInput, 'blocklyInvalidInput');
|
|
aria.setState(htmlInput, aria.State.INVALID, true);
|
|
} else {
|
|
dom.removeClass(htmlInput, 'blocklyInvalidInput');
|
|
aria.setState(htmlInput, aria.State.INVALID, false);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Updates the size of the field based on the text.
|
|
* @protected
|
|
*/
|
|
FieldMultilineInput.prototype.updateSize_ = function() {
|
|
const nodes = this.textGroup_.childNodes;
|
|
let totalWidth = 0;
|
|
let totalHeight = 0;
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
const tspan = /** @type {!Element} */ (nodes[i]);
|
|
const textWidth = dom.getTextWidth(tspan);
|
|
if (textWidth > totalWidth) {
|
|
totalWidth = textWidth;
|
|
}
|
|
totalHeight += this.getConstants().FIELD_TEXT_HEIGHT +
|
|
(i > 0 ? this.getConstants().FIELD_BORDER_RECT_Y_PADDING : 0);
|
|
}
|
|
if (this.isBeingEdited_) {
|
|
// The default width is based on the longest line in the display text,
|
|
// but when it's being edited, width should be calculated based on the
|
|
// absolute longest line, even if it would be truncated after editing.
|
|
// Otherwise we would get wrong editor width when there are more
|
|
// lines than this.maxLines_.
|
|
const actualEditorLines = this.value_.split('\n');
|
|
const dummyTextElement = dom.createSvgElement(
|
|
Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'});
|
|
const fontSize = this.getConstants().FIELD_TEXT_FONTSIZE;
|
|
const fontWeight = this.getConstants().FIELD_TEXT_FONTWEIGHT;
|
|
const fontFamily = this.getConstants().FIELD_TEXT_FONTFAMILY;
|
|
|
|
for (var i = 0; i < actualEditorLines.length; i++) {
|
|
if (actualEditorLines[i].length > this.maxDisplayLength) {
|
|
actualEditorLines[i] =
|
|
actualEditorLines[i].substring(0, this.maxDisplayLength);
|
|
}
|
|
dummyTextElement.textContent = actualEditorLines[i];
|
|
const lineWidth = dom.getFastTextWidth(
|
|
dummyTextElement, fontSize, fontWeight, fontFamily);
|
|
if (lineWidth > totalWidth) {
|
|
totalWidth = lineWidth;
|
|
}
|
|
}
|
|
|
|
const scrollbarWidth =
|
|
this.htmlInput_.offsetWidth - this.htmlInput_.clientWidth;
|
|
totalWidth += scrollbarWidth;
|
|
}
|
|
if (this.borderRect_) {
|
|
totalHeight += this.getConstants().FIELD_BORDER_RECT_Y_PADDING * 2;
|
|
totalWidth += this.getConstants().FIELD_BORDER_RECT_X_PADDING * 2;
|
|
this.borderRect_.setAttribute('width', totalWidth);
|
|
this.borderRect_.setAttribute('height', totalHeight);
|
|
}
|
|
this.size_.width = totalWidth;
|
|
this.size_.height = totalHeight;
|
|
|
|
this.positionBorderRect_();
|
|
};
|
|
|
|
/**
|
|
* Show the inline free-text editor on top of the text.
|
|
* Overrides the default behaviour to force rerender in order to
|
|
* correct block size, based on editor text.
|
|
* @param {Event=} _opt_e Optional mouse event that triggered the field to open,
|
|
* or undefined if triggered programmatically.
|
|
* @param {boolean=} opt_quietInput True if editor should be created without
|
|
* focus. Defaults to false.
|
|
* @override
|
|
*/
|
|
FieldMultilineInput.prototype.showEditor_ = function(_opt_e, opt_quietInput) {
|
|
FieldMultilineInput.superClass_.showEditor_.call(
|
|
this, _opt_e, opt_quietInput);
|
|
this.forceRerender();
|
|
};
|
|
|
|
/**
|
|
* Create the text input editor widget.
|
|
* @return {!HTMLTextAreaElement} The newly created text input editor.
|
|
* @protected
|
|
*/
|
|
FieldMultilineInput.prototype.widgetCreate_ = function() {
|
|
const div = WidgetDiv.getDiv();
|
|
const scale = this.workspace_.getScale();
|
|
|
|
const htmlInput =
|
|
/** @type {HTMLTextAreaElement} */ (document.createElement('textarea'));
|
|
htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
|
|
htmlInput.setAttribute('spellcheck', this.spellcheck_);
|
|
const fontSize = (this.getConstants().FIELD_TEXT_FONTSIZE * scale) + 'pt';
|
|
div.style.fontSize = fontSize;
|
|
htmlInput.style.fontSize = fontSize;
|
|
const borderRadius = (FieldTextInput.BORDERRADIUS * scale) + 'px';
|
|
htmlInput.style.borderRadius = borderRadius;
|
|
const paddingX = this.getConstants().FIELD_BORDER_RECT_X_PADDING * scale;
|
|
const paddingY = this.getConstants().FIELD_BORDER_RECT_Y_PADDING * scale / 2;
|
|
htmlInput.style.padding =
|
|
paddingY + 'px ' + paddingX + 'px ' + paddingY + 'px ' + paddingX + 'px';
|
|
const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
|
|
this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
|
|
htmlInput.style.lineHeight = (lineHeight * scale) + 'px';
|
|
|
|
div.appendChild(htmlInput);
|
|
|
|
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
|
|
htmlInput.untypedDefaultValue_ = this.value_;
|
|
htmlInput.oldValue_ = null;
|
|
if (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;
|
|
};
|
|
|
|
/**
|
|
* Sets the maxLines config for this field.
|
|
* @param {number} maxLines Defines the maximum number of lines allowed,
|
|
* before scrolling functionality is enabled.
|
|
*/
|
|
FieldMultilineInput.prototype.setMaxLines = function(maxLines) {
|
|
if (typeof maxLines === 'number' && maxLines > 0 &&
|
|
maxLines !== this.maxLines_) {
|
|
this.maxLines_ = maxLines;
|
|
this.forceRerender();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the maxLines config of this field.
|
|
* @return {number} The maxLines config value.
|
|
*/
|
|
FieldMultilineInput.prototype.getMaxLines = function() {
|
|
return this.maxLines_;
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
FieldMultilineInput.prototype.onHtmlInputKeyDown_ = function(e) {
|
|
if (e.keyCode !== KeyCodes.ENTER) {
|
|
FieldMultilineInput.superClass_.onHtmlInputKeyDown_.call(this, e);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* CSS for multiline field. See css.js for use.
|
|
*/
|
|
Css.register(`
|
|
.blocklyHtmlTextAreaInput {
|
|
font-family: monospace;
|
|
resize: none;
|
|
overflow: hidden;
|
|
height: 100%;
|
|
text-align: left;
|
|
}
|
|
|
|
.blocklyHtmlTextAreaInputOverflowedY {
|
|
overflow-y: scroll;
|
|
}
|
|
`);
|
|
|
|
fieldRegistry.register('field_multilinetext', FieldMultilineInput);
|
|
|
|
exports = FieldMultilineInput;
|