field.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  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 Field. Used for editable titles, variables, etc.
  22. * This is an abstract class that defines the UI on the block. Actual
  23. * instances would be Blockly.FieldTextInput, Blockly.FieldDropdown, etc.
  24. * @author fraser@google.com (Neil Fraser)
  25. */
  26. 'use strict';
  27. goog.provide('Blockly.Field');
  28. goog.require('goog.asserts');
  29. goog.require('goog.dom');
  30. goog.require('goog.math.Size');
  31. goog.require('goog.style');
  32. goog.require('goog.userAgent');
  33. /**
  34. * Abstract class for an editable field.
  35. * @param {string} text The initial content of the field.
  36. * @param {Function=} opt_validator 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. * @constructor
  41. */
  42. Blockly.Field = function(text, opt_validator) {
  43. this.size_ = new goog.math.Size(0, 25);
  44. this.setValue(text);
  45. this.setValidator(opt_validator);
  46. };
  47. /**
  48. * Temporary cache of text widths.
  49. * @type {Object}
  50. * @private
  51. */
  52. Blockly.Field.cacheWidths_ = null;
  53. /**
  54. * Number of current references to cache.
  55. * @type {number}
  56. * @private
  57. */
  58. Blockly.Field.cacheReference_ = 0;
  59. /**
  60. * Name of field. Unique within each block.
  61. * Static labels are usually unnamed.
  62. * @type {string=}
  63. */
  64. Blockly.Field.prototype.name = undefined;
  65. /**
  66. * Maximum characters of text to display before adding an ellipsis.
  67. * @type {number}
  68. */
  69. Blockly.Field.prototype.maxDisplayLength = 50;
  70. /**
  71. * Visible text to display.
  72. * @type {string}
  73. * @private
  74. */
  75. Blockly.Field.prototype.text_ = '';
  76. /**
  77. * Block this field is attached to. Starts as null, then in set in init.
  78. * @type {Blockly.Block}
  79. * @private
  80. */
  81. Blockly.Field.prototype.sourceBlock_ = null;
  82. /**
  83. * Is the field visible, or hidden due to the block being collapsed?
  84. * @type {boolean}
  85. * @private
  86. */
  87. Blockly.Field.prototype.visible_ = true;
  88. /**
  89. * Validation function called when user edits an editable field.
  90. * @type {Function}
  91. * @private
  92. */
  93. Blockly.Field.prototype.validator_ = null;
  94. /**
  95. * Non-breaking space.
  96. * @const
  97. */
  98. Blockly.Field.NBSP = '\u00A0';
  99. /**
  100. * Editable fields are saved by the XML renderer, non-editable fields are not.
  101. */
  102. Blockly.Field.prototype.EDITABLE = true;
  103. /**
  104. * Attach this field to a block.
  105. * @param {!Blockly.Block} block The block containing this field.
  106. */
  107. Blockly.Field.prototype.setSourceBlock = function(block) {
  108. goog.asserts.assert(!this.sourceBlock_, 'Field already bound to a block.');
  109. this.sourceBlock_ = block;
  110. };
  111. /**
  112. * Install this field on a block.
  113. */
  114. Blockly.Field.prototype.init = function() {
  115. if (this.fieldGroup_) {
  116. // Field has already been initialized once.
  117. return;
  118. }
  119. // Build the DOM.
  120. this.fieldGroup_ = Blockly.createSvgElement('g', {}, null);
  121. if (!this.visible_) {
  122. this.fieldGroup_.style.display = 'none';
  123. }
  124. this.borderRect_ = Blockly.createSvgElement('rect',
  125. {'rx': 4,
  126. 'ry': 4,
  127. 'x': -Blockly.BlockSvg.SEP_SPACE_X / 2,
  128. 'y': 0,
  129. 'height': 16}, this.fieldGroup_, this.sourceBlock_.workspace);
  130. /** @type {!Element} */
  131. this.textElement_ = Blockly.createSvgElement('text',
  132. {'class': 'blocklyText', 'y': this.size_.height - 12.5},
  133. this.fieldGroup_);
  134. this.updateEditable();
  135. this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
  136. this.mouseUpWrapper_ =
  137. Blockly.bindEventWithChecks_(this.fieldGroup_, 'mouseup', this,
  138. this.onMouseUp_);
  139. // Force a render.
  140. this.updateTextNode_();
  141. };
  142. /**
  143. * Dispose of all DOM objects belonging to this editable field.
  144. */
  145. Blockly.Field.prototype.dispose = function() {
  146. if (this.mouseUpWrapper_) {
  147. Blockly.unbindEvent_(this.mouseUpWrapper_);
  148. this.mouseUpWrapper_ = null;
  149. }
  150. this.sourceBlock_ = null;
  151. goog.dom.removeNode(this.fieldGroup_);
  152. this.fieldGroup_ = null;
  153. this.textElement_ = null;
  154. this.borderRect_ = null;
  155. this.validator_ = null;
  156. };
  157. /**
  158. * Add or remove the UI indicating if this field is editable or not.
  159. */
  160. Blockly.Field.prototype.updateEditable = function() {
  161. var group = this.fieldGroup_;
  162. if (!this.EDITABLE || !group) {
  163. return;
  164. }
  165. if (this.sourceBlock_.isEditable()) {
  166. Blockly.addClass_(group, 'blocklyEditableText');
  167. Blockly.removeClass_(group, 'blocklyNonEditableText');
  168. this.fieldGroup_.style.cursor = this.CURSOR;
  169. } else {
  170. Blockly.addClass_(group, 'blocklyNonEditableText');
  171. Blockly.removeClass_(group, 'blocklyEditableText');
  172. this.fieldGroup_.style.cursor = '';
  173. }
  174. };
  175. /**
  176. * Gets whether this editable field is visible or not.
  177. * @return {boolean} True if visible.
  178. */
  179. Blockly.Field.prototype.isVisible = function() {
  180. return this.visible_;
  181. };
  182. /**
  183. * Sets whether this editable field is visible or not.
  184. * @param {boolean} visible True if visible.
  185. */
  186. Blockly.Field.prototype.setVisible = function(visible) {
  187. if (this.visible_ == visible) {
  188. return;
  189. }
  190. this.visible_ = visible;
  191. var root = this.getSvgRoot();
  192. if (root) {
  193. root.style.display = visible ? 'block' : 'none';
  194. this.render_();
  195. }
  196. };
  197. /**
  198. * Sets a new validation function for editable fields.
  199. * @param {Function} handler New validation function, or null.
  200. */
  201. Blockly.Field.prototype.setValidator = function(handler) {
  202. this.validator_ = handler;
  203. };
  204. /**
  205. * Gets the validation function for editable fields.
  206. * @return {Function} Validation function, or null.
  207. */
  208. Blockly.Field.prototype.getValidator = function() {
  209. return this.validator_;
  210. };
  211. /**
  212. * Validates a change. Does nothing. Subclasses may override this.
  213. * @param {string} text The user's text.
  214. * @return {string} No change needed.
  215. */
  216. Blockly.Field.prototype.classValidator = function(text) {
  217. return text;
  218. };
  219. /**
  220. * Calls the validation function for this field, as well as all the validation
  221. * function for the field's class and its parents.
  222. * @param {string} text Proposed text.
  223. * @return {?string} Revised text, or null if invalid.
  224. */
  225. Blockly.Field.prototype.callValidator = function(text) {
  226. var classResult = this.classValidator(text);
  227. if (classResult === null) {
  228. // Class validator rejects value. Game over.
  229. return null;
  230. } else if (classResult !== undefined) {
  231. text = classResult;
  232. }
  233. var userValidator = this.getValidator();
  234. if (userValidator) {
  235. var userResult = userValidator.call(this, text);
  236. if (userResult === null) {
  237. // User validator rejects value. Game over.
  238. return null;
  239. } else if (userResult !== undefined) {
  240. text = userResult;
  241. }
  242. }
  243. return text;
  244. };
  245. /**
  246. * Gets the group element for this editable field.
  247. * Used for measuring the size and for positioning.
  248. * @return {!Element} The group element.
  249. */
  250. Blockly.Field.prototype.getSvgRoot = function() {
  251. return /** @type {!Element} */ (this.fieldGroup_);
  252. };
  253. /**
  254. * Draws the border with the correct width.
  255. * Saves the computed width in a property.
  256. * @private
  257. */
  258. Blockly.Field.prototype.render_ = function() {
  259. if (this.visible_ && this.textElement_) {
  260. var key = this.textElement_.textContent + '\n' +
  261. this.textElement_.className.baseVal;
  262. if (Blockly.Field.cacheWidths_ && Blockly.Field.cacheWidths_[key]) {
  263. var width = Blockly.Field.cacheWidths_[key];
  264. } else {
  265. try {
  266. var width = this.textElement_.getComputedTextLength();
  267. } catch (e) {
  268. // MSIE 11 is known to throw "Unexpected call to method or property
  269. // access." if Blockly is hidden.
  270. var width = this.textElement_.textContent.length * 8;
  271. }
  272. if (Blockly.Field.cacheWidths_) {
  273. Blockly.Field.cacheWidths_[key] = width;
  274. }
  275. }
  276. if (this.borderRect_) {
  277. this.borderRect_.setAttribute('width',
  278. width + Blockly.BlockSvg.SEP_SPACE_X);
  279. }
  280. } else {
  281. var width = 0;
  282. }
  283. this.size_.width = width;
  284. };
  285. /**
  286. * Start caching field widths. Every call to this function MUST also call
  287. * stopCache. Caches must not survive between execution threads.
  288. */
  289. Blockly.Field.startCache = function() {
  290. Blockly.Field.cacheReference_++;
  291. if (!Blockly.Field.cacheWidths_) {
  292. Blockly.Field.cacheWidths_ = {};
  293. }
  294. };
  295. /**
  296. * Stop caching field widths. Unless caching was already on when the
  297. * corresponding call to startCache was made.
  298. */
  299. Blockly.Field.stopCache = function() {
  300. Blockly.Field.cacheReference_--;
  301. if (!Blockly.Field.cacheReference_) {
  302. Blockly.Field.cacheWidths_ = null;
  303. }
  304. };
  305. /**
  306. * Returns the height and width of the field.
  307. * @return {!goog.math.Size} Height and width.
  308. */
  309. Blockly.Field.prototype.getSize = function() {
  310. if (!this.size_.width) {
  311. this.render_();
  312. }
  313. return this.size_;
  314. };
  315. /**
  316. * Returns the height and width of the field,
  317. * accounting for the workspace scaling.
  318. * @return {!goog.math.Size} Height and width.
  319. * @private
  320. */
  321. Blockly.Field.prototype.getScaledBBox_ = function() {
  322. var bBox = this.borderRect_.getBBox();
  323. // Create new object, as getBBox can return an uneditable SVGRect in IE.
  324. return new goog.math.Size(bBox.width * this.sourceBlock_.workspace.scale,
  325. bBox.height * this.sourceBlock_.workspace.scale);
  326. };
  327. /**
  328. * Get the text from this field.
  329. * @return {string} Current text.
  330. */
  331. Blockly.Field.prototype.getText = function() {
  332. return this.text_;
  333. };
  334. /**
  335. * Set the text in this field. Trigger a rerender of the source block.
  336. * @param {*} text New text.
  337. */
  338. Blockly.Field.prototype.setText = function(text) {
  339. if (text === null) {
  340. // No change if null.
  341. return;
  342. }
  343. text = String(text);
  344. if (text === this.text_) {
  345. // No change.
  346. return;
  347. }
  348. this.text_ = text;
  349. this.updateTextNode_();
  350. if (this.sourceBlock_ && this.sourceBlock_.rendered) {
  351. this.sourceBlock_.render();
  352. this.sourceBlock_.bumpNeighbours_();
  353. }
  354. };
  355. /**
  356. * Update the text node of this field to display the current text.
  357. * @private
  358. */
  359. Blockly.Field.prototype.updateTextNode_ = function() {
  360. if (!this.textElement_) {
  361. // Not rendered yet.
  362. return;
  363. }
  364. var text = this.text_;
  365. if (text.length > this.maxDisplayLength) {
  366. // Truncate displayed string and add an ellipsis ('...').
  367. text = text.substring(0, this.maxDisplayLength - 2) + '\u2026';
  368. }
  369. // Replace whitespace with non-breaking spaces so the text doesn't collapse.
  370. text = text.replace(/\s/g, Blockly.Field.NBSP);
  371. if (this.sourceBlock_.RTL && text) {
  372. // The SVG is LTR, force text to be RTL.
  373. text += '\u200F';
  374. }
  375. if (!text) {
  376. // Prevent the field from disappearing if empty.
  377. text = Blockly.Field.NBSP;
  378. }
  379. // Replace the text.
  380. goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_));
  381. var textNode = document.createTextNode(text);
  382. this.textElement_.appendChild(textNode);
  383. // Cached width is obsolete. Clear it.
  384. this.size_.width = 0;
  385. };
  386. /**
  387. * By default there is no difference between the human-readable text and
  388. * the language-neutral values. Subclasses (such as dropdown) may define this.
  389. * @return {string} Current text.
  390. */
  391. Blockly.Field.prototype.getValue = function() {
  392. return this.getText();
  393. };
  394. /**
  395. * By default there is no difference between the human-readable text and
  396. * the language-neutral values. Subclasses (such as dropdown) may define this.
  397. * @param {string} newText New text.
  398. */
  399. Blockly.Field.prototype.setValue = function(newText) {
  400. if (newText === null) {
  401. // No change if null.
  402. return;
  403. }
  404. var oldText = this.getValue();
  405. if (oldText == newText) {
  406. return;
  407. }
  408. if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
  409. Blockly.Events.fire(new Blockly.Events.Change(
  410. this.sourceBlock_, 'field', this.name, oldText, newText));
  411. }
  412. this.setText(newText);
  413. };
  414. /**
  415. * Handle a mouse up event on an editable field.
  416. * @param {!Event} e Mouse up event.
  417. * @private
  418. */
  419. Blockly.Field.prototype.onMouseUp_ = function(e) {
  420. if ((goog.userAgent.IPHONE || goog.userAgent.IPAD) &&
  421. !goog.userAgent.isVersionOrHigher('537.51.2') &&
  422. e.layerX !== 0 && e.layerY !== 0) {
  423. // Old iOS spawns a bogus event on the next touch after a 'prompt()' edit.
  424. // Unlike the real events, these have a layerX and layerY set.
  425. return;
  426. } else if (Blockly.isRightButton(e)) {
  427. // Right-click.
  428. return;
  429. } else if (this.sourceBlock_.workspace.isDragging()) {
  430. // Drag operation is concluding. Don't open the editor.
  431. return;
  432. } else if (this.sourceBlock_.isEditable()) {
  433. // Non-abstract sub-classes must define a showEditor_ method.
  434. this.showEditor_(e);
  435. // The field is handling the touch, but we also want the blockSvg onMouseUp
  436. // handler to fire, so we will leave the touch identifier as it is.
  437. // The next onMouseUp is responsible for nulling it out.
  438. }
  439. };
  440. /**
  441. * Change the tooltip text for this field.
  442. * @param {string|!Element} newTip Text for tooltip or a parent element to
  443. * link to for its tooltip.
  444. */
  445. Blockly.Field.prototype.setTooltip = function(newTip) {
  446. // Non-abstract sub-classes may wish to implement this. See FieldLabel.
  447. };
  448. /**
  449. * Return the absolute coordinates of the top-left corner of this field.
  450. * The origin (0,0) is the top-left corner of the page body.
  451. * @return {!goog.math.Coordinate} Object with .x and .y properties.
  452. * @private
  453. */
  454. Blockly.Field.prototype.getAbsoluteXY_ = function() {
  455. return goog.style.getPageOffset(this.borderRect_);
  456. };