blockly.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. // Copyright 2012 United States Government, as represented by the Secretary of Defense, Under
  2. // Secretary of Defense (Personnel & Readiness).
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
  5. // in compliance with the License. You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software distributed under the License
  10. // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
  11. // or implied. See the License for the specific language governing permissions and limitations under
  12. // the License.
  13. /// @module vwf/view/blockly
  14. /// @requires vwf/view
  15. define( [ "module", "vwf/view", "jquery", "vwf/model/blockly/JS-Interpreter/acorn" ], function( module, view, $, acorn ) {
  16. var self;
  17. var createBlocklyDivs = true;
  18. var blocksInWorkspace = {};
  19. var handleChangeEvents = true;
  20. var blockIdIterator;
  21. return view.load( module, {
  22. // == Module Definition ====================================================================
  23. // -- initialize ---------------------------------------------------------------------------
  24. initialize: function( options ) {
  25. self = this;
  26. this.arguments = Array.prototype.slice.call( arguments );
  27. if ( options === undefined ) { options = {}; }
  28. this.delayedProperties = {
  29. "blockly_toolbox": undefined,
  30. "blockly_defaultXml": undefined,
  31. "blockly_autoClose": undefined
  32. };
  33. this.options = ( options !== undefined ? options : {} );
  34. this.options.blocklyPath = options.blocklyPath ? options.blocklyPath : './blockly/';
  35. this.options.divParent = options.divParent ? options.divParent : 'blocklyWrapper';
  36. this.options.divName = options.divName ? options.divName : 'blocklyDiv';
  37. this.options.toolbox = options.toolbox ? options.toolbox : 'toolbox';
  38. this.options.createButton = options.createButton !== undefined ? options.createButton : true;
  39. },
  40. createdNode: function( nodeID, childID, childExtendsID, childImplementsIDs,
  41. childSource, childType, childIndex, childName, callback /* ( ready ) */) {
  42. if ( childID == this.kernel.application() ) {
  43. if ( createBlocklyDivs ) {
  44. this.state.scenes[ childID ] = {
  45. "toolbox": undefined,
  46. "defaultXml": undefined
  47. }
  48. if ( this.options.createButton ) {
  49. $( 'body' ).append(
  50. "<div id='"+ self.options.divParent +"'>" +
  51. "<div id='" + self.options.divParent + "-top'/>" +
  52. "<div id='" + self.options.divName + "'/>" +
  53. "<div id='runButton' onclick='onRun()'>Run</div>" +
  54. "</div>" ).children(":last");
  55. } else {
  56. $( 'body' ).append(
  57. "<div id='"+ self.options.divParent +"'>" +
  58. "<div id='" + self.options.divParent + "-top'/>" +
  59. "<div id='" + self.options.divName + "'/>" +
  60. "</div>" ).children(":last");
  61. }
  62. createBlocklyDivs = false;
  63. }
  64. } else {
  65. var node = this.state.nodes[ childID ];
  66. if ( node === undefined && isBlockly3Node( childID ) ) {
  67. this.state.nodes[ childID ] = node = this.state.createNode( nodeID, childID, childExtendsID, childImplementsIDs,
  68. childSource, childType, childIndex, childName, callback );
  69. }
  70. }
  71. },
  72. initializedNode: function( nodeID, childID, childExtendsID, childImplementsIDs, childSource, childType, childName ) {
  73. self = this;
  74. if ( childID === this.kernel.application() ) {
  75. self.kernel.setProperty( childID, "toolbox", self.options.toolbox );
  76. Blockly.inject( document.getElementById( self.options.divName ), {
  77. path: this.options.blocklyPath,
  78. toolbox: document.getElementById( self.options.toolbox ),
  79. trashcan: false,
  80. } );
  81. // HACK: Fix Blockly's hijacking of the backspace and delete keys in password fields
  82. Blockly.isTargetInput_ = function ( event ){
  83. return event.target.type === "textarea" || event.target.type === "text" || event.target.type === "password";
  84. };
  85. Blockly.addChangeListener( function( event ) {
  86. if ( handleChangeEvents ) {
  87. if ( self.state.blockly.node !== undefined ) {
  88. var i, block;
  89. var previousBlockIds = Object.keys( blocksInWorkspace );
  90. var previousBlockCount = previousBlockIds.length;
  91. var blocks = Blockly.mainWorkspace.getAllBlocks();
  92. var blockCount = blocks.length;
  93. var topBlockCount = Blockly.mainWorkspace.topBlocks_.length;
  94. self.kernel.fireEvent( self.kernel.application(), "blocklyContentChanged", [ true ] );
  95. self.kernel.setProperty( self.state.blockly.node.ID, "blockly_blockCount", blockCount );
  96. self.kernel.setProperty( self.state.blockly.node.ID, "blockly_topBlockCount", topBlockCount );
  97. if ( blockCount > previousBlockCount ) {
  98. for ( i = 0; i < blocks.length; i++ ) {
  99. block = blocks[ i ];
  100. if ( blocksInWorkspace[ block.id ] === undefined ) {
  101. blocksInWorkspace[ block.id ] = { "id": block.id, "type": block.type };
  102. self.kernel.fireEvent( self.state.blockly.node.ID, "blocklyBlockAdded", [ block.id, block.type ] );
  103. }
  104. }
  105. } else if ( blockCount < previousBlockCount ) {
  106. var activeBlocks = {};
  107. for ( i = 0; i < blocks.length; i++ ) {
  108. activeBlocks[ blocks[ i ].id ] = blocks[ i ];
  109. }
  110. var blockIDsRemoved = [];
  111. for ( var id in blocksInWorkspace ) {
  112. if ( activeBlocks[ id ] === undefined ) {
  113. blockIDsRemoved.push( id );
  114. }
  115. }
  116. for ( i = 0; i < blockIDsRemoved.length; i++ ) {
  117. block = blocksInWorkspace[ blockIDsRemoved[ i ] ];
  118. self.kernel.fireEvent( self.state.blockly.node.ID, "blocklyBlockRemoved", [ block.id, block.type ] );
  119. delete blocksInWorkspace[ blockIDsRemoved[ i ] ];
  120. }
  121. } else {
  122. // Set the appropriate model properties based on this change
  123. var xmlDom = Blockly.Xml.workspaceToDom( Blockly.getMainWorkspace() );
  124. console.log (xmlDom);
  125. if ( xmlDom ) {
  126. var newXmlText = Blockly.Xml.domToText( xmlDom );
  127. self.kernel.setProperty( self.state.blockly.node.ID, "blockly_xml",
  128. Blockly.Xml.domToText( xmlDom ) );
  129. }
  130. self.kernel.setProperty( self.state.blockly.node.ID, "blockly_code",
  131. Blockly.JavaScript.workspaceToCode() );
  132. }
  133. }
  134. } else {
  135. handleChangeEvents = true;
  136. }
  137. });
  138. }
  139. },
  140. // -- deletedNode ------------------------------------------------------------------------------
  141. deletedNode: function( nodeID ) {
  142. if ( this.state.nodes[ nodeID ] !== undefined ) {
  143. delete this.state.nodes[ nodeID ];
  144. }
  145. },
  146. // -- addedChild -------------------------------------------------------------------------------
  147. //addedChild: function( nodeID, childID, childName ) { },
  148. // -- removedChild -----------------------------------------------------------------------------
  149. //removedChild: function( nodeID, childID ) { },
  150. // -- createdProperty --------------------------------------------------------------------------
  151. createdProperty: function (nodeID, propertyName, propertyValue) {
  152. this.satProperty(nodeID, propertyName, propertyValue);
  153. },
  154. // -- initializedProperty ----------------------------------------------------------------------
  155. initializedProperty: function ( nodeID, propertyName, propertyValue ) {
  156. this.satProperty(nodeID, propertyName, propertyValue);
  157. },
  158. // TODO: deletedProperty
  159. // -- satProperty ------------------------------------------------------------------------------
  160. satProperty: function ( nodeID, propertyName, propertyValue ) {
  161. var node = this.state.nodes[ nodeID ];
  162. //this.logger.infox( "S === satProperty ", nodeID, propertyName, propertyValue );
  163. if ( propertyValue === undefined ) {
  164. return;
  165. }
  166. if ( nodeID == this.kernel.application() ) {
  167. var app = this.state.scenes[ nodeID ];
  168. switch ( propertyName ) {
  169. case "blockly_activeNodeID":
  170. var newActiveNodeId = propertyValue;
  171. var previousActiveNode = this.state.blockly.node;
  172. var blocklyNode = this.state.nodes[ newActiveNodeId ];
  173. // If the new node is the same as the old - exit early to prevent
  174. // breaking synchronization.
  175. if ( previousActiveNode === newActiveNodeId ) {
  176. break;
  177. }
  178. if ( blocklyNode !== undefined ) {
  179. var show = true;
  180. //If there was already an active blockly node, deal with it before
  181. //activating the new one
  182. if ( previousActiveNode !== undefined ) {
  183. getBlockXML( previousActiveNode );
  184. setBlocklyUIVisibility( previousActiveNode, false );
  185. show = ( previousActiveNode.ID !== newActiveNodeId );
  186. this.state.blockly.node = undefined;
  187. }
  188. // If the new active node is different than the old,
  189. // then we need to load its program into the toolbox
  190. if ( show ) {
  191. if ( blocklyNode.toolbox !== undefined ) {
  192. loadToolbox( blocklyNode.toolbox );
  193. } else if ( app.toolbox !== undefined ) {
  194. loadToolbox( app.toolbox );
  195. }
  196. if ( blocklyNode.defaultXml !== undefined ) {
  197. loadDefaultXml( blocklyNode.defaultXml );
  198. } else if ( app.defaultXml !== undefined ) {
  199. loadDefaultXml( app.defaultXml );
  200. }
  201. this.state.blockly.node = blocklyNode;
  202. setBlockXML( blocklyNode );
  203. setBlocklyUIVisibility( blocklyNode, true );
  204. }
  205. } else {
  206. if ( previousActiveNode !== undefined ) {
  207. getBlockXML( previousActiveNode );
  208. setBlocklyUIVisibility( previousActiveNode, false );
  209. this.state.blockly.node = undefined;
  210. }
  211. }
  212. break;
  213. case "blockly_toolbox":
  214. app.toolbox = propertyValue;
  215. loadToolbox( propertyValue );
  216. break;
  217. case "blockly_defaultXml":
  218. app.defaultXml = propertyValue;
  219. loadDefaultXml( propertyValue )
  220. break;
  221. case "blockly_autoClose":
  222. if ( Blockly && Blockly.Toolbox && Blockly.Toolbox.flyout_ ){
  223. Blockly.Toolbox.flyout_.autoClose = Boolean( propertyValue );
  224. } else {
  225. this.delayedProperties.blockly_autoClose = Boolean( propertyValue );
  226. }
  227. break;
  228. }
  229. } else if ( this.state.blockly.node && ( nodeID === this.state.blockly.node.ID ) ) {
  230. switch ( propertyName ) {
  231. case "blockly_xml":
  232. var clientThatSetProperty = this.kernel.client();
  233. var me = this.kernel.moniker();
  234. if ( clientThatSetProperty !== me ) {
  235. var xmlText = propertyValue;
  236. handleChangeEvents = false;
  237. setWorkspaceFromXmlText( xmlText, true );
  238. }
  239. break;
  240. default:
  241. break;
  242. }
  243. }
  244. },
  245. // -- gotProperty ------------------------------------------------------------------------------
  246. // gotProperty: function ( nodeID, propertyName, propertyValue ) {
  247. // },
  248. // -- calledMethod -----------------------------------------------------------------------------
  249. // calledMethod: function( nodeID, methodName, methodParameters, methodValue ) {
  250. // },
  251. // -- firedEvent -----------------------------------------------------------------------------
  252. // firedEvent: function( nodeID, eventName, parameters ) {
  253. // },
  254. // -- ticked -----------------------------------------------------------------------------------
  255. //ticked: function( vwfTime ) {
  256. //},
  257. // -- render -----------------------------------------------------------------------------------
  258. //render: function(renderer, scene, camera) {
  259. //}
  260. } );
  261. function isBlockly3Node( nodeID ) {
  262. return self.kernel.test( nodeID,
  263. "self::element(*,'http://vwf.example.com/blockly/controller.vwf')",
  264. nodeID );
  265. }
  266. function isBlocklyNode( implementsIDs ) {
  267. var found = false;
  268. if ( implementsIDs ) {
  269. for ( var i = 0; i < implementsIDs.length && !found; i++ ) {
  270. found = ( implementsIDs[i] == "http://vwf.example.com/blockly/controller.vwf" );
  271. }
  272. }
  273. return found;
  274. }
  275. function setBlockXML( node ) {
  276. handleChangeEvents = false;
  277. var xmlText = node.blocks;
  278. var clearBeforeSet = true;
  279. setWorkspaceFromXmlText( xmlText, clearBeforeSet );
  280. var blocks = Blockly.mainWorkspace.getAllBlocks();
  281. var blockCount = blocks.length;
  282. var topBlockCount = Blockly.mainWorkspace.topBlocks_.length;
  283. blocksInWorkspace = {};
  284. for ( var i = 0; i < blocks.length; i++ ) {
  285. blocksInWorkspace[ blocks[ i ].id ] = { "id": blocks[ i ].id, "type": blocks[ i ].type };
  286. }
  287. self.kernel.setProperty( node.ID, "blockly_blockCount", blockCount );
  288. self.kernel.setProperty( node.ID, "blockly_topBlockCount", topBlockCount );
  289. }
  290. function getBlockXML( node ) {
  291. var xml = Blockly.Xml.workspaceToDom( Blockly.getMainWorkspace() );
  292. if ( xml ) {
  293. node.blocks = Blockly.Xml.domToText( xml );
  294. }
  295. node.code = Blockly.JavaScript.workspaceToCode();
  296. Blockly.mainWorkspace.clear();
  297. }
  298. function setBlocklyUIVisibility( node, show ) {
  299. var div = document.getElementById( self.options.divParent );
  300. div.style.visibility = show ? 'visible' : 'hidden';
  301. div.style.pointerEvents = show ? 'all' : 'none';
  302. if ( self.delayedProperties !== undefined ) {
  303. for ( var prop in self.delayedProperties ) {
  304. if ( self.delayedProperties[ prop ] !== undefined ) {
  305. self.satProperty( self.kernel.application(), prop, self.delayedProperties[ prop ] );
  306. }
  307. }
  308. self.delayedProperties = undefined;
  309. }
  310. self.kernel.fireEvent( node.ID, "blocklyVisibleChanged", [ show ] );
  311. }
  312. function loadToolbox( toolboxDef ) {
  313. // check the 'Changing the Toolbox' section at
  314. // https://code.google.com/p/blockly/wiki/Toolbox
  315. // for more information on this function call
  316. if ( Blockly && Blockly.mainWorkspace ) {
  317. var len = toolboxDef.length;
  318. if ( toolboxDef.indexOf( '.xml' ) === ( len - 4 ) ) {
  319. $.ajax( {
  320. url: toolboxDef,
  321. type: 'GET',
  322. dataType: 'text',
  323. timeout: 1000,
  324. async: false,
  325. error: function( jqXHR, textStatus, errorThrown ) {
  326. self.logger.errorx( "loadToolbox",
  327. "Error loading XML document (" + textStatus + "): " + toolboxDef );
  328. },
  329. success: function( xml ) {
  330. cleanUpdateToolbox( xml );
  331. }
  332. } );
  333. } else if ( toolboxDef.indexOf( '<xml' ) !== -1 ){
  334. cleanUpdateToolbox( toolboxDef );
  335. } else {
  336. var element = document.getElementById( toolboxDef );
  337. if ( element ) {
  338. cleanUpdateToolbox( element );
  339. } else {
  340. self.logger.warnx( "Unable to load Blockly toolbox: " + toolboxDef );
  341. }
  342. }
  343. } else {
  344. self.delayedProperties.blockly_toolbox = toolboxDef;
  345. self.logger.warnx( "Blockly not initilized unable to set the toolbox: " + toolboxDef );
  346. }
  347. }
  348. function loadDefaultXml( xml ) {
  349. if ( Blockly && Blockly.mainWorkspace ) {
  350. BlocklyApps.loadBlocks( xml );
  351. } else {
  352. self.delayedProperties.defaultXml = toolboxDef;
  353. self.logger.warnx( "Blockly not initilized unable to load default xml: " + xml );
  354. }
  355. }
  356. // cleanUpdateToolbox properly updates the blockly toolbox by setting the
  357. // flyout and workspace bounds according to the new flyout width.
  358. function cleanUpdateToolbox( xml ) {
  359. // negateOverlap is being use to negate some of the effect of the MARGIN variable
  360. // in Blockly.createDom_ when deleting blocks over the flyout
  361. var negateOverlap = 35;
  362. Blockly.updateToolbox( xml );
  363. Blockly.mainWorkspace.scrollX = Blockly.mainWorkspace.flyout_.width_ + negateOverlap;
  364. var translation = 'translate(' + Blockly.mainWorkspace.scrollX + ', 0)';
  365. Blockly.mainWorkspace.getCanvas().setAttribute('transform', translation);
  366. }
  367. // domCopyToWorkspace copies the saved blocks to the workspace exactly
  368. // This preserves the stored block IDs
  369. function domCopyToWorkspace( workspace, xml ) {
  370. var width = Blockly.svgSize().width;
  371. for (var x = 0, xmlChild; xmlChild = xml.childNodes[x]; x++) {
  372. if (xmlChild.nodeName.toLowerCase() == 'block') {
  373. var block = Blockly.Xml.domToBlock( workspace, xmlChild );
  374. var xmlDescendants = xmlChild.getElementsByTagName( "block" );
  375. blockIdIterator = 0;
  376. setChildBlockIDs( block, xmlChild, xmlDescendants );
  377. var blockX = parseInt(xmlChild.getAttribute('x'), 10);
  378. var blockY = parseInt(xmlChild.getAttribute('y'), 10);
  379. if (!isNaN(blockX) && !isNaN(blockY)) {
  380. block.moveBy(Blockly.RTL ? width - blockX : blockX, blockY);
  381. }
  382. }
  383. }
  384. }
  385. function setChildBlockIDs( block, blockXml, xmlDescendants ) {
  386. var childBlock, childXml;
  387. block.id = blockXml.id;
  388. for ( var i = 0; i < block.childBlocks_.length; i++ ) {
  389. childBlock = block.childBlocks_[ i ];
  390. childXml = xmlDescendants[ blockIdIterator++ ];
  391. setChildBlockIDs( childBlock, childXml, xmlDescendants );
  392. }
  393. }
  394. function setWorkspaceFromXmlText( xmlText, clearBeforeSet ) {
  395. var xmlDom = null;
  396. try {
  397. xmlDom = Blockly.Xml.textToDom( xmlText );
  398. } catch ( e ) {
  399. var q = window.confirm( "XML is invalid" );
  400. if ( !q ) {
  401. return;
  402. }
  403. }
  404. if ( xmlDom ) {
  405. console.log (xmlDom);
  406. clearBeforeSet && Blockly.mainWorkspace.clear();
  407. domCopyToWorkspace( Blockly.mainWorkspace, xmlDom );
  408. }
  409. }
  410. } );