diff --git a/demos/blocklyfactory/app_controller.js b/demos/blocklyfactory/app_controller.js new file mode 100644 index 000000000..4e05e720f --- /dev/null +++ b/demos/blocklyfactory/app_controller.js @@ -0,0 +1,453 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview The AppController Class brings together the Block + * Factory, Block Library, and Block Exporter functionality into a single web + * app. + * + * @author quachtina96 (Tina Quach) + */ +goog.provide('AppController'); + +goog.require('BlockFactory'); +goog.require('BlockLibraryController'); +goog.require('BlockExporterController'); +goog.require('goog.dom.classlist'); +goog.require('goog.string'); + +/** + * Controller for the Blockly Factory + * @constructor + */ +AppController = function() { + // Initialize Block Library + this.blockLibraryName = 'blockLibrary'; + this.blockLibraryController = + new BlockLibraryController(this.blockLibraryName); + this.blockLibraryController.populateBlockLibrary(); + + // Initialize Block Exporter + this.exporter = + new BlockExporterController(this.blockLibraryController.storage); +}; + +/** + * Tied to the 'Import Block Library' button. Imports block library from file to + * Block Factory. Expects user to upload a single file of JSON mapping each + * block type to its xml text representation. + */ +AppController.prototype.importBlockLibraryFromFile = function() { + var self = this; + var files = document.getElementById('files'); + // If the file list is empty, the user likely canceled in the dialog. + if (files.files.length > 0) { + // The input tag doesn't have the "multiple" attribute + // so the user can only choose 1 file. + var file = files.files[0]; + var fileReader = new FileReader(); + + // Create a map of block type to xml text from the file when it has been + // read. + fileReader.addEventListener('load', function(event) { + var fileContents = event.target.result; + // Create empty object to hold the read block library information. + var blockXmlTextMap = Object.create(null); + try { + // Parse the file to get map of block type to xml text. + blockXmlTextMap = self.formatBlockLibForImport_(fileContents); + } catch (e) { + var message = 'Could not load your block library file.\n' + window.alert(message + '\nFile Name: ' + file.name); + return; + } + + // Create a new block library storage object with inputted block library. + var blockLibStorage = new BlockLibraryStorage( + self.blockLibraryName, blockXmlTextMap); + + // Update block library controller with the new block library + // storage. + self.blockLibraryController.setBlockLibStorage(blockLibStorage); + // Update the block library dropdown. + self.blockLibraryController.populateBlockLibrary(); + // Update the exporter's block library storage. + self.exporter.setBlockLibStorage(blockLibStorage); + }); + // Read the file. + fileReader.readAsText(file); + } +}; + +/** + * Tied to the 'Export Block Library' button. Exports block library to file that + * contains JSON mapping each block type to its xml text representation. + */ +AppController.prototype.exportBlockLibraryToFile = function() { + // Get map of block type to xml. + var blockLib = this.blockLibraryController.getBlockLibrary(); + // Concatenate the xmls, each separated by a blank line. + var blockLibText = this.formatBlockLibForExport_(blockLib); + // Get file name. + var filename = prompt('Enter the file name under which to save your block' + + 'library.'); + // Download file if all necessary parameters are provided. + if (filename) { + BlockFactory.createAndDownloadFile_(blockLibText, filename, 'xml'); + } else { + alert('Could not export Block Library without file name under which to ' + + 'save library.'); + } +}; + +/** + * Converts an object mapping block type to xml to text file for output. + * @private + * + * @param {!Object} blockXmlMap - object mapping block type to xml + * @return {string} String of each block's xml separated by a new line. + */ +AppController.prototype.formatBlockLibForExport_ = function(blockXmlMap) { + var blockXmls = []; + for (var blockType in blockXmlMap) { + blockXmls.push(blockXmlMap[blockType]); + } + return blockXmls.join("\n\n"); +}; + +/** + * Converts imported block library to an object mapping block type to block xml. + * @private + * + * @param {string} xmlText - String containing each block's xml optionally + * separated by whitespace. + * @return {!Object} object mapping block type to xml text. + */ +AppController.prototype.formatBlockLibForImport_ = function(xmlText) { + // Get array of xmls. + var xmlText = goog.string.collapseWhitespace(xmlText); + var blockXmls = goog.string.splitLimit(xmlText, '', 500); + + // Create and populate map. + var blockXmlTextMap = Object.create(null); + // The line above is equivalent of {} except that this object is TRULY + // empty. It doesn't have built-in attributes/functions such as length or + // toString. + for (var i = 0, xml; xml = blockXmls[i]; i++) { + var blockType = this.getBlockTypeFromXml_(xml); + blockXmlTextMap[blockType] = xml; + } + + return blockXmlTextMap; +}; + +/** + * Extracts out block type from xml text, the kind that is saved in block + * library storage. + * @private + * + * @param {!string} xmlText - A block's xml text. + * @return {string} The block type that corresponds to the provided xml text. + */ +AppController.prototype.getBlockTypeFromXml_ = function(xmlText) { + var xmlText = Blockly.Options.parseToolboxTree(xmlText); + // Find factory base block. + var factoryBaseBlockXml = xmlText.getElementsByTagName('block')[0]; + // Get field elements from factory base. + var fields = factoryBaseBlockXml.getElementsByTagName('field'); + for (var i = 0; i < fields.length; i++) { + // The field whose name is 'NAME' holds the block type as its value. + if (fields[i].getAttribute('name') == 'NAME') { + return fields[i].childNodes[0].nodeValue; + } + } +}; + +/** + * Updates the Block Factory tab to show selected block when user selects a + * different block in the block library dropdown. Tied to block library dropdown + * in index.html. + * + * @param {!Element} blockLibraryDropdown - HTML select element from which the + * user selects a block to work on. + */ +AppController.prototype.onSelectedBlockChanged = function(blockLibraryDropdown) { + // Get selected block type. + var blockType = this.blockLibraryController.getSelectedBlockType( + blockLibraryDropdown); + // Update Block Factory page by showing the selected block. + this.blockLibraryController.openBlock(blockType); +}; + +/** + * Add tab handlers to allow switching between the Block Factory + * tab and the Block Exporter tab. + * + * @param {string} blockFactoryTabID - ID of element containing Block Factory + * tab + * @param {string} blockExporterTabID - ID of element containing Block + * Exporter tab + */ +AppController.prototype.addTabHandlers = + function(blockFactoryTabID, blockExporterTabID) { + // Assign this instance of Block Factory Expansion to self in order to + // keep the reference to this object upon tab click. + var self = this; + // Get div elements representing tabs + var blockFactoryTab = goog.dom.getElement(blockFactoryTabID); + var blockExporterTab = goog.dom.getElement(blockExporterTabID); + // Add event listeners. + blockFactoryTab.addEventListener('click', + function() { + self.onFactoryTab(blockFactoryTab, blockExporterTab); + }); + blockExporterTab.addEventListener('click', + function() { + self.onExporterTab(blockFactoryTab, blockExporterTab); + }); +}; + +/** + * Tied to 'Block Factory' Tab. Shows Block Factory and Block Library. + * + * @param {string} blockFactoryTab - div element that is the Block Factory tab + * @param {string} blockExporterTab - div element that is the Block Exporter tab + */ +AppController.prototype.onFactoryTab = + function(blockFactoryTab, blockExporterTab) { + // Turn factory tab on and exporter tab off. + goog.dom.classlist.addRemove(blockFactoryTab, 'taboff', 'tabon'); + goog.dom.classlist.addRemove(blockExporterTab, 'tabon', 'taboff'); + + // Hide container of exporter. + BlockFactory.hide('blockLibraryExporter'); + + // Resize to render workspaces' toolboxes correctly. + window.dispatchEvent(new Event('resize')); +}; + +/** + * Tied to 'Block Exporter' Tab. Shows Block Exporter. + * + * @param {string} blockFactoryTab - div element that is the Block Factory tab + * @param {string} blockExporterTab - div element that is the Block Exporter tab + */ +AppController.prototype.onExporterTab = + function(blockFactoryTab, blockExporterTab) { + // Turn exporter tab on and factory tab off. + goog.dom.classlist.addRemove(blockFactoryTab, 'tabon', 'taboff'); + goog.dom.classlist.addRemove(blockExporterTab, 'taboff', 'tabon'); + + // Update toolbox to reflect current block library. + this.exporter.updateToolbox(); + + // Show container of exporter. + BlockFactory.show('blockLibraryExporter'); + + // Resize to render workspaces' toolboxes correctly. + window.dispatchEvent(new Event('resize')); +}; + +/** + * Assign button click handlers for the exporter. + */ +AppController.prototype.assignExporterClickHandlers = function() { + var self = this; + // Export blocks when the user submits the export settings. + document.getElementById('exporterSubmitButton').addEventListener('click', + function() { + self.exporter.exportBlocks(); + }); + document.getElementById('clearSelectedButton').addEventListener('click', + function() { + self.exporter.clearSelectedBlocks(); + }); + document.getElementById('addAllButton').addEventListener('click', + function() { + self.exporter.addAllBlocksToWorkspace(); + }); +}; + +/** + * Assign button click handlers for the block library. + */ +AppController.prototype.assignLibraryClickHandlers = function() { + var self = this; + // Assign button click handlers for Block Library. + document.getElementById('saveToBlockLibraryButton').addEventListener('click', + function() { + self.blockLibraryController.saveToBlockLibrary(); + }); + + document.getElementById('removeBlockFromLibraryButton').addEventListener( + 'click', + function() { + self.blockLibraryController.removeFromBlockLibrary(); + }); + + document.getElementById('clearBlockLibraryButton').addEventListener('click', + function() { + self.blockLibraryController.clearBlockLibrary(); + }); + + var dropdown = document.getElementById('blockLibraryDropdown'); + dropdown.addEventListener('change', + function() { + self.onSelectedBlockChanged(dropdown); + }); +}; + +/** + * Assign button click handlers for the block factory. + */ +AppController.prototype.assignFactoryClickHandlers = function() { + var self = this; + // Assign button event handlers for Block Factory. + document.getElementById('localSaveButton') + .addEventListener('click', function() { + self.exportBlockLibraryToFile(); + }); + document.getElementById('helpButton').addEventListener('click', + function() { + open('https://developers.google.com/blockly/custom-blocks/block-factory', + 'BlockFactoryHelp'); + }); + document.getElementById('downloadBlocks').addEventListener('click', + function() { + BlockFactory.downloadTextArea('blocks', 'languagePre'); + }); + document.getElementById('downloadGenerator').addEventListener('click', + function() { + BlockFactory.downloadTextArea('generator', 'generatorPre'); + }); + document.getElementById('files').addEventListener('change', + function() { + // Warn user. + var replace = confirm('This imported block library will ' + + 'replace your current block library.'); + if (replace) { + self.importBlockLibraryFromFile(); + // Clear this so that the change event still fires even if the + // same file is chosen again. If the user re-imports a file, we + // want to reload the workspace with its contents. + this.value = null; + } + }); + document.getElementById('createNewBlockButton') + .addEventListener('click', function() { + BlockFactory.mainWorkspace.clear(); + BlockFactory.showStarterBlock(); + BlockLibraryView.selectDefaultOption('blockLibraryDropdown'); + }); +}; + +/** + * Add event listeners for the block factory. + */ +AppController.prototype.addFactoryEventListeners = function() { + BlockFactory.mainWorkspace.addChangeListener(BlockFactory.updateLanguage); + document.getElementById('direction') + .addEventListener('change', BlockFactory.updatePreview); + document.getElementById('languageTA') + .addEventListener('change', BlockFactory.updatePreview); + document.getElementById('languageTA') + .addEventListener('keyup', BlockFactory.updatePreview); + document.getElementById('format') + .addEventListener('change', BlockFactory.formatChange); + document.getElementById('language') + .addEventListener('change', BlockFactory.updatePreview); +}; + +/** + * Handle Blockly Storage with App Engine. + */ +AppController.prototype.initializeBlocklyStorage = function() { + BlocklyStorage.HTTPREQUEST_ERROR = + 'There was a problem with the request.\n'; + BlocklyStorage.LINK_ALERT = + 'Share your blocks with this link:\n\n%1'; + BlocklyStorage.HASH_ERROR = + 'Sorry, "%1" doesn\'t correspond with any saved Blockly file.'; + BlocklyStorage.XML_ERROR = 'Could not load your saved file.\n' + + 'Perhaps it was created with a different version of Blockly?'; + var linkButton = document.getElementById('linkButton'); + linkButton.style.display = 'inline-block'; + linkButton.addEventListener('click', + function() { + BlocklyStorage.link(BlockFactory.mainWorkspace);}); + BlockFactory.disableEnableLink(); +}; +/** + * Initialize Blockly and layout. Called on page load. + */ +AppController.prototype.init = function() { + // Handle Blockly Storage with App Engine + if ('BlocklyStorage' in window) { + this.initializeBlocklyStorage(); + } + + // Assign click handlers. + this.assignExporterClickHandlers(); + this.assignLibraryClickHandlers(); + this.assignFactoryClickHandlers(); + + // Handle resizing of Block Factory elements. + var expandList = [ + document.getElementById('blockly'), + document.getElementById('blocklyMask'), + document.getElementById('preview'), + document.getElementById('languagePre'), + document.getElementById('languageTA'), + document.getElementById('generatorPre') + ]; + + var onresize = function(e) { + for (var i = 0, expand; expand = expandList[i]; i++) { + expand.style.width = (expand.parentNode.offsetWidth - 2) + 'px'; + expand.style.height = (expand.parentNode.offsetHeight - 2) + 'px'; + } + }; + onresize(); + window.addEventListener('resize', onresize); + + // Inject Block Factory Main Workspace. + var toolbox = document.getElementById('toolbox'); + BlockFactory.mainWorkspace = Blockly.inject('blockly', + {collapse: false, + toolbox: toolbox, + media: '../../media/'}); + + // Add tab handlers for switching between Block Factory and Block Exporter. + this.addTabHandlers("blockfactory_tab", "blocklibraryExporter_tab"); + + this.exporter.addChangeListenersToSelectorWorkspace(); + + // Create the root block on Block Factory main workspace. + if ('BlocklyStorage' in window && window.location.hash.length > 1) { + BlocklyStorage.retrieveXml(window.location.hash.substring(1), + BlockFactory.mainWorkspace); + } else { + BlockFactory.showStarterBlock(); + } + BlockFactory.mainWorkspace.clearUndo(); + + // Add Block Factory event listeners. + this.addFactoryEventListeners(); +}; diff --git a/demos/blocklyfactory/block_exporter_controller.js b/demos/blocklyfactory/block_exporter_controller.js new file mode 100644 index 000000000..04892e5d8 --- /dev/null +++ b/demos/blocklyfactory/block_exporter_controller.js @@ -0,0 +1,272 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Javascript for the Block Exporter Controller class. Allows + * users to export block definitions and generator stubs of their saved blocks + * easily using a visual interface. Depends on Block Exporter View and Block + * Exporter Tools classes. Interacts with Export Settings in the index.html. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockExporterController'); +goog.require('BlockExporterView'); +goog.require('BlockExporterTools'); +goog.require('goog.dom.xml'); + +/** + * BlockExporter Controller Class + * @constructor + * + * @param {!BlockLibrary.Storage} blockLibStorage - Block Library Storage. + */ +BlockExporterController = function(blockLibStorage) { + // BlockLibrary.Storage object containing user's saved blocks + this.blockLibStorage = blockLibStorage; + // Utils for generating code to export + this.tools = new BlockExporterTools(); + // View provides the selector workspace and export settings UI. + this.view = new BlockExporterView( + //Xml representation of the toolbox + this.tools.generateToolboxFromLibrary(this.blockLibStorage)); +}; + +/** + * Set the block library storage object from which exporter exports. + * + * @param {!BlockLibraryStorage} blockLibStorage - Block Library Storage object + * that stores the blocks. + */ +BlockExporterController.prototype.setBlockLibStorage = + function(blockLibStorage) { + this.blockLibStorage = blockLibStorage; +}; + +/** + * Get the block library storage object from which exporter exports. + * + * @return {!BlockLibraryStorage} blockLibStorage - Block Library Storage object + * that stores the blocks. + */ +BlockExporterController.prototype.getBlockLibStorage = + function(blockLibStorage) { + return this.blockLibStorage; +}; + +/** + * Get the selected block types. + * @private + * + * @return {!Array.} Types of blocks in workspace. + */ +BlockExporterController.prototype.getSelectedBlockTypes_ = function() { + var selectedBlocks = this.view.getSelectedBlocks(); + var blockTypes = []; + for (var i = 0, block; block = selectedBlocks[i]; i++) { + blockTypes.push(block.type); + } + return blockTypes; +}; + +/** + * Get selected blocks from selector workspace, pulls info from the Export + * Settings form in Block Exporter, and downloads block code accordingly. + */ +BlockExporterController.prototype.exportBlocks = function() { + var blockTypes = this.getSelectedBlockTypes_(); + var blockXmlMap = this.blockLibStorage.getBlockXmlMap(blockTypes); + + // Pull inputs from the Export Settings form. + var definitionFormat = document.getElementById('exportFormat').value; + var language = document.getElementById('exportLanguage').value; + var blockDef_filename = document.getElementById('blockDef_filename').value; + var generatorStub_filename = document.getElementById( + 'generatorStub_filename').value; + var wantBlockDef = document.getElementById('blockDefCheck').checked; + var wantGenStub = document.getElementById('genStubCheck').checked; + + if (wantBlockDef) { + // User wants to export selected blocks' definitions. + if (!blockDef_filename) { + // User needs to enter filename. + alert('Please enter a filename for your block definition(s) download.'); + } else { + // Get block definition code in the selected format for the blocks. + var blockDefs = this.tools.getBlockDefs(blockXmlMap, + definitionFormat); + // Download the file. + BlockFactory.createAndDownloadFile_( + blockDefs, blockDef_filename, definitionFormat); + } + } + + if (wantGenStub) { + // User wants to export selected blocks' generator stubs. + if (!generatorStub_filename) { + // User needs to enter filename. + alert('Please enter a filename for your generator stub(s) download.'); + } else { + // Get generator stub code in the selected language for the blocks. + var genStubs = this.tools.getGeneratorCode(blockXmlMap, + language); + // Download the file. + BlockFactory.createAndDownloadFile_( + genStubs, generatorStub_filename, language); + } + } +}; + +/** + * Update the Exporter's toolbox with either the given toolbox xml or toolbox + * xml generated from blocks stored in block library. + * + * @param {Element} opt_toolboxXml - Xml to define toolbox of the selector + * workspace. + */ +BlockExporterController.prototype.updateToolbox = function(opt_toolboxXml) { + // Use given xml or xml generated from updated block library. + var updatedToolbox = opt_toolboxXml || + this.tools.generateToolboxFromLibrary(this.blockLibStorage); + // Update the view's toolbox. + this.view.setToolbox(updatedToolbox); + // Render the toolbox in the selector workspace. + this.view.renderToolbox(updatedToolbox); + // Disable any selected blocks. + var selectedBlocks = this.getSelectedBlockTypes_(); + for (var i = 0, blockType; blockType = selectedBlocks[i]; i++) { + this.setBlockEnabled(blockType, false); + } +}; + +/** + * Enable or Disable block in selector workspace's toolbox. + * + * @param {!string} blockType - Type of block to disable or enable. + * @param {!boolean} enable - True to enable the block, false to disable block. + */ +BlockExporterController.prototype.setBlockEnabled = + function(blockType, enable) { + // Get toolbox xml, category, and block elements. + var toolboxXml = this.view.toolbox; + var category = goog.dom.xml.selectSingleNode(toolboxXml, + '//category[@name="' + blockType + '"]'); + var block = goog.dom.getFirstElementChild(category); + // Enable block. + goog.dom.xml.setAttributes(block, {disabled: !enable}); +}; + +/** + * Add change listeners to the exporter's selector workspace. + */ +BlockExporterController.prototype.addChangeListenersToSelectorWorkspace + = function() { + // Assign the BlockExporterController to 'self' to be called in the change + // listeners. This keeps it in scope--otherwise, 'this' in the change + // listeners refers to the wrong thing. + var self = this; + var selector = this.view.selectorWorkspace; + selector.addChangeListener( + function(event) { + self.onSelectBlockForExport_(event); + }); + selector.addChangeListener( + function(event) { + self.onDeselectBlockForExport_(event); + }); +}; + +/** + * Callback function for when a user selects a block for export in selector + * workspace. Disables selected block so that the user only exports one + * copy of starter code per block. Attached to the blockly create event in block + * factory expansion's init. + * @private + * + * @param {!Blockly.Events} event - The fired Blockly event. + */ +BlockExporterController.prototype.onSelectBlockForExport_ = function(event) { + // The user created a block in selector workspace. + if (event.type == Blockly.Events.CREATE) { + // Get type of block created. + var block = this.view.selectorWorkspace.getBlockById(event.blockId); + var blockType = block.type; + // Disable the selected block. Users can only export one copy of starter + // code per block. + this.setBlockEnabled(blockType, false); + // Show currently selected blocks in helper text. + this.view.listSelectedBlocks(this.getSelectedBlockTypes_()); + } +}; + +/** + * Callback function for when a user deselects a block in selector + * workspace by deleting it. Re-enables block so that the user may select it for + * export + * @private + * + * @param {!Blockly.Events} event - The fired Blockly event. + */ +BlockExporterController.prototype.onDeselectBlockForExport_ = function(event) { + // The user deleted a block in selector workspace. + if (event.type == Blockly.Events.DELETE) { + // Get type of block created. + var deletedBlockXml = event.oldXml; + var blockType = deletedBlockXml.getAttribute('type'); + // Enable the deselected block. + this.setBlockEnabled(blockType, true); + // Show currently selected blocks in helper text. + this.view.listSelectedBlocks(this.getSelectedBlockTypes_()); + } +}; + +/** + * Tied to the 'Clear Selected Blocks' button in the Block Exporter. + * Deselects all blocks on the selector workspace by deleting them and updating + * text accordingly. + */ +BlockExporterController.prototype.clearSelectedBlocks = function() { + // Clear selector workspace. + this.view.clearSelectorWorkspace(); +}; + +/** + * Tied to the 'Add All Stored Blocks' button in the Block Exporter. + * Adds all blocks stored in block library to the selector workspace. + */ +BlockExporterController.prototype.addAllBlocksToWorkspace = function() { + // Clear selector workspace. + this.view.clearSelectorWorkspace(); + + // Add and evaluate all blocks' definitions. + var allBlockTypes = this.blockLibStorage.getBlockTypes(); + var blockXmlMap = this.blockLibStorage.getBlockXmlMap(allBlockTypes); + this.tools.addBlockDefinitions(blockXmlMap); + + // For every block, render in selector workspace. + for (var i = 0, blockType; blockType = allBlockTypes[i]; i++) { + this.view.addBlock(blockType); + } + + // Clean up workspace. + this.view.cleanUpSelectorWorkspace(); +}; diff --git a/demos/blocklyfactory/block_exporter_tools.js b/demos/blocklyfactory/block_exporter_tools.js new file mode 100644 index 000000000..65c50ddad --- /dev/null +++ b/demos/blocklyfactory/block_exporter_tools.js @@ -0,0 +1,222 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Javascript for the BlockExporter Tools class, which generates + * block definitions and generator stubs for given block types. Also generates + * toolbox xml for the exporter's workspace. Depends on the BlockFactory for + * its code generation functions. + * + * @author quachtina96 (Tina Quach) + */ +'use strict'; + +goog.provide('BlockExporterTools'); + +goog.require('BlockFactory'); +goog.require('goog.dom'); +goog.require('goog.dom.xml'); + +/** +* Block Exporter Tools Class +* @constructor +*/ +BlockExporterTools = function() { + // Create container for hidden workspace. + this.container = goog.dom.createDom('div', { + 'id': 'blockExporterTools_hiddenWorkspace' + }, ''); // Empty quotes for empty div. + // Hide hidden workspace. + this.container.style.display = 'none'; + goog.dom.appendChild(document.body, this.container); + /** + * Hidden workspace for the Block Exporter that holds pieces that make + * up the block + * @type {Blockly.Workspace} + */ + this.hiddenWorkspace = Blockly.inject(this.container.id, + {collapse: false, + media: '../../media/'}); +}; + +/** + * Get Blockly Block object from xml that encodes the blocks used to design + * the block. + * @private + * + * @param {!Element} xml - Xml element that encodes the blocks used to design + * the block. For example, the block xmls saved in block library. + * @return {!Blockly.Block} - Root block (factory_base block) which contains + * all information needed to generate block definition or null. + */ +BlockExporterTools.prototype.getRootBlockFromXml_ = function(xml) { + // Render xml in hidden workspace. + this.hiddenWorkspace.clear(); + Blockly.Xml.domToWorkspace(xml, this.hiddenWorkspace); + // Get root block. + var rootBlock = this.hiddenWorkspace.getTopBlocks()[0] || null; + return rootBlock; +}; + +/** + * Get Blockly Block by rendering pre-defined block in workspace. + * @private + * + * @param {!Element} blockType - Type of block. + * @return {!Blockly.Block} the Blockly.Block of desired type. + */ +BlockExporterTools.prototype.getDefinedBlock_ = function(blockType) { + this.hiddenWorkspace.clear(); + return this.hiddenWorkspace.newBlock(blockType); +}; + +/** + * Return the given language code of each block type in an array. + * + * @param {!Object} blockXmlMap - Map of block type to xml. + * @param {string} definitionFormat - 'JSON' or 'JavaScript' + * @return {string} The concatenation of each block's language code in the + * desired format. + */ +BlockExporterTools.prototype.getBlockDefs = + function(blockXmlMap, definitionFormat) { + var blockCode = []; + for (var blockType in blockXmlMap) { + var xml = blockXmlMap[blockType]; + if (xml) { + // Render and get block from hidden workspace. + var rootBlock = this.getRootBlockFromXml_(xml); + if (rootBlock) { + // Generate the block's definition. + var code = BlockFactory.getBlockDefinition(blockType, rootBlock, + definitionFormat, this.hiddenWorkspace); + // Add block's definition to the definitions to return. + } else { + // Append warning comment and write to console. + var code = '// No block definition generated for ' + blockType + + '. Could not find root block in xml stored for this block.'; + console.log('No block definition generated for ' + blockType + + '. Could not find root block in xml stored for this block.'); + } + } else { + // Append warning comment and write to console. + var code = '// No block definition generated for ' + blockType + + '. Block was not found in Block Library Storage.'; + console.log('No block definition generated for ' + blockType + + '. Block was not found in Block Library Storage.'); + } + blockCode.push(code); + } + return blockCode.join("\n\n"); +}; + +/** + * Return the generator code of each block type in an array in a given language. + * + * @param {!Object} blockXmlMap - Map of block type to xml. + * @param {string} generatorLanguage - e.g.'JavaScript', 'Python', 'PHP', 'Lua', + * 'Dart' + * @return {string} The concatenation of each block's generator code in the + * desired format. + */ +BlockExporterTools.prototype.getGeneratorCode = + function(blockXmlMap, generatorLanguage) { + var multiblockCode = []; + // Define the custom blocks in order to be able to create instances of + // them in the exporter workspace. + this.addBlockDefinitions(blockXmlMap); + + for (var blockType in blockXmlMap) { + var xml = blockXmlMap[blockType]; + if (xml) { + // Render the preview block in the hidden workspace. + var tempBlock = this.getDefinedBlock_(blockType); + // Get generator stub for the given block and add to generator code. + var blockGenCode = + BlockFactory.getGeneratorStub(tempBlock, generatorLanguage); + } else { + // Append warning comment and write to console. + var blockGenCode = '// No generator stub generated for ' + blockType + + '. Block was not found in Block Library Storage.'; + console.log('No block generator stub generated for ' + blockType + + '. Block was not found in Block Library Storage.'); + } + multiblockCode.push(blockGenCode); + } + return multiblockCode.join("\n\n"); +}; + +/** + * Evaluates block definition code of each block in given object mapping + * block type to xml. Called in order to be able to create instances of the + * blocks in the exporter workspace. + * + * @param {!Object} blockXmlMap - Map of block type to xml. + */ +BlockExporterTools.prototype.addBlockDefinitions = function(blockXmlMap) { + var blockDefs = this.getBlockDefs(blockXmlMap, 'JavaScript'); + eval(blockDefs); +}; + +/** + * Pulls information about all blocks in the block library to generate xml + * for the selector workpace's toolbox. + * + * @return {!Element} Xml representation of the toolbox. + */ +BlockExporterTools.prototype.generateToolboxFromLibrary + = function(blockLibStorage) { + // Create DOM for XML. + var xmlDom = goog.dom.createDom('xml', { + 'id' : 'blockExporterTools_toolbox', + 'style' : 'display:none' + }); + + var allBlockTypes = blockLibStorage.getBlockTypes(); + // Object mapping block type to XML. + var blockXmlMap = blockLibStorage.getBlockXmlMap(allBlockTypes); + + // Define the custom blocks in order to be able to create instances of + // them in the exporter workspace. + this.addBlockDefinitions(blockXmlMap); + + for (var blockType in blockXmlMap) { + // Create category DOM element. + var categoryElement = goog.dom.createDom('category'); + categoryElement.setAttribute('name',blockType); + + // Get block. + var block = this.getDefinedBlock_(blockType); + + // Get preview block XML. + var blockChild = Blockly.Xml.blockToDom(block); + blockChild.removeAttribute('id'); + + // Add block to category and category to XML. + categoryElement.appendChild(blockChild); + xmlDom.appendChild(categoryElement); + } + + // If there are no blocks in library, append dummy category. + var categoryElement = goog.dom.createDom('category'); + categoryElement.setAttribute('name','Next Saved Block'); + xmlDom.appendChild(categoryElement); + return xmlDom; +}; diff --git a/demos/blocklyfactory/block_exporter_view.js b/demos/blocklyfactory/block_exporter_view.js new file mode 100644 index 000000000..ee1d76c6c --- /dev/null +++ b/demos/blocklyfactory/block_exporter_view.js @@ -0,0 +1,145 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Javascript for the Block Exporter View class. Takes care of + * generating the selector workspace through which users select blocks to + * export. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockExporterView'); + +goog.require('goog.dom'); + +/** + * BlockExporter View Class + * @constructor + * + * @param {Element} toolbox - Xml for the toolbox of the selector workspace. + */ +BlockExporterView = function(toolbox) { + // Xml representation of the toolbox + if (toolbox.hasChildNodes) { + this.toolbox = toolbox; + } else { + // Toolbox is empty. Append dummy category to toolbox because toolbox + // cannot switch between category and flyout-only mode after injection. + var categoryElement = goog.dom.createDom('category'); + categoryElement.setAttribute('name', 'Next Saved Block'); + toolbox.appendChild(categoryElement); + this.toolbox = toolbox; + } + // Workspace users use to select blocks for export + this.selectorWorkspace = + Blockly.inject('exportSelector', + {collapse: false, + toolbox: this.toolbox, + grid: + {spacing: 20, + length: 3, + colour: '#ccc', + snap: true} + }); +}; + +/** + * Update the toolbox of this instance of BlockExporterView. + * + * @param {Element} toolboxXml - Xml for the toolbox of the selector workspace. + */ +BlockExporterView.prototype.setToolbox = function(toolboxXml) { + // Parse the provided toolbox tree into a consistent DOM format. + this.toolbox = Blockly.Options.parseToolboxTree(toolboxXml); +}; + +/** + * Renders the toolbox in the workspace. Used to update the toolbox upon + * switching between Block Factory tab and Block Exporter Tab. + */ +BlockExporterView.prototype.renderToolbox = function() { + this.selectorWorkspace.updateToolbox(this.toolbox); +}; + +/** + * Updates the helper text. + * + * @param {string} newText - New helper text. + * @param {boolean} opt_append - True if appending to helper Text, false if + * replacing. + */ +BlockExporterView.prototype.updateHelperText = function(newText, opt_append) { + if (opt_append) { + goog.dom.getElement('helperText').textContent = + goog.dom.getElement('helperText').textContent + newText; + } else { + goog.dom.getElement('helperText').textContent = newText; + } +}; + +/** + * Updates the helper text to show list of currently selected blocks. + * + * @param {!Array.} selectedBlockTypes - Array of blocks selected in workspace. + */ +BlockExporterView.prototype.listSelectedBlocks = function(selectedBlockTypes) { + var selectedBlocksText = selectedBlockTypes.join(', '); + this.updateHelperText('Currently Selected: ' + selectedBlocksText); +}; + +/** + * Renders block of given type on selector workspace assuming block has already + * been defined. + * + * @param {string} blockType - Type of block to add to selector workspce. + */ +BlockExporterView.prototype.addBlock = function(blockType) { + var newBlock = this.selectorWorkspace.newBlock(blockType); + newBlock.initSvg(); + newBlock.render(); +}; + +/** + * Clears selector workspace. + */ +BlockExporterView.prototype.clearSelectorWorkspace = function() { + this.selectorWorkspace.clear(); +}; + +/** + * Neatly layout the blocks in selector workspace. + */ +BlockExporterView.prototype.cleanUpSelectorWorkspace = function() { + this.selectorWorkspace.cleanUp_(); +}; + +/** + * Returns array of selected blocks. + * + * @return {Array.} Array of all blocks in selector workspace. + */ +BlockExporterView.prototype.getSelectedBlocks = function() { + return this.selectorWorkspace.getAllBlocks(); +}; + + diff --git a/demos/blocklyfactory/block_library_controller.js b/demos/blocklyfactory/block_library_controller.js new file mode 100644 index 000000000..0b7210118 --- /dev/null +++ b/demos/blocklyfactory/block_library_controller.js @@ -0,0 +1,219 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Contains the code for Block Library Controller, which + * depends on Block Library Storage and Block Library UI. Provides the + * interfaces for the user to + * - save their blocks to the browser + * - re-open and edit saved blocks + * - delete blocks + * - clear their block library + * Depends on BlockFactory functions defined in factory.js. + * + * @author quachtina96 (Tina Quach) + */ +'use strict'; + +goog.provide('BlockLibraryController'); + +goog.require('BlockLibraryStorage'); +goog.require('BlockLibraryView'); +goog.require('BlockFactory'); + +/** + * Block Library Controller Class + * @constructor + * + * @param {string} blockLibraryName - Desired name of Block Library, also used + * to create the key for where it's stored in local storage. + * @param {!BlockLibraryStorage} opt_blockLibraryStorage - optional storage + * object that allows user to import a block library. + */ +BlockLibraryController = function(blockLibraryName, opt_blockLibraryStorage) { + this.name = blockLibraryName; + // Create a new, empty Block Library Storage object, or load existing one. + this.storage = opt_blockLibraryStorage || new BlockLibraryStorage(this.name); +}; + +/** + * Returns the block type of the block the user is building. + * @private + * + * @return {string} The current block's type. + */ +BlockLibraryController.prototype.getCurrentBlockType_ = function() { + var rootBlock = BlockFactory.getRootBlock(BlockFactory.mainWorkspace); + var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase(); + // Replace white space with underscores + return blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1'); +}; + +/** + * Removes current block from Block Library + * + * @param {string} blockType - Type of block. + */ +BlockLibraryController.prototype.removeFromBlockLibrary = function() { + var blockType = this.getCurrentBlockType_(); + this.storage.removeBlock(blockType); + this.storage.saveToLocalStorage(); + this.populateBlockLibrary(); +}; + +/** + * Updates the workspace to show the block user selected from library + * + * @param {string} blockType - Block to edit on block factory. + */ +BlockLibraryController.prototype.openBlock = function(blockType) { + var xml = this.storage.getBlockXml(blockType); + BlockFactory.mainWorkspace.clear(); + Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace); +}; + +/** + * Returns type of block selected from library. + * + * @param {Element} blockLibraryDropdown - The block library dropdown. + * @return {string} Type of block selected. + */ +BlockLibraryController.prototype.getSelectedBlockType = + function(blockLibraryDropdown) { + return BlockLibraryView.getSelected(blockLibraryDropdown); +}; + +/** + * Confirms with user before clearing the block library in local storage and + * updating the dropdown. + */ +BlockLibraryController.prototype.clearBlockLibrary = function() { + var check = confirm( + 'Click OK to clear your block library.'); + if (check) { + // Clear Block Library Storage. + this.storage.clear(); + this.storage.saveToLocalStorage(); + // Update dropdown. + BlockLibraryView.clearOptions('blockLibraryDropdown'); + // Add a default, blank option to dropdown for when no block from library is + // selected. + BlockLibraryView.addDefaultOption('blockLibraryDropdown'); + } +}; + +/** + * Saves current block to local storage and updates dropdown. + */ +BlockLibraryController.prototype.saveToBlockLibrary = function() { + var blockType = this.getCurrentBlockType_(); + // If block under that name already exists, confirm that user wants to replace + // saved block. + if (this.isInBlockLibrary(blockType)) { + var replace = confirm('You already have a block called ' + blockType + + ' in your library. Click OK to replace.'); + if (!replace) { + // Do not save if user doesn't want to replace the saved block. + return; + } + } + + // Save block. + var xmlElement = Blockly.Xml.workspaceToDom(BlockFactory.mainWorkspace); + this.storage.addBlock(blockType, xmlElement); + this.storage.saveToLocalStorage(); + + // Do not add another option to dropdown if replacing. + if (replace) { + return; + } + BlockLibraryView.addOption( + blockType, blockType, 'blockLibraryDropdown', true, true); +}; + +/** + * Checks to see if the given blockType is already in Block Library + * + * @param {string} blockType - Type of block. + * @return {boolean} Boolean indicating whether or not block is in the library. + */ +BlockLibraryController.prototype.isInBlockLibrary = function(blockType) { + var blockLibrary = this.storage.blocks; + return (blockType in blockLibrary && blockLibrary[blockType] != null); +}; + +/** + * Populates the dropdown menu. + */ +BlockLibraryController.prototype.populateBlockLibrary = function() { + BlockLibraryView.clearOptions('blockLibraryDropdown'); + // Add a default, blank option to dropdown for when no block from library is + // selected. + BlockLibraryView.addDefaultOption('blockLibraryDropdown'); + // Add option for each saved block. + var blockLibrary = this.storage.blocks; + for (var block in blockLibrary) { + // Make sure the block wasn't deleted. + if (blockLibrary[block] != null) { + BlockLibraryView.addOption( + block, block, 'blockLibraryDropdown', false, true); + } + } +}; + +/** + * Return block library mapping block type to xml. + * + * @return {Object} Object mapping block type to xml text. + */ +BlockLibraryController.prototype.getBlockLibrary = function() { + return this.storage.getBlockXmlTextMap(); +}; + +/** + * Set the block library storage object from which exporter exports. + * + * @param {!BlockLibraryStorage} blockLibStorage - Block Library Storage + * object. + */ +BlockLibraryController.prototype.setBlockLibStorage + = function(blockLibStorage) { + this.storage = blockLibStorage; +}; + +/** + * Get the block library storage object from which exporter exports. + * + * @return {!BlockLibraryStorage} blockLibStorage - Block Library Storage object + * that stores the blocks. + */ +BlockLibraryController.prototype.getBlockLibStorage = + function(blockLibStorage) { + return this.blockLibStorage; +}; + +/** + * Get the block library storage object from which exporter exports. + * + * @return {boolean} True if the Block Library is empty, false otherwise. + */ +BlockLibraryController.prototype.hasEmptyBlockLib = function() { + return this.storage.isEmpty(); +}; diff --git a/demos/blocklyfactory/block_library_storage.js b/demos/blocklyfactory/block_library_storage.js new file mode 100644 index 000000000..4bca70242 --- /dev/null +++ b/demos/blocklyfactory/block_library_storage.js @@ -0,0 +1,167 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Javascript for Block Library's Storage Class. + * Depends on Block Library for its namespace. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockLibraryStorage'); + +/** + * Represents a block library's storage. + * @constructor + * + * @param {string} blockLibraryName - Desired name of Block Library, also used + * to create the key for where it's stored in local storage. + * @param {Object} opt_blocks - Object mapping block type to xml. + */ +BlockLibraryStorage = function(blockLibraryName, opt_blocks) { + // Add prefix to this.name to avoid collisions in local storage. + this.name = 'BlockLibraryStorage.' + blockLibraryName; + if (!opt_blocks) { + // Initialize this.blocks by loading from local storage. + this.loadFromLocalStorage(); + if (this.blocks == null) { + this.blocks = Object.create(null); + // The line above is equivalent of {} except that this object is TRULY + // empty. It doesn't have built-in attributes/functions such as length or + // toString. + this.saveToLocalStorage(); + } + } else { + this.blocks = opt_blocks; + this.saveToLocalStorage(); + } +}; + +/** + * Reads the named block library from local storage and saves it in this.blocks. + */ +BlockLibraryStorage.prototype.loadFromLocalStorage = function() { + // goog.global is synonymous to window, and allows for flexibility + // between browsers. + var object = goog.global.localStorage[this.name]; + this.blocks = object ? JSON.parse(object) : null; +}; + +/** + * Writes the current block library (this.blocks) to local storage. + */ +BlockLibraryStorage.prototype.saveToLocalStorage = function() { + goog.global.localStorage[this.name] = JSON.stringify(this.blocks); +}; + +/** + * Clears the current block library. + */ +BlockLibraryStorage.prototype.clear = function() { + this.blocks = Object.create(null); + // The line above is equivalent of {} except that this object is TRULY + // empty. It doesn't have built-in attributes/functions such as length or + // toString. +}; + +/** + * Saves block to block library. + * + * @param {string} blockType - Type of block. + * @param {Element} blockXML - The block's XML pulled from workspace. + */ +BlockLibraryStorage.prototype.addBlock = function(blockType, blockXML) { + var prettyXml = Blockly.Xml.domToPrettyText(blockXML); + this.blocks[blockType] = prettyXml; +}; + +/** + * Removes block from current block library (this.blocks). + * + * @param {string} blockType - Type of block. + */ +BlockLibraryStorage.prototype.removeBlock = function(blockType) { + delete this.blocks[blockType]; +}; + +/** + * Returns the xml of given block type stored in current block library + * (this.blocks). + * + * @param {string} blockType - Type of block. + * @return {Element} The xml that represents the block type or null. + */ +BlockLibraryStorage.prototype.getBlockXml = function(blockType) { + var xml = this.blocks[blockType] || null; + if (xml) { + var xml = Blockly.Xml.textToDom(xml); + } + return xml; +}; + + +/** + * Returns map of each block type to its corresponding xml stored in current + * block library (this.blocks). + * + * @param {Array.} blockTypes - Types of blocks. + * @return {!Object} Map of block type to corresponding xml. + */ +BlockLibraryStorage.prototype.getBlockXmlMap = function(blockTypes) { + var blockXmlMap = {}; + for (var i = 0; i < blockTypes.length; i++) { + var blockType = blockTypes[i]; + var xml = this.getBlockXml(blockType); + blockXmlMap[blockType] = xml; + } + return blockXmlMap; +}; + +/** + * Returns array of all block types stored in current block library. + * + * @return {!Array.} Array of block types stored in library. + */ +BlockLibraryStorage.prototype.getBlockTypes = function() { + return Object.keys(this.blocks); +}; + +/** + * Checks to see if block library is empty. + * + * @return {boolean} True if empty, false otherwise. + */ +BlockLibraryStorage.prototype.isEmpty = function() { + for (var blockType in this.blocks) { + return false; + } + return true; +}; + +/** + * Returns array of all block types stored in current block library. + * + * @return {!Array.} Map of block type to corresponding xml text. + */ +BlockLibraryStorage.prototype.getBlockXmlTextMap = function() { + return this.blocks; +}; diff --git a/demos/blocklyfactory/block_library_view.js b/demos/blocklyfactory/block_library_view.js new file mode 100644 index 000000000..151055a42 --- /dev/null +++ b/demos/blocklyfactory/block_library_view.js @@ -0,0 +1,117 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Javascript for Block Library's UI for pulling blocks from the + * Block Library's storage to edit in Block Factory. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockLibraryView'); + +/** + * Creates a node of a given element type and appends to the node with given id. + * + * @param {string} optionIdentifier - String used to identify option. + * @param {string} optionText - Text to display in the dropdown for the option. + * @param {string} dropdownID - ID for HTML select element. + * @param {boolean} selected - Whether or not the option should be selected on + * the dropdown. + * @param {boolean} enabled - Whether or not the option should be enabled. + */ +BlockLibraryView.addOption + = function(optionIdentifier, optionText, dropdownID, selected, enabled) { + var dropdown = document.getElementById(dropdownID); + var option = document.createElement('option'); + // The value attribute of a dropdown's option is not visible in the UI, but is + // useful for identifying different options that may have the same text. + option.value = optionIdentifier; + // The text attribute is what the user sees in the dropdown for the option. + option.text = optionText; + option.selected = selected; + option.disabled = !enabled; + dropdown.add(option); +}; + +/** + * Adds a default, blank option to dropdown for when no block from library is + * selected. + * + * @param {string} dropdownID - ID of HTML select element + */ +BlockLibraryView.addDefaultOption = function(dropdownID) { + BlockLibraryView.addOption( + 'BLOCK_LIBRARY_DEFAULT_BLANK', '', dropdownID, true, false); +}; + +/** + * Selects the default, blank option in dropdown identified by given ID. + * + * @param {string} dropdownID - ID of HTML select element + */ +BlockLibraryView.selectDefaultOption = function(dropdownID) { + var dropdown = document.getElementById(dropdownID); + // Deselect currently selected option. + var index = dropdown.selectedIndex; + dropdown.options[index].selected = false; + // Select default option, always the first in the dropdown. + var defaultOption = dropdown.options[0]; + defaultOption.selected = true; +}; + +/** + * Returns block type of selected block. + * + * @param {Element} dropdown - HTML select element. + * @return {string} Type of block selected. + */ +BlockLibraryView.getSelected = function(dropdown) { + var index = dropdown.selectedIndex; + return dropdown.options[index].value; +}; + +/** + * Removes option currently selected in dropdown from dropdown menu. + * + * @param {string} dropdownID - ID of HTML select element within which to find + * the selected option. + */ +BlockLibraryView.removeSelectedOption = function(dropdownID) { + var dropdown = document.getElementById(dropdownID); + if (dropdown) { + dropdown.remove(dropdown.selectedIndex); + } +}; + +/** + * Removes all options from dropdown. + * + * @param {string} dropdownID - ID of HTML select element to clear options of. + */ +BlockLibraryView.clearOptions = function(dropdownID) { + var dropdown = document.getElementById(dropdownID); + while (dropdown.length > 0) { + dropdown.remove(dropdown.length - 1); + } +}; + diff --git a/demos/blocklyfactory/factory.css b/demos/blocklyfactory/factory.css new file mode 100644 index 000000000..171e7927e --- /dev/null +++ b/demos/blocklyfactory/factory.css @@ -0,0 +1,199 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +html, body { + height: 100%; +} + +body { + background-color: #fff; + font-family: sans-serif; + margin: 0 5px; + overflow: hidden +} + +h1 { + font-weight: normal; + font-size: 140%; +} + +h3 { + margin-top: 5px; + margin-bottom: 0; +} + +table { + height: 100%; + width: 100%; +} + +td { + vertical-align: top; + padding: 0; +} + +p { + display: block; + -webkit-margin-before: 0em; + -webkit-margin-after: 0em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + padding: 5px 0px; +} + + +#blockly { + position: fixed; +} + +#blocklyMask { + background-color: #000; + cursor: not-allowed; + display: none; + position: fixed; + opacity: 0.2; + z-index: 9; +} + +#preview { + position: absolute; +} + +pre, +#languageTA { + border: #ddd 1px solid; + margin-top: 0; + position: absolute; + overflow: scroll; +} + +#languageTA { + display: none; + font: 10pt monospace; +} + +.downloadButton { + padding: 5px; +} + +button:disabled, .buttonStyle:disabled { + opacity: 0.6; +} + +button>*, .buttonStyle>* { + opacity: 1; + vertical-align: text-bottom; +} + +button, .buttonStyle { + border-radius: 4px; + border: 1px solid #ddd; + background-color: #eee; + color: #000; + padding: 10px; + margin: 10px 5px; + font-size: small; +} + +.buttonStyle:hover:not(:disabled), button:hover:not(:disabled) { + box-shadow: 2px 2px 5px #888; +} + +.buttonStyle:hover:not(:disabled)>*, button:hover:not(:disabled)>* { + opacity: 1; +} + +#linkButton { + display: none; +} + +#blockFactoryContent { + height: 87%; +} + +#blockLibraryContainer { + vertical-align: bottom; +} + +#blockLibraryControls { + text-align: right; + vertical-align: middle; +} + +#previewContainer { + vertical-align: bottom; +} + +#buttonContainer { + text-align: right; + vertical-align: middle; +} + +#files { + position: absolute; + visibility: hidden; +} + +#toolbox { + display: none; +} + +#blocklyWorkspaceContainer { + height: 95%; + padding: 2px; + width: 50%; +} + +#blockLibraryExporter { + clear: both; + display: none; + height: 100%; +} + +#exportSelector { + float: left; + height: 75%; + width: 60%; +} + +#exportSettings { + margin: auto; + padding: 16px; + overflow: hidden; +} + +#exporterHiddenWorkspace { + display: none; +} + +/* Tabs */ + +.tab { + float: left; + padding: 5px 19px; +} + +.tab.tabon { + background-color: #ddd; +} + +.tab.taboff { + cursor: pointer; +} diff --git a/demos/blocklyfactory/factory.js b/demos/blocklyfactory/factory.js new file mode 100644 index 000000000..12712a0a6 --- /dev/null +++ b/demos/blocklyfactory/factory.js @@ -0,0 +1,989 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview JavaScript for Blockly's Block Factory application through + * which users can build blocks using a visual interface and dynamically + * generate a preview block and starter code for the block (block definition and + * generator stub. Uses the Block Factory namespace. + * + * @author fraser@google.com (Neil Fraser), quachtina96 (Tina Quach) + */ +'use strict'; + +/** + * Namespace for Block Factory. + */ +goog.provide('BlockFactory'); + +goog.require('goog.dom.classes'); + +/** + * Workspace for user to build block. + * @type {Blockly.Workspace} + */ +BlockFactory.mainWorkspace = null; + +/** + * Workspace for preview of block. + * @type {Blockly.Workspace} + */ +BlockFactory.previewWorkspace = null; + +/** + * Name of block if not named. + */ +BlockFactory.UNNAMED = 'unnamed'; + +/** + * Existing direction ('ltr' vs 'rtl') of preview. + */ +BlockFactory.oldDir = null; + +// UI + +/** + * Inject code into a pre tag, with syntax highlighting. + * Safe from HTML/script injection. + * @param {string} code Lines of code. + * @param {string} id ID of
 element to inject into.
+ */
+BlockFactory.injectCode = function(code, id) {
+  var pre = document.getElementById(id);
+  pre.textContent = code;
+  code = pre.innerHTML;
+  code = prettyPrintOne(code, 'js');
+  pre.innerHTML = code;
+};
+
+// Utils
+
+/**
+ * Escape a string.
+ * @param {string} string String to escape.
+ * @return {string} Escaped string surrouned by quotes.
+ */
+BlockFactory.escapeString = function(string) {
+  return JSON.stringify(string);
+};
+
+/**
+ * Return the uneditable container block that everything else attaches to in
+ * given workspace
+ *
+ * @param {!Blockly.Workspace} workspace - where the root block lives
+ * @return {Blockly.Block} root block
+ */
+BlockFactory.getRootBlock = function(workspace) {
+  var blocks = workspace.getTopBlocks(false);
+  for (var i = 0, block; block = blocks[i]; i++) {
+    if (block.type == 'factory_base') {
+      return block;
+    }
+  }
+  return null;
+};
+
+// Language Code: Block Definitions
+
+/**
+ * Change the language code format.
+ */
+BlockFactory.formatChange = function() {
+  var mask = document.getElementById('blocklyMask');
+  var languagePre = document.getElementById('languagePre');
+  var languageTA = document.getElementById('languageTA');
+  if (document.getElementById('format').value == 'Manual') {
+    Blockly.hideChaff();
+    mask.style.display = 'block';
+    languagePre.style.display = 'none';
+    languageTA.style.display = 'block';
+    var code = languagePre.textContent.trim();
+    languageTA.value = code;
+    languageTA.focus();
+    BlockFactory.updatePreview();
+  } else {
+    mask.style.display = 'none';
+    languageTA.style.display = 'none';
+    languagePre.style.display = 'block';
+    BlockFactory.updateLanguage();
+  }
+  BlockFactory.disableEnableLink();
+};
+
+/**
+ * Get block definition code for the current block.
+ *
+ * @param {string} blockType - Type of block.
+ * @param {!Blockly.Block} rootBlock - RootBlock from main workspace in which
+ *    user uses Block Factory Blocks to create a custom block.
+ * @param {string} format - 'JSON' or 'JavaScript'.
+ * @param {!Blockly.Workspace} workspace - Where the root block lives.
+ * @return {string} Block definition.
+ */
+BlockFactory.getBlockDefinition = function(blockType, rootBlock, format, workspace) {
+  blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1');
+  switch (format) {
+    case 'JSON':
+      var code = BlockFactory.formatJson_(blockType, rootBlock);
+      break;
+    case 'JavaScript':
+      var code = BlockFactory.formatJavaScript_(blockType, rootBlock, workspace);
+      break;
+  }
+  return code;
+};
+
+/**
+ * Update the language code based on constructs made in Blockly.
+ */
+BlockFactory.updateLanguage = function() {
+  var rootBlock = BlockFactory.getRootBlock(BlockFactory.mainWorkspace);
+  if (!rootBlock) {
+    return;
+  }
+  var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase();
+  if (!blockType) {
+    blockType = BlockFactory.UNNAMED;
+  }
+  var format = document.getElementById('format').value;
+  var code = BlockFactory.getBlockDefinition(blockType, rootBlock, format,
+      BlockFactory.mainWorkspace);
+  BlockFactory.injectCode(code, 'languagePre');
+  BlockFactory.updatePreview();
+};
+
+/**
+ * Update the language code as JSON.
+ * @param {string} blockType Name of block.
+ * @param {!Blockly.Block} rootBlock Factory_base block.
+ * @return {string} Generanted language code.
+ * @private
+ */
+BlockFactory.formatJson_ = function(blockType, rootBlock) {
+  var JS = {};
+  // Type is not used by Blockly, but may be used by a loader.
+  JS.type = blockType;
+  // Generate inputs.
+  var message = [];
+  var args = [];
+  var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
+  var lastInput = null;
+  while (contentsBlock) {
+    if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
+      var fields = BlockFactory.getFieldsJson_(
+          contentsBlock.getInputTargetBlock('FIELDS'));
+      for (var i = 0; i < fields.length; i++) {
+        if (typeof fields[i] == 'string') {
+          message.push(fields[i].replace(/%/g, '%%'));
+        } else {
+          args.push(fields[i]);
+          message.push('%' + args.length);
+        }
+      }
+
+      var input = {type: contentsBlock.type};
+      // Dummy inputs don't have names.  Other inputs do.
+      if (contentsBlock.type != 'input_dummy') {
+        input.name = contentsBlock.getFieldValue('INPUTNAME');
+      }
+      var check = JSON.parse(
+          BlockFactory.getOptTypesFrom(contentsBlock, 'TYPE') || 'null');
+      if (check) {
+        input.check = check;
+      }
+      var align = contentsBlock.getFieldValue('ALIGN');
+      if (align != 'LEFT') {
+        input.align = align;
+      }
+      args.push(input);
+      message.push('%' + args.length);
+      lastInput = contentsBlock;
+    }
+    contentsBlock = contentsBlock.nextConnection &&
+        contentsBlock.nextConnection.targetBlock();
+  }
+  // Remove last input if dummy and not empty.
+  if (lastInput && lastInput.type == 'input_dummy') {
+    var fields = lastInput.getInputTargetBlock('FIELDS');
+    if (fields && BlockFactory.getFieldsJson_(fields).join('').trim() != '') {
+      var align = lastInput.getFieldValue('ALIGN');
+      if (align != 'LEFT') {
+        JS.lastDummyAlign0 = align;
+      }
+      args.pop();
+      message.pop();
+    }
+  }
+  JS.message0 = message.join(' ');
+  if (args.length) {
+    JS.args0 = args;
+  }
+  // Generate inline/external switch.
+  if (rootBlock.getFieldValue('INLINE') == 'EXT') {
+    JS.inputsInline = false;
+  } else if (rootBlock.getFieldValue('INLINE') == 'INT') {
+    JS.inputsInline = true;
+  }
+  // Generate output, or next/previous connections.
+  switch (rootBlock.getFieldValue('CONNECTIONS')) {
+    case 'LEFT':
+      JS.output =
+          JSON.parse(
+              BlockFactory.getOptTypesFrom(rootBlock, 'OUTPUTTYPE') || 'null');
+      break;
+    case 'BOTH':
+      JS.previousStatement =
+          JSON.parse(
+              BlockFactory.getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
+      JS.nextStatement =
+          JSON.parse(
+              BlockFactory.getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
+      break;
+    case 'TOP':
+      JS.previousStatement =
+          JSON.parse(
+              BlockFactory.getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
+      break;
+    case 'BOTTOM':
+      JS.nextStatement =
+          JSON.parse(
+              BlockFactory.getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
+      break;
+  }
+  // Generate colour.
+  var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
+  if (colourBlock && !colourBlock.disabled) {
+    var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
+    JS.colour = hue;
+  }
+  JS.tooltip = '';
+  JS.helpUrl = 'http://www.example.com/';
+  return JSON.stringify(JS, null, '  ');
+};
+
+/**
+ * Update the language code as JavaScript.
+ * @param {string} blockType Name of block.
+ * @param {!Blockly.Block} rootBlock Factory_base block.
+ * @param {!Blockly.Workspace} workspace - Where the root block lives.
+
+ * @return {string} Generated language code.
+ * @private
+ */
+BlockFactory.formatJavaScript_ = function(blockType, rootBlock, workspace) {
+  var code = [];
+  code.push("Blockly.Blocks['" + blockType + "'] = {");
+  code.push("  init: function() {");
+  // Generate inputs.
+  var TYPES = {'input_value': 'appendValueInput',
+               'input_statement': 'appendStatementInput',
+               'input_dummy': 'appendDummyInput'};
+  var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
+  while (contentsBlock) {
+    if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
+      var name = '';
+      // Dummy inputs don't have names.  Other inputs do.
+      if (contentsBlock.type != 'input_dummy') {
+        name =
+            BlockFactory.escapeString(contentsBlock.getFieldValue('INPUTNAME'));
+      }
+      code.push('    this.' + TYPES[contentsBlock.type] + '(' + name + ')');
+      var check = BlockFactory.getOptTypesFrom(contentsBlock, 'TYPE');
+      if (check) {
+        code.push('        .setCheck(' + check + ')');
+      }
+      var align = contentsBlock.getFieldValue('ALIGN');
+      if (align != 'LEFT') {
+        code.push('        .setAlign(Blockly.ALIGN_' + align + ')');
+      }
+      var fields = BlockFactory.getFieldsJs_(
+          contentsBlock.getInputTargetBlock('FIELDS'));
+      for (var i = 0; i < fields.length; i++) {
+        code.push('        .appendField(' + fields[i] + ')');
+      }
+      // Add semicolon to last line to finish the statement.
+      code[code.length - 1] += ';';
+    }
+    contentsBlock = contentsBlock.nextConnection &&
+        contentsBlock.nextConnection.targetBlock();
+  }
+  // Generate inline/external switch.
+  if (rootBlock.getFieldValue('INLINE') == 'EXT') {
+    code.push('    this.setInputsInline(false);');
+  } else if (rootBlock.getFieldValue('INLINE') == 'INT') {
+    code.push('    this.setInputsInline(true);');
+  }
+  // Generate output, or next/previous connections.
+  switch (rootBlock.getFieldValue('CONNECTIONS')) {
+    case 'LEFT':
+      code.push(BlockFactory.connectionLineJs_('setOutput', 'OUTPUTTYPE', workspace));
+      break;
+    case 'BOTH':
+      code.push(
+          BlockFactory.connectionLineJs_('setPreviousStatement', 'TOPTYPE', workspace));
+      code.push(
+          BlockFactory.connectionLineJs_('setNextStatement', 'BOTTOMTYPE', workspace));
+      break;
+    case 'TOP':
+      code.push(
+          BlockFactory.connectionLineJs_('setPreviousStatement', 'TOPTYPE', workspace));
+      break;
+    case 'BOTTOM':
+      code.push(
+          BlockFactory.connectionLineJs_('setNextStatement', 'BOTTOMTYPE', workspace));
+      break;
+  }
+  // Generate colour.
+  var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
+  if (colourBlock && !colourBlock.disabled) {
+    var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
+    if (!isNaN(hue)) {
+      code.push('    this.setColour(' + hue + ');');
+    }
+  }
+  code.push("    this.setTooltip('');");
+  code.push("    this.setHelpUrl('http://www.example.com/');");
+  code.push('  }');
+  code.push('};');
+  return code.join('\n');
+};
+
+/**
+ * Create JS code required to create a top, bottom, or value connection.
+ * @param {string} functionName JavaScript function name.
+ * @param {string} typeName Name of type input.
+ * @param {!Blockly.Workspace} workspace - Where the root block lives.
+ * @return {string} Line of JavaScript code to create connection.
+ * @private
+ */
+BlockFactory.connectionLineJs_ = function(functionName, typeName, workspace) {
+  var type = BlockFactory.getOptTypesFrom(
+      BlockFactory.getRootBlock(workspace), typeName);
+  if (type) {
+    type = ', ' + type;
+  } else {
+    type = '';
+  }
+  return '    this.' + functionName + '(true' + type + ');';
+};
+
+/**
+ * Returns field strings and any config.
+ * @param {!Blockly.Block} block Input block.
+ * @return {!Array.} Field strings.
+ * @private
+ */
+BlockFactory.getFieldsJs_ = function(block) {
+  var fields = [];
+  while (block) {
+    if (!block.disabled && !block.getInheritedDisabled()) {
+      switch (block.type) {
+        case 'field_static':
+          // Result: 'hello'
+          fields.push(BlockFactory.escapeString(block.getFieldValue('TEXT')));
+          break;
+        case 'field_input':
+          // Result: new Blockly.FieldTextInput('Hello'), 'GREET'
+          fields.push('new Blockly.FieldTextInput(' +
+              BlockFactory.escapeString(block.getFieldValue('TEXT')) + '), ' +
+              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
+          break;
+        case 'field_number':
+          // Result: new Blockly.FieldNumber(10, 0, 100, 1), 'NUMBER'
+          var args = [
+            Number(block.getFieldValue('VALUE')),
+            Number(block.getFieldValue('MIN')),
+            Number(block.getFieldValue('MAX')),
+            Number(block.getFieldValue('PRECISION'))
+          ];
+          // Remove any trailing arguments that aren't needed.
+          if (args[3] == 0) {
+            args.pop();
+            if (args[2] == Infinity) {
+              args.pop();
+              if (args[1] == -Infinity) {
+                args.pop();
+              }
+            }
+          }
+          fields.push('new Blockly.FieldNumber(' + args.join(', ') + '), ' +
+              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
+          break;
+        case 'field_angle':
+          // Result: new Blockly.FieldAngle(90), 'ANGLE'
+          fields.push('new Blockly.FieldAngle(' +
+              parseFloat(block.getFieldValue('ANGLE')) + '), ' +
+              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
+          break;
+        case 'field_checkbox':
+          // Result: new Blockly.FieldCheckbox('TRUE'), 'CHECK'
+          fields.push('new Blockly.FieldCheckbox(' +
+              BlockFactory.escapeString(block.getFieldValue('CHECKED')) +
+               '), ' +
+              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
+          break;
+        case 'field_colour':
+          // Result: new Blockly.FieldColour('#ff0000'), 'COLOUR'
+          fields.push('new Blockly.FieldColour(' +
+              BlockFactory.escapeString(block.getFieldValue('COLOUR')) +
+              '), ' +
+              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
+          break;
+        case 'field_date':
+          // Result: new Blockly.FieldDate('2015-02-04'), 'DATE'
+          fields.push('new Blockly.FieldDate(' +
+              BlockFactory.escapeString(block.getFieldValue('DATE')) + '), ' +
+              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
+          break;
+        case 'field_variable':
+          // Result: new Blockly.FieldVariable('item'), 'VAR'
+          var varname
+              = BlockFactory.escapeString(block.getFieldValue('TEXT') || null);
+          fields.push('new Blockly.FieldVariable(' + varname + '), ' +
+              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
+          break;
+        case 'field_dropdown':
+          // Result:
+          // new Blockly.FieldDropdown([['yes', '1'], ['no', '0']]), 'TOGGLE'
+          var options = [];
+          for (var i = 0; i < block.optionCount_; i++) {
+            options[i] = '[' +
+                BlockFactory.escapeString(block.getFieldValue('USER' + i)) +
+                ', ' +
+                BlockFactory.escapeString(block.getFieldValue('CPU' + i)) + ']';
+          }
+          if (options.length) {
+            fields.push('new Blockly.FieldDropdown([' +
+                options.join(', ') + ']), ' +
+                BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
+          }
+          break;
+        case 'field_image':
+          // Result: new Blockly.FieldImage('http://...', 80, 60)
+          var src = BlockFactory.escapeString(block.getFieldValue('SRC'));
+          var width = Number(block.getFieldValue('WIDTH'));
+          var height = Number(block.getFieldValue('HEIGHT'));
+          var alt = BlockFactory.escapeString(block.getFieldValue('ALT'));
+          fields.push('new Blockly.FieldImage(' +
+              src + ', ' + width + ', ' + height + ', ' + alt + ')');
+          break;
+      }
+    }
+    block = block.nextConnection && block.nextConnection.targetBlock();
+  }
+  return fields;
+};
+
+/**
+ * Returns field strings and any config.
+ * @param {!Blockly.Block} block Input block.
+ * @return {!Array.} Array of static text and field configs.
+ * @private
+ */
+BlockFactory.getFieldsJson_ = function(block) {
+  var fields = [];
+  while (block) {
+    if (!block.disabled && !block.getInheritedDisabled()) {
+      switch (block.type) {
+        case 'field_static':
+          // Result: 'hello'
+          fields.push(block.getFieldValue('TEXT'));
+          break;
+        case 'field_input':
+          fields.push({
+            type: block.type,
+            name: block.getFieldValue('FIELDNAME'),
+            text: block.getFieldValue('TEXT')
+          });
+          break;
+        case 'field_number':
+          var obj = {
+            type: block.type,
+            name: block.getFieldValue('FIELDNAME'),
+            value: parseFloat(block.getFieldValue('VALUE'))
+          };
+          var min = parseFloat(block.getFieldValue('MIN'));
+          if (min > -Infinity) {
+            obj.min = min;
+          }
+          var max = parseFloat(block.getFieldValue('MAX'));
+          if (max < Infinity) {
+            obj.max = max;
+          }
+          var precision = parseFloat(block.getFieldValue('PRECISION'));
+          if (precision) {
+            obj.precision = precision;
+          }
+          fields.push(obj);
+          break;
+        case 'field_angle':
+          fields.push({
+            type: block.type,
+            name: block.getFieldValue('FIELDNAME'),
+            angle: Number(block.getFieldValue('ANGLE'))
+          });
+          break;
+        case 'field_checkbox':
+          fields.push({
+            type: block.type,
+            name: block.getFieldValue('FIELDNAME'),
+            checked: block.getFieldValue('CHECKED') == 'TRUE'
+          });
+          break;
+        case 'field_colour':
+          fields.push({
+            type: block.type,
+            name: block.getFieldValue('FIELDNAME'),
+            colour: block.getFieldValue('COLOUR')
+          });
+          break;
+        case 'field_date':
+          fields.push({
+            type: block.type,
+            name: block.getFieldValue('FIELDNAME'),
+            date: block.getFieldValue('DATE')
+          });
+          break;
+        case 'field_variable':
+          fields.push({
+            type: block.type,
+            name: block.getFieldValue('FIELDNAME'),
+            variable: block.getFieldValue('TEXT') || null
+          });
+          break;
+        case 'field_dropdown':
+          var options = [];
+          for (var i = 0; i < block.optionCount_; i++) {
+            options[i] = [block.getFieldValue('USER' + i),
+                block.getFieldValue('CPU' + i)];
+          }
+          if (options.length) {
+            fields.push({
+              type: block.type,
+              name: block.getFieldValue('FIELDNAME'),
+              options: options
+            });
+          }
+          break;
+        case 'field_image':
+          fields.push({
+            type: block.type,
+            src: block.getFieldValue('SRC'),
+            width: Number(block.getFieldValue('WIDTH')),
+            height: Number(block.getFieldValue('HEIGHT')),
+            alt: block.getFieldValue('ALT')
+          });
+          break;
+      }
+    }
+    block = block.nextConnection && block.nextConnection.targetBlock();
+  }
+  return fields;
+};
+
+/**
+ * Fetch the type(s) defined in the given input.
+ * Format as a string for appending to the generated code.
+ * @param {!Blockly.Block} block Block with input.
+ * @param {string} name Name of the input.
+ * @return {?string} String defining the types.
+ */
+BlockFactory.getOptTypesFrom = function(block, name) {
+  var types = BlockFactory.getTypesFrom_(block, name);
+  if (types.length == 0) {
+    return undefined;
+  } else if (types.indexOf('null') != -1) {
+    return 'null';
+  } else if (types.length == 1) {
+    return types[0];
+  } else {
+    return '[' + types.join(', ') + ']';
+  }
+};
+
+/**
+ * Fetch the type(s) defined in the given input.
+ * @param {!Blockly.Block} block Block with input.
+ * @param {string} name Name of the input.
+ * @return {!Array.} List of types.
+ * @private
+ */
+BlockFactory.getTypesFrom_ = function(block, name) {
+  var typeBlock = block.getInputTargetBlock(name);
+  var types;
+  if (!typeBlock || typeBlock.disabled) {
+    types = [];
+  } else if (typeBlock.type == 'type_other') {
+    types = [BlockFactory.escapeString(typeBlock.getFieldValue('TYPE'))];
+  } else if (typeBlock.type == 'type_group') {
+    types = [];
+    for (var n = 0; n < typeBlock.typeCount_; n++) {
+      types = types.concat(BlockFactory.getTypesFrom_(typeBlock, 'TYPE' + n));
+    }
+    // Remove duplicates.
+    var hash = Object.create(null);
+    for (var n = types.length - 1; n >= 0; n--) {
+      if (hash[types[n]]) {
+        types.splice(n, 1);
+      }
+      hash[types[n]] = true;
+    }
+  } else {
+    types = [BlockFactory.escapeString(typeBlock.valueType)];
+  }
+  return types;
+};
+
+// Generator Code
+
+/**
+ * Get the generator code for a given block.
+ *
+ * @param {!Blockly.Block} block - Rendered block in preview workspace.
+ * @param {string} generatorLanguage - 'JavaScript', 'Python', 'PHP', 'Lua',
+ *     'Dart'.
+ * @return {string} Generator code for multiple blocks.
+ */
+BlockFactory.getGeneratorStub = function(block, generatorLanguage) {
+  function makeVar(root, name) {
+    name = name.toLowerCase().replace(/\W/g, '_');
+    return '  var ' + root + '_' + name;
+  }
+  // The makevar function lives in the original update generator.
+  var language = generatorLanguage;
+  var code = [];
+  code.push("Blockly." + language + "['" + block.type +
+            "'] = function(block) {");
+
+  // Generate getters for any fields or inputs.
+  for (var i = 0, input; input = block.inputList[i]; i++) {
+    for (var j = 0, field; field = input.fieldRow[j]; j++) {
+      var name = field.name;
+      if (!name) {
+        continue;
+      }
+      if (field instanceof Blockly.FieldVariable) {
+        // Subclass of Blockly.FieldDropdown, must test first.
+        code.push(makeVar('variable', name) +
+                  " = Blockly." + language +
+                  ".variableDB_.getName(block.getFieldValue('" + name +
+                  "'), Blockly.Variables.NAME_TYPE);");
+      } else if (field instanceof Blockly.FieldAngle) {
+        // Subclass of Blockly.FieldTextInput, must test first.
+        code.push(makeVar('angle', name) +
+                  " = block.getFieldValue('" + name + "');");
+      } else if (Blockly.FieldDate && field instanceof Blockly.FieldDate) {
+        // Blockly.FieldDate may not be compiled into Blockly.
+        code.push(makeVar('date', name) +
+                  " = block.getFieldValue('" + name + "');");
+      } else if (field instanceof Blockly.FieldColour) {
+        code.push(makeVar('colour', name) +
+                  " = block.getFieldValue('" + name + "');");
+      } else if (field instanceof Blockly.FieldCheckbox) {
+        code.push(makeVar('checkbox', name) +
+                  " = block.getFieldValue('" + name + "') == 'TRUE';");
+      } else if (field instanceof Blockly.FieldDropdown) {
+        code.push(makeVar('dropdown', name) +
+                  " = block.getFieldValue('" + name + "');");
+      } else if (field instanceof Blockly.FieldNumber) {
+        code.push(makeVar('number', name) +
+                  " = block.getFieldValue('" + name + "');");
+      } else if (field instanceof Blockly.FieldTextInput) {
+        code.push(makeVar('text', name) +
+                  " = block.getFieldValue('" + name + "');");
+      }
+    }
+    var name = input.name;
+    if (name) {
+      if (input.type == Blockly.INPUT_VALUE) {
+        code.push(makeVar('value', name) +
+                  " = Blockly." + language + ".valueToCode(block, '" + name +
+                  "', Blockly." + language + ".ORDER_ATOMIC);");
+      } else if (input.type == Blockly.NEXT_STATEMENT) {
+        code.push(makeVar('statements', name) +
+                  " = Blockly." + language + ".statementToCode(block, '" +
+                  name + "');");
+      }
+    }
+  }
+  // Most languages end lines with a semicolon.  Python does not.
+  var lineEnd = {
+    'JavaScript': ';',
+    'Python': '',
+    'PHP': ';',
+    'Dart': ';'
+  };
+  code.push("  // TODO: Assemble " + language + " into code variable.");
+  if (block.outputConnection) {
+    code.push("  var code = '...';");
+    code.push("  // TODO: Change ORDER_NONE to the correct strength.");
+    code.push("  return [code, Blockly." + language + ".ORDER_NONE];");
+  } else {
+    code.push("  var code = '..." + (lineEnd[language] || '') + "\\n';");
+    code.push("  return code;");
+  }
+  code.push("};");
+
+  return code.join('\n');
+};
+
+/**
+ * Update the generator code.
+ * @param {!Blockly.Block} block Rendered block in preview workspace.
+ */
+BlockFactory.updateGenerator = function(block) {
+  var language = document.getElementById('language').value;
+  var generatorStub = BlockFactory.getGeneratorStub(block, language);
+  BlockFactory.injectCode(generatorStub, 'generatorPre');
+};
+
+// Preview Block
+
+/**
+ * Update the preview display.
+ */
+BlockFactory.updatePreview = function() {
+  // Toggle between LTR/RTL if needed (also used in first display).
+  var newDir = document.getElementById('direction').value;
+  if (BlockFactory.oldDir != newDir) {
+    if (BlockFactory.previewWorkspace) {
+      BlockFactory.previewWorkspace.dispose();
+    }
+    var rtl = newDir == 'rtl';
+    BlockFactory.previewWorkspace = Blockly.inject('preview',
+        {rtl: rtl,
+         media: '../../media/',
+         scrollbars: true});
+    BlockFactory.oldDir = newDir;
+  }
+  BlockFactory.previewWorkspace.clear();
+
+  // Fetch the code and determine its format (JSON or JavaScript).
+  var format = document.getElementById('format').value;
+  if (format == 'Manual') {
+    var code = document.getElementById('languageTA').value;
+    // If the code is JSON, it will parse, otherwise treat as JS.
+    try {
+      JSON.parse(code);
+      format = 'JSON';
+    } catch (e) {
+      format = 'JavaScript';
+    }
+  } else {
+    var code = document.getElementById('languagePre').textContent;
+  }
+  if (!code.trim()) {
+    // Nothing to render.  Happens while cloud storage is loading.
+    return;
+  }
+
+  // Backup Blockly.Blocks object so that main workspace and preview don't
+  // collide if user creates a 'factory_base' block, for instance.
+  var backupBlocks = Blockly.Blocks;
+  console.log(backupBlocks);
+  try {
+    // Make a shallow copy.
+    Blockly.Blocks = Object.create(null);
+    for (var prop in backupBlocks) {
+      Blockly.Blocks[prop] = backupBlocks[prop];
+    }
+
+    if (format == 'JSON') {
+      var json = JSON.parse(code);
+      Blockly.Blocks[json.type || BlockFactory.UNNAMED] = {
+        init: function() {
+          this.jsonInit(json);
+        }
+      };
+    } else if (format == 'JavaScript') {
+      eval(code);
+    } else {
+      throw 'Unknown format: ' + format;
+    }
+
+    // Look for a block on Blockly.Blocks that does not match the backup.
+    var blockType = null;
+    console.log('Blockly Blocks types');
+    for (var type in Blockly.Blocks) {
+      console.log(type);
+      if (typeof Blockly.Blocks[type].init == 'function' &&
+          Blockly.Blocks[type] != backupBlocks[type]) {
+        blockType = type;
+        console.log('found non matching type');
+        console.log(blockType);
+        break;
+      }
+    }
+    if (!blockType) {
+      console.log('non matching type NOT FOUND');
+      return;
+    }
+
+    // Create the preview block.
+    var previewBlock = BlockFactory.previewWorkspace.newBlock(blockType);
+    previewBlock.initSvg();
+    previewBlock.render();
+    previewBlock.setMovable(false);
+    previewBlock.setDeletable(false);
+    previewBlock.moveBy(15, 10);
+    BlockFactory.previewWorkspace.clearUndo();
+    BlockFactory.updateGenerator(previewBlock);
+  } finally {
+    Blockly.Blocks = backupBlocks;
+  }
+};
+
+// File Import, Creation, Download
+
+/**
+ * Generate a file from the contents of a given text area and
+ * download that file.
+ * @param {string} filename The name of the file to create.
+ * @param {string} id The text area to download.
+*/
+BlockFactory.downloadTextArea = function(filename, id) {
+  var code = document.getElementById(id).textContent;
+  BlockFactory.createAndDownloadFile_(code, filename, 'plain');
+};
+
+/**
+ * Create a file with the given attributes and download it.
+ * @param {string} contents - The contents of the file.
+ * @param {string} filename - The name of the file to save to.
+ * @param {string} fileType - The type of the file to save.
+ * @private
+ */
+BlockFactory.createAndDownloadFile_ = function(contents, filename, fileType) {
+  var data = new Blob([contents], {type: 'text/' + fileType});
+  var clickEvent = new MouseEvent("click", {
+    "view": window,
+    "bubbles": true,
+    "cancelable": false
+  });
+
+  var a = document.createElement('a');
+  a.href = window.URL.createObjectURL(data);
+  a.download = filename;
+  a.textContent = 'Download file!';
+  a.dispatchEvent(clickEvent);
+};
+
+/**
+ * Save the workspace's xml representation to a file.
+ * @private
+ */
+BlockFactory.saveWorkspaceToFile = function() {
+  var xmlElement = Blockly.Xml.workspaceToDom(BlockFactory.mainWorkspace);
+  var prettyXml = Blockly.Xml.domToPrettyText(xmlElement);
+  BlockFactory.createAndDownloadFile_(prettyXml, 'blockXml', 'xml');
+};
+
+/**
+ * Imports xml file for a block to the workspace.
+ */
+BlockFactory.importBlockFromFile = function() {
+  var files = document.getElementById('files');
+  // If the file list is empty, they user likely canceled in the dialog.
+  if (files.files.length > 0) {
+    // The input tag doesn't have the "mulitple" attribute
+    // so the user can only choose 1 file.
+    var file = files.files[0];
+    var fileReader = new FileReader();
+    fileReader.addEventListener('load', function(event) {
+      var fileContents = event.target.result;
+      var xml = '';
+      try {
+        xml = Blockly.Xml.textToDom(fileContents);
+      } catch (e) {
+        var message = 'Could not load your saved file.\n'+
+          'Perhaps it was created with a different version of Blockly?';
+        window.alert(message + '\nXML: ' + fileContents);
+        return;
+      }
+      BlockFactory.mainWorkspace.clear();
+      Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace);
+    });
+
+    fileReader.readAsText(file);
+  }
+};
+
+/**
+ * Disable link and save buttons if the format is 'Manual', enable otherwise.
+ */
+BlockFactory.disableEnableLink = function() {
+  var linkButton = document.getElementById('linkButton');
+  var saveBlockButton = document.getElementById('localSaveButton');
+  var saveToLibButton = document.getElementById('saveToBlockLibraryButton');
+  var disabled = document.getElementById('format').value == 'Manual';
+  linkButton.disabled = disabled;
+  saveBlockButton.disabled = disabled;
+  saveToLibButton.disabled = disabled;
+};
+
+// Block Factory Expansion View Utils
+
+/**
+ * Render starter block (factory_base).
+ */
+ BlockFactory.showStarterBlock = function() {
+    var xml = '';
+    Blockly.Xml.domToWorkspace(
+        Blockly.Xml.textToDom(xml), BlockFactory.mainWorkspace);
+};
+
+/**
+ * Hides element so that it's invisible and doesn't take up space.
+ *
+ * @param {string} elementID - ID of element to hide.
+ */
+BlockFactory.hide = function(elementID) {
+  document.getElementById(elementID).style.display = 'none';
+};
+
+/**
+ * Un-hides an element.
+ *
+ * @param {string} elementID - ID of element to hide.
+ */
+BlockFactory.show = function(elementID) {
+  document.getElementById(elementID).style.display = 'block';
+};
+
+/**
+ * Hides element so that it's invisible but still takes up space.
+ *
+ * @param {string} elementID - ID of element to hide.
+ */
+BlockFactory.makeInvisible = function(elementID) {
+  document.getElementById(elementID).visibility = 'hidden';
+};
+
+/**
+ * Makes element visible.
+ *
+ * @param {string} elementID - ID of element to hide.
+ */
+BlockFactory.makeVisible = function(elementID) {
+  document.getElementById(elementID).visibility = 'visible';
+};
+
diff --git a/demos/blocklyfactory/index.html b/demos/blocklyfactory/index.html
new file mode 100644
index 000000000..4ebe83837
--- /dev/null
+++ b/demos/blocklyfactory/index.html
@@ -0,0 +1,264 @@
+
+
+
+
+
+  
+  
+  Blockly Demo: Blockly Factory
+  
+  
+  
+  
+  
+  
+  
+  
+  
+  
+  
+  
+  
+  
+  
+  
+  
+
+
+  

Blockly > + Demos > Blockly Factory

+ +
+
Block Factory
+
+
Block Library Exporter
+
+ +
+
+ + +
+
+

Drag blocks into your workspace to select them for download.

+
+ +
+ +
+

Block Export Settings

+
+
+ Download Block Definition: +
+ Language code: +
+ Block Definition(s) File Name:
+
+
+ Download Generator Stubs: +
+
+ Block Generator Stub(s) File Name:
+
+
+
+ +
+
+ + + + + + + + + +
+ + + + + +
+ +

Block Library:

+ +
+
+ + + +
+
+ + + + + +
+

Preview: + +

+
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + + + +
+
+
+

Language code: + + +

+
+

+              
+            
+

Generator stub: + + +

+
+

+            
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 20 + 65 + 120 + 160 + 210 + 230 + 260 + 290 + 330 + + + + \ No newline at end of file diff --git a/demos/blocklyfactory/link.png b/demos/blocklyfactory/link.png new file mode 100644 index 000000000..11dfd8284 Binary files /dev/null and b/demos/blocklyfactory/link.png differ