field_textarea.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2012 Google Inc.
  6. * https://blockly.googlecode.com/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview Text Area field.
  22. * @author fraser@google.com (Neil Fraser)
  23. * @author Andrew Mee
  24. * @author acbart@vt.edu (Austin Cory Bart)
  25. */
  26. 'use strict';
  27. goog.provide('Blockly.FieldTextArea');
  28. goog.require('Blockly.Field');
  29. goog.require('Blockly.Msg');
  30. goog.require('goog.asserts');
  31. goog.require('goog.dom');
  32. goog.require('goog.userAgent');
  33. /**
  34. * Class for an editable text field.
  35. * @param {string} text The initial content of the field.
  36. * @param {Function} opt_changeHandler An optional function that is called
  37. * to validate any constraints on what the user entered. Takes the new
  38. * text as an argument and returns either the accepted text, a replacement
  39. * text, or null to abort the change.
  40. * @extends {Blockly.Field}
  41. * @constructor
  42. */
  43. Blockly.FieldTextArea = function(text, opt_changeHandler) {
  44. Blockly.FieldTextArea.superClass_.constructor.call(this, text);
  45. this.changeHandler_ = opt_changeHandler;
  46. };
  47. goog.inherits(Blockly.FieldTextArea, Blockly.Field);
  48. /**
  49. * Point size of text. Should match blocklyText's font-size in CSS.
  50. */
  51. Blockly.FieldTextArea.FONTSIZE = 11;
  52. /**
  53. * Clone this FieldTextArea.
  54. * @return {!Blockly.FieldTextArea} The result of calling the constructor again
  55. * with the current values of the arguments used during construction.
  56. */
  57. Blockly.FieldTextArea.prototype.clone = function() {
  58. return new Blockly.FieldTextArea(this.getText(), this.changeHandler_);
  59. };
  60. /**
  61. * Mouse cursor style when over the hotspot that initiates the editor.
  62. */
  63. Blockly.FieldTextArea.prototype.CURSOR = 'text';
  64. /**
  65. * Allow browser to spellcheck this field.
  66. * @private
  67. */
  68. Blockly.FieldTextArea.prototype.spellcheck_ = true;
  69. /**
  70. * Close the input widget if this input is being deleted.
  71. */
  72. Blockly.FieldTextArea.prototype.dispose = function() {
  73. Blockly.WidgetDiv.hideIfOwner(this);
  74. Blockly.FieldTextArea.superClass_.dispose.call(this);
  75. };
  76. /**
  77. * Set the text in this field.
  78. * @param {?string} text New text.
  79. * @override
  80. */
  81. Blockly.FieldTextArea.prototype.setText = function(text) {
  82. if (text === null) {
  83. // No change if null.
  84. return;
  85. }
  86. var oldText = this.text_;
  87. if (this.sourceBlock_ && this.changeHandler_) {
  88. var validated = this.changeHandler_(text);
  89. // If the new text is invalid, validation returns null.
  90. // In this case we still want to display the illegal result.
  91. if (validated !== null && validated !== undefined) {
  92. text = validated;
  93. }
  94. }
  95. //Blockly.Field.prototype.setText.call(this, text);
  96. if (text === null || text === this.text_) {
  97. // No change if null.
  98. return;
  99. }
  100. this.text_ = text;
  101. this.updateTextNode_();
  102. if (this.sourceBlock_ && this.sourceBlock_.rendered) {
  103. this.sourceBlock_.render();
  104. this.sourceBlock_.bumpNeighbours_();
  105. Blockly.Events.fire(new Blockly.Events.Change(this.sourceBlock_, 'field', this.name, oldText, text));
  106. //this.sourceBlock_.workspace.fireChangeEvent();
  107. }
  108. };
  109. /**
  110. * Update the text node of this field to display the current text.
  111. * @private
  112. */
  113. Blockly.FieldTextArea.prototype.updateTextNode_ = function() {
  114. if (!this.textElement_) {
  115. // Not rendered yet.
  116. return;
  117. }
  118. this.textElement_.setAttribute('class', 'blocklyText blocklyTextCode');
  119. var text = this.text_;
  120. // Empty the text element.
  121. if (this.textElement_ !== undefined) {
  122. goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_));
  123. }
  124. // Replace whitespace with non-breaking spaces so the text doesn't collapse.
  125. if (Blockly.RTL && text) {
  126. // The SVG is LTR, force text to be RTL.
  127. text += '\u200F';
  128. }
  129. if (!text) {
  130. // Prevent the field from disappearing if empty.
  131. text = Blockly.Field.NBSP;
  132. }
  133. //text = text.replace(/\s/g, Blockly.Field.NBSP);
  134. var y=12.5 + 2; // Hack to fix placement
  135. var that=this;
  136. //var textNode = document.createTextNode(text);
  137. text.split(/\n/).map(function(textline){
  138. textline = textline.replace(/\s/g, Blockly.Field.NBSP);
  139. var tspan = Blockly.createSvgElement('tspan', {x:0,y:y}, that.textElement_);
  140. var textNode = document.createTextNode(textline);
  141. tspan.appendChild(textNode);
  142. y+=20;
  143. });
  144. // Cached width is obsolete. Clear it.
  145. this.size_.width = 0;
  146. var that = this;
  147. this.fixAfterLoad = setTimeout(function() {
  148. that.render_();
  149. if (this.sourceBlock_ && this.sourceBlock_.rendered) {
  150. that.sourceBlock_.render();
  151. }
  152. }, 0);
  153. };
  154. /**
  155. * Draws the border with the correct width.
  156. * Saves the computed width in a property.
  157. * @private
  158. */
  159. Blockly.FieldTextArea.prototype.render_ = function() {
  160. if (!this.visible_ || !this.textElement_) {
  161. return;
  162. }
  163. try {
  164. this.size_.width = this.textElement_.getBBox().width + 5;
  165. } catch (e) {
  166. this.size_.width = this.textElement_.textContent.length*8 + 5;
  167. }
  168. this.size_.height= (this.text_.split(/\n/).length ||1)*20 + (Blockly.BlockSvg.SEP_SPACE_Y+5) ;
  169. if (this.borderRect_) {
  170. this.borderRect_.setAttribute('width',
  171. this.size_.width + Blockly.BlockSvg.SEP_SPACE_X);
  172. this.borderRect_.setAttribute('height',
  173. this.size_.height - (Blockly.BlockSvg.SEP_SPACE_Y+5));
  174. }
  175. };
  176. /**
  177. * Show the inline free-text editor on top of the text.
  178. * @param {boolean=} opt_quietInput True if editor should be created without
  179. * focus. Defaults to false.
  180. * @private
  181. */
  182. Blockly.FieldTextArea.prototype.showEditor_ = function(opt_quietInput) {
  183. var quietInput = opt_quietInput || false;
  184. if (!quietInput && (goog.userAgent.MOBILE || goog.userAgent.ANDROID ||
  185. goog.userAgent.IPAD)) {
  186. // Mobile browsers have issues with in-line textareas (focus & keyboards).
  187. var newValue = window.prompt(Blockly.Msg.CHANGE_VALUE_TITLE, this.text_);
  188. if (this.changeHandler_) {
  189. var override = this.changeHandler_(newValue);
  190. if (override !== undefined) {
  191. newValue = override;
  192. }
  193. }
  194. if (newValue !== null) {
  195. this.setText(newValue);
  196. }
  197. return;
  198. }
  199. Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_());
  200. var div = Blockly.WidgetDiv.DIV;
  201. // Create the input.
  202. var htmlInput = goog.dom.createDom('textarea', 'blocklyHtmlInput');
  203. var fontSize = (Blockly.FieldTextArea.FONTSIZE *this.sourceBlock_.workspace.scale) + 'pt';
  204. div.style.fontSize = fontSize;
  205. htmlInput.style.fontSize = fontSize;
  206. htmlInput.style.fontFamily = 'monospace';
  207. htmlInput.setAttribute('spellcheck', this.spellcheck_);
  208. Blockly.FieldTextArea.htmlInput_ = htmlInput;
  209. htmlInput.style.resize = 'none';
  210. htmlInput.style['line-height'] = '20px';
  211. htmlInput.style['overflow'] = 'hidden';
  212. htmlInput.style.height = '100%';//this.size_.height - Blockly.BlockSvg.SEP_SPACE_Y + 'px';
  213. div.appendChild(htmlInput);
  214. htmlInput.value = htmlInput.defaultValue = this.text_;
  215. htmlInput.oldValue_ = null;
  216. this.validate_();
  217. this.resizeEditor_();
  218. if (!quietInput) {
  219. htmlInput.focus();
  220. htmlInput.select();
  221. }
  222. // Bind to keydown -- trap Enter without IME and Esc to hide.
  223. htmlInput.onKeyDownWrapper_ =
  224. Blockly.bindEvent_(htmlInput, 'keydown', this, this.onHtmlInputKeyDown_);
  225. // Bind to keyup -- trap Enter; resize after every keystroke.
  226. // Bind to keyup -- trap Enter and Esc; resize after every keystroke.
  227. htmlInput.onKeyUpWrapper_ =
  228. Blockly.bindEvent_(htmlInput, 'keyup', this, this.onHtmlInputChange_);
  229. // Bind to keyPress -- repeatedly resize when holding down a key.
  230. htmlInput.onKeyPressWrapper_ =
  231. Blockly.bindEvent_(htmlInput, 'keypress', this, this.onHtmlInputChange_);
  232. var workspaceSvg = this.sourceBlock_.workspace.getCanvas();
  233. htmlInput.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this);
  234. this.sourceBlock_.workspace.addChangeListener(htmlInput.onWorkspaceChangeWrapper_);
  235. };
  236. /**
  237. * Handle key down to the editor.
  238. * @param {!Event} e Keyboard event.
  239. * @private
  240. */
  241. Blockly.FieldTextArea.prototype.onHtmlInputKeyDown_ = function(e) {
  242. var htmlInput = Blockly.FieldTextArea.htmlInput_;
  243. var escKey = 27;
  244. if (e.keyCode == escKey) {
  245. this.setText(htmlInput.defaultValue);
  246. Blockly.WidgetDiv.hide();
  247. }
  248. };
  249. /**
  250. * Handle a change to the editor.
  251. * @param {!Event} e Keyboard event.
  252. * @private
  253. */
  254. Blockly.FieldTextArea.prototype.onHtmlInputChange_ = function(e) {
  255. var htmlInput = Blockly.FieldTextArea.htmlInput_;
  256. var escKey = 27;
  257. if (e.keyCode == escKey) {
  258. // Esc
  259. this.setText(htmlInput.defaultValue);
  260. Blockly.WidgetDiv.hide();
  261. } else {
  262. // Update source block.
  263. var text = htmlInput.value;
  264. if (text !== htmlInput.oldValue_) {
  265. htmlInput.oldValue_ = text;
  266. this.setText(text);
  267. this.validate_();
  268. } else if (goog.userAgent.WEBKIT) {
  269. // Cursor key. Render the source block to show the caret moving.
  270. // Chrome only (version 26, OS X).
  271. this.sourceBlock_.render();
  272. }
  273. this.resizeEditor_();
  274. }
  275. };
  276. /**
  277. * Check to see if the contents of the editor validates.
  278. * Style the editor accordingly.
  279. * @private
  280. */
  281. Blockly.FieldTextArea.prototype.validate_ = function() {
  282. var valid = true;
  283. goog.asserts.assertObject(Blockly.FieldTextArea.htmlInput_);
  284. var htmlInput = /** @type {!Element} */ (Blockly.FieldTextArea.htmlInput_);
  285. if (this.changeHandler_) {
  286. valid = this.changeHandler_(htmlInput.value);
  287. }
  288. if (valid === null) {
  289. Blockly.addClass_(htmlInput, 'blocklyInvalidInput');
  290. } else {
  291. Blockly.removeClass_(htmlInput, 'blocklyInvalidInput');
  292. }
  293. };
  294. /**
  295. * Resize the editor and the underlying block to fit the text.
  296. * @private
  297. */
  298. Blockly.FieldTextArea.prototype.resizeEditor_ = function() {
  299. var div = Blockly.WidgetDiv.DIV;
  300. var bBox = this.fieldGroup_.getBBox();
  301. var htmlInput = Blockly.FieldTextArea.htmlInput_;
  302. //div.style.width = bBox.width + 'px';
  303. if (htmlInput.clientHeight < htmlInput.scrollHeight) {
  304. div.style.width = (bBox.width * this.sourceBlock_.workspace.scale) + 'px';
  305. } else {
  306. div.style.width = bBox.width * this.sourceBlock_.workspace.scale + 'px';
  307. }
  308. div.style.height = bBox.height * this.sourceBlock_.workspace.scale + 'px';
  309. // Position the editor
  310. var xy = this.getAbsoluteXY_();
  311. // In RTL mode block fields and LTR input fields the left edge moves,
  312. // whereas the right edge is fixed. Reposition the editor.
  313. if (this.sourceBlock_.RTL) {
  314. var borderBBox = this.getScaledBBox_();
  315. xy.x += borderBBox.width;
  316. xy.x -= div.offsetWidth;
  317. }
  318. // Shift by a few pixels to line up exactly.
  319. xy.y += 1;
  320. if (goog.userAgent.GECKO && Blockly.WidgetDiv.DIV.style.top) {
  321. // Firefox mis-reports the location of the border by a pixel
  322. // once the WidgetDiv is moved into position.
  323. xy.x -= 1;
  324. xy.y -= 1;
  325. }
  326. if (goog.userAgent.WEBKIT) {
  327. xy.y -= 3;
  328. }
  329. div.style.left = xy.x + 'px';
  330. div.style.top = xy.y + 'px';
  331. };
  332. /**
  333. * Close the editor, save the results, and dispose of the editable
  334. * text field's elements.
  335. * @return {!Function} Closure to call on destruction of the WidgetDiv.
  336. * @private
  337. */
  338. Blockly.FieldTextArea.prototype.widgetDispose_ = function() {
  339. var thisField = this;
  340. return function() {
  341. var htmlInput = Blockly.FieldTextArea.htmlInput_;
  342. // Save the edit (if it validates).
  343. var text = htmlInput.value;
  344. if (thisField.changeHandler_) {
  345. text = thisField.changeHandler_(text);
  346. if (text === null) {
  347. // Invalid edit.
  348. text = htmlInput.defaultValue;
  349. }
  350. }
  351. thisField.setText(text);
  352. thisField.sourceBlock_.rendered && thisField.sourceBlock_.render();
  353. Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
  354. Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);
  355. thisField.sourceBlock_.workspace.removeChangeListener(htmlInput.onWorkspaceChangeWrapper_);
  356. Blockly.FieldTextArea.htmlInput_ = null;
  357. // Delete the width property.
  358. var style = Blockly.WidgetDiv.DIV.style;
  359. style.width = 'auto';
  360. style.height = 'auto';
  361. style.fontSize = '';
  362. };
  363. };