connection.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2011 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 Components for creating connections between blocks.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.Connection');
  26. goog.require('goog.asserts');
  27. goog.require('goog.dom');
  28. /**
  29. * Class for a connection between blocks.
  30. * @param {!Blockly.Block} source The block establishing this connection.
  31. * @param {number} type The type of the connection.
  32. * @constructor
  33. */
  34. Blockly.Connection = function(source, type) {
  35. /**
  36. * @type {!Blockly.Block}
  37. * @private
  38. */
  39. this.sourceBlock_ = source;
  40. /** @type {number} */
  41. this.type = type;
  42. // Shortcut for the databases for this connection's workspace.
  43. if (source.workspace.connectionDBList) {
  44. this.db_ = source.workspace.connectionDBList[type];
  45. this.dbOpposite_ =
  46. source.workspace.connectionDBList[Blockly.OPPOSITE_TYPE[type]];
  47. this.hidden_ = !this.db_;
  48. }
  49. };
  50. /**
  51. * Constants for checking whether two connections are compatible.
  52. */
  53. Blockly.Connection.CAN_CONNECT = 0;
  54. Blockly.Connection.REASON_SELF_CONNECTION = 1;
  55. Blockly.Connection.REASON_WRONG_TYPE = 2;
  56. Blockly.Connection.REASON_TARGET_NULL = 3;
  57. Blockly.Connection.REASON_CHECKS_FAILED = 4;
  58. Blockly.Connection.REASON_DIFFERENT_WORKSPACES = 5;
  59. Blockly.Connection.REASON_SHADOW_PARENT = 6;
  60. /**
  61. * Connection this connection connects to. Null if not connected.
  62. * @type {Blockly.Connection}
  63. */
  64. Blockly.Connection.prototype.targetConnection = null;
  65. /**
  66. * List of compatible value types. Null if all types are compatible.
  67. * @type {Array}
  68. * @private
  69. */
  70. Blockly.Connection.prototype.check_ = null;
  71. /**
  72. * DOM representation of a shadow block, or null if none.
  73. * @type {Element}
  74. * @private
  75. */
  76. Blockly.Connection.prototype.shadowDom_ = null;
  77. /**
  78. * Horizontal location of this connection.
  79. * @type {number}
  80. * @private
  81. */
  82. Blockly.Connection.prototype.x_ = 0;
  83. /**
  84. * Vertical location of this connection.
  85. * @type {number}
  86. * @private
  87. */
  88. Blockly.Connection.prototype.y_ = 0;
  89. /**
  90. * Has this connection been added to the connection database?
  91. * @type {boolean}
  92. * @private
  93. */
  94. Blockly.Connection.prototype.inDB_ = false;
  95. /**
  96. * Connection database for connections of this type on the current workspace.
  97. * @type {Blockly.ConnectionDB}
  98. * @private
  99. */
  100. Blockly.Connection.prototype.db_ = null;
  101. /**
  102. * Connection database for connections compatible with this type on the
  103. * current workspace.
  104. * @type {Blockly.ConnectionDB}
  105. * @private
  106. */
  107. Blockly.Connection.prototype.dbOpposite_ = null;
  108. /**
  109. * Whether this connections is hidden (not tracked in a database) or not.
  110. * @type {boolean}
  111. * @private
  112. */
  113. Blockly.Connection.prototype.hidden_ = null;
  114. /**
  115. * Connect two connections together. This is the connection on the superior
  116. * block.
  117. * @param {!Blockly.Connection} childConnection Connection on inferior block.
  118. * @private
  119. */
  120. Blockly.Connection.prototype.connect_ = function(childConnection) {
  121. var parentConnection = this;
  122. var parentBlock = parentConnection.getSourceBlock();
  123. var childBlock = childConnection.getSourceBlock();
  124. // Disconnect any existing parent on the child connection.
  125. if (childConnection.isConnected()) {
  126. childConnection.disconnect();
  127. }
  128. if (parentConnection.isConnected()) {
  129. // Other connection is already connected to something.
  130. // Disconnect it and reattach it or bump it as needed.
  131. var orphanBlock = parentConnection.targetBlock();
  132. var shadowDom = parentConnection.getShadowDom();
  133. // Temporarily set the shadow DOM to null so it does not respawn.
  134. parentConnection.setShadowDom(null);
  135. // Displaced shadow blocks dissolve rather than reattaching or bumping.
  136. if (orphanBlock.isShadow()) {
  137. // Save the shadow block so that field values are preserved.
  138. shadowDom = Blockly.Xml.blockToDom(orphanBlock);
  139. orphanBlock.dispose();
  140. orphanBlock = null;
  141. } else if (parentConnection.type == Blockly.INPUT_VALUE) {
  142. // Value connections.
  143. // If female block is already connected, disconnect and bump the male.
  144. if (!orphanBlock.outputConnection) {
  145. throw 'Orphan block does not have an output connection.';
  146. }
  147. // Attempt to reattach the orphan at the end of the newly inserted
  148. // block. Since this block may be a row, walk down to the end
  149. // or to the first (and only) shadow block.
  150. var connection = Blockly.Connection.lastConnectionInRow_(
  151. childBlock, orphanBlock);
  152. if (connection) {
  153. orphanBlock.outputConnection.connect(connection);
  154. orphanBlock = null;
  155. }
  156. } else if (parentConnection.type == Blockly.NEXT_STATEMENT) {
  157. // Statement connections.
  158. // Statement blocks may be inserted into the middle of a stack.
  159. // Split the stack.
  160. if (!orphanBlock.previousConnection) {
  161. throw 'Orphan block does not have a previous connection.';
  162. }
  163. // Attempt to reattach the orphan at the bottom of the newly inserted
  164. // block. Since this block may be a stack, walk down to the end.
  165. var newBlock = childBlock;
  166. while (newBlock.nextConnection) {
  167. var nextBlock = newBlock.getNextBlock();
  168. if (nextBlock && !nextBlock.isShadow()) {
  169. newBlock = nextBlock;
  170. } else {
  171. if (orphanBlock.previousConnection.checkType_(
  172. newBlock.nextConnection)) {
  173. newBlock.nextConnection.connect(orphanBlock.previousConnection);
  174. orphanBlock = null;
  175. }
  176. break;
  177. }
  178. }
  179. }
  180. if (orphanBlock) {
  181. // Unable to reattach orphan.
  182. parentConnection.disconnect();
  183. if (Blockly.Events.recordUndo) {
  184. // Bump it off to the side after a moment.
  185. var group = Blockly.Events.getGroup();
  186. setTimeout(function() {
  187. // Verify orphan hasn't been deleted or reconnected (user on meth).
  188. if (orphanBlock.workspace && !orphanBlock.getParent()) {
  189. Blockly.Events.setGroup(group);
  190. if (orphanBlock.outputConnection) {
  191. orphanBlock.outputConnection.bumpAwayFrom_(parentConnection);
  192. } else if (orphanBlock.previousConnection) {
  193. orphanBlock.previousConnection.bumpAwayFrom_(parentConnection);
  194. }
  195. Blockly.Events.setGroup(false);
  196. }
  197. }, Blockly.BUMP_DELAY);
  198. }
  199. }
  200. // Restore the shadow DOM.
  201. parentConnection.setShadowDom(shadowDom);
  202. }
  203. var event;
  204. if (Blockly.Events.isEnabled()) {
  205. event = new Blockly.Events.Move(childBlock);
  206. }
  207. // Establish the connections.
  208. Blockly.Connection.connectReciprocally_(parentConnection, childConnection);
  209. // Demote the inferior block so that one is a child of the superior one.
  210. childBlock.setParent(parentBlock);
  211. if (event) {
  212. event.recordNew();
  213. Blockly.Events.fire(event);
  214. }
  215. };
  216. /**
  217. * Sever all links to this connection (not including from the source object).
  218. */
  219. Blockly.Connection.prototype.dispose = function() {
  220. if (this.isConnected()) {
  221. throw 'Disconnect connection before disposing of it.';
  222. }
  223. if (this.inDB_) {
  224. this.db_.removeConnection_(this);
  225. }
  226. if (Blockly.highlightedConnection_ == this) {
  227. Blockly.highlightedConnection_ = null;
  228. }
  229. if (Blockly.localConnection_ == this) {
  230. Blockly.localConnection_ = null;
  231. }
  232. this.db_ = null;
  233. this.dbOpposite_ = null;
  234. };
  235. /**
  236. * Get the source block for this connection.
  237. * @return {Blockly.Block} The source block, or null if there is none.
  238. */
  239. Blockly.Connection.prototype.getSourceBlock = function() {
  240. return this.sourceBlock_;
  241. };
  242. /**
  243. * Does the connection belong to a superior block (higher in the source stack)?
  244. * @return {boolean} True if connection faces down or right.
  245. */
  246. Blockly.Connection.prototype.isSuperior = function() {
  247. return this.type == Blockly.INPUT_VALUE ||
  248. this.type == Blockly.NEXT_STATEMENT;
  249. };
  250. /**
  251. * Is the connection connected?
  252. * @return {boolean} True if connection is connected to another connection.
  253. */
  254. Blockly.Connection.prototype.isConnected = function() {
  255. return !!this.targetConnection;
  256. };
  257. /**
  258. * Checks whether the current connection can connect with the target
  259. * connection.
  260. * @param {Blockly.Connection} target Connection to check compatibility with.
  261. * @return {number} Blockly.Connection.CAN_CONNECT if the connection is legal,
  262. * an error code otherwise.
  263. * @private
  264. */
  265. Blockly.Connection.prototype.canConnectWithReason_ = function(target) {
  266. if (!target) {
  267. return Blockly.Connection.REASON_TARGET_NULL;
  268. }
  269. if (this.isSuperior()) {
  270. var blockA = this.sourceBlock_;
  271. var blockB = target.getSourceBlock();
  272. } else {
  273. var blockB = this.sourceBlock_;
  274. var blockA = target.getSourceBlock();
  275. }
  276. if (blockA && blockA == blockB) {
  277. return Blockly.Connection.REASON_SELF_CONNECTION;
  278. } else if (target.type != Blockly.OPPOSITE_TYPE[this.type]) {
  279. return Blockly.Connection.REASON_WRONG_TYPE;
  280. } else if (blockA && blockB && blockA.workspace !== blockB.workspace) {
  281. return Blockly.Connection.REASON_DIFFERENT_WORKSPACES;
  282. } else if (!this.checkType_(target)) {
  283. return Blockly.Connection.REASON_CHECKS_FAILED;
  284. } else if (blockA.isShadow() && !blockB.isShadow()) {
  285. return Blockly.Connection.REASON_SHADOW_PARENT;
  286. }
  287. return Blockly.Connection.CAN_CONNECT;
  288. };
  289. /**
  290. * Checks whether the current connection and target connection are compatible
  291. * and throws an exception if they are not.
  292. * @param {Blockly.Connection} target The connection to check compatibility
  293. * with.
  294. * @private
  295. */
  296. Blockly.Connection.prototype.checkConnection_ = function(target) {
  297. switch (this.canConnectWithReason_(target)) {
  298. case Blockly.Connection.CAN_CONNECT:
  299. break;
  300. case Blockly.Connection.REASON_SELF_CONNECTION:
  301. throw 'Attempted to connect a block to itself.';
  302. case Blockly.Connection.REASON_DIFFERENT_WORKSPACES:
  303. // Usually this means one block has been deleted.
  304. throw 'Blocks not on same workspace.';
  305. case Blockly.Connection.REASON_WRONG_TYPE:
  306. throw 'Attempt to connect incompatible types.';
  307. case Blockly.Connection.REASON_TARGET_NULL:
  308. throw 'Target connection is null.';
  309. case Blockly.Connection.REASON_CHECKS_FAILED:
  310. throw 'Connection checks failed.';
  311. case Blockly.Connection.REASON_SHADOW_PARENT:
  312. throw 'Connecting non-shadow to shadow block.';
  313. default:
  314. throw 'Unknown connection failure: this should never happen!';
  315. }
  316. };
  317. /**
  318. * Check if the two connections can be dragged to connect to each other.
  319. * @param {!Blockly.Connection} candidate A nearby connection to check.
  320. * @return {boolean} True if the connection is allowed, false otherwise.
  321. */
  322. Blockly.Connection.prototype.isConnectionAllowed = function(candidate) {
  323. // Type checking.
  324. var canConnect = this.canConnectWithReason_(candidate);
  325. if (canConnect != Blockly.Connection.CAN_CONNECT) {
  326. return false;
  327. }
  328. // Don't offer to connect an already connected left (male) value plug to
  329. // an available right (female) value plug. Don't offer to connect the
  330. // bottom of a statement block to one that's already connected.
  331. if (candidate.type == Blockly.OUTPUT_VALUE ||
  332. candidate.type == Blockly.PREVIOUS_STATEMENT) {
  333. if (candidate.isConnected() || this.isConnected()) {
  334. return false;
  335. }
  336. }
  337. // Offering to connect the left (male) of a value block to an already
  338. // connected value pair is ok, we'll splice it in.
  339. // However, don't offer to splice into an immovable block.
  340. if (candidate.type == Blockly.INPUT_VALUE && candidate.isConnected() &&
  341. !candidate.targetBlock().isMovable() &&
  342. !candidate.targetBlock().isShadow()) {
  343. return false;
  344. }
  345. // Don't let a block with no next connection bump other blocks out of the
  346. // stack. But covering up a shadow block or stack of shadow blocks is fine.
  347. // Similarly, replacing a terminal statement with another terminal statement
  348. // is allowed.
  349. if (this.type == Blockly.PREVIOUS_STATEMENT &&
  350. candidate.isConnected() &&
  351. !this.sourceBlock_.nextConnection &&
  352. !candidate.targetBlock().isShadow() &&
  353. candidate.targetBlock().nextConnection) {
  354. return false;
  355. }
  356. // Don't let blocks try to connect to themselves or ones they nest.
  357. if (Blockly.draggingConnections_.indexOf(candidate) != -1) {
  358. return false;
  359. }
  360. return true;
  361. };
  362. /**
  363. * Connect this connection to another connection.
  364. * @param {!Blockly.Connection} otherConnection Connection to connect to.
  365. */
  366. Blockly.Connection.prototype.connect = function(otherConnection) {
  367. if (this.targetConnection == otherConnection) {
  368. // Already connected together. NOP.
  369. return;
  370. }
  371. this.checkConnection_(otherConnection);
  372. // Determine which block is superior (higher in the source stack).
  373. if (this.isSuperior()) {
  374. // Superior block.
  375. this.connect_(otherConnection);
  376. } else {
  377. // Inferior block.
  378. otherConnection.connect_(this);
  379. }
  380. };
  381. /**
  382. * Update two connections to target each other.
  383. * @param {Blockly.Connection} first The first connection to update.
  384. * @param {Blockly.Connection} second The second conneciton to update.
  385. * @private
  386. */
  387. Blockly.Connection.connectReciprocally_ = function(first, second) {
  388. goog.asserts.assert(first && second, 'Cannot connect null connections.');
  389. first.targetConnection = second;
  390. second.targetConnection = first;
  391. };
  392. /**
  393. * Does the given block have one and only one connection point that will accept
  394. * an orphaned block?
  395. * @param {!Blockly.Block} block The superior block.
  396. * @param {!Blockly.Block} orphanBlock The inferior block.
  397. * @return {Blockly.Connection} The suitable connection point on 'block',
  398. * or null.
  399. * @private
  400. */
  401. Blockly.Connection.singleConnection_ = function(block, orphanBlock) {
  402. var connection = false;
  403. for (var i = 0; i < block.inputList.length; i++) {
  404. var thisConnection = block.inputList[i].connection;
  405. if (thisConnection && thisConnection.type == Blockly.INPUT_VALUE &&
  406. orphanBlock.outputConnection.checkType_(thisConnection)) {
  407. if (connection) {
  408. return null; // More than one connection.
  409. }
  410. connection = thisConnection;
  411. }
  412. }
  413. return connection;
  414. };
  415. /**
  416. * Walks down a row a blocks, at each stage checking if there are any
  417. * connections that will accept the orphaned block. If at any point there
  418. * are zero or multiple eligible connections, returns null. Otherwise
  419. * returns the only input on the last block in the chain.
  420. * Terminates early for shadow blocks.
  421. * @param {!Blockly.Block} startBlock The block on which to start the search.
  422. * @param {!Blockly.Block} orphanBlock The block that is looking for a home.
  423. * @return {Blockly.Connection} The suitable connection point on the chain
  424. * of blocks, or null.
  425. * @private
  426. */
  427. Blockly.Connection.lastConnectionInRow_ = function(startBlock, orphanBlock) {
  428. var newBlock = startBlock;
  429. var connection;
  430. while (connection = Blockly.Connection.singleConnection_(
  431. /** @type {!Blockly.Block} */ (newBlock), orphanBlock)) {
  432. // '=' is intentional in line above.
  433. newBlock = connection.targetBlock();
  434. if (!newBlock || newBlock.isShadow()) {
  435. return connection;
  436. }
  437. }
  438. return null;
  439. };
  440. /**
  441. * Disconnect this connection.
  442. */
  443. Blockly.Connection.prototype.disconnect = function() {
  444. var otherConnection = this.targetConnection;
  445. goog.asserts.assert(otherConnection, 'Source connection not connected.');
  446. goog.asserts.assert(otherConnection.targetConnection == this,
  447. 'Target connection not connected to source connection.');
  448. var parentBlock, childBlock, parentConnection;
  449. if (this.isSuperior()) {
  450. // Superior block.
  451. parentBlock = this.sourceBlock_;
  452. childBlock = otherConnection.getSourceBlock();
  453. parentConnection = this;
  454. } else {
  455. // Inferior block.
  456. parentBlock = otherConnection.getSourceBlock();
  457. childBlock = this.sourceBlock_;
  458. parentConnection = otherConnection;
  459. }
  460. this.disconnectInternal_(parentBlock, childBlock);
  461. parentConnection.respawnShadow_();
  462. };
  463. /**
  464. * Disconnect two blocks that are connected by this connection.
  465. * @param {!Blockly.Block} parentBlock The superior block.
  466. * @param {!Blockly.Block} childBlock The inferior block.
  467. * @private
  468. */
  469. Blockly.Connection.prototype.disconnectInternal_ = function(parentBlock,
  470. childBlock) {
  471. var event;
  472. if (Blockly.Events.isEnabled()) {
  473. event = new Blockly.Events.Move(childBlock);
  474. }
  475. var otherConnection = this.targetConnection;
  476. otherConnection.targetConnection = null;
  477. this.targetConnection = null;
  478. childBlock.setParent(null);
  479. if (event) {
  480. event.recordNew();
  481. Blockly.Events.fire(event);
  482. }
  483. };
  484. /**
  485. * Respawn the shadow block if there was one connected to the this connection.
  486. * @private
  487. */
  488. Blockly.Connection.prototype.respawnShadow_ = function() {
  489. var parentBlock = this.getSourceBlock();
  490. var shadow = this.getShadowDom();
  491. if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) {
  492. var blockShadow =
  493. Blockly.Xml.domToBlock(shadow, parentBlock.workspace);
  494. if (blockShadow.outputConnection) {
  495. this.connect(blockShadow.outputConnection);
  496. } else if (blockShadow.previousConnection) {
  497. this.connect(blockShadow.previousConnection);
  498. } else {
  499. throw 'Child block does not have output or previous statement.';
  500. }
  501. }
  502. };
  503. /**
  504. * Returns the block that this connection connects to.
  505. * @return {Blockly.Block} The connected block or null if none is connected.
  506. */
  507. Blockly.Connection.prototype.targetBlock = function() {
  508. if (this.isConnected()) {
  509. return this.targetConnection.getSourceBlock();
  510. }
  511. return null;
  512. };
  513. /**
  514. * Is this connection compatible with another connection with respect to the
  515. * value type system. E.g. square_root("Hello") is not compatible.
  516. * @param {!Blockly.Connection} otherConnection Connection to compare against.
  517. * @return {boolean} True if the connections share a type.
  518. * @private
  519. */
  520. Blockly.Connection.prototype.checkType_ = function(otherConnection) {
  521. if (!this.check_ || !otherConnection.check_) {
  522. // One or both sides are promiscuous enough that anything will fit.
  523. return true;
  524. }
  525. // Find any intersection in the check lists.
  526. for (var i = 0; i < this.check_.length; i++) {
  527. if (otherConnection.check_.indexOf(this.check_[i]) != -1) {
  528. return true;
  529. }
  530. }
  531. // No intersection.
  532. return false;
  533. };
  534. /**
  535. * Change a connection's compatibility.
  536. * @param {*} check Compatible value type or list of value types.
  537. * Null if all types are compatible.
  538. * @return {!Blockly.Connection} The connection being modified
  539. * (to allow chaining).
  540. */
  541. Blockly.Connection.prototype.setCheck = function(check) {
  542. if (check) {
  543. // Ensure that check is in an array.
  544. if (!goog.isArray(check)) {
  545. check = [check];
  546. }
  547. this.check_ = check;
  548. // The new value type may not be compatible with the existing connection.
  549. if (this.isConnected() && !this.checkType_(this.targetConnection)) {
  550. var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
  551. child.unplug();
  552. // Bump away.
  553. this.sourceBlock_.bumpNeighbours_();
  554. }
  555. } else {
  556. this.check_ = null;
  557. }
  558. return this;
  559. };
  560. /**
  561. * Change a connection's shadow block.
  562. * @param {Element} shadow DOM representation of a block or null.
  563. */
  564. Blockly.Connection.prototype.setShadowDom = function(shadow) {
  565. this.shadowDom_ = shadow;
  566. };
  567. /**
  568. * Return a connection's shadow block.
  569. * @return {Element} shadow DOM representation of a block or null.
  570. */
  571. Blockly.Connection.prototype.getShadowDom = function() {
  572. return this.shadowDom_;
  573. };