connection.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  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.highlightedConnectionBad_ == this) {
  230. Blockly.highlightedConnectionBad_ = null;
  231. }
  232. if (Blockly.localConnection_ == this) {
  233. Blockly.localConnection_ = null;
  234. }
  235. this.db_ = null;
  236. this.dbOpposite_ = null;
  237. };
  238. /**
  239. * Get the source block for this connection.
  240. * @return {Blockly.Block} The source block, or null if there is none.
  241. */
  242. Blockly.Connection.prototype.getSourceBlock = function() {
  243. return this.sourceBlock_;
  244. };
  245. /**
  246. * Does the connection belong to a superior block (higher in the source stack)?
  247. * @return {boolean} True if connection faces down or right.
  248. */
  249. Blockly.Connection.prototype.isSuperior = function() {
  250. return this.type == Blockly.INPUT_VALUE ||
  251. this.type == Blockly.NEXT_STATEMENT;
  252. };
  253. /**
  254. * Is the connection connected?
  255. * @return {boolean} True if connection is connected to another connection.
  256. */
  257. Blockly.Connection.prototype.isConnected = function() {
  258. return !!this.targetConnection;
  259. };
  260. /**
  261. * Checks whether the current connection can connect with the target
  262. * connection.
  263. * @param {Blockly.Connection} target Connection to check compatibility with.
  264. * @return {number} Blockly.Connection.CAN_CONNECT if the connection is legal,
  265. * an error code otherwise.
  266. * @private
  267. */
  268. Blockly.Connection.prototype.canConnectWithReason_ = function(target) {
  269. if (!target) {
  270. return Blockly.Connection.REASON_TARGET_NULL;
  271. }
  272. if (this.isSuperior()) {
  273. var blockA = this.sourceBlock_;
  274. var blockB = target.getSourceBlock();
  275. } else {
  276. var blockB = this.sourceBlock_;
  277. var blockA = target.getSourceBlock();
  278. }
  279. if (blockA && blockA == blockB) {
  280. return Blockly.Connection.REASON_SELF_CONNECTION;
  281. } else if (target.type != Blockly.OPPOSITE_TYPE[this.type]) {
  282. return Blockly.Connection.REASON_WRONG_TYPE;
  283. } else if (blockA && blockB && blockA.workspace !== blockB.workspace) {
  284. return Blockly.Connection.REASON_DIFFERENT_WORKSPACES;
  285. // for BlocksCAD, turn off type checking so we can highlight bad connections too.
  286. // call checkType_ later to distinguish this condition.
  287. // } else if (!this.checkType_(target)) {
  288. // return Blockly.Connection.REASON_CHECKS_FAILED;
  289. } else if (blockA.isShadow() && !blockB.isShadow()) {
  290. return Blockly.Connection.REASON_SHADOW_PARENT;
  291. }
  292. return Blockly.Connection.CAN_CONNECT;
  293. };
  294. /**
  295. * Checks whether the current connection and target connection are compatible
  296. * and throws an exception if they are not.
  297. * @param {Blockly.Connection} target The connection to check compatibility
  298. * with.
  299. * @private
  300. */
  301. Blockly.Connection.prototype.checkConnection_ = function(target) {
  302. switch (this.canConnectWithReason_(target)) {
  303. case Blockly.Connection.CAN_CONNECT:
  304. break;
  305. case Blockly.Connection.REASON_SELF_CONNECTION:
  306. throw 'Attempted to connect a block to itself.';
  307. case Blockly.Connection.REASON_DIFFERENT_WORKSPACES:
  308. // Usually this means one block has been deleted.
  309. throw 'Blocks not on same workspace.';
  310. case Blockly.Connection.REASON_WRONG_TYPE:
  311. throw 'Attempt to connect incompatible types.';
  312. case Blockly.Connection.REASON_TARGET_NULL:
  313. throw 'Target connection is null.';
  314. case Blockly.Connection.REASON_CHECKS_FAILED:
  315. throw 'Connection checks failed.';
  316. case Blockly.Connection.REASON_SHADOW_PARENT:
  317. throw 'Connecting non-shadow to shadow block.';
  318. default:
  319. throw 'Unknown connection failure: this should never happen!';
  320. }
  321. };
  322. /**
  323. * Check if the two connections can be dragged to connect to each other.
  324. * @param {!Blockly.Connection} candidate A nearby connection to check.
  325. * @return {boolean} True if the connection is allowed, false otherwise.
  326. * For BlocksCAD: note that isConnectionAllowed no longer does type checking
  327. * so that we can highlight both legal and illegal connections.
  328. * CAN_CONNECT will not check to see if you have text going into a number block.
  329. */
  330. Blockly.Connection.prototype.isConnectionAllowed = function(candidate) {
  331. // Type checking.
  332. var canConnect = this.canConnectWithReason_(candidate);
  333. if (canConnect != Blockly.Connection.CAN_CONNECT) {
  334. return false;
  335. }
  336. // Don't offer to connect an already connected left (male) value plug to
  337. // an available right (female) value plug. Don't offer to connect the
  338. // bottom of a statement block to one that's already connected.
  339. if (candidate.type == Blockly.OUTPUT_VALUE ||
  340. candidate.type == Blockly.PREVIOUS_STATEMENT) {
  341. if (candidate.isConnected() || this.isConnected()) {
  342. return false;
  343. }
  344. }
  345. // Offering to connect the left (male) of a value block to an already
  346. // connected value pair is ok, we'll splice it in.
  347. // However, don't offer to splice into an immovable block.
  348. if (candidate.type == Blockly.INPUT_VALUE && candidate.isConnected() &&
  349. !candidate.targetBlock().isMovable() &&
  350. !candidate.targetBlock().isShadow()) {
  351. return false;
  352. }
  353. // Don't let a block with no next connection bump other blocks out of the
  354. // stack. But covering up a shadow block or stack of shadow blocks is fine.
  355. // Similarly, replacing a terminal statement with another terminal statement
  356. // is allowed.
  357. if (this.type == Blockly.PREVIOUS_STATEMENT &&
  358. candidate.isConnected() &&
  359. !this.sourceBlock_.nextConnection &&
  360. !candidate.targetBlock().isShadow() &&
  361. candidate.targetBlock().nextConnection) {
  362. return false;
  363. }
  364. // Don't let blocks try to connect to themselves or ones they nest.
  365. if (Blockly.draggingConnections_.indexOf(candidate) != -1) {
  366. return false;
  367. }
  368. return true;
  369. };
  370. /**
  371. * Connect this connection to another connection.
  372. * @param {!Blockly.Connection} otherConnection Connection to connect to.
  373. */
  374. Blockly.Connection.prototype.connect = function(otherConnection) {
  375. if (this.targetConnection == otherConnection) {
  376. // Already connected together. NOP.
  377. return;
  378. }
  379. this.checkConnection_(otherConnection);
  380. // Determine which block is superior (higher in the source stack).
  381. if (this.isSuperior()) {
  382. // Superior block.
  383. this.connect_(otherConnection);
  384. } else {
  385. // Inferior block.
  386. otherConnection.connect_(this);
  387. }
  388. };
  389. /**
  390. * Update two connections to target each other.
  391. * @param {Blockly.Connection} first The first connection to update.
  392. * @param {Blockly.Connection} second The second conneciton to update.
  393. * @private
  394. */
  395. Blockly.Connection.connectReciprocally_ = function(first, second) {
  396. goog.asserts.assert(first && second, 'Cannot connect null connections.');
  397. first.targetConnection = second;
  398. second.targetConnection = first;
  399. };
  400. /**
  401. * Does the given block have one and only one connection point that will accept
  402. * an orphaned block?
  403. * @param {!Blockly.Block} block The superior block.
  404. * @param {!Blockly.Block} orphanBlock The inferior block.
  405. * @return {Blockly.Connection} The suitable connection point on 'block',
  406. * or null.
  407. * @private
  408. */
  409. Blockly.Connection.singleConnection_ = function(block, orphanBlock) {
  410. var connection = false;
  411. for (var i = 0; i < block.inputList.length; i++) {
  412. var thisConnection = block.inputList[i].connection;
  413. if (thisConnection && thisConnection.type == Blockly.INPUT_VALUE &&
  414. orphanBlock.outputConnection.checkType_(thisConnection)) {
  415. if (connection) {
  416. return null; // More than one connection.
  417. }
  418. connection = thisConnection;
  419. }
  420. }
  421. return connection;
  422. };
  423. /**
  424. * Walks down a row a blocks, at each stage checking if there are any
  425. * connections that will accept the orphaned block. If at any point there
  426. * are zero or multiple eligible connections, returns null. Otherwise
  427. * returns the only input on the last block in the chain.
  428. * Terminates early for shadow blocks.
  429. * @param {!Blockly.Block} startBlock The block on which to start the search.
  430. * @param {!Blockly.Block} orphanBlock The block that is looking for a home.
  431. * @return {Blockly.Connection} The suitable connection point on the chain
  432. * of blocks, or null.
  433. * @private
  434. */
  435. Blockly.Connection.lastConnectionInRow_ = function(startBlock, orphanBlock) {
  436. var newBlock = startBlock;
  437. var connection;
  438. while (connection = Blockly.Connection.singleConnection_(
  439. /** @type {!Blockly.Block} */ (newBlock), orphanBlock)) {
  440. // '=' is intentional in line above.
  441. newBlock = connection.targetBlock();
  442. if (!newBlock || newBlock.isShadow()) {
  443. return connection;
  444. }
  445. }
  446. return null;
  447. };
  448. /**
  449. * Disconnect this connection.
  450. */
  451. Blockly.Connection.prototype.disconnect = function() {
  452. var otherConnection = this.targetConnection;
  453. goog.asserts.assert(otherConnection, 'Source connection not connected.');
  454. goog.asserts.assert(otherConnection.targetConnection == this,
  455. 'Target connection not connected to source connection.');
  456. var parentBlock, childBlock, parentConnection;
  457. if (this.isSuperior()) {
  458. // Superior block.
  459. parentBlock = this.sourceBlock_;
  460. childBlock = otherConnection.getSourceBlock();
  461. parentConnection = this;
  462. } else {
  463. // Inferior block.
  464. parentBlock = otherConnection.getSourceBlock();
  465. childBlock = this.sourceBlock_;
  466. parentConnection = otherConnection;
  467. }
  468. this.disconnectInternal_(parentBlock, childBlock);
  469. parentConnection.respawnShadow_();
  470. };
  471. /**
  472. * Disconnect two blocks that are connected by this connection.
  473. * @param {!Blockly.Block} parentBlock The superior block.
  474. * @param {!Blockly.Block} childBlock The inferior block.
  475. * @private
  476. */
  477. Blockly.Connection.prototype.disconnectInternal_ = function(parentBlock,
  478. childBlock) {
  479. var event;
  480. if (Blockly.Events.isEnabled()) {
  481. event = new Blockly.Events.Move(childBlock);
  482. }
  483. var otherConnection = this.targetConnection;
  484. otherConnection.targetConnection = null;
  485. this.targetConnection = null;
  486. childBlock.setParent(null);
  487. if (event) {
  488. event.recordNew();
  489. Blockly.Events.fire(event);
  490. }
  491. };
  492. /**
  493. * Respawn the shadow block if there was one connected to the this connection.
  494. * @private
  495. */
  496. Blockly.Connection.prototype.respawnShadow_ = function() {
  497. var parentBlock = this.getSourceBlock();
  498. var shadow = this.getShadowDom();
  499. if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) {
  500. var blockShadow =
  501. Blockly.Xml.domToBlock(shadow, parentBlock.workspace);
  502. if (blockShadow.outputConnection) {
  503. this.connect(blockShadow.outputConnection);
  504. } else if (blockShadow.previousConnection) {
  505. this.connect(blockShadow.previousConnection);
  506. } else {
  507. throw 'Child block does not have output or previous statement.';
  508. }
  509. }
  510. };
  511. /**
  512. * Returns the block that this connection connects to.
  513. * @return {Blockly.Block} The connected block or null if none is connected.
  514. */
  515. Blockly.Connection.prototype.targetBlock = function() {
  516. if (this.isConnected()) {
  517. return this.targetConnection.getSourceBlock();
  518. }
  519. return null;
  520. };
  521. /**
  522. * Is this connection compatible with another connection with respect to the
  523. * value type system. E.g. square_root("Hello") is not compatible.
  524. * @param {!Blockly.Connection} otherConnection Connection to compare against.
  525. * @return {boolean} True if the connections share a type.
  526. * @private
  527. */
  528. Blockly.Connection.prototype.checkType_ = function(otherConnection) {
  529. if (!this.check_ || !otherConnection.check_) {
  530. // One or both sides are promiscuous enough that anything will fit.
  531. return true;
  532. }
  533. // Find any intersection in the check lists.
  534. for (var i = 0; i < this.check_.length; i++) {
  535. if (otherConnection.check_.indexOf(this.check_[i]) != -1) {
  536. return true;
  537. }
  538. }
  539. // No intersection.
  540. return false;
  541. };
  542. /**
  543. * Change a connection's compatibility.
  544. * @param {*} check Compatible value type or list of value types.
  545. * Null if all types are compatible.
  546. * @return {!Blockly.Connection} The connection being modified
  547. * (to allow chaining).
  548. */
  549. Blockly.Connection.prototype.setCheck = function(check) {
  550. if (check) {
  551. // Ensure that check is in an array.
  552. if (!goog.isArray(check)) {
  553. check = [check];
  554. }
  555. this.check_ = check;
  556. // The new value type may not be compatible with the existing connection.
  557. if (this.isConnected() && !this.checkType_(this.targetConnection)) {
  558. var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
  559. child.unplug();
  560. // Bump away.
  561. this.sourceBlock_.bumpNeighbours_();
  562. }
  563. } else {
  564. this.check_ = null;
  565. }
  566. return this;
  567. };
  568. /**
  569. * Change a connection's shadow block.
  570. * @param {Element} shadow DOM representation of a block or null.
  571. */
  572. Blockly.Connection.prototype.setShadowDom = function(shadow) {
  573. this.shadowDom_ = shadow;
  574. };
  575. /**
  576. * Return a connection's shadow block.
  577. * @return {Element} shadow DOM representation of a block or null.
  578. */
  579. Blockly.Connection.prototype.getShadowDom = function() {
  580. return this.shadowDom_;
  581. };