From f6f39c48f53cf6dcd9fef51ee525e66dea131655 Mon Sep 17 00:00:00 2001 From: Tina Quach Date: Mon, 15 Aug 2016 17:45:08 -0700 Subject: [PATCH] Blockly Factory: Factory Utils (#535) * created FactoryUtils, to share useful generator code between the different apps within Blockly Factory * undo removal of alert upon empty block library nit removed specification of wfactory exporter param in comment for export controller * remove alert from index and nit comment in wfactory generator --- demos/blocklyfactory/app_controller.js | 23 +- .../block_exporter_controller.js | 6 +- demos/blocklyfactory/block_exporter_tools.js | 25 +- demos/blocklyfactory/block_exporter_view.js | 2 +- .../block_library_controller.js | 2 +- demos/blocklyfactory/factory.js | 765 +----------------- demos/blocklyfactory/factory_utils.js | 732 +++++++++++++++++ demos/blocklyfactory/index.html | 1 + .../workspacefactory/wfactory_generator.js | 1 + 9 files changed, 767 insertions(+), 790 deletions(-) create mode 100644 demos/blocklyfactory/factory_utils.js diff --git a/demos/blocklyfactory/app_controller.js b/demos/blocklyfactory/app_controller.js index 275e375bc..59ac2c47f 100644 --- a/demos/blocklyfactory/app_controller.js +++ b/demos/blocklyfactory/app_controller.js @@ -28,6 +28,7 @@ goog.provide('AppController'); goog.require('BlockFactory'); +goog.require('FactoryUtils'); goog.require('BlockLibraryController'); goog.require('BlockExporterController'); goog.require('goog.dom.classlist'); @@ -120,7 +121,7 @@ AppController.prototype.exportBlockLibraryToFile = function() { 'library.'); // Download file if all necessary parameters are provided. if (filename) { - BlockFactory.createAndDownloadFile_(blockLibText, filename, 'xml'); + FactoryUtils.createAndDownloadFile(blockLibText, filename, 'xml'); } else { alert('Could not export Block Library without file name under which to ' + 'save library.'); @@ -160,9 +161,10 @@ AppController.prototype.formatBlockLibForImport_ = function(xmlText) { // 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; + for (var i = 0, xmlText; xmlText = blockXmls[i]; i++) { + var blockType = this.getBlockTypeFromXml_(xmlText); + // TODO(quachtina96): Make the xml pretty before storing in map. + blockXmlTextMap[blockType] = xmlText; } return blockXmlTextMap; @@ -264,19 +266,19 @@ AppController.prototype.onTab = function() { this.exporter.updateToolbox(); // Show container of exporter. - BlockFactory.show('blockLibraryExporter'); - BlockFactory.hide('workspaceFactoryContent'); + FactoryUtils.show('blockLibraryExporter'); + FactoryUtils.hide('workspaceFactoryContent'); } else if (this.selectedTab == 'BLOCK_FACTORY') { // Hide container of exporter. - BlockFactory.hide('blockLibraryExporter'); - BlockFactory.hide('workspaceFactoryContent'); + FactoryUtils.hide('blockLibraryExporter'); + FactoryUtils.hide('workspaceFactoryContent'); } else if (this.selectedTab == 'WORKSPACE_FACTORY') { // Hide container of exporter. - BlockFactory.hide('blockLibraryExporter'); + FactoryUtils.hide('blockLibraryExporter'); // Show workspace factory container. - BlockFactory.show('workspaceFactoryContent'); + FactoryUtils.show('workspaceFactoryContent'); } // Resize to render workspaces' toolboxes correctly for all tabs. @@ -376,7 +378,6 @@ AppController.prototype.assignFactoryClickHandlers = function() { }); document.getElementById('createNewBlockButton') .addEventListener('click', function() { - BlockFactory.mainWorkspace.clear(); BlockFactory.showStarterBlock(); BlockLibraryView.selectDefaultOption('blockLibraryDropdown'); }); diff --git a/demos/blocklyfactory/block_exporter_controller.js b/demos/blocklyfactory/block_exporter_controller.js index 10e7601ce..527a53915 100644 --- a/demos/blocklyfactory/block_exporter_controller.js +++ b/demos/blocklyfactory/block_exporter_controller.js @@ -30,6 +30,8 @@ 'use strict'; goog.provide('BlockExporterController'); + +goog.require('FactoryUtils'); goog.require('BlockExporterView'); goog.require('BlockExporterTools'); goog.require('goog.dom.xml'); @@ -142,7 +144,7 @@ BlockExporterController.prototype.export = function() { var blockDefs = this.tools.getBlockDefs(blockXmlMap, definitionFormat); // Download the file. - BlockFactory.createAndDownloadFile_( + FactoryUtils.createAndDownloadFile( blockDefs, blockDef_filename, definitionFormat); } } @@ -157,7 +159,7 @@ BlockExporterController.prototype.export = function() { var genStubs = this.tools.getGeneratorCode(blockXmlMap, language); // Download the file. - BlockFactory.createAndDownloadFile_( + FactoryUtils.createAndDownloadFile( genStubs, generatorStub_filename, language); } } diff --git a/demos/blocklyfactory/block_exporter_tools.js b/demos/blocklyfactory/block_exporter_tools.js index dc88cb8ea..7c9bd02ea 100644 --- a/demos/blocklyfactory/block_exporter_tools.js +++ b/demos/blocklyfactory/block_exporter_tools.js @@ -21,7 +21,7 @@ /** * @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 + * toolbox xml for the exporter's workspace. Depends on the FactoryUtils for * its code generation functions. * * @author quachtina96 (Tina Quach) @@ -30,7 +30,7 @@ goog.provide('BlockExporterTools'); -goog.require('BlockFactory'); +goog.require('FactoryUtils'); goog.require('goog.dom'); goog.require('goog.dom.xml'); @@ -75,18 +75,6 @@ BlockExporterTools.prototype.getRootBlockFromXml_ = function(xml) { 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. * @@ -105,7 +93,7 @@ BlockExporterTools.prototype.getBlockDefs = var rootBlock = this.getRootBlockFromXml_(xml); if (rootBlock) { // Generate the block's definition. - var code = BlockFactory.getBlockDefinition(blockType, rootBlock, + var code = FactoryUtils.getBlockDefinition(blockType, rootBlock, definitionFormat, this.hiddenWorkspace); // Add block's definition to the definitions to return. } else { @@ -147,10 +135,11 @@ BlockExporterTools.prototype.getGeneratorCode = var xml = blockXmlMap[blockType]; if (xml) { // Render the preview block in the hidden workspace. - var tempBlock = this.getDefinedBlock_(blockType); + var tempBlock = + FactoryUtils.getDefinedBlock(blockType, this.hiddenWorkspace); // Get generator stub for the given block and add to generator code. var blockGenCode = - BlockFactory.getGeneratorStub(tempBlock, generatorLanguage); + FactoryUtils.getGeneratorStub(tempBlock, generatorLanguage); } else { // Append warning comment and write to console. var blockGenCode = '// No generator stub generated for ' + blockType + @@ -204,7 +193,7 @@ BlockExporterTools.prototype.generateToolboxFromLibrary categoryElement.setAttribute('name',blockType); // Get block. - var block = this.getDefinedBlock_(blockType); + var block = FactoryUtils.getDefinedBlock(blockType, this.hiddenWorkspace); // Get preview block XML. var blockChild = Blockly.Xml.blockToDom(block); diff --git a/demos/blocklyfactory/block_exporter_view.js b/demos/blocklyfactory/block_exporter_view.js index ee1d76c6c..413c60fd3 100644 --- a/demos/blocklyfactory/block_exporter_view.js +++ b/demos/blocklyfactory/block_exporter_view.js @@ -130,7 +130,7 @@ BlockExporterView.prototype.clearSelectorWorkspace = function() { * Neatly layout the blocks in selector workspace. */ BlockExporterView.prototype.cleanUpSelectorWorkspace = function() { - this.selectorWorkspace.cleanUp_(); + this.selectorWorkspace.cleanUp(); }; /** diff --git a/demos/blocklyfactory/block_library_controller.js b/demos/blocklyfactory/block_library_controller.js index 77fb2f6ab..514eeaaeb 100644 --- a/demos/blocklyfactory/block_library_controller.js +++ b/demos/blocklyfactory/block_library_controller.js @@ -60,7 +60,7 @@ BlockLibraryController = function(blockLibraryName, opt_blockLibraryStorage) { * @return {string} The current block's type. */ BlockLibraryController.prototype.getCurrentBlockType_ = function() { - var rootBlock = BlockFactory.getRootBlock(BlockFactory.mainWorkspace); + var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase(); // Replace white space with underscores return blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1'); diff --git a/demos/blocklyfactory/factory.js b/demos/blocklyfactory/factory.js index b75146df1..17d1846ff 100644 --- a/demos/blocklyfactory/factory.js +++ b/demos/blocklyfactory/factory.js @@ -22,7 +22,8 @@ * @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. + * generator stub. Uses the Block Factory namespace. Depends on the FactoryUtils + * for its code generation functions. * * @author fraser@google.com (Neil Fraser), quachtina96 (Tina Quach) */ @@ -33,7 +34,8 @@ */ goog.provide('BlockFactory'); -goog.require('goog.dom.classes'); +goog.require('FactoryUtils'); + /** * Workspace for user to build block. @@ -57,8 +59,6 @@ BlockFactory.UNNAMED = 'unnamed'; */ BlockFactory.oldDir = null; -// UI - /** * Inject code into a pre tag, with syntax highlighting. * Safe from HTML/script injection. @@ -73,36 +73,6 @@ BlockFactory.injectCode = function(code, id) { 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. */ @@ -128,34 +98,11 @@ BlockFactory.formatChange = function() { 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); + var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); if (!rootBlock) { return; } @@ -164,599 +111,22 @@ BlockFactory.updateLanguage = function() { blockType = BlockFactory.UNNAMED; } var format = document.getElementById('format').value; - var code = BlockFactory.getBlockDefinition(blockType, rootBlock, format, + var code = FactoryUtils.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); + var generatorStub = FactoryUtils.getGeneratorStub(block, language); BlockFactory.injectCode(generatorStub, 'generatorPre'); }; -// Preview Block - /** * Update the preview display. */ @@ -798,7 +168,6 @@ BlockFactory.updatePreview = function() { // 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); @@ -821,19 +190,14 @@ BlockFactory.updatePreview = function() { // 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; } @@ -851,81 +215,6 @@ BlockFactory.updatePreview = function() { } }; -// 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. */ @@ -939,12 +228,10 @@ BlockFactory.disableEnableLink = function() { saveToLibButton.disabled = disabled; }; -// Block Factory Expansion View Utils - /** * Render starter block (factory_base). */ - BlockFactory.showStarterBlock = function() { +BlockFactory.showStarterBlock = function() { BlockFactory.mainWorkspace.clear(); var xml = ''; @@ -952,39 +239,3 @@ BlockFactory.disableEnableLink = function() { 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/factory_utils.js b/demos/blocklyfactory/factory_utils.js new file mode 100644 index 000000000..e726aacc7 --- /dev/null +++ b/demos/blocklyfactory/factory_utils.js @@ -0,0 +1,732 @@ +/** + * @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 FactoryUtils is a namespace that holds block starter code + * generation functions shared by the Block Factory, Workspace Factory, and + * Exporter applications within Blockly Factory. Holds functions to generate + * block definitions and generator stubs and to create and download files. + * + * @author fraser@google.com (Neil Fraser), quachtina96 (Tina Quach) + */ + 'use strict'; + +/** + * Namespace for FactoryUtils. + */ +goog.provide('FactoryUtils'); + +goog.require('goog.dom.classes'); + +/** + * 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. + */ +FactoryUtils.getBlockDefinition = function(blockType, rootBlock, format, workspace) { + blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1'); + switch (format) { + case 'JSON': + var code = FactoryUtils.formatJson_(blockType, rootBlock); + break; + case 'JavaScript': + var code = FactoryUtils.formatJavaScript_(blockType, rootBlock, workspace); + break; + } + return 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. + */ +FactoryUtils.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 language code as JSON. + * @param {string} blockType Name of block. + * @param {!Blockly.Block} rootBlock Factory_base block. + * @return {string} Generanted language code. + * @private + */ +FactoryUtils.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 = FactoryUtils.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( + FactoryUtils.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 && FactoryUtils.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( + FactoryUtils.getOptTypesFrom(rootBlock, 'OUTPUTTYPE') || 'null'); + break; + case 'BOTH': + JS.previousStatement = + JSON.parse( + FactoryUtils.getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null'); + JS.nextStatement = + JSON.parse( + FactoryUtils.getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null'); + break; + case 'TOP': + JS.previousStatement = + JSON.parse( + FactoryUtils.getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null'); + break; + case 'BOTTOM': + JS.nextStatement = + JSON.parse( + FactoryUtils.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 + */ +FactoryUtils.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 = + FactoryUtils.escapeString(contentsBlock.getFieldValue('INPUTNAME')); + } + code.push(' this.' + TYPES[contentsBlock.type] + '(' + name + ')'); + var check = FactoryUtils.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 = FactoryUtils.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(FactoryUtils.connectionLineJs_('setOutput', 'OUTPUTTYPE', workspace)); + break; + case 'BOTH': + code.push( + FactoryUtils.connectionLineJs_('setPreviousStatement', 'TOPTYPE', workspace)); + code.push( + FactoryUtils.connectionLineJs_('setNextStatement', 'BOTTOMTYPE', workspace)); + break; + case 'TOP': + code.push( + FactoryUtils.connectionLineJs_('setPreviousStatement', 'TOPTYPE', workspace)); + break; + case 'BOTTOM': + code.push( + FactoryUtils.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 + */ +FactoryUtils.connectionLineJs_ = function(functionName, typeName, workspace) { + var type = FactoryUtils.getOptTypesFrom( + FactoryUtils.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 + */ +FactoryUtils.getFieldsJs_ = function(block) { + var fields = []; + while (block) { + if (!block.disabled && !block.getInheritedDisabled()) { + switch (block.type) { + case 'field_static': + // Result: 'hello' + fields.push(FactoryUtils.escapeString(block.getFieldValue('TEXT'))); + break; + case 'field_input': + // Result: new Blockly.FieldTextInput('Hello'), 'GREET' + fields.push('new Blockly.FieldTextInput(' + + FactoryUtils.escapeString(block.getFieldValue('TEXT')) + '), ' + + FactoryUtils.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(', ') + '), ' + + FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + break; + case 'field_angle': + // Result: new Blockly.FieldAngle(90), 'ANGLE' + fields.push('new Blockly.FieldAngle(' + + parseFloat(block.getFieldValue('ANGLE')) + '), ' + + FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + break; + case 'field_checkbox': + // Result: new Blockly.FieldCheckbox('TRUE'), 'CHECK' + fields.push('new Blockly.FieldCheckbox(' + + FactoryUtils.escapeString(block.getFieldValue('CHECKED')) + + '), ' + + FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + break; + case 'field_colour': + // Result: new Blockly.FieldColour('#ff0000'), 'COLOUR' + fields.push('new Blockly.FieldColour(' + + FactoryUtils.escapeString(block.getFieldValue('COLOUR')) + + '), ' + + FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + break; + case 'field_date': + // Result: new Blockly.FieldDate('2015-02-04'), 'DATE' + fields.push('new Blockly.FieldDate(' + + FactoryUtils.escapeString(block.getFieldValue('DATE')) + '), ' + + FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + break; + case 'field_variable': + // Result: new Blockly.FieldVariable('item'), 'VAR' + var varname + = FactoryUtils.escapeString(block.getFieldValue('TEXT') || null); + fields.push('new Blockly.FieldVariable(' + varname + '), ' + + FactoryUtils.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] = '[' + + FactoryUtils.escapeString(block.getFieldValue('USER' + i)) + + ', ' + + FactoryUtils.escapeString(block.getFieldValue('CPU' + i)) + ']'; + } + if (options.length) { + fields.push('new Blockly.FieldDropdown([' + + options.join(', ') + ']), ' + + FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + } + break; + case 'field_image': + // Result: new Blockly.FieldImage('http://...', 80, 60) + var src = FactoryUtils.escapeString(block.getFieldValue('SRC')); + var width = Number(block.getFieldValue('WIDTH')); + var height = Number(block.getFieldValue('HEIGHT')); + var alt = FactoryUtils.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 + */ +FactoryUtils.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. + */ +FactoryUtils.getOptTypesFrom = function(block, name) { + var types = FactoryUtils.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 + */ +FactoryUtils.getTypesFrom_ = function(block, name) { + var typeBlock = block.getInputTargetBlock(name); + var types; + if (!typeBlock || typeBlock.disabled) { + types = []; + } else if (typeBlock.type == 'type_other') { + types = [FactoryUtils.escapeString(typeBlock.getFieldValue('TYPE'))]; + } else if (typeBlock.type == 'type_group') { + types = []; + for (var n = 0; n < typeBlock.typeCount_; n++) { + types = types.concat(FactoryUtils.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 = [FactoryUtils.escapeString(typeBlock.valueType)]; + } + return types; +}; + +/** + * Escape a string. + * @param {string} string String to escape. + * @return {string} Escaped string surrouned by quotes. + */ +FactoryUtils.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 + */ +FactoryUtils.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; +}; + +// TODO(quachtina96): Move hide, show, makeInvisible, and makeVisible to a new +// AppView namespace. + +/** + * Hides element so that it's invisible and doesn't take up space. + * + * @param {string} elementID - ID of element to hide. + */ +FactoryUtils.hide = function(elementID) { + document.getElementById(elementID).style.display = 'none'; +}; + +/** + * Un-hides an element. + * + * @param {string} elementID - ID of element to hide. + */ +FactoryUtils.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. + */ +FactoryUtils.makeInvisible = function(elementID) { + document.getElementById(elementID).visibility = 'hidden'; +}; + +/** + * Makes element visible. + * + * @param {string} elementID - ID of element to hide. + */ +FactoryUtils.makeVisible = function(elementID) { + document.getElementById(elementID).visibility = 'visible'; +}; + +/** + * 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. + */ +FactoryUtils.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); +}; + +/** + * Get Blockly Block by rendering pre-defined block in workspace. + * + * @param {!Element} blockType - Type of block that has already been defined. + * @param {!Blockly.Workspace} workspace - Workspace on which to render + * the block. + * @return {!Blockly.Block} the Blockly.Block of desired type. + */ +FactoryUtils.getDefinedBlock = function(blockType, workspace) { + workspace.clear(); + return workspace.newBlock(blockType); +}; diff --git a/demos/blocklyfactory/index.html b/demos/blocklyfactory/index.html index b1548698a..ad93808f1 100644 --- a/demos/blocklyfactory/index.html +++ b/demos/blocklyfactory/index.html @@ -10,6 +10,7 @@ + diff --git a/demos/blocklyfactory/workspacefactory/wfactory_generator.js b/demos/blocklyfactory/workspacefactory/wfactory_generator.js index b4db0831b..9b86b4f7f 100644 --- a/demos/blocklyfactory/workspacefactory/wfactory_generator.js +++ b/demos/blocklyfactory/workspacefactory/wfactory_generator.js @@ -177,3 +177,4 @@ FactoryGenerator.prototype.setShadowBlocksInHiddenWorkspace_ = function() { } } }; +