field.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  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.bindEvent_(this.fieldGroup_, 'mouseup', this, this.onMouseUp_);
  138. // Force a render.
  139. this.updateTextNode_();
  140. };
  141. /**
  142. * Dispose of all DOM objects belonging to this editable field.
  143. */
  144. Blockly.Field.prototype.dispose = function() {
  145. if (this.mouseUpWrapper_) {
  146. Blockly.unbindEvent_(this.mouseUpWrapper_);
  147. this.mouseUpWrapper_ = null;
  148. }
  149. this.sourceBlock_ = null;
  150. goog.dom.removeNode(this.fieldGroup_);
  151. this.fieldGroup_ = null;
  152. this.textElement_ = null;
  153. this.borderRect_ = null;
  154. this.validator_ = null;
  155. };
  156. /**
  157. * Add or remove the UI indicating if this field is editable or not.
  158. */
  159. Blockly.Field.prototype.updateEditable = function() {
  160. if (!this.EDITABLE || !this.fieldGroup_) {
  161. return;
  162. }
  163. if (this.sourceBlock_.isEditable()) {
  164. Blockly.addClass_(/** @type {!Element} */ (this.fieldGroup_),
  165. 'blocklyEditableText');
  166. Blockly.removeClass_(/** @type {!Element} */ (this.fieldGroup_),
  167. 'blocklyNonEditableText');
  168. this.fieldGroup_.style.cursor = this.CURSOR;
  169. } else {
  170. Blockly.addClass_(/** @type {!Element} */ (this.fieldGroup_),
  171. 'blocklyNonEditableText');
  172. Blockly.removeClass_(/** @type {!Element} */ (this.fieldGroup_),
  173. 'blocklyEditableText');
  174. this.fieldGroup_.style.cursor = '';
  175. }
  176. };
  177. /**
  178. * Gets whether this editable field is visible or not.
  179. * @return {boolean} True if visible.
  180. */
  181. Blockly.Field.prototype.isVisible = function() {
  182. return this.visible_;
  183. };
  184. /**
  185. * Sets whether this editable field is visible or not.
  186. * @param {boolean} visible True if visible.
  187. */
  188. Blockly.Field.prototype.setVisible = function(visible) {
  189. if (this.visible_ == visible) {
  190. return;
  191. }
  192. this.visible_ = visible;
  193. var root = this.getSvgRoot();
  194. if (root) {
  195. root.style.display = visible ? 'block' : 'none';
  196. this.render_();
  197. }
  198. };
  199. /**
  200. * Sets a new validation function for editable fields.
  201. * @param {Function} handler New validation function, or null.
  202. */
  203. Blockly.Field.prototype.setValidator = function(handler) {
  204. this.validator_ = handler;
  205. };
  206. /**
  207. * Gets the validation function for editable fields.
  208. * @return {Function} Validation function, or null.
  209. */
  210. Blockly.Field.prototype.getValidator = function() {
  211. return this.validator_;
  212. };
  213. /**
  214. * Validates a change. Does nothing. Subclasses may override this.
  215. * @param {string} text The user's text.
  216. * @return {string} No change needed.
  217. */
  218. Blockly.Field.prototype.classValidator = function(text) {
  219. return text;
  220. };
  221. /**
  222. * Calls the validation function for this field, as well as all the validation
  223. * function for the field's class and its parents.
  224. * @param {string} text Proposed text.
  225. * @return {?string} Revised text, or null if invalid.
  226. */
  227. Blockly.Field.prototype.callValidator = function(text) {
  228. var classResult = this.classValidator(text);
  229. if (classResult === null) {
  230. // Class validator rejects value. Game over.
  231. return null;
  232. } else if (classResult !== undefined) {
  233. text = classResult;
  234. }
  235. var userValidator = this.getValidator();
  236. if (userValidator) {
  237. var userResult = userValidator.call(this, text);
  238. if (userResult === null) {
  239. // User validator rejects value. Game over.
  240. return null;
  241. } else if (userResult !== undefined) {
  242. text = userResult;
  243. }
  244. }
  245. return text;
  246. };
  247. /**
  248. * Gets the group element for this editable field.
  249. * Used for measuring the size and for positioning.
  250. * @return {!Element} The group element.
  251. */
  252. Blockly.Field.prototype.getSvgRoot = function() {
  253. return /** @type {!Element} */ (this.fieldGroup_);
  254. };
  255. /**
  256. * Draws the border with the correct width.
  257. * Saves the computed width in a property.
  258. * @private
  259. */
  260. Blockly.Field.prototype.render_ = function() {
  261. if (this.visible_ && this.textElement_) {
  262. var key = this.textElement_.textContent + '\n' +
  263. this.textElement_.className.baseVal;
  264. if (Blockly.Field.cacheWidths_ && Blockly.Field.cacheWidths_[key]) {
  265. var width = Blockly.Field.cacheWidths_[key];
  266. } else {
  267. try {
  268. var width = this.textElement_.getComputedTextLength();
  269. } catch (e) {
  270. // MSIE 11 is known to throw "Unexpected call to method or property
  271. // access." if Blockly is hidden.
  272. var width = this.textElement_.textContent.length * 8;
  273. }
  274. if (Blockly.Field.cacheWidths_) {
  275. Blockly.Field.cacheWidths_[key] = width;
  276. }
  277. }
  278. if (this.borderRect_) {
  279. this.borderRect_.setAttribute('width',
  280. width + Blockly.BlockSvg.SEP_SPACE_X);
  281. }
  282. } else {
  283. var width = 0;
  284. }
  285. this.size_.width = width;
  286. };
  287. /**
  288. * Start caching field widths. Every call to this function MUST also call
  289. * stopCache. Caches must not survive between execution threads.
  290. */
  291. Blockly.Field.startCache = function() {
  292. Blockly.Field.cacheReference_++;
  293. if (!Blockly.Field.cacheWidths_) {
  294. Blockly.Field.cacheWidths_ = {};
  295. }
  296. };
  297. /**
  298. * Stop caching field widths. Unless caching was already on when the
  299. * corresponding call to startCache was made.
  300. */
  301. Blockly.Field.stopCache = function() {
  302. Blockly.Field.cacheReference_--;
  303. if (!Blockly.Field.cacheReference_) {
  304. Blockly.Field.cacheWidths_ = null;
  305. }
  306. };
  307. /**
  308. * Returns the height and width of the field.
  309. * @return {!goog.math.Size} Height and width.
  310. */
  311. Blockly.Field.prototype.getSize = function() {
  312. if (!this.size_.width) {
  313. this.render_();
  314. }
  315. return this.size_;
  316. };
  317. /**
  318. * Returns the height and width of the field,
  319. * accounting for the workspace scaling.
  320. * @return {!goog.math.Size} Height and width.
  321. * @private
  322. */
  323. Blockly.Field.prototype.getScaledBBox_ = function() {
  324. var bBox = this.borderRect_.getBBox();
  325. // Create new object, as getBBox can return an uneditable SVGRect in IE.
  326. return new goog.math.Size(bBox.width * this.sourceBlock_.workspace.scale,
  327. bBox.height * this.sourceBlock_.workspace.scale);
  328. };
  329. /**
  330. * Get the text from this field.
  331. * @return {string} Current text.
  332. */
  333. Blockly.Field.prototype.getText = function() {
  334. return this.text_;
  335. };
  336. /**
  337. * Set the text in this field. Trigger a rerender of the source block.
  338. * @param {*} text New text.
  339. */
  340. Blockly.Field.prototype.setText = function(text) {
  341. if (text === null) {
  342. // No change if null.
  343. return;
  344. }
  345. text = String(text);
  346. if (text === this.text_) {
  347. // No change.
  348. return;
  349. }
  350. this.text_ = text;
  351. this.updateTextNode_();
  352. if (this.sourceBlock_ && this.sourceBlock_.rendered) {
  353. this.sourceBlock_.render();
  354. this.sourceBlock_.bumpNeighbours_();
  355. }
  356. };
  357. /**
  358. * Update the text node of this field to display the current text.
  359. * @private
  360. */
  361. Blockly.Field.prototype.updateTextNode_ = function() {
  362. if (!this.textElement_) {
  363. // Not rendered yet.
  364. return;
  365. }
  366. var text = this.text_;
  367. if (text.length > this.maxDisplayLength) {
  368. // Truncate displayed string and add an ellipsis ('...').
  369. text = text.substring(0, this.maxDisplayLength - 2) + '\u2026';
  370. }
  371. // Empty the text element.
  372. goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_));
  373. // Replace whitespace with non-breaking spaces so the text doesn't collapse.
  374. text = text.replace(/\s/g, Blockly.Field.NBSP);
  375. if (this.sourceBlock_.RTL && text) {
  376. // The SVG is LTR, force text to be RTL.
  377. text += '\u200F';
  378. }
  379. if (!text) {
  380. // Prevent the field from disappearing if empty.
  381. text = Blockly.Field.NBSP;
  382. }
  383. var textNode = document.createTextNode(text);
  384. this.textElement_.appendChild(textNode);
  385. // Cached width is obsolete. Clear it.
  386. this.size_.width = 0;
  387. };
  388. /**
  389. * By default there is no difference between the human-readable text and
  390. * the language-neutral values. Subclasses (such as dropdown) may define this.
  391. * @return {string} Current text.
  392. */
  393. Blockly.Field.prototype.getValue = function() {
  394. return this.getText();
  395. };
  396. /**
  397. * By default there is no difference between the human-readable text and
  398. * the language-neutral values. Subclasses (such as dropdown) may define this.
  399. * @param {string} newText New text.
  400. */
  401. Blockly.Field.prototype.setValue = function(newText) {
  402. if (newText === null) {
  403. // No change if null.
  404. return;
  405. }
  406. var oldText = this.getValue();
  407. if (oldText == newText) {
  408. return;
  409. }
  410. if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
  411. Blockly.Events.fire(new Blockly.Events.Change(
  412. this.sourceBlock_, 'field', this.name, oldText, newText));
  413. }
  414. this.setText(newText);
  415. };
  416. /**
  417. * Handle a mouse up event on an editable field.
  418. * @param {!Event} e Mouse up event.
  419. * @private
  420. */
  421. Blockly.Field.prototype.onMouseUp_ = function(e) {
  422. if ((goog.userAgent.IPHONE || goog.userAgent.IPAD) &&
  423. !goog.userAgent.isVersionOrHigher('537.51.2') &&
  424. e.layerX !== 0 && e.layerY !== 0) {
  425. // Old iOS spawns a bogus event on the next touch after a 'prompt()' edit.
  426. // Unlike the real events, these have a layerX and layerY set.
  427. return;
  428. } else if (Blockly.isRightButton(e)) {
  429. // Right-click.
  430. return;
  431. } else if (this.sourceBlock_.workspace.isDragging()) {
  432. // Drag operation is concluding. Don't open the editor.
  433. return;
  434. } else if (this.sourceBlock_.isEditable()) {
  435. // Non-abstract sub-classes must define a showEditor_ method.
  436. this.showEditor_();
  437. }
  438. };
  439. /**
  440. * Change the tooltip text for this field.
  441. * @param {string|!Element} newTip Text for tooltip or a parent element to
  442. * link to for its tooltip.
  443. */
  444. Blockly.Field.prototype.setTooltip = function(newTip) {
  445. // Non-abstract sub-classes may wish to implement this. See FieldLabel.
  446. };
  447. /**
  448. * Return the absolute coordinates of the top-left corner of this field.
  449. * The origin (0,0) is the top-left corner of the page body.
  450. * @return {!goog.math.Coordinate} Object with .x and .y properties.
  451. * @private
  452. */
  453. Blockly.Field.prototype.getAbsoluteXY_ = function() {
  454. return goog.style.getPageOffset(this.borderRect_);
  455. };