app_controller.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. /**
  2. * @license
  3. * Blockly Demos: Block Factory
  4. *
  5. * Copyright 2016 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 The AppController Class brings together the Block
  22. * Factory, Block Library, and Block Exporter functionality into a single web
  23. * app.
  24. *
  25. * @author quachtina96 (Tina Quach)
  26. */
  27. goog.provide('AppController');
  28. goog.require('BlockFactory');
  29. goog.require('FactoryUtils');
  30. goog.require('BlockLibraryController');
  31. goog.require('BlockExporterController');
  32. goog.require('goog.dom.classlist');
  33. goog.require('goog.ui.PopupColorPicker');
  34. goog.require('goog.ui.ColorPicker');
  35. /**
  36. * Controller for the Blockly Factory
  37. * @constructor
  38. */
  39. AppController = function() {
  40. // Initialize Block Library
  41. this.blockLibraryName = 'blockLibrary';
  42. this.blockLibraryController =
  43. new BlockLibraryController(this.blockLibraryName);
  44. this.blockLibraryController.populateBlockLibrary();
  45. // Construct Workspace Factory Controller.
  46. this.workspaceFactoryController = new WorkspaceFactoryController
  47. ('workspacefactory_toolbox', 'toolbox_blocks', 'preview_blocks');
  48. // Initialize Block Exporter
  49. this.exporter =
  50. new BlockExporterController(this.blockLibraryController.storage);
  51. // Map of tab type to the div element for the tab.
  52. this.tabMap = Object.create(null);
  53. this.tabMap[AppController.BLOCK_FACTORY] =
  54. document.getElementById('blockFactory_tab');
  55. this.tabMap[AppController.WORKSPACE_FACTORY] =
  56. document.getElementById('workspaceFactory_tab');
  57. this.tabMap[AppController.EXPORTER] =
  58. document.getElementById('blocklibraryExporter_tab');
  59. // Last selected tab.
  60. this.lastSelectedTab = null;
  61. // Selected tab.
  62. this.selectedTab = AppController.BLOCK_FACTORY;
  63. };
  64. // Constant values representing the three tabs in the controller.
  65. AppController.BLOCK_FACTORY = 'BLOCK_FACTORY';
  66. AppController.WORKSPACE_FACTORY = 'WORKSPACE_FACTORY';
  67. AppController.EXPORTER = 'EXPORTER';
  68. /**
  69. * Tied to the 'Import Block Library' button. Imports block library from file to
  70. * Block Factory. Expects user to upload a single file of JSON mapping each
  71. * block type to its XML text representation.
  72. */
  73. AppController.prototype.importBlockLibraryFromFile = function() {
  74. var self = this;
  75. var files = document.getElementById('files');
  76. // If the file list is empty, the user likely canceled in the dialog.
  77. if (files.files.length > 0) {
  78. // The input tag doesn't have the "multiple" attribute
  79. // so the user can only choose 1 file.
  80. var file = files.files[0];
  81. var fileReader = new FileReader();
  82. // Create a map of block type to XML text from the file when it has been
  83. // read.
  84. fileReader.addEventListener('load', function(event) {
  85. var fileContents = event.target.result;
  86. // Create empty object to hold the read block library information.
  87. var blockXmlTextMap = Object.create(null);
  88. try {
  89. // Parse the file to get map of block type to XML text.
  90. blockXmlTextMap = self.formatBlockLibraryForImport_(fileContents);
  91. } catch (e) {
  92. var message = 'Could not load your block library file.\n'
  93. window.alert(message + '\nFile Name: ' + file.name);
  94. return;
  95. }
  96. // Create a new block library storage object with inputted block library.
  97. var blockLibStorage = new BlockLibraryStorage(
  98. self.blockLibraryName, blockXmlTextMap);
  99. // Update block library controller with the new block library
  100. // storage.
  101. self.blockLibraryController.setBlockLibraryStorage(blockLibStorage);
  102. // Update the block library dropdown.
  103. self.blockLibraryController.populateBlockLibrary();
  104. // Update the exporter's block library storage.
  105. self.exporter.setBlockLibraryStorage(blockLibStorage);
  106. });
  107. // Read the file.
  108. fileReader.readAsText(file);
  109. }
  110. };
  111. /**
  112. * Tied to the 'Export Block Library' button. Exports block library to file that
  113. * contains JSON mapping each block type to its XML text representation.
  114. */
  115. AppController.prototype.exportBlockLibraryToFile = function() {
  116. // Get map of block type to XML.
  117. var blockLib = this.blockLibraryController.getBlockLibrary();
  118. // Concatenate the XMLs, each separated by a blank line.
  119. var blockLibText = this.formatBlockLibraryForExport_(blockLib);
  120. // Get file name.
  121. var filename = prompt('Enter the file name under which to save your block ' +
  122. 'library.', 'library.xml');
  123. // Download file if all necessary parameters are provided.
  124. if (filename) {
  125. FactoryUtils.createAndDownloadFile(blockLibText, filename, 'xml');
  126. } else {
  127. alert('Could not export Block Library without file name under which to ' +
  128. 'save library.');
  129. }
  130. };
  131. /**
  132. * Converts an object mapping block type to XML to text file for output.
  133. * @param {!Object} blockXmlMap Object mapping block type to XML.
  134. * @return {string} XML text containing the block XMLs.
  135. * @private
  136. */
  137. AppController.prototype.formatBlockLibraryForExport_ = function(blockXmlMap) {
  138. // Create DOM for XML.
  139. var xmlDom = goog.dom.createDom('xml', {
  140. 'xmlns':"http://www.w3.org/1999/xhtml"
  141. });
  142. // Append each block node to XML DOM.
  143. for (var blockType in blockXmlMap) {
  144. var blockXmlDom = Blockly.Xml.textToDom(blockXmlMap[blockType]);
  145. var blockNode = blockXmlDom.firstElementChild;
  146. xmlDom.appendChild(blockNode);
  147. }
  148. // Return the XML text.
  149. return Blockly.Xml.domToText(xmlDom);
  150. };
  151. /**
  152. * Converts imported block library to an object mapping block type to block XML.
  153. * @param {string} xmlText String representation of an XML with each block as
  154. * a child node.
  155. * @return {!Object} Object mapping block type to XML text.
  156. * @private
  157. */
  158. AppController.prototype.formatBlockLibraryForImport_ = function(xmlText) {
  159. var xmlDom = Blockly.Xml.textToDom(xmlText);
  160. // Get array of XMLs. Use an asterisk (*) instead of a tag name for the XPath
  161. // selector, to match all elements at that level and get all factory_base
  162. // blocks.
  163. var blockNodes = goog.dom.xml.selectNodes(xmlDom, '*');
  164. // Create empty map. The line below creates a truly empy object. It doesn't
  165. // have built-in attributes/functions such as length or toString.
  166. var blockXmlTextMap = Object.create(null);
  167. // Populate map.
  168. for (var i = 0, blockNode; blockNode = blockNodes[i]; i++) {
  169. // Add outer XML tag to the block for proper injection in to the
  170. // main workspace.
  171. // Create DOM for XML.
  172. var xmlDom = goog.dom.createDom('xml', {
  173. 'xmlns':"http://www.w3.org/1999/xhtml"
  174. });
  175. xmlDom.appendChild(blockNode);
  176. xmlText = Blockly.Xml.domToText(xmlDom);
  177. // All block types should be lowercase.
  178. var blockType = this.getBlockTypeFromXml_(xmlText).toLowerCase();
  179. blockXmlTextMap[blockType] = xmlText;
  180. }
  181. return blockXmlTextMap;
  182. };
  183. /**
  184. * Extracts out block type from XML text, the kind that is saved in block
  185. * library storage.
  186. * @param {string} xmlText A block's XML text.
  187. * @return {string} The block type that corresponds to the provided XML text.
  188. * @private
  189. */
  190. AppController.prototype.getBlockTypeFromXml_ = function(xmlText) {
  191. var xmlDom = Blockly.Xml.textToDom(xmlText);
  192. // Find factory base block.
  193. var factoryBaseBlockXml = xmlDom.getElementsByTagName('block')[0];
  194. // Get field elements from factory base.
  195. var fields = factoryBaseBlockXml.getElementsByTagName('field');
  196. for (var i = 0; i < fields.length; i++) {
  197. // The field whose name is 'NAME' holds the block type as its value.
  198. if (fields[i].getAttribute('name') == 'NAME') {
  199. return fields[i].childNodes[0].nodeValue;
  200. }
  201. }
  202. };
  203. /**
  204. * Add click handlers to each tab to allow switching between the Block Factory,
  205. * Workspace Factory, and Block Exporter tab.
  206. * @param {!Object} tabMap Map of tab name to div element that is the tab.
  207. */
  208. AppController.prototype.addTabHandlers = function(tabMap) {
  209. var self = this;
  210. for (var tabName in tabMap) {
  211. var tab = tabMap[tabName];
  212. // Use an additional closure to correctly assign the tab callback.
  213. tab.addEventListener('click', self.makeTabClickHandler_(tabName));
  214. }
  215. };
  216. /**
  217. * Set the selected tab.
  218. * @param {string} tabName AppController.BLOCK_FACTORY,
  219. * AppController.WORKSPACE_FACTORY, or AppController.EXPORTER
  220. * @private
  221. */
  222. AppController.prototype.setSelected_ = function(tabName) {
  223. this.lastSelectedTab = this.selectedTab;
  224. this.selectedTab = tabName;
  225. };
  226. /**
  227. * Creates the tab click handler specific to the tab specified.
  228. * @param {string} tabName AppController.BLOCK_FACTORY,
  229. * AppController.WORKSPACE_FACTORY, or AppController.EXPORTER
  230. * @return {!Function} The tab click handler.
  231. * @private
  232. */
  233. AppController.prototype.makeTabClickHandler_ = function(tabName) {
  234. var self = this;
  235. return function() {
  236. self.setSelected_(tabName);
  237. self.onTab();
  238. };
  239. };
  240. /**
  241. * Called on each tab click. Hides and shows specific content based on which tab
  242. * (Block Factory, Workspace Factory, or Exporter) is selected.
  243. */
  244. AppController.prototype.onTab = function() {
  245. // Get tab div elements.
  246. var blockFactoryTab = this.tabMap[AppController.BLOCK_FACTORY];
  247. var exporterTab = this.tabMap[AppController.EXPORTER];
  248. var workspaceFactoryTab = this.tabMap[AppController.WORKSPACE_FACTORY];
  249. // Warn user if they have unsaved changes when leaving Block Factory.
  250. if (this.lastSelectedTab == AppController.BLOCK_FACTORY &&
  251. this.selectedTab != AppController.BLOCK_FACTORY) {
  252. var hasUnsavedChanges =
  253. !FactoryUtils.savedBlockChanges(this.blockLibraryController);
  254. if (hasUnsavedChanges &&
  255. !confirm('You have unsaved changes in Block Factory.')) {
  256. // If the user doesn't want to switch tabs with unsaved changes,
  257. // stay on Block Factory Tab.
  258. this.setSelected_(AppController.BLOCK_FACTORY);
  259. this.lastSelectedTab = AppController.BLOCK_FACTORY;
  260. return;
  261. }
  262. }
  263. // Only enable key events in workspace factory if workspace factory tab is
  264. // selected.
  265. this.workspaceFactoryController.keyEventsEnabled =
  266. this.selectedTab == AppController.WORKSPACE_FACTORY;
  267. // Turn selected tab on and other tabs off.
  268. this.styleTabs_();
  269. if (this.selectedTab == AppController.EXPORTER) {
  270. // Hide other tabs.
  271. FactoryUtils.hide('workspaceFactoryContent');
  272. FactoryUtils.hide('blockFactoryContent');
  273. // Show exporter tab.
  274. FactoryUtils.show('blockLibraryExporter');
  275. // Need accurate state in order to know which blocks are used in workspace
  276. // factory.
  277. this.workspaceFactoryController.saveStateFromWorkspace();
  278. // Update exporter's list of the types of blocks used in workspace factory.
  279. var usedBlockTypes = this.workspaceFactoryController.getAllUsedBlockTypes();
  280. this.exporter.setUsedBlockTypes(usedBlockTypes);
  281. // Update exporter's block selector to reflect current block library.
  282. this.exporter.updateSelector();
  283. // Update the exporter's preview to reflect any changes made to the blocks.
  284. this.exporter.updatePreview();
  285. } else if (this.selectedTab == AppController.BLOCK_FACTORY) {
  286. // Hide other tabs.
  287. FactoryUtils.hide('blockLibraryExporter');
  288. FactoryUtils.hide('workspaceFactoryContent');
  289. // Show Block Factory.
  290. FactoryUtils.show('blockFactoryContent');
  291. } else if (this.selectedTab == AppController.WORKSPACE_FACTORY) {
  292. // Hide other tabs.
  293. FactoryUtils.hide('blockLibraryExporter');
  294. FactoryUtils.hide('blockFactoryContent');
  295. // Show workspace factory container.
  296. FactoryUtils.show('workspaceFactoryContent');
  297. // Update block library category.
  298. var categoryXml = this.exporter.getBlockLibraryCategory();
  299. var blockTypes = this.blockLibraryController.getStoredBlockTypes();
  300. this.workspaceFactoryController.setBlockLibCategory(categoryXml,
  301. blockTypes);
  302. }
  303. // Resize to render workspaces' toolboxes correctly for all tabs.
  304. window.dispatchEvent(new Event('resize'));
  305. };
  306. /**
  307. * Called on each tab click. Styles the tabs to reflect which tab is selected.
  308. * @private
  309. */
  310. AppController.prototype.styleTabs_ = function() {
  311. for (var tabName in this.tabMap) {
  312. if (this.selectedTab == tabName) {
  313. goog.dom.classlist.addRemove(this.tabMap[tabName], 'taboff', 'tabon');
  314. } else {
  315. goog.dom.classlist.addRemove(this.tabMap[tabName], 'tabon', 'taboff');
  316. }
  317. }
  318. };
  319. /**
  320. * Assign button click handlers for the exporter.
  321. */
  322. AppController.prototype.assignExporterClickHandlers = function() {
  323. var self = this;
  324. document.getElementById('button_setBlocks').addEventListener('click',
  325. function() {
  326. self.openModal('dropdownDiv_setBlocks');
  327. });
  328. document.getElementById('dropdown_addAllUsed').addEventListener('click',
  329. function() {
  330. self.exporter.selectUsedBlocks();
  331. self.exporter.updatePreview();
  332. self.closeModal();
  333. });
  334. document.getElementById('dropdown_addAllFromLib').addEventListener('click',
  335. function() {
  336. self.exporter.selectAllBlocks();
  337. self.exporter.updatePreview();
  338. self.closeModal();
  339. });
  340. document.getElementById('clearSelectedButton').addEventListener('click',
  341. function() {
  342. self.exporter.clearSelectedBlocks();
  343. self.exporter.updatePreview();
  344. });
  345. // Export blocks when the user submits the export settings.
  346. document.getElementById('exporterSubmitButton').addEventListener('click',
  347. function() {
  348. self.exporter.export();
  349. });
  350. };
  351. /**
  352. * Assign change listeners for the exporter. These allow for the dynamic update
  353. * of the exporter preview.
  354. */
  355. AppController.prototype.assignExporterChangeListeners = function() {
  356. var self = this;
  357. var blockDefCheck = document.getElementById('blockDefCheck');
  358. var genStubCheck = document.getElementById('genStubCheck');
  359. // Select the block definitions and generator stubs on default.
  360. blockDefCheck.checked = true;
  361. genStubCheck.checked = true;
  362. // Checking the block definitions checkbox displays preview of code to export.
  363. document.getElementById('blockDefCheck').addEventListener('change',
  364. function(e) {
  365. self.ifCheckedEnable(blockDefCheck.checked,
  366. ['blockDefs', 'blockDefSettings']);
  367. });
  368. // Preview updates when user selects different block definition format.
  369. document.getElementById('exportFormat').addEventListener('change',
  370. function(e) {
  371. self.exporter.updatePreview();
  372. });
  373. // Checking the generator stub checkbox displays preview of code to export.
  374. document.getElementById('genStubCheck').addEventListener('change',
  375. function(e) {
  376. self.ifCheckedEnable(genStubCheck.checked,
  377. ['genStubs', 'genStubSettings']);
  378. });
  379. // Preview updates when user selects different generator stub language.
  380. document.getElementById('exportLanguage').addEventListener('change',
  381. function(e) {
  382. self.exporter.updatePreview();
  383. });
  384. };
  385. /**
  386. * If given checkbox is checked, enable the given elements. Otherwise, disable.
  387. * @param {boolean} enabled True if enabled, false otherwise.
  388. * @param {!Array.<string>} idArray Array of element IDs to enable when
  389. * checkbox is checked.
  390. */
  391. AppController.prototype.ifCheckedEnable = function(enabled, idArray) {
  392. for (var i = 0, id; id = idArray[i]; i++) {
  393. var element = document.getElementById(id);
  394. if (enabled) {
  395. element.classList.remove('disabled');
  396. } else {
  397. element.classList.add('disabled');
  398. }
  399. var fields = element.querySelectorAll('input, textarea, select');
  400. for (var j = 0, field; field = fields[j]; j++) {
  401. field.disabled = !enabled;
  402. }
  403. }
  404. };
  405. /**
  406. * Assign button click handlers for the block library.
  407. */
  408. AppController.prototype.assignLibraryClickHandlers = function() {
  409. var self = this;
  410. // Button for saving block to library.
  411. document.getElementById('saveToBlockLibraryButton').addEventListener('click',
  412. function() {
  413. self.blockLibraryController.saveToBlockLibrary();
  414. });
  415. // Button for removing selected block from library.
  416. document.getElementById('removeBlockFromLibraryButton').addEventListener(
  417. 'click',
  418. function() {
  419. self.blockLibraryController.removeFromBlockLibrary();
  420. });
  421. // Button for clearing the block library.
  422. document.getElementById('clearBlockLibraryButton').addEventListener('click',
  423. function() {
  424. self.blockLibraryController.clearBlockLibrary();
  425. });
  426. // Hide and show the block library dropdown.
  427. document.getElementById('button_blockLib').addEventListener('click',
  428. function() {
  429. self.openModal('dropdownDiv_blockLib');
  430. });
  431. };
  432. /**
  433. * Assign button click handlers for the block factory.
  434. */
  435. AppController.prototype.assignBlockFactoryClickHandlers = function() {
  436. var self = this;
  437. // Assign button event handlers for Block Factory.
  438. document.getElementById('localSaveButton')
  439. .addEventListener('click', function() {
  440. self.exportBlockLibraryToFile();
  441. });
  442. document.getElementById('helpButton').addEventListener('click',
  443. function() {
  444. open('https://developers.google.com/blockly/custom-blocks/block-factory',
  445. 'BlockFactoryHelp');
  446. });
  447. document.getElementById('files').addEventListener('change',
  448. function() {
  449. // Warn user.
  450. var replace = confirm('This imported block library will ' +
  451. 'replace your current block library.');
  452. if (replace) {
  453. self.importBlockLibraryFromFile();
  454. // Clear this so that the change event still fires even if the
  455. // same file is chosen again. If the user re-imports a file, we
  456. // want to reload the workspace with its contents.
  457. this.value = null;
  458. }
  459. });
  460. document.getElementById('createNewBlockButton')
  461. .addEventListener('click', function() {
  462. // If there are unsaved changes warn user, check if they'd like to
  463. // proceed with unsaved changes, and act accordingly.
  464. var proceedWithUnsavedChanges =
  465. self.blockLibraryController.warnIfUnsavedChanges();
  466. if (!proceedWithUnsavedChanges) {
  467. return;
  468. }
  469. BlockFactory.showStarterBlock();
  470. self.blockLibraryController.setNoneSelected();
  471. // Close the Block Library Dropdown.
  472. self.closeModal();
  473. });
  474. };
  475. /**
  476. * Add event listeners for the block factory.
  477. */
  478. AppController.prototype.addBlockFactoryEventListeners = function() {
  479. // Update code on changes to block being edited.
  480. BlockFactory.mainWorkspace.addChangeListener(BlockFactory.updateLanguage);
  481. // Disable blocks not attached to the factory_base block.
  482. BlockFactory.mainWorkspace.addChangeListener(Blockly.Events.disableOrphans);
  483. // Update the buttons on the screen based on whether
  484. // changes have been saved.
  485. var self = this;
  486. BlockFactory.mainWorkspace.addChangeListener(function() {
  487. self.blockLibraryController.updateButtons(FactoryUtils.savedBlockChanges(
  488. self.blockLibraryController));
  489. });
  490. document.getElementById('direction')
  491. .addEventListener('change', BlockFactory.updatePreview);
  492. document.getElementById('languageTA')
  493. .addEventListener('change', BlockFactory.updatePreview);
  494. document.getElementById('languageTA')
  495. .addEventListener('keyup', BlockFactory.updatePreview);
  496. document.getElementById('format')
  497. .addEventListener('change', BlockFactory.formatChange);
  498. document.getElementById('language')
  499. .addEventListener('change', BlockFactory.updatePreview);
  500. };
  501. /**
  502. * Handle Blockly Storage with App Engine.
  503. */
  504. AppController.prototype.initializeBlocklyStorage = function() {
  505. BlocklyStorage.HTTPREQUEST_ERROR =
  506. 'There was a problem with the request.\n';
  507. BlocklyStorage.LINK_ALERT =
  508. 'Share your blocks with this link:\n\n%1';
  509. BlocklyStorage.HASH_ERROR =
  510. 'Sorry, "%1" doesn\'t correspond with any saved Blockly file.';
  511. BlocklyStorage.XML_ERROR = 'Could not load your saved file.\n' +
  512. 'Perhaps it was created with a different version of Blockly?';
  513. var linkButton = document.getElementById('linkButton');
  514. linkButton.style.display = 'inline-block';
  515. linkButton.addEventListener('click',
  516. function() {
  517. BlocklyStorage.link(BlockFactory.mainWorkspace);});
  518. BlockFactory.disableEnableLink();
  519. };
  520. /**
  521. * Handle resizing of elements.
  522. */
  523. AppController.prototype.onresize = function(event) {
  524. if (this.selectedTab == AppController.BLOCK_FACTORY) {
  525. // Handle resizing of Block Factory elements.
  526. var expandList = [
  527. document.getElementById('blocklyPreviewContainer'),
  528. document.getElementById('blockly'),
  529. document.getElementById('blocklyMask'),
  530. document.getElementById('preview'),
  531. document.getElementById('languagePre'),
  532. document.getElementById('languageTA'),
  533. document.getElementById('generatorPre'),
  534. ];
  535. for (var i = 0, expand; expand = expandList[i]; i++) {
  536. expand.style.width = (expand.parentNode.offsetWidth - 2) + 'px';
  537. expand.style.height = (expand.parentNode.offsetHeight - 2) + 'px';
  538. }
  539. } else if (this.selectedTab == AppController.EXPORTER) {
  540. // Handle resize of Exporter block options.
  541. this.exporter.view.centerPreviewBlocks();
  542. }
  543. };
  544. /**
  545. * Handler for the window's 'onbeforeunload' event. When a user has unsaved
  546. * changes and refreshes or leaves the page, confirm that they want to do so
  547. * before actually refreshing.
  548. */
  549. AppController.prototype.confirmLeavePage = function() {
  550. if ((!BlockFactory.isStarterBlock() &&
  551. !FactoryUtils.savedBlockChanges(this.blockLibraryController)) ||
  552. this.workspaceFactoryController.hasUnsavedChanges()) {
  553. // When a string is assigned to the returnValue Event property, a dialog box
  554. // appears, asking the users for confirmation to leave the page.
  555. return 'You will lose any unsaved changes. Are you sure you want ' +
  556. 'to exit this page?';
  557. }
  558. };
  559. /**
  560. * Show a modal element, usually a dropdown list.
  561. * @param {string} id ID of element to show.
  562. */
  563. AppController.prototype.openModal = function(id) {
  564. Blockly.hideChaff();
  565. this.modalName_ = id;
  566. document.getElementById(id).style.display = 'block';
  567. document.getElementById('modalShadow').style.display = 'block';
  568. };
  569. /**
  570. * Hide a previously shown modal element.
  571. */
  572. AppController.prototype.closeModal = function() {
  573. var id = this.modalName_;
  574. if (!id) {
  575. return;
  576. }
  577. document.getElementById(id).style.display = 'none';
  578. document.getElementById('modalShadow').style.display = 'none';
  579. this.modalName_ = null;
  580. };
  581. /**
  582. * Name of currently open modal.
  583. * @type {string?}
  584. * @private
  585. */
  586. AppController.prototype.modalName_ = null;
  587. /**
  588. * Initialize Blockly and layout. Called on page load.
  589. */
  590. AppController.prototype.init = function() {
  591. // Block Factory has a dependency on bits of Closure that core Blockly
  592. // doesn't have. When you run this from file:// without a copy of Closure,
  593. // it breaks it non-obvious ways. Warning about this for now until the
  594. // dependency is broken.
  595. // TODO: #668.
  596. if (!window.goog.dom.xml) {
  597. alert('Sorry: Closure dependency not found. We are working on removing ' +
  598. 'this dependency. In the meantime, you can use our hosted demo\n ' +
  599. 'https://blockly-demo.appspot.com/static/demos/blockfactory/index.html' +
  600. '\nor use these instructions to continue running locally:\n' +
  601. 'https://developers.google.com/blockly/guides/modify/web/closure');
  602. return;
  603. }
  604. var self = this;
  605. // Handle Blockly Storage with App Engine.
  606. if ('BlocklyStorage' in window) {
  607. this.initializeBlocklyStorage();
  608. }
  609. // Assign click handlers.
  610. this.assignExporterClickHandlers();
  611. this.assignLibraryClickHandlers();
  612. this.assignBlockFactoryClickHandlers();
  613. // Hide and show the block library dropdown.
  614. document.getElementById('modalShadow').addEventListener('click',
  615. function() {
  616. self.closeModal();
  617. });
  618. this.onresize();
  619. window.addEventListener('resize', function() {
  620. self.onresize();
  621. });
  622. // Inject Block Factory Main Workspace.
  623. var toolbox = document.getElementById('blockfactory_toolbox');
  624. BlockFactory.mainWorkspace = Blockly.inject('blockly',
  625. {collapse: false,
  626. toolbox: toolbox,
  627. media: '../../media/'});
  628. // Add tab handlers for switching between Block Factory and Block Exporter.
  629. this.addTabHandlers(this.tabMap);
  630. // Assign exporter change listeners.
  631. this.assignExporterChangeListeners();
  632. // Create the root block on Block Factory main workspace.
  633. if ('BlocklyStorage' in window && window.location.hash.length > 1) {
  634. BlocklyStorage.retrieveXml(window.location.hash.substring(1),
  635. BlockFactory.mainWorkspace);
  636. } else {
  637. BlockFactory.showStarterBlock();
  638. }
  639. BlockFactory.mainWorkspace.clearUndo();
  640. // Add Block Factory event listeners.
  641. this.addBlockFactoryEventListeners();
  642. // Workspace Factory init.
  643. WorkspaceFactoryInit.initWorkspaceFactory(this.workspaceFactoryController);
  644. };