From 774a76f2755090987e2de08804bc87b38de9a88e Mon Sep 17 00:00:00 2001 From: Neil Fraser Date: Tue, 30 Jun 2015 22:51:06 -0700 Subject: [PATCH] Allowing manual code editing in Block Factory. --- demos/blockfactory/factory.js | 337 ++++++++++++++++++++-------------- demos/blockfactory/index.html | 28 ++- 2 files changed, 226 insertions(+), 139 deletions(-) diff --git a/demos/blockfactory/factory.js b/demos/blockfactory/factory.js index d523e6f72..3bc0b9f41 100644 --- a/demos/blockfactory/factory.js +++ b/demos/blockfactory/factory.js @@ -23,11 +23,6 @@ */ 'use strict'; -/** - * The type of the generated block. - */ -var blockType = ''; - /** * Workspace for user to build block. * @type Blockly.Workspace @@ -41,47 +36,67 @@ var mainWorkspace = null; var previewWorkspace = null; /** - * When the workspace changes, update the three other displays. + * Name of block if not named. */ -function onchange() { - var name = ''; +var UNNAMED = 'unnamed'; + +/** + * Change the language code format. + */ +function formatChange() { + var mask = document.getElementById('blocklyMask'); + var languagePre = document.getElementById('languagePre'); + var languageTA = document.getElementById('languageTA'); + if (document.getElementById('format').value == 'Manual') { + mask.style.display = 'block'; + languagePre.style.display = 'none'; + languageTA.style.display = 'block'; + var code = languagePre.textContent.trim(); + languageTA.value = code; + languageTA.focus(); + updatePreview(); + } else { + mask.style.display = 'none'; + languageTA.style.display = 'none'; + languagePre.style.display = 'block'; + updateLanguage(); + } + disableEnableLink(); +} + +/** + * Update the language code based on constructs made in Blockly. + */ +function updateLanguage() { var rootBlock = getRootBlock(); - if (rootBlock) { - name = rootBlock.getFieldValue('NAME'); + if (!rootBlock) { + return; } - blockType = name.replace(/\W/g, '_').replace(/^(\d)/, '_\\1').toLowerCase(); + var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase(); if (!blockType) { - blockType = 'unnamed'; + blockType = UNNAMED; } - updateLanguage(); - updateGenerator(); + blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1'); + switch (document.getElementById('format').value) { + case 'JSON': + var code = formatJson_(blockType, rootBlock); + break; + case 'JavaScript': + var code = formatJavaScript_(blockType, rootBlock); + break; + } + injectCode(code, 'languagePre'); updatePreview(); } -/** - * Update the language code. - */ -function updateLanguage() { - Blockly.removeAllRanges(); - var code = []; - var rootBlock = getRootBlock(); - if (rootBlock) { - switch (document.getElementById('format').value) { - case 'JSON': - formatJson(code, rootBlock); - break; - case 'JavaScript': - formatJavaScript(code, rootBlock); - break; - } - } - injectCode(code, 'languagePre'); -} - /** * 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 */ -function formatJson(code, rootBlock) { +function formatJson_(blockType, rootBlock) { var JS = {}; // ID is not used by Blockly, but may be used by a loader. JS.id = blockType; @@ -171,13 +186,18 @@ function formatJson(code, rootBlock) { } JS.tooltip = ''; JS.helpUrl = 'http://www.example.com/'; - code.push(JSON.stringify(JS, null, ' ')); + return JSON.stringify(JS, null, ' '); } /** * Update the language code as JavaScript. + * @param {string} blockType Name of block. + * @param {!Blockly.Block} rootBlock Factory_base block. + * @return {string} Generanted language code. + * @private */ -function formatJavaScript(code, rootBlock) { +function formatJavaScript_(blockType, rootBlock) { + var code = []; code.push("Blockly.Blocks['" + blockType + "'] = {"); code.push(" init: function() {"); // Generate inputs. @@ -241,8 +261,9 @@ function formatJavaScript(code, rootBlock) { } code.push(" this.setTooltip('');"); code.push(" this.setHelpUrl('http://www.example.com/');"); - code.push(" }"); - code.push("};"); + code.push(' }'); + code.push('};'); + return code.join('\n'); } /** @@ -494,89 +515,77 @@ function getTypesFrom_(block, name) { /** * Update the generator code. + * @param {!Blockly.Block} block Rendered block in preview workspace. */ -function updateGenerator() { - Blockly.removeAllRanges(); +function updateGenerator(block) { function makeVar(root, name) { name = name.toLowerCase().replace(/\W/g, '_'); return ' var ' + root + '_' + name; } var language = document.getElementById('language').value; var code = []; - code.push("Blockly." + language + "['" + blockType + + code.push("Blockly." + language + "['" + block.type + "'] = function(block) {"); - var rootBlock = getRootBlock(); - if (rootBlock) { - // Loop through every block, and generate getters for any fields or inputs. - var blocks = rootBlock.getDescendants(); - for (var x = 0, block; block = blocks[x]; x++) { - if (block.disabled || block.getInheritedDisabled()) { + + // 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; } - switch (block.type) { - case 'field_input': - var name = block.getFieldValue('FIELDNAME'); - code.push(makeVar('text', name) + - " = block.getFieldValue('" + name + "');"); - break; - case 'field_angle': - var name = block.getFieldValue('FIELDNAME'); - code.push(makeVar('angle', name) + - " = block.getFieldValue('" + name + "');"); - break; - case 'field_dropdown': - var name = block.getFieldValue('FIELDNAME'); - code.push(makeVar('dropdown', name) + - " = block.getFieldValue('" + name + "');"); - break; - case 'field_checkbox': - var name = block.getFieldValue('FIELDNAME'); - code.push(makeVar('checkbox', name) + - " = block.getFieldValue('" + name + "') == 'TRUE';"); - break; - case 'field_colour': - var name = block.getFieldValue('FIELDNAME'); - code.push(makeVar('colour', name) + - " = block.getFieldValue('" + name + "');"); - break; - case 'field_date': - var name = block.getFieldValue('FIELDNAME'); - code.push(makeVar('date', name) + - " = block.getFieldValue('" + name + "');"); - break; - case 'field_variable': - var name = block.getFieldValue('FIELDNAME'); - code.push(makeVar('variable', name) + - " = Blockly." + language + - ".variableDB_.getName(block.getFieldValue('" + name + - "'), Blockly.Variables.NAME_TYPE);"); - break; - case 'input_value': - var name = block.getFieldValue('INPUTNAME'); - code.push(makeVar('value', name) + - " = Blockly." + language + ".valueToCode(block, '" + name + - "', Blockly." + language + ".ORDER_ATOMIC);"); - break; - case 'input_statement': - var name = block.getFieldValue('INPUTNAME'); - code.push(makeVar('statements', name) + - " = Blockly." + language + ".statementToCode(block, '" + - name + "');"); - break; + 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.FieldTextInput) { + code.push(makeVar('text', name) + + " = block.getFieldValue('" + name + "');"); } } - code.push(" // TODO: Assemble " + language + " into code variable."); - code.push(" var code = \'...\';"); - if (rootBlock.getFieldValue('CONNECTIONS') == 'LEFT') { - code.push(" // TODO: Change ORDER_NONE to the correct strength."); - code.push(" return [code, Blockly." + language + ".ORDER_NONE];"); - } else { - code.push(" return code;"); + 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 + "');"); + } } } + code.push(" // TODO: Assemble " + language + " into code variable."); + code.push(" var code = \'...\';"); + if (block.outputConnection) { + code.push(" // TODO: Change ORDER_NONE to the correct strength."); + code.push(" return [code, Blockly." + language + ".ORDER_NONE];"); + } else { + code.push(" return code;"); + } code.push("};"); - injectCode(code, 'generatorPre'); + injectCode(code.join('\n'), 'generatorPre'); } /** @@ -588,6 +597,7 @@ var oldDir = null; * Update the preview display. */ function updatePreview() { + // Toggle between LTR/RTL if needed (also used in first display). var newDir = document.getElementById('direction').value; if (oldDir != newDir) { if (previewWorkspace) { @@ -601,45 +611,86 @@ function updatePreview() { oldDir = newDir; } previewWorkspace.clear(); - if (Blockly.Blocks[blockType]) { - throw 'Block name collides with existing property: ' + blockType; + + // 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 code = document.getElementById('languagePre').textContent.trim(); - if (!code) { + if (!code.trim()) { // Nothing to render. Happens while cloud storage is loading. return; } - var format = document.getElementById('format').value; - if (format == 'JSON') { - Blockly.Blocks[blockType] = { - init: function() { - this.jsonInit(JSON.parse(code)); + + // 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; + try { + // Make a shallow copy. + Blockly.Blocks = {}; + for (var prop in backupBlocks) { + Blockly.Blocks[prop] = backupBlocks[prop]; + } + + if (format == 'JSON') { + var json = JSON.parse(code); + Blockly.Blocks[json.id || UNNAMED] = { + init: function() { + this.jsonInit(json); + } + }; + } else if (format == 'JavaScript') { + eval(code); + } else { + throw 'Unknown format: ' + format; + } + + // Look for a block on Blockly.Blocks that does not match the backup. + var blockType = null; + for (var type in Blockly.Blocks) { + if (typeof Blockly.Blocks[type].init == 'function' && + Blockly.Blocks[type] != backupBlocks[type]) { + blockType = type; + break; } - }; - } else if (format == 'JavaScript') { - eval(code); - } else { - throw 'Unknown format: ' + format; + } + if (!blockType) { + return; + } + + // Create the preview block. + var previewBlock = Blockly.Block.obtain(previewWorkspace, blockType); + previewBlock.initSvg(); + previewBlock.render(); + previewBlock.setMovable(false); + previewBlock.setDeletable(false); + previewBlock.moveBy(15, 10); + + updateGenerator(previewBlock); + } finally { + Blockly.Blocks = backupBlocks; } - // Create the preview block. - var previewBlock = Blockly.Block.obtain(previewWorkspace, blockType); - delete Blockly.Blocks[blockType]; - previewBlock.initSvg(); - previewBlock.render(); - previewBlock.setMovable(false); - previewBlock.setDeletable(false); - previewBlock.moveBy(15, 10); } /** * Inject code into a pre tag, with syntax highlighting. * Safe from HTML/script injection. - * @param {!Array.} code Array of lines of code. + * @param {string} code Lines of code. * @param {string} id ID of
 element to inject into.
  */
 function injectCode(code, id) {
+  Blockly.removeAllRanges();
   var pre = document.getElementById(id);
-  pre.textContent = code.join('\n');
+  pre.textContent = code;
   code = pre.innerHTML;
   code = prettyPrintOne(code, 'js');
   pre.innerHTML = code;
@@ -659,6 +710,14 @@ function getRootBlock() {
   return null;
 }
 
+/**
+ * Disable the link button if the format is 'Manual', enable otherwise.
+ */
+function disableEnableLink() {
+  var linkButton = document.getElementById('linkButton');
+  linkButton.disabled = document.getElementById('format').value == 'Manual';
+}
+
 /**
  * Initialize Blockly and layout.  Called on page load.
  */
@@ -676,17 +735,21 @@ function init() {
     linkButton.style.display = 'inline-block';
     linkButton.addEventListener('click',
         function() {BlocklyStorage.link(mainWorkspace);});
+    disableEnableLink();
   }
 
-  document.getElementById('helpButton').addEventListener('click', function() {
+  document.getElementById('helpButton').addEventListener('click',
+    function() {
       open('https://developers.google.com/blockly/custom-blocks/block-factory',
            'BlockFactoryHelp');
     });
 
   var expandList = [
     document.getElementById('blockly'),
+    document.getElementById('blocklyMask'),
     document.getElementById('preview'),
     document.getElementById('languagePre'),
+    document.getElementById('languageTA'),
     document.getElementById('generatorPre')
   ];
   var onresize = function(e) {
@@ -714,12 +777,16 @@ function init() {
     rootBlock.setDeletable(false);
   }
 
-  mainWorkspace.addChangeListener(onchange);
+  mainWorkspace.addChangeListener(updateLanguage);
   document.getElementById('direction')
       .addEventListener('change', updatePreview);
-  document.getElementById('format').addEventListener('change',
-      function() {updateLanguage(); updatePreview();});
+  document.getElementById('languageTA')
+      .addEventListener('change', updatePreview);
+  document.getElementById('languageTA')
+      .addEventListener('keyup', updatePreview);
+  document.getElementById('format')
+      .addEventListener('change', formatChange);
   document.getElementById('language')
-      .addEventListener('change', updateGenerator);
+      .addEventListener('change', updatePreview);
 }
 window.addEventListener('load', init);
diff --git a/demos/blockfactory/index.html b/demos/blockfactory/index.html
index 22ef08919..dfa98de78 100644
--- a/demos/blockfactory/index.html
+++ b/demos/blockfactory/index.html
@@ -38,15 +38,29 @@
     #blockly {
       position: fixed;
     }
+    #blocklyMask {
+      background-color: #000;
+      cursor: not-allowed;
+      display: none;
+      position: fixed;
+      opacity: 0.2;
+      z-index: 9;
+    }
     #preview {
       position: absolute;
     }
-    pre {
+    pre,
+    #languageTA {
+      border: #ddd 1px solid;
       margin-top: 0;
       position: absolute;
-      border: #ddd 1px solid;
       overflow: scroll;
     }
+    #languageTA {
+      display: none;
+      font-family: monospace;
+      font-size: 10pt;
+    }
 
     button {
       border-radius: 4px;
@@ -57,14 +71,17 @@
       margin: 0 5px;
       font-size: large;
     }
-    button:hover {
+    button:hover:not(:disabled) {
       box-shadow: 2px 2px 5px #888;
     }
+    button:disabled {
+      opacity: 0.6;
+    }
     button>* {
       opacity: 0.6;
       vertical-align: text-bottom;
     }
-    button:hover>* {
+    button:hover:not(:disabled)>* {
       opacity: 1;
     }
     #linkButton {
@@ -110,6 +127,7 @@
     
       
         
+
@@ -124,6 +142,7 @@ @@ -131,6 +150,7 @@

+