Implementing multiline field text area for blocks

2,577 views
Skip to first unread message

s...@mitre.org

unread,
Jan 15, 2016, 4:12:22 PM1/15/16
to Blockly
Hello,

I am trying to implement a multiple line text area within a blockly block rather than a single line text input.

I have a simple expression block that looks like the following:

Blockly.Blocks['expression_expr'] = {
    init: function () {
        this.appendDummyInput()
                .appendField("EXPR")
                .appendField(new Blockly.FieldTextInput("......."), "expr");
        this.setPreviousStatement(true);
        this.setColour(210);
    }
};

Which creates a single line text input field to the block. I need a multiline text input field, but this is not currently implemented in blockly.

So I am attempting to implement multiline field text area in blockly myself.

I created a new file, js\blockly\core\field_textarea.js, to implement a field text area in blockly.

And to get blockly to use/incorporate this new field text area, I had to make changes to the files 
js\blockly\blockly_uncompressed.js 
js\blockly\core\block.js
js\blockly\core\blockly.js

And I changed the expression block to use the new FieldTextArea instead of the FieldTextInput.

The problem with this implementation is that while I am able to create a multiline text area for editing the contents of the text input, I am not able to get the block to render as a multiline text field.

I am using the showEditor_ method to get a multiline text area for editing, but I can't figure out how to get it to render as a multiline field when not editing.

I have included my implementation of the field text area code below.

Has anyone been able to implement a multiline field text area for blockly blocks? if so please let me know, or if anyone else has any suggestions how to implement this capability.

Thanks,

/**
 */
'use strict';

goog.provide('Blockly.FieldTextArea');

goog.require('Blockly.Field');
goog.require('Blockly.Msg');
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_changeHandler 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.FieldTextArea = function(text, opt_changeHandler) {
  Blockly.FieldTextArea.superClass_.constructor.call(this, text);
  this.setChangeHandler(opt_changeHandler);
};
goog.inherits(Blockly.FieldTextArea, Blockly.Field);

/**
 * Point size of text.  Should match blocklyText's font-size in CSS.
 */
Blockly.FieldTextArea.FONTSIZE = 11;

/**
 * Mouse cursor style when over the hotspot that initiates the editor.
 */
Blockly.FieldTextArea.prototype.CURSOR = 'text';

/**
 * Close the input widget if this input is being deleted.
 */
Blockly.FieldTextArea.prototype.dispose = function() {
  Blockly.WidgetDiv.hideIfOwner(this);
  Blockly.FieldTextArea.superClass_.dispose.call(this);
};

/**
 * Set the text in this field.
 * @param {?string} text New text.
 * @override
 */
Blockly.FieldTextArea.prototype.setText = function(text) {
  if (text === null) {
    // No change if null.
    return;
  }
  if (this.sourceBlock_ && this.changeHandler_) {
    var validated = this.changeHandler_(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.setText.call(this, text);
};

/**
 * 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.
 * @private
 */
Blockly.FieldTextArea.prototype.showEditor_ = function(opt_quietInput) {
  var quietInput = opt_quietInput || false;
  if (!quietInput && (goog.userAgent.MOBILE || goog.userAgent.ANDROID ||
                      goog.userAgent.IPAD)) {
    // Mobile browsers have issues with in-line textareas (focus & keyboards).
    var newValue = window.prompt(Blockly.Msg.CHANGE_VALUE_TITLE, this.text_);
    if (this.sourceBlock_ && this.changeHandler_) {
      var override = this.changeHandler_(newValue);
      if (override !== undefined) {
        newValue = override;
      }
    }
    if (newValue !== null) {
      this.setText(newValue);
    }
    return;
  }

  Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_());
  var div = Blockly.WidgetDiv.DIV;
  // Create the input.
  var htmlInput = goog.dom.createDom('textarea', 'blocklyHtmlTextArea');
  var fontSize = (Blockly.FieldTextArea.FONTSIZE *
                  this.sourceBlock_.workspace.scale) + 'pt';
  div.style.fontSize = fontSize;
  htmlInput.style.fontSize = fontSize;
  Blockly.FieldTextArea.htmlInput_ = htmlInput;
  htmlInput.style.height = '100px';
  htmlInput.style.width = '500px';
  div.appendChild(htmlInput);

  htmlInput.value = htmlInput.defaultValue = this.text_;
  htmlInput.oldValue_ = null;
  this.validate_();
  this.resizeEditor_();
  if (!quietInput) {
    htmlInput.focus();
    htmlInput.select();
  }

  // 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_);
  var workspaceSvg = this.sourceBlock_.workspace.getCanvas();
  htmlInput.onWorkspaceChangeWrapper_ =
      Blockly.bindEvent_(workspaceSvg, 'blocklyWorkspaceChange', this,
      this.resizeEditor_);
};

/**
 * Handle key down to the editor.
 * @param {!Event} e Keyboard event.
 * @private
 */
Blockly.FieldTextArea.prototype.onHtmlInputKeyDown_ = function(e) {
  var htmlInput = Blockly.FieldTextArea.htmlInput_;
  var tabKey = 9, enterKey = 13, escKey = 27;
  if (e.keyCode == enterKey) {
    Blockly.WidgetDiv.hide();
  } else if (e.keyCode == escKey) {
    this.setText(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.FieldTextArea.prototype.onHtmlInputChange_ = function(e) {
  var htmlInput = Blockly.FieldTextArea.htmlInput_;
  var escKey = 27;
  if (e.keyCode != escKey) {
    // Update source block.
    var text = htmlInput.value;
    if (text !== htmlInput.oldValue_) {
      this.sourceBlock_.setShadow(false);
      htmlInput.oldValue_ = text;
      this.setText(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();
    }
  }
};

/**
 * Check to see if the contents of the editor validates.
 * Style the editor accordingly.
 * @private
 */
Blockly.FieldTextArea.prototype.validate_ = function() {
  var valid = true;
  goog.asserts.assertObject(Blockly.FieldTextArea.htmlInput_);
  var htmlInput = Blockly.FieldTextArea.htmlInput_;
  if (this.sourceBlock_ && this.changeHandler_) {
    valid = this.changeHandler_(htmlInput.value);
  }
  if (valid === null) {
    Blockly.addClass_(htmlInput, 'blocklyInvalidTextArea');
  } else {
    Blockly.removeClass_(htmlInput, 'blocklyInvalidTextArea');
  }
};

/**
 * Resize the editor and the underlying block to fit the text.
 * @private
 */
Blockly.FieldTextArea.prototype.resizeEditor_ = function() {
  var div = Blockly.WidgetDiv.DIV;
  var bBox = this.fieldGroup_.getBBox();
  div.style.width = bBox.width * this.sourceBlock_.workspace.scale + 'px';
  div.style.height = bBox.height * this.sourceBlock_.workspace.scale + 'px';
  var xy = this.getAbsoluteXY_();
  // In RTL mode block fields and LTR input fields the left edge moves,
  // whereas the right edge is fixed.  Reposition the editor.
  if (this.sourceBlock_.RTL) {
    var borderBBox = this.getScaledBBox_();
    xy.x += borderBBox.width;
    xy.x -= div.offsetWidth;
  }
  // Shift by a few pixels to line up exactly.
  xy.y += 1;
  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 -= 1;
    xy.y -= 1;
  }
  if (goog.userAgent.WEBKIT) {
    xy.y -= 3;
  }
  div.style.left = xy.x + 'px';
  div.style.top = xy.y + 'px';
};

/**
 * Draws the border with the correct width.
 * Saves the computed width in a property.
 * @private
 */
Blockly.FieldTextArea.prototype.render_ = function() {

this.size_.width = this.textElement_.getBBox().width + 5;

 this.size_.height= (this.text_.split(/\n/).length ||1)*20 + (Blockly.BlockSvg.SEP_SPACE_Y+5) ;
        
  if (this.borderRect_) {
    this.borderRect_.setAttribute('width',
         this.size_.width + Blockly.BlockSvg.SEP_SPACE_X);
        this.borderRect_.setAttribute('height',
         this.size_.height -  (Blockly.BlockSvg.SEP_SPACE_Y+5));
  }

};
  
/**
 * 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
 */
Blockly.FieldTextArea.prototype.widgetDispose_ = function() {
  var thisField = this;
  return function() {
    var htmlInput = Blockly.FieldTextArea.htmlInput_;
    // Save the edit (if it validates).
    var text = htmlInput.value;
    if (thisField.sourceBlock_ && thisField.changeHandler_) {
      var text1 = thisField.changeHandler_(text);
      if (text1 === null) {
        // Invalid edit.
        text = htmlInput.defaultValue;
      } else if (text1 !== undefined) {
        // Change handler has changed the text.
        text = text1;
      }
    }
    thisField.setText(text);
    thisField.sourceBlock_.rendered && thisField.sourceBlock_.render();
    Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_);
    Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
    Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);
    Blockly.unbindEvent_(htmlInput.onWorkspaceChangeWrapper_);
    Blockly.FieldTextArea.htmlInput_ = null;
    // Delete style properties.
    var style = Blockly.WidgetDiv.DIV.style;
    style.width = 'auto';
    style.height = 'auto';
    style.fontSize = '';
  };
};

/**
 * 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.FieldTextArea.numberValidator = function(text) {
  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 = 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.FieldTextArea.nonnegativeIntegerValidator = function(text) {
  var n = Blockly.FieldTextArea.numberValidator(text);
  if (n) {
    n = String(Math.max(0, Math.floor(n)));
  }
  return n;
};


 

Avi

unread,
Jan 16, 2016, 3:23:50 PM1/16/16
to Blockly
Message has been deleted
Message has been deleted

s...@mitre.org

unread,
Jan 21, 2016, 12:56:44 PM1/21/16
to Blockly
In case anyone is interested, I have implemented a multiline field text area in blockly
 
First added the new field text area JavaScript file, js/blockly/core/field_textarea.js, see below

Them updated the file js/blockly/blockly_uncompressed.js to use the new field text area code
Added two lines, 
   First line to the dependency section to add the dependency
(at line 45) 
goog.addDependency("../../../" + dir + "/core/field_textarea.js", ['Blockly.FieldTextArea'], ['Blockly.FieldTextInput', 'Blockly.Msg', 'goog.asserts', 'goog.dom', 'goog.userAgent', 'goog.events.KeyCodes']);
   Second line to the load section to load the new field text area into blockly
       (at line 201)
goog.require('Blockly.FieldTextArea');

Then updated the file js\blockly\core\blockly.js
Added the line to include the new field text area into blockly
(at line 40)
goog.require('Blockly.FieldTextArea');
Then updated the file js\blockly\core\block.js
Added three lines to the switch statement in the method interpolate_
(at line 1020)
                case 'field_textarea':
                  field = new Blockly.FieldTextArea(element['text']);
                  break;

And last, changed the expression block to use the new FieldTextArea instead of the FieldTextInput.

Blockly.Blocks['expression_expr'] = {
    init: function () {
        this.appendDummyInput()
                .appendField("EXPR")
                .appendField(new Blockly.FieldTextArea("......."), "expr");
        this.setPreviousStatement(true);
        this.setColour(210);
    }
};


/**
 * js/blockly/core/field_textarea.js
 *
 */

'use strict';

goog.provide('Blockly.FieldTextArea');

goog.require('Blockly.FieldTextInput');
goog.require('Blockly.Msg');
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_changeHandler 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.FieldTextArea = function(text, opt_changeHandler) {
  Blockly.FieldTextArea.superClass_.constructor.call(this, text);
  this.setChangeHandler(opt_changeHandler);
};
goog.inherits(Blockly.FieldTextArea, Blockly.FieldTextInput);

/**
 * Close the input widget if this input is being deleted.
 */
Blockly.FieldTextArea.prototype.dispose = function() {
  Blockly.WidgetDiv.hideIfOwner(this);
  Blockly.FieldTextArea.superClass_.dispose.call(this);
};

/**
 * Update the text node of this field to display the current text.
 * @private
 */
Blockly.FieldTextArea.prototype.updateTextNode_ = function() {
  if (!this.textElement_) {
    // Not rendered yet.
    return;
  }
  var text = this.text_;

  // Empty the text element.
  goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_));

  // Replace whitespace with non-breaking spaces so the text doesn't collapse.
  text = text.replace(/ /g, Blockly.Field.NBSP);
  if (this.sourceBlock_.RTL && text) {
    // The SVG is LTR, force text to be RTL.
    text += '\u200F';
  }
  if (!text) {
    // Prevent the field from disappearing if empty.
    text = Blockly.Field.NBSP;
  }

  if (text.length > this.maxDisplayLength) {
    var temp = "";
    var done = false;
    while (!done) {
        temp = temp + text.substring(0, this.maxDisplayLength);
        temp = temp.concat("\n");
        text = text.substring(this.maxDisplayLength, text.length);
        if (text.length < this.maxDisplayLength) {
            temp = temp + text;
            done = true;
        }
    }
    text = temp;
  }
  
  var lines = text.split('\n');
  var dy = '0em';
  for (var i = 0; i < lines.length; i++) {
    var tspanElement = Blockly.createSvgElement('tspan',
        {'dy': dy, 'x': 0}, this.textElement_);
    dy = '1em';
    var textNode = document.createTextNode(lines[i]);
    tspanElement.appendChild(textNode);
  }

  // Cached width is obsolete.  Clear it.
  this.size_.width = 0;
  htmlInput.style['line-height'] = '15px';
  htmlInput.style.height = '100%';
  htmlInput.style.width = '100%';
  var text = this.text_;  
  if (text.length > this.maxDisplayLength) {
    var temp = "";
    var done = false;
    while (!done) {
        temp = temp + text.substring(0, this.maxDisplayLength);
        temp = temp.concat("\n");
        text = text.substring(this.maxDisplayLength, text.length);
        if (text.length < this.maxDisplayLength) {
            temp = temp + text;
            done = true;
        }
    }
    text = temp;
}
 this.size_.height= (text.split('\n').length ||1)*20 + 
Message has been deleted

Denis W

unread,
Aug 16, 2016, 5:10:06 AM8/16/16
to Blockly
Hello,
I have not yet tested, but this seems a very nice block to have !
Are there any plans on integrating this type of block into standard Blockly delivery ?

Regards,
Denis

Denis W

unread,
Aug 23, 2016, 6:24:11 PM8/23/16
to Blockly
No plans on getting acces to multiline textArea field type ?
I don't like fiddling around with standard block my elements, which will later prevent from keeping up with new version or event break down when not expected...
Am I the only one this type of field would make happy ? Or did I miss a point, and this is already there ?
Thanks for your help !
Denis

Александр Страшко

unread,
Oct 12, 2017, 9:41:33 AM10/12/17
to Blockly
Hi Denis You're not the one who needs multi-line input fields!

They would be very useful to me in App Inventor.  But the AI ​​developers said that they will make such a block only when it appears in Blockly. And this, apparently, never!

Interestingly, in the blocks for children on code.org there are multiline fields, working on Shift + Enter.

среда, 24 августа 2016 г., 1:24:11 UTC+3 пользователь Denis W написал:

CyaNn Algoid

unread,
Oct 16, 2018, 8:18:49 AM10/16/18
to Blockly
You can perhapse use built in mixins feature to work-around your problem.

Screenshot from 2018-10-16 14-17-03.png


This example is not really representative, but imagine the top block named "source code" and a block to add new line of code.
It is a solution.

TRUNG NGUYỄN HOÀNG ĐỨC

unread,
Oct 16, 2018, 10:41:46 AM10/16/18
to blo...@googlegroups.com
That doesnt solve the problem , what he want is to modify the behaviour of Blockly in Field object , to be exact , Shift+Enter = Newline 

Vào Th 3, 16 thg 10, 2018 lúc 19:18 CyaNn Algoid <cya...@gmail.com> đã viết:
--
You received this message because you are subscribed to the Google Groups "Blockly" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blockly+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Chaitanya Dhareshwar

unread,
Nov 6, 2018, 12:27:01 PM11/6/18
to Blockly
Ok, so from what I'm seeing there's a "comments" capability in Blockly. You should be able to clone out the code that does that and use it in a way suitable for your project. 

You can see how this works:
1. In https://blockly-demo.appspot.com/static/demos/code/index.html - right click any of the blocks and "add comment"
2. This should add a "?" icon on the block. Clicking it will give you a beige/yellow editor, which is a proper textarea with enter = newline
3. If you export the script this gets converted into a comment - which means Blockly is capable of processing this data

So you can most likely clone out the functionality and rename it, with some modding achieve the intended outcome.

@Александр Страшко you may want to reach out to your service provider - maybe they can use this tip. 

Best,
C

TRUNG NGUYỄN HOÀNG ĐỨC

unread,
Nov 9, 2018, 11:03:41 PM11/9/18
to blo...@googlegroups.com

What a workaround , that should do the job (y)

To unsubscribe from this group and stop receiving emails from it, send an email to blockly+unsubscribe@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages