field_textinput.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2012 Google Inc.
  6. * https://developers.google.com/blockly/
  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 input field.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.FieldTextInput');
  26. goog.require('Blockly.Field');
  27. goog.require('Blockly.Msg');
  28. goog.require('goog.asserts');
  29. goog.require('goog.dom');
  30. goog.require('goog.userAgent');
  31. /**
  32. * Class for an editable text field.
  33. * @param {string} text The initial content of the field.
  34. * @param {Function=} opt_validator An optional function that is called
  35. * to validate any constraints on what the user entered. Takes the new
  36. * text as an argument and returns either the accepted text, a replacement
  37. * text, or null to abort the change.
  38. * @extends {Blockly.Field}
  39. * @constructor
  40. */
  41. Blockly.FieldTextInput = function(text, opt_validator) {
  42. Blockly.FieldTextInput.superClass_.constructor.call(this, text,
  43. opt_validator);
  44. };
  45. goog.inherits(Blockly.FieldTextInput, Blockly.Field);
  46. /**
  47. * Point size of text. Should match blocklyText's font-size in CSS.
  48. */
  49. Blockly.FieldTextInput.FONTSIZE = 11;
  50. /**
  51. * Mouse cursor style when over the hotspot that initiates the editor.
  52. */
  53. Blockly.FieldTextInput.prototype.CURSOR = 'text';
  54. /**
  55. * Allow browser to spellcheck this field.
  56. * @private
  57. */
  58. Blockly.FieldTextInput.prototype.spellcheck_ = true;
  59. /**
  60. * Close the input widget if this input is being deleted.
  61. */
  62. Blockly.FieldTextInput.prototype.dispose = function() {
  63. Blockly.WidgetDiv.hideIfOwner(this);
  64. Blockly.FieldTextInput.superClass_.dispose.call(this);
  65. };
  66. /**
  67. * Set the text in this field.
  68. * @param {?string} text New text.
  69. * @override
  70. */
  71. Blockly.FieldTextInput.prototype.setValue = function(text) {
  72. if (text === null) {
  73. return; // No change if null.
  74. }
  75. if (this.sourceBlock_) {
  76. var validated = this.callValidator(text);
  77. // If the new text is invalid, validation returns null.
  78. // In this case we still want to display the illegal result.
  79. if (validated !== null) {
  80. text = validated;
  81. }
  82. }
  83. Blockly.Field.prototype.setValue.call(this, text);
  84. };
  85. /**
  86. * Set whether this field is spellchecked by the browser.
  87. * @param {boolean} check True if checked.
  88. */
  89. Blockly.FieldTextInput.prototype.setSpellcheck = function(check) {
  90. this.spellcheck_ = check;
  91. };
  92. /**
  93. * Show the inline free-text editor on top of the text.
  94. * @param {boolean=} opt_quietInput True if editor should be created without
  95. * focus. Defaults to false.
  96. * @private
  97. */
  98. Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) {
  99. this.workspace_ = this.sourceBlock_.workspace;
  100. var quietInput = opt_quietInput || false;
  101. if (!quietInput && (goog.userAgent.MOBILE || goog.userAgent.ANDROID ||
  102. goog.userAgent.IPAD)) {
  103. // Mobile browsers have issues with in-line textareas (focus & keyboards).
  104. var newValue = window.prompt(Blockly.Msg.CHANGE_VALUE_TITLE, this.text_);
  105. if (this.sourceBlock_) {
  106. newValue = this.callValidator(newValue);
  107. }
  108. this.setValue(newValue);
  109. return;
  110. }
  111. Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_());
  112. var div = Blockly.WidgetDiv.DIV;
  113. // Create the input.
  114. var htmlInput = goog.dom.createDom('input', 'blocklyHtmlInput');
  115. htmlInput.setAttribute('spellcheck', this.spellcheck_);
  116. var fontSize =
  117. (Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt';
  118. div.style.fontSize = fontSize;
  119. htmlInput.style.fontSize = fontSize;
  120. /** @type {!HTMLInputElement} */
  121. Blockly.FieldTextInput.htmlInput_ = htmlInput;
  122. div.appendChild(htmlInput);
  123. htmlInput.value = htmlInput.defaultValue = this.text_;
  124. htmlInput.oldValue_ = null;
  125. this.validate_();
  126. this.resizeEditor_();
  127. if (!quietInput) {
  128. htmlInput.focus();
  129. htmlInput.select();
  130. }
  131. // Bind to keydown -- trap Enter without IME and Esc to hide.
  132. htmlInput.onKeyDownWrapper_ =
  133. Blockly.bindEvent_(htmlInput, 'keydown', this, this.onHtmlInputKeyDown_);
  134. // Bind to keyup -- trap Enter; resize after every keystroke.
  135. htmlInput.onKeyUpWrapper_ =
  136. Blockly.bindEvent_(htmlInput, 'keyup', this, this.onHtmlInputChange_);
  137. // Bind to keyPress -- repeatedly resize when holding down a key.
  138. htmlInput.onKeyPressWrapper_ =
  139. Blockly.bindEvent_(htmlInput, 'keypress', this, this.onHtmlInputChange_);
  140. htmlInput.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this);
  141. this.workspace_.addChangeListener(htmlInput.onWorkspaceChangeWrapper_);
  142. };
  143. /**
  144. * Handle key down to the editor.
  145. * @param {!Event} e Keyboard event.
  146. * @private
  147. */
  148. Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) {
  149. var htmlInput = Blockly.FieldTextInput.htmlInput_;
  150. var tabKey = 9, enterKey = 13, escKey = 27;
  151. if (e.keyCode == enterKey) {
  152. Blockly.WidgetDiv.hide();
  153. } else if (e.keyCode == escKey) {
  154. htmlInput.value = htmlInput.defaultValue;
  155. Blockly.WidgetDiv.hide();
  156. } else if (e.keyCode == tabKey) {
  157. Blockly.WidgetDiv.hide();
  158. this.sourceBlock_.tab(this, !e.shiftKey);
  159. e.preventDefault();
  160. }
  161. };
  162. /**
  163. * Handle a change to the editor.
  164. * @param {!Event} e Keyboard event.
  165. * @private
  166. */
  167. Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(e) {
  168. var htmlInput = Blockly.FieldTextInput.htmlInput_;
  169. // Update source block.
  170. var text = htmlInput.value;
  171. if (text !== htmlInput.oldValue_) {
  172. htmlInput.oldValue_ = text;
  173. this.setValue(text);
  174. this.validate_();
  175. } else if (goog.userAgent.WEBKIT) {
  176. // Cursor key. Render the source block to show the caret moving.
  177. // Chrome only (version 26, OS X).
  178. this.sourceBlock_.render();
  179. }
  180. this.resizeEditor_();
  181. Blockly.svgResize(this.sourceBlock_.workspace);
  182. };
  183. /**
  184. * Check to see if the contents of the editor validates.
  185. * Style the editor accordingly.
  186. * @private
  187. */
  188. Blockly.FieldTextInput.prototype.validate_ = function() {
  189. var valid = true;
  190. goog.asserts.assertObject(Blockly.FieldTextInput.htmlInput_);
  191. var htmlInput = Blockly.FieldTextInput.htmlInput_;
  192. if (this.sourceBlock_) {
  193. valid = this.callValidator(htmlInput.value);
  194. }
  195. if (valid === null) {
  196. Blockly.addClass_(htmlInput, 'blocklyInvalidInput');
  197. } else {
  198. Blockly.removeClass_(htmlInput, 'blocklyInvalidInput');
  199. }
  200. };
  201. /**
  202. * Resize the editor and the underlying block to fit the text.
  203. * @private
  204. */
  205. Blockly.FieldTextInput.prototype.resizeEditor_ = function() {
  206. var div = Blockly.WidgetDiv.DIV;
  207. var bBox = this.fieldGroup_.getBBox();
  208. div.style.width = bBox.width * this.workspace_.scale + 'px';
  209. div.style.height = bBox.height * this.workspace_.scale + 'px';
  210. var xy = this.getAbsoluteXY_();
  211. // In RTL mode block fields and LTR input fields the left edge moves,
  212. // whereas the right edge is fixed. Reposition the editor.
  213. if (this.sourceBlock_.RTL) {
  214. var borderBBox = this.getScaledBBox_();
  215. xy.x += borderBBox.width;
  216. xy.x -= div.offsetWidth;
  217. }
  218. // Shift by a few pixels to line up exactly.
  219. xy.y += 1;
  220. if (goog.userAgent.GECKO && Blockly.WidgetDiv.DIV.style.top) {
  221. // Firefox mis-reports the location of the border by a pixel
  222. // once the WidgetDiv is moved into position.
  223. xy.x -= 1;
  224. xy.y -= 1;
  225. }
  226. if (goog.userAgent.WEBKIT) {
  227. xy.y -= 3;
  228. }
  229. div.style.left = xy.x + 'px';
  230. div.style.top = xy.y + 'px';
  231. };
  232. /**
  233. * Close the editor, save the results, and dispose of the editable
  234. * text field's elements.
  235. * @return {!Function} Closure to call on destruction of the WidgetDiv.
  236. * @private
  237. */
  238. Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
  239. var thisField = this;
  240. return function() {
  241. var htmlInput = Blockly.FieldTextInput.htmlInput_;
  242. // Save the edit (if it validates).
  243. var text = htmlInput.value;
  244. if (thisField.sourceBlock_) {
  245. var text1 = thisField.callValidator(text);
  246. if (text1 === null) {
  247. // Invalid edit.
  248. text = htmlInput.defaultValue;
  249. } else {
  250. // Validation function has changed the text.
  251. text = text1;
  252. }
  253. }
  254. thisField.setValue(text);
  255. thisField.sourceBlock_.rendered && thisField.sourceBlock_.render();
  256. Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_);
  257. Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
  258. Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);
  259. thisField.workspace_.removeChangeListener(
  260. htmlInput.onWorkspaceChangeWrapper_);
  261. Blockly.FieldTextInput.htmlInput_ = null;
  262. // Delete style properties.
  263. var style = Blockly.WidgetDiv.DIV.style;
  264. style.width = 'auto';
  265. style.height = 'auto';
  266. style.fontSize = '';
  267. };
  268. };
  269. /**
  270. * Ensure that only a number may be entered.
  271. * @param {string} text The user's text.
  272. * @return {?string} A string representing a valid number, or null if invalid.
  273. */
  274. Blockly.FieldTextInput.numberValidator = function(text) {
  275. console.warn('Blockly.FieldTextInput.numberValidator is deprecated. ' +
  276. 'Use Blockly.FieldNumber instead.');
  277. if (text === null) {
  278. return null;
  279. }
  280. text = String(text);
  281. // TODO: Handle cases like 'ten', '1.203,14', etc.
  282. // 'O' is sometimes mistaken for '0' by inexperienced users.
  283. text = text.replace(/O/ig, '0');
  284. // Strip out thousands separators.
  285. text = text.replace(/,/g, '');
  286. var n = parseFloat(text || 0);
  287. return isNaN(n) ? null : String(n);
  288. };
  289. /**
  290. * Ensure that only a nonnegative integer may be entered.
  291. * @param {string} text The user's text.
  292. * @return {?string} A string representing a valid int, or null if invalid.
  293. */
  294. Blockly.FieldTextInput.nonnegativeIntegerValidator = function(text) {
  295. var n = Blockly.FieldTextInput.numberValidator(text);
  296. if (n) {
  297. n = String(Math.max(0, Math.floor(n)));
  298. }
  299. return n;
  300. };