From 1956baa963c52bb07c4ec552cca3545ab3fda57e Mon Sep 17 00:00:00 2001 From: Andrew n marshall Date: Thu, 17 Aug 2017 12:40:58 -0700 Subject: [PATCH] BlockFactory: Adding JSON/JavaScript import support (#1235) Updates the BlockFactory main workspace based on the preview block generated from manual construction. This is a continuation of PR #1216 by @JC-Orozco, rebased on latest develop branch. Bring it latest code up to Google styling and standards. --- .gitignore | 2 +- demos/blockfactory/app_controller.js | 4 +- .../block_definition_extractor.js | 749 ++++++++++++++++++ demos/blockfactory/factory.js | 122 ++- demos/blockfactory/factory_utils.js | 22 +- demos/blockfactory/index.html | 14 +- 6 files changed, 878 insertions(+), 35 deletions(-) create mode 100644 demos/blockfactory/block_definition_extractor.js diff --git a/.gitignore b/.gitignore index 53eebc859..4216e2f62 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ npm-debug.log .project *.pyc *.komodoproject -/nbproject/private/ \ No newline at end of file +/nbproject/private/ diff --git a/demos/blockfactory/app_controller.js b/demos/blockfactory/app_controller.js index e3fc1bd23..16869bdf4 100644 --- a/demos/blockfactory/app_controller.js +++ b/demos/blockfactory/app_controller.js @@ -584,9 +584,9 @@ AppController.prototype.addBlockFactoryEventListeners = function() { document.getElementById('direction') .addEventListener('change', BlockFactory.updatePreview); document.getElementById('languageTA') - .addEventListener('change', BlockFactory.updatePreview); + .addEventListener('change', BlockFactory.manualEdit); document.getElementById('languageTA') - .addEventListener('keyup', BlockFactory.updatePreview); + .addEventListener('keyup', BlockFactory.manualEdit); document.getElementById('format') .addEventListener('change', BlockFactory.formatChange); document.getElementById('language') diff --git a/demos/blockfactory/block_definition_extractor.js b/demos/blockfactory/block_definition_extractor.js new file mode 100644 index 000000000..5c907491d --- /dev/null +++ b/demos/blockfactory/block_definition_extractor.js @@ -0,0 +1,749 @@ +/** + * Copyright 2017 Juan Carlos Orozco Arena + * Apache License Version 2.0 + */ + +/** + * @fileoverview + * The BlockDefinitionExtractor is a class that generates a workspace DOM + * suitable for the BlockFactory's block editor, derived from an example + * Blockly.Block. + * + * + * var workspaceDom = new BlockDefinitionExtractor() + * .buildBlockFactoryWorkspace(exampleBlocklyBlock); + * Blockly.Xml.domToWorkspace(workspaceDom, BlockFactory.mainWorkspace); + * + * + * The exampleBlocklyBlock is usually the block loaded into the + * preview workspace after manually entering the block definition. + * + * @author JC-Orozco (Juan Carlos Orozco), AnmAtAnm (Andrew n marshall) + */ +'use strict'; + +/** + * Namespace for BlockDefinitionExtractor. + */ +goog.provide('BlockDefinitionExtractor'); + + +/** + * Class to contain all functions needed to extract block definition from + * the block preview data structure. + * @namespace + */ +BlockDefinitionExtractor = BlockDefinitionExtractor || Object.create(null); + +/** + * Builds a BlockFactory workspace that reflects the block structure of the + * exmaple block. + * + * @param {!Blockly.Block} block The reference block from which the definition + * will be extracted. + * @return {!Element} Returns the root workspace DOM for the block editor + * workspace. + */ +BlockDefinitionExtractor.buildBlockFactoryWorkspace = function(block) { + var workspaceXml = goog.dom.createDom('xml'); + workspaceXml.append( + BlockDefinitionExtractor.factoryBase_(block, block.type)); + + return workspaceXml; +}; + +/** + * Helper function to create a new Element with the provided attributes and + * inner text. + * + * @param {string} name New element tag name. + * @param {Map} opt_attrs Optional list of attributes. + * @param {string?} opt_text Optional inner text. + * @return {!Element} The newly created element. + * @private + */ +BlockDefinitionExtractor.newDomElement_ = function(name, opt_attrs, opt_text) { + // Avoid createDom(..)'s attributes argument for being too HTML specific. + var elem = goog.dom.createDom(name); + if (opt_attrs) { + for (var key in opt_attrs) { + elem.setAttribute(key, opt_attrs[key]); + } + } + if (opt_text) { + elem.append(opt_text); + } + return elem; +}; + +/** + * Creates an connection type constraint Element representing the + * requested type. + * + * @param {string} type Type name of desired connection constraint. + * @return {!Element} The representing the the constraint type. + * @private + */ +BlockDefinitionExtractor.buildBlockForType_ = function(type) { + switch (type) { + case 'Null': + return BlockDefinitionExtractor.typeNull_(); + case 'Boolean': + return BlockDefinitionExtractor.typeBoolean_(); + case 'Number': + return BlockDefinitionExtractor.typeNumber_(); + case 'String': + return BlockDefinitionExtractor.typeString_(); + case 'Array': + return BlockDefinitionExtractor.typeList_(); + default: + return BlockDefinitionExtractor.typeOther_(type); + } +}; + +/** + * Constructs a element representing the type constraints of the + * provided connection. + * + * @param {!Blockly.Connection} connection The connection with desired + * connection constraints. + * @return {!Element} The root element of the constraint definition. + * @private + */ +BlockDefinitionExtractor.buildTypeConstraintBlockForConnection_ = + function(connection) +{ + var typeBlock; + if (connection.check_) { + if (connection.check_.length < 1) { + typeBlock = BlockDefinitionExtractor.typeNullShadow_(); + } else if (connection.check_.length === 1) { + typeBlock = BlockDefinitionExtractor.buildBlockForType_( + connection.check_[0]); + } else if (connection.check_.length > 1 ) { + typeBlock = BlockDefinitionExtractor.typeGroup_(connection.check_); + } + } else { + typeBlock = BlockDefinitionExtractor.typeNullShadow_(); + } + return typeBlock; +}; + +/** + * Creates the root "factory_base" element for the block definition. + * + * @param {!Blockly.Block} block The example block from which to extract the + * definition. + * @param {string} name Block name. + * @return {!Element} The factory_base block element. + * @private + */ +BlockDefinitionExtractor.factoryBase_ = function(block, name) { + BlockDefinitionExtractor.src = {root: block, current: block}; + var factoryBaseEl = + BlockDefinitionExtractor.newDomElement_('block', {type: 'factory_base'}); + factoryBaseEl.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'NAME'}, name)); + factoryBaseEl.append(BlockDefinitionExtractor.buildInlineField_(block)); + + BlockDefinitionExtractor.buildConnections_(block, factoryBaseEl); + + var inputsStatement = BlockDefinitionExtractor.newDomElement_( + 'statement', {name: 'INPUTS'}); + inputsStatement.append(BlockDefinitionExtractor.parseInputs_(block)); + factoryBaseEl.append(inputsStatement); + + var tooltipValue = + BlockDefinitionExtractor.newDomElement_('value', {name: 'TOOLTIP'}); + tooltipValue.append(BlockDefinitionExtractor.text_(block.tooltip)); + factoryBaseEl.append(tooltipValue); + + var helpUrlValue = + BlockDefinitionExtractor.newDomElement_('value', {name: 'HELPURL'}); + helpUrlValue.append(BlockDefinitionExtractor.text_(block.helpUrl)); + factoryBaseEl.append(helpUrlValue); + + // Convert colour_ to hue value 0-360 degrees + // TODO(#1247): Solve off-by-one errors. + // TODO: Deal with colors that don't map to standard hues. (Needs improved + // block definitions.) + var colour_hue = Math.floor( + goog.color.hexToHsv(block.colour_)[0]); // Off by one... sometimes + var colourBlock = BlockDefinitionExtractor.colourBlockFromHue_(colour_hue); + var colourInputValue = + BlockDefinitionExtractor.newDomElement_('value', {name: 'COLOUR'}); + colourInputValue.append(colourBlock); + factoryBaseEl.append(colourInputValue); + return factoryBaseEl; +}; + +/** + * Generates the appropriate element for the block definition's + * CONNECTIONS field, which determines the next, previous, and output + * connections. + * + * @param {!Blockly.Block} block The example block from which to extract the + * definition. + * @param {!Element} factoryBaseEl The root of the block definition. + * @private + */ +BlockDefinitionExtractor.buildConnections_ = function(block, factoryBaseEl) { + var connections = 'NONE'; + if (block.outputConnection) { + connections = 'LEFT'; + } else { + if (block.previousConnection) { + if (block.nextConnection) { + connections = 'BOTH'; + } else { + connections = 'TOP'; + } + } else if (block.nextConnection) { + connections = 'BOTTOM'; + } + } + factoryBaseEl.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'CONNECTIONS'}, connections)); + + if (connections === 'LEFT') { + var inputValue = + BlockDefinitionExtractor.newDomElement_('value', {name: 'OUTPUTTYPE'}); + inputValue.append( + BlockDefinitionExtractor.buildTypeConstraintBlockForConnection_( + block.outputConnection)); + factoryBaseEl.append(inputValue); + } else { + if (connections === 'UP' || connections === 'BOTH') { + var inputValue = + BlockDefinitionExtractor.newDomElement_('value', {name: 'TOPTYPE'}); + inputValue.append( + BlockDefinitionExtractor.buildTypeConstraintBlockForConnection_( + block.previousConnection)); + factoryBaseEl.append(inputValue); + } + if (connections === 'DOWN' || connections === 'BOTH') { + var inputValue = BlockDefinitionExtractor.newDomElement_( + 'value', {name: 'BOTTOMTYPE'}); + inputValue.append( + BlockDefinitionExtractor.buildTypeConstraintBlockForConnection_( + block.nextConnection)); + factoryBaseEl.append(inputValue); + } + } +}; + +/** + * Generates the appropriate element for the block definition's INLINE + * field. + * + * @param {!Blockly.Block} block The example block from which to extract the + * definition. + * @return {Element} The INLINE with value 'AUTO', 'INT' (internal) or + * 'EXT' (external). + * @private + */ +BlockDefinitionExtractor.buildInlineField_ = function(block) { + var inline = 'AUTO'; // When block.inputsInlineDefault === undefined + if (block.inputsInlineDefault === true) { + inline = 'INT'; + } else if (block.inputsInlineDefault === false) { + inline = 'EXT'; + } + return BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'INLINE'}, inline); +}; + +/** + * Constructs a sequence of elements that represent the inputs of the + * provided block. + * + * @param {!Blockly.Block} block The source block to copy the inputs of. + * @return {Element} The fist element of the sequence + * (and the root of the constructed DOM). + * @private + */ +BlockDefinitionExtractor.parseInputs_ = function(block) { + var firstInputDefElement = null; + var lastInputDefElement = null; + for (var i = 0; i < block.inputList.length; i++) { + var input = block.inputList[i]; + var align = 'LEFT'; // Left alignment is the default. + if (input.align === Blockly.ALIGN_CENTRE) { + align = 'CENTRE'; + } else if (input.align === Blockly.ALIGN_RIGHT) { + align = 'RIGHT'; + } + + var inputDefElement = BlockDefinitionExtractor.input_(input, align); + if (lastInputDefElement) { + var next = BlockDefinitionExtractor.newDomElement_('next'); + next.append(inputDefElement); + lastInputDefElement.append(next); + } else { + firstInputDefElement = inputDefElement; + } + lastInputDefElement = inputDefElement; + } + return firstInputDefElement; +}; + +/** + * Creates a element representing a block input. + * + * @param {!Blockly.Input} input The input object. + * @param {string} align Can be left, right or centre. + * @return {!Element} The element that defines the input. + * @private + */ +BlockDefinitionExtractor.input_ = function(input, align) { + var isDummy = (input.type === Blockly.DUMMY_INPUT); + var inputTypeAttr = + isDummy ? 'input_dummy' : + (input.type === Blockly.INPUT_VALUE) ? 'input_value' : 'input_statement'; + var inputDefBlock = + BlockDefinitionExtractor.newDomElement_('block', {type: inputTypeAttr}); + + if (!isDummy) { + inputDefBlock.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'INPUTNAME'}, input.name)); + } + inputDefBlock.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'ALIGN'}, align)); + + var fieldsDef = BlockDefinitionExtractor.newDomElement_( + 'statement', {name: 'FIELDS'}); + var fieldsXml = BlockDefinitionExtractor.buildFields_(input.fieldRow); + fieldsDef.append(fieldsXml); + inputDefBlock.append(fieldsDef); + + if (!isDummy) { + var typeValue = BlockDefinitionExtractor.newDomElement_( + 'value', {name: 'TYPE'}); + typeValue.append( + BlockDefinitionExtractor.buildTypeConstraintBlockForConnection_( + input.connection)); + inputDefBlock.append(typeValue); + } + + return inputDefBlock; +}; + +/** + * Constructs a sequence elements representing the field definition. + * @param {Array} fieldRow A list of fields in a Blockly.Input. + * @return {Element} The fist element of the sequence + * (and the root of the constructed DOM). + * @private + */ +BlockDefinitionExtractor.buildFields_ = function(fieldRow) { + var firstFieldDefElement = null; + var lastFieldDefElement = null; + + for (var i = 0; i < fieldRow.length; i++) { + var field = fieldRow[i]; + var fieldDefElement = BlockDefinitionExtractor.buildFieldElement_(field); + + if (lastFieldDefElement) { + var next = BlockDefinitionExtractor.newDomElement_('next'); + next.append(fieldDefElement); + lastFieldDefElement.append(next); + } else { + firstFieldDefElement = fieldDefElement; + } + lastFieldDefElement = fieldDefElement; + } + + return firstFieldDefElement; +}; + +/** + * Constructs a element that describes the provided Blockly.Field. + * @param {!Blockly.Field} field The field from which the definition is copied. + * @param {!Element} A for the Field definition. + * @private + */ +BlockDefinitionExtractor.buildFieldElement_ = function(field) { + if (field instanceof Blockly.FieldLabel) { + return BlockDefinitionExtractor.buildFieldLabel_(field.text_); + } else if (field instanceof Blockly.FieldTextInput) { + return BlockDefinitionExtractor.buildFieldInput_(field.name, field.text_); + } else if (field instanceof Blockly.FieldNumber) { + return BlockDefinitionExtractor.buildFieldNumber_( + field.name, field.text_, field.min_, field.max_, field.presicion_); + } else if (field instanceof Blockly.FieldAngle) { + return BlockDefinitionExtractor.buildFieldAngle_(field.name, field.text_); + } else if (field instanceof Blockly.FieldCheckbox) { + return BlockDefinitionExtractor.buildFieldCheckbox_(field.name, field.state_); + } else if (field instanceof Blockly.FieldColour) { + return BlockDefinitionExtractor.buildFieldColour_(field.name, field.colour_); + } else if (field instanceof Blockly.FieldImage) { + return BlockDefinitionExtractor.buildFieldImage_( + field.src_, field.width_, field.height_, field.text_); + } else if (field instanceof Blockly.FieldVariable) { + // FieldVariable must be before FieldDropdown, because FieldVariable is a + // subclass. + return BlockDefinitionExtractor.buildFieldVariable_(field.name, field.text_); + } else if (field instanceof Blockly.FieldDropdown) { + return BlockDefinitionExtractor.buildFieldDropdown_(field); + } + throw Error('Unrecognized field class: ' + field.constructor.name); +}; + + +/** + * Creates a element representing a FieldLabel definition. + * @param {string} text + * @return {Element} The XML for FieldLabel definition. + * @private + */ +BlockDefinitionExtractor.buildFieldLabel_ = function(text) { + var fieldBlock = + BlockDefinitionExtractor.newDomElement_('block', {type: 'field_static'}); + fieldBlock.append( + BlockDefinitionExtractor.newDomElement_('field', {name: 'TEXT'}, text)); + return fieldBlock; +}; + +/** + * Creates a element representing a FieldInput (text input) definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {string} text The default text string. + * @return {Element} The XML for FieldInput definition. + * @private + */ +BlockDefinitionExtractor.buildFieldInput_ = function(fieldName, text) { + var fieldInput = + BlockDefinitionExtractor.newDomElement_('block', {type: 'field_input'}); + fieldInput.append( + BlockDefinitionExtractor.newDomElement_('field', {name: 'TEXT'}, text)); + fieldInput.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + return fieldInput; +}; + +/** + * Creates a element representing a FieldNumber definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {number} value The field's default value. + * @param {number} min The minimum allowed value, or negative infinity. + * @param {number} max The maximum allowed value, or positive infinity. + * @param {number} precision The precision allowed for the number. + * @return {Element} The XML for FieldNumber definition. + * @private + */ +BlockDefinitionExtractor.buildFieldNumber_ = + function(fieldName, value, min, max, precision) +{ + var fieldNumber = + BlockDefinitionExtractor.newDomElement_('block', {type: 'field_number'}); + fieldNumber.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'VALUE'}, value)); + fieldNumber.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + fieldNumber.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'MIN'}, min)); + fieldNumber.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'MAX'}, max)); + fieldNumber.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'PRECISION'}, precision)); + return fieldNumber; +}; + +/** + * Creates a element representing a FieldAngle definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {number} angle The field's default value. + * @return {Element} The XML for FieldAngle definition. + * @private + */ +BlockDefinitionExtractor.buildFieldAngle_ = function(angle, fieldName) { + var fieldAngle = + BlockDefinitionExtractor.newDomElement_('block', {type: 'field_angle'}); + fieldAngle.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'ANGLE'}, angle)); + fieldAngle.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + return fieldAngle; +}; + +/** + * Creates a element representing a FieldDropdown definition. + * + * @param {Blockly.FieldDropdown} dropdown + * @return {Element} The element representing a similar FieldDropdown. + * @private + */ +BlockDefinitionExtractor.buildFieldDropdown_ = function(dropdown) { + var menuGenerator = dropdown.menuGenerator_; + if (typeof menuGenerator === 'function') { + var options = menuGenerator(); + } else if (goog.isArray(menuGenerator)) { + var options = menuGenerator; + } else { + throw new Error('Unrecognized type of menuGenerator: ' + menuGenerator); + } + + var fieldDropdown = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'field_dropdown'}); + var optionsStr = '['; + + var mutation = BlockDefinitionExtractor.newDomElement_('mutation'); + fieldDropdown.append(mutation); + fieldDropdown.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, dropdown.name)); + for (var i=0; i element representing a FieldCheckbox definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {string} checked The field's default value, true or false. + * @return {Element} The XML for FieldCheckbox definition. + * @private + */ +BlockDefinitionExtractor.buildFieldCheckbox_ = + function(fieldName, checked) +{ + var fieldCheckbox = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'field_checkbox'}); + fieldCheckbox.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'CHECKED'}, checked)); + fieldCheckbox.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + return fieldCheckbox; +}; + +/** + * Creates a element representing a FieldColour definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {string} colour The field's default value as a string. + * @return {Element} The XML for FieldColour definition. + * @private + */ +BlockDefinitionExtractor.buildFieldColour_ = + function(fieldName, colour) +{ + var fieldColour = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'field_colour'}); + fieldColour.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'COLOUR'}, colour)); + fieldColour.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + return fieldColour; +}; + +/** + * Creates a element representing a FieldVaraible definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {string} varName The variables + * @return {Element} The element representing the FieldVariable. + * @private + */ +BlockDefinitionExtractor.buildFieldVariable_ = function(fieldName, varName) { + var fieldVar = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'field_variable'}); + fieldVar.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + fieldVar.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'TEXT'}, varName)); + return fieldVar; +}; + +/** + * Creates a element representing a FieldImage definition. + * + * @param {string} src The URL of the field image. + * @param {number} width The pixel width of the source image + * @param {number} height The pixel height of the source image. + * @param {string} alt Alterante text to describe image. + * @private + */ +BlockDefinitionExtractor.buildFieldImage_ = + function(src, width, height, alt) +{ + var block1 = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'field_image'}); + block1.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'SRC'}, src)); + block1.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'WIDTH'}, width)); + block1.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'HEIGHT'}, height)); + block1.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'ALT'}, alt)); +}; + +/** + * Creates a element a group of allowed connection constraint types. + * + * @param {Array} types List of type names in this group. + * @return {Element} The element representing the group, with child + * types attached. + * @private + */ +BlockDefinitionExtractor.typeGroup_ = function(types) { + var typeGroupBlock = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'type_group'}); + typeGroupBlock.append(BlockDefinitionExtractor.newDomElement_( + 'mutation', {types:types.length})); + for (var i=0; i block element representing the default null connection + * constraint. + * @return {Element} The element representing the "null" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeNullShadow_ = function() { + return BlockDefinitionExtractor.newDomElement_( + 'shadow', {type: 'type_null'}); +}; + +/** + * Creates a element representing null in a connection constraint. + * @return {Element} The element representing the "null" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeNull_ = function() { + return BlockDefinitionExtractor.newDomElement_('block', {type: 'type_null'}); +}; + +/** + * Creates a element representing the a boolean in a connection + * constraint. + * @return {Element} The element representing the "boolean" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeBoolean_ = function() { + return BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'type_boolean'}); +}; + +/** + * Creates a element representing the a number in a connection + * constraint. + * @return {Element} The element representing the "number" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeNumber_ = function() { + return BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'type_number'}); +}; + +/** + * Creates a element representing the a string in a connection + * constraint. + * @return {Element} The element representing the "string" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeString_ = function() { + return BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'type_string'}); +}; + +/** + * Creates a element representing the a list in a connection + * constraint. + * @return {Element} The element representing the "list" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeList_ = function() { + return BlockDefinitionExtractor.newDomElement_('block', {type: 'type_list'}); +}; + +/** + * Creates a element representing the given custom connection + * constraint type name. + * + * @param {string} type The connection constratin type name. + * @return {Element} The element representing a custom input type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeOther_ = function(type) { + var block = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'type_other'}); + block.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'TYPE'}, type)); + return block; +}; + +/** + * Creates a block Element for the color_hue block, with the given hue. + * @param hue {number} The hue value, from 0 to 360. + * @return {Element} The Element representing a colour_hue block + * with the given hue. + * @private + */ +BlockDefinitionExtractor.colourBlockFromHue_ = function(hue) { + var colourBlock = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'colour_hue'}); + colourBlock.append(BlockDefinitionExtractor.newDomElement_('mutation', { + colour: Blockly.hueToRgb(hue) + })); + colourBlock.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'HUE'}, hue.toString())); + return colourBlock; +}; + +/** + * Creates a block Element for a text block with the given text. + * + * @param text {string} The text value of the block. + * @return {Element} The element representing a "text" block. + * @private + */ +BlockDefinitionExtractor.text_ = function(text) { + var textBlock = + BlockDefinitionExtractor.newDomElement_('block', {type: 'text'}); + if (text) { + textBlock.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'TEXT'}, text)); + } // Else, use empty string default. + return textBlock; +}; diff --git a/demos/blockfactory/factory.js b/demos/blockfactory/factory.js index 974cd8a7e..821984b52 100644 --- a/demos/blockfactory/factory.js +++ b/demos/blockfactory/factory.js @@ -37,7 +37,6 @@ goog.provide('BlockFactory'); goog.require('FactoryUtils'); goog.require('StandardCategories'); - /** * Workspace for user to build block. * @type {Blockly.Workspace} @@ -52,14 +51,32 @@ BlockFactory.previewWorkspace = null; /** * Name of block if not named. + * @type string */ BlockFactory.UNNAMED = 'unnamed'; /** * Existing direction ('ltr' vs 'rtl') of preview. + * @type string */ BlockFactory.oldDir = null; +/** + * Flag to signal that an update came from a manual update to the JSON or JavaScript. + * definition manually. + * @type boolean + */ +// TODO: Replace global state with parameter passed to functions. +BlockFactory.updateBlocksFlag = false; + +/** + * Delayed flag to avoid infinite update after updating the JSON or JavaScript. + * definition manually. + * @type boolean + */ +// TODO: Replace global state with parameter passed to functions. +BlockFactory.updateBlocksFlagDelayed = false; + /* * The starting XML for the Block Factory main workspace. Contains the * unmovable, undeletable factory_base block. @@ -85,7 +102,8 @@ BlockFactory.formatChange = function() { var mask = document.getElementById('blocklyMask'); var languagePre = document.getElementById('languagePre'); var languageTA = document.getElementById('languageTA'); - if (document.getElementById('format').value == 'Manual') { + if (document.getElementById('format').value == 'Manual-JSON' || + document.getElementById('format').value == 'Manual-JS') { Blockly.hideChaff(); mask.style.display = 'block'; languagePre.style.display = 'none'; @@ -98,6 +116,9 @@ BlockFactory.formatChange = function() { mask.style.display = 'none'; languageTA.style.display = 'none'; languagePre.style.display = 'block'; + var code = languagePre.textContent.trim(); + languageTA.value = code; + BlockFactory.updateLanguage(); } BlockFactory.disableEnableLink(); @@ -115,10 +136,26 @@ BlockFactory.updateLanguage = function() { if (!blockType) { blockType = BlockFactory.UNNAMED; } - var format = document.getElementById('format').value; - var code = FactoryUtils.getBlockDefinition(blockType, rootBlock, format, - BlockFactory.mainWorkspace); - FactoryUtils.injectCode(code, 'languagePre'); + + if (!BlockFactory.updateBlocksFlag) { + var format = document.getElementById('format').value; + if (format == 'Manual-JSON') { + format = 'JSON'; + } else if (format == 'Manual-JS') { + format = 'JavaScript'; + } + + var code = FactoryUtils.getBlockDefinition(blockType, rootBlock, format, + BlockFactory.mainWorkspace); + FactoryUtils.injectCode(code, 'languagePre'); + if (!BlockFactory.updateBlocksFlagDelayed) { + var languagePre = document.getElementById('languagePre'); + var languageTA = document.getElementById('languageTA'); + code = languagePre.textContent.trim(); + languageTA.value = code; + } + } + BlockFactory.updatePreview(); }; @@ -151,20 +188,8 @@ BlockFactory.updatePreview = function() { } 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; - } + var format = BlockFactory.getBlockDefinitionFormat(); + var code = document.getElementById('languageTA').value; if (!code.trim()) { // Nothing to render. Happens while cloud storage is loading. return; @@ -188,9 +213,13 @@ BlockFactory.updatePreview = function() { } }; } else if (format == 'JavaScript') { - eval(code); - } else { - throw 'Unknown format: ' + format; + try { + eval(code); + } catch (e) { + // TODO: Display error in the UI + console.error("Error while evaluating JavaScript formatted block definition", e); + return; + } } // Look for a block on Blockly.Blocks that does not match the backup. @@ -232,12 +261,36 @@ BlockFactory.updatePreview = function() { } else { rootBlock.setWarningText(null); } - + } catch(err) { + // TODO: Show error on the UI + console.log(err); + BlockFactory.updateBlocksFlag = false + BlockFactory.updateBlocksFlagDelayed = false } finally { Blockly.Blocks = backupBlocks; } }; +/** + * Gets the format from the Block Definitions' format selector/drop-down. + * @return Either 'JavaScript' or 'JSON'. + * @throws If selector value is not recognized. + */ +BlockFactory.getBlockDefinitionFormat = function() { + switch (document.getElementById('format').value) { + case 'JSON': + case 'Manual-JSON': + return 'JSON'; + + case 'JavaScript': + case 'Manual-JS': + return 'JavaScript'; + + default: + throw 'Unknown format: ' + format; + } +} + /** * Disable link and save buttons if the format is 'Manual', enable otherwise. */ @@ -245,7 +298,7 @@ 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'; + var disabled = document.getElementById('format').value.substr(0, 6) == 'Manual'; linkButton.disabled = disabled; saveBlockButton.disabled = disabled; saveToLibButton.disabled = disabled; @@ -265,12 +318,25 @@ BlockFactory.showStarterBlock = function() { */ BlockFactory.isStarterBlock = function() { var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); - // The starter block does not have blocks nested into the factory_base block. - return !(rootBlock.getChildren().length > 0 || + return rootBlock && !( + // The starter block does not have blocks nested into the factory_base block. + rootBlock.getChildren().length > 0 || // The starter block's name is the default, 'block_type'. rootBlock.getFieldValue('NAME').trim().toLowerCase() != 'block_type' || // The starter block has no connections. rootBlock.getFieldValue('CONNECTIONS') != 'NONE' || // The starter block has automatic inputs. - rootBlock.getFieldValue('INLINE') != 'AUTO'); + rootBlock.getFieldValue('INLINE') != 'AUTO' + ); }; + +/** + * Updates blocks from the manually edited js or json from their text area. + */ +BlockFactory.manualEdit = function() { + // TODO(#1267): Replace these global state flags with parameters passed to + // the right functions. + BlockFactory.updateBlocksFlag = true; + BlockFactory.updateBlocksFlagDelayed = true; + BlockFactory.updateLanguage(); +} diff --git a/demos/blockfactory/factory_utils.js b/demos/blockfactory/factory_utils.js index d66225c3a..63f9864eb 100644 --- a/demos/blockfactory/factory_utils.js +++ b/demos/blockfactory/factory_utils.js @@ -24,15 +24,18 @@ * 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) + * @author fraser@google.com (Neil Fraser), quachtina96 (Tina Quach), JC-Orozco + * (Juan Carlos Orozco) */ - 'use strict'; +'use strict'; /** * Namespace for FactoryUtils. */ goog.provide('FactoryUtils'); +goog.require('BlockDefinitionExtractor'); + /** * Get block definition code for the current block. @@ -73,10 +76,23 @@ FactoryUtils.cleanBlockType = function(blockType) { * 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'. + * or 'Dart'. * @return {string} Generator code for multiple blocks. */ FactoryUtils.getGeneratorStub = function(block, generatorLanguage) { + // Build factory blocks from block + if (BlockFactory.updateBlocksFlag) { // TODO: Move this to updatePreview() + BlockFactory.mainWorkspace.clear(); + var xml = BlockDefinitionExtractor.buildBlockFactoryWorkspace(block); + Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace); + // Calculate timer to avoid infinite update loops + // TODO(#1267): Remove the global variables and any infinite loops. + BlockFactory.updateBlocksFlag = false; + setTimeout( + function() {BlockFactory.updateBlocksFlagDelayed = false;}, 3000); + } + BlockFactory.lastUpdatedBlock = block; // Variable to share the block value. + function makeVar(root, name) { name = name.toLowerCase().replace(/\W/g, '_'); return ' var ' + root + '_' + name; diff --git a/demos/blockfactory/index.html b/demos/blockfactory/index.html index 9190bd132..ccfc05654 100644 --- a/demos/blockfactory/index.html +++ b/demos/blockfactory/index.html @@ -10,6 +10,7 @@ + @@ -354,10 +355,21 @@

Block Definition: +