Allowing manual code editing in Block Factory.

This commit is contained in:
Neil Fraser
2015-06-30 22:51:06 -07:00
parent 345b0cb668
commit 774a76f275
2 changed files with 226 additions and 139 deletions

View File

@@ -23,11 +23,6 @@
*/ */
'use strict'; 'use strict';
/**
* The type of the generated block.
*/
var blockType = '';
/** /**
* Workspace for user to build block. * Workspace for user to build block.
* @type Blockly.Workspace * @type Blockly.Workspace
@@ -41,47 +36,67 @@ var mainWorkspace = null;
var previewWorkspace = null; var previewWorkspace = null;
/** /**
* When the workspace changes, update the three other displays. * Name of block if not named.
*/ */
function onchange() { var UNNAMED = 'unnamed';
var name = '';
/**
* 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(); var rootBlock = getRootBlock();
if (rootBlock) { if (!rootBlock) {
name = rootBlock.getFieldValue('NAME'); return;
} }
blockType = name.replace(/\W/g, '_').replace(/^(\d)/, '_\\1').toLowerCase(); var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase();
if (!blockType) { if (!blockType) {
blockType = 'unnamed'; blockType = UNNAMED;
} }
updateLanguage(); blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1');
updateGenerator(); 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(); 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. * 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 = {}; var JS = {};
// ID is not used by Blockly, but may be used by a loader. // ID is not used by Blockly, but may be used by a loader.
JS.id = blockType; JS.id = blockType;
@@ -171,13 +186,18 @@ function formatJson(code, rootBlock) {
} }
JS.tooltip = ''; JS.tooltip = '';
JS.helpUrl = 'http://www.example.com/'; JS.helpUrl = 'http://www.example.com/';
code.push(JSON.stringify(JS, null, ' ')); return JSON.stringify(JS, null, ' ');
} }
/** /**
* Update the language code as JavaScript. * 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("Blockly.Blocks['" + blockType + "'] = {");
code.push(" init: function() {"); code.push(" init: function() {");
// Generate inputs. // Generate inputs.
@@ -241,8 +261,9 @@ function formatJavaScript(code, rootBlock) {
} }
code.push(" this.setTooltip('');"); code.push(" this.setTooltip('');");
code.push(" this.setHelpUrl('http://www.example.com/');"); 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. * Update the generator code.
* @param {!Blockly.Block} block Rendered block in preview workspace.
*/ */
function updateGenerator() { function updateGenerator(block) {
Blockly.removeAllRanges();
function makeVar(root, name) { function makeVar(root, name) {
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
return ' var ' + root + '_' + name; return ' var ' + root + '_' + name;
} }
var language = document.getElementById('language').value; var language = document.getElementById('language').value;
var code = []; var code = [];
code.push("Blockly." + language + "['" + blockType + code.push("Blockly." + language + "['" + block.type +
"'] = function(block) {"); "'] = function(block) {");
var rootBlock = getRootBlock();
if (rootBlock) { // Generate getters for any fields or inputs.
// Loop through every block, and generate getters for any fields or inputs. for (var i = 0, input; input = block.inputList[i]; i++) {
var blocks = rootBlock.getDescendants(); for (var j = 0, field; field = input.fieldRow[j]; j++) {
for (var x = 0, block; block = blocks[x]; x++) { var name = field.name;
if (block.disabled || block.getInheritedDisabled()) { if (!name) {
continue; continue;
} }
switch (block.type) { if (field instanceof Blockly.FieldVariable) {
case 'field_input': // Subclass of Blockly.FieldDropdown, must test first.
var name = block.getFieldValue('FIELDNAME'); code.push(makeVar('variable', name) +
code.push(makeVar('text', name) + " = Blockly." + language +
" = block.getFieldValue('" + name + "');"); ".variableDB_.getName(block.getFieldValue('" + name +
break; "'), Blockly.Variables.NAME_TYPE);");
case 'field_angle': } else if (field instanceof Blockly.FieldAngle) {
var name = block.getFieldValue('FIELDNAME'); // Subclass of Blockly.FieldTextInput, must test first.
code.push(makeVar('angle', name) + code.push(makeVar('angle', name) +
" = block.getFieldValue('" + name + "');"); " = block.getFieldValue('" + name + "');");
break; } else if (Blockly.FieldDate && field instanceof Blockly.FieldDate) {
case 'field_dropdown': // Blockly.FieldDate may not be compiled into Blockly.
var name = block.getFieldValue('FIELDNAME'); code.push(makeVar('date', name) +
code.push(makeVar('dropdown', name) + " = block.getFieldValue('" + name + "');");
" = block.getFieldValue('" + name + "');"); } else if (field instanceof Blockly.FieldColour) {
break; code.push(makeVar('colour', name) +
case 'field_checkbox': " = block.getFieldValue('" + name + "');");
var name = block.getFieldValue('FIELDNAME'); } else if (field instanceof Blockly.FieldCheckbox) {
code.push(makeVar('checkbox', name) + code.push(makeVar('checkbox', name) +
" = block.getFieldValue('" + name + "') == 'TRUE';"); " = block.getFieldValue('" + name + "') == 'TRUE';");
break; } else if (field instanceof Blockly.FieldDropdown) {
case 'field_colour': code.push(makeVar('dropdown', name) +
var name = block.getFieldValue('FIELDNAME'); " = block.getFieldValue('" + name + "');");
code.push(makeVar('colour', name) + } else if (field instanceof Blockly.FieldTextInput) {
" = block.getFieldValue('" + name + "');"); code.push(makeVar('text', name) +
break; " = block.getFieldValue('" + name + "');");
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;
} }
} }
code.push(" // TODO: Assemble " + language + " into code variable."); var name = input.name;
code.push(" var code = \'...\';"); if (name) {
if (rootBlock.getFieldValue('CONNECTIONS') == 'LEFT') { if (input.type == Blockly.INPUT_VALUE) {
code.push(" // TODO: Change ORDER_NONE to the correct strength."); code.push(makeVar('value', name) +
code.push(" return [code, Blockly." + language + ".ORDER_NONE];"); " = Blockly." + language + ".valueToCode(block, '" + name +
} else { "', Blockly." + language + ".ORDER_ATOMIC);");
code.push(" return code;"); } 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("};"); code.push("};");
injectCode(code, 'generatorPre'); injectCode(code.join('\n'), 'generatorPre');
} }
/** /**
@@ -588,6 +597,7 @@ var oldDir = null;
* Update the preview display. * Update the preview display.
*/ */
function updatePreview() { function updatePreview() {
// Toggle between LTR/RTL if needed (also used in first display).
var newDir = document.getElementById('direction').value; var newDir = document.getElementById('direction').value;
if (oldDir != newDir) { if (oldDir != newDir) {
if (previewWorkspace) { if (previewWorkspace) {
@@ -601,45 +611,86 @@ function updatePreview() {
oldDir = newDir; oldDir = newDir;
} }
previewWorkspace.clear(); 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.trim()) {
if (!code) {
// Nothing to render. Happens while cloud storage is loading. // Nothing to render. Happens while cloud storage is loading.
return; return;
} }
var format = document.getElementById('format').value;
if (format == 'JSON') { // Backup Blockly.Blocks object so that main workspace and preview don't
Blockly.Blocks[blockType] = { // collide if user creates a 'factory_base' block, for instance.
init: function() { var backupBlocks = Blockly.Blocks;
this.jsonInit(JSON.parse(code)); 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') { if (!blockType) {
eval(code); return;
} else { }
throw 'Unknown format: ' + format;
// 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. * Inject code into a pre tag, with syntax highlighting.
* Safe from HTML/script injection. * Safe from HTML/script injection.
* @param {!Array.<string>} code Array of lines of code. * @param {string} code Lines of code.
* @param {string} id ID of <pre> element to inject into. * @param {string} id ID of <pre> element to inject into.
*/ */
function injectCode(code, id) { function injectCode(code, id) {
Blockly.removeAllRanges();
var pre = document.getElementById(id); var pre = document.getElementById(id);
pre.textContent = code.join('\n'); pre.textContent = code;
code = pre.innerHTML; code = pre.innerHTML;
code = prettyPrintOne(code, 'js'); code = prettyPrintOne(code, 'js');
pre.innerHTML = code; pre.innerHTML = code;
@@ -659,6 +710,14 @@ function getRootBlock() {
return null; 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. * Initialize Blockly and layout. Called on page load.
*/ */
@@ -676,17 +735,21 @@ function init() {
linkButton.style.display = 'inline-block'; linkButton.style.display = 'inline-block';
linkButton.addEventListener('click', linkButton.addEventListener('click',
function() {BlocklyStorage.link(mainWorkspace);}); 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', open('https://developers.google.com/blockly/custom-blocks/block-factory',
'BlockFactoryHelp'); 'BlockFactoryHelp');
}); });
var expandList = [ var expandList = [
document.getElementById('blockly'), document.getElementById('blockly'),
document.getElementById('blocklyMask'),
document.getElementById('preview'), document.getElementById('preview'),
document.getElementById('languagePre'), document.getElementById('languagePre'),
document.getElementById('languageTA'),
document.getElementById('generatorPre') document.getElementById('generatorPre')
]; ];
var onresize = function(e) { var onresize = function(e) {
@@ -714,12 +777,16 @@ function init() {
rootBlock.setDeletable(false); rootBlock.setDeletable(false);
} }
mainWorkspace.addChangeListener(onchange); mainWorkspace.addChangeListener(updateLanguage);
document.getElementById('direction') document.getElementById('direction')
.addEventListener('change', updatePreview); .addEventListener('change', updatePreview);
document.getElementById('format').addEventListener('change', document.getElementById('languageTA')
function() {updateLanguage(); updatePreview();}); .addEventListener('change', updatePreview);
document.getElementById('languageTA')
.addEventListener('keyup', updatePreview);
document.getElementById('format')
.addEventListener('change', formatChange);
document.getElementById('language') document.getElementById('language')
.addEventListener('change', updateGenerator); .addEventListener('change', updatePreview);
} }
window.addEventListener('load', init); window.addEventListener('load', init);

View File

@@ -38,15 +38,29 @@
#blockly { #blockly {
position: fixed; position: fixed;
} }
#blocklyMask {
background-color: #000;
cursor: not-allowed;
display: none;
position: fixed;
opacity: 0.2;
z-index: 9;
}
#preview { #preview {
position: absolute; position: absolute;
} }
pre { pre,
#languageTA {
border: #ddd 1px solid;
margin-top: 0; margin-top: 0;
position: absolute; position: absolute;
border: #ddd 1px solid;
overflow: scroll; overflow: scroll;
} }
#languageTA {
display: none;
font-family: monospace;
font-size: 10pt;
}
button { button {
border-radius: 4px; border-radius: 4px;
@@ -57,14 +71,17 @@
margin: 0 5px; margin: 0 5px;
font-size: large; font-size: large;
} }
button:hover { button:hover:not(:disabled) {
box-shadow: 2px 2px 5px #888; box-shadow: 2px 2px 5px #888;
} }
button:disabled {
opacity: 0.6;
}
button>* { button>* {
opacity: 0.6; opacity: 0.6;
vertical-align: text-bottom; vertical-align: text-bottom;
} }
button:hover>* { button:hover:not(:disabled)>* {
opacity: 1; opacity: 1;
} }
#linkButton { #linkButton {
@@ -110,6 +127,7 @@
<tr> <tr>
<td width="50%" height="95%" style="padding: 2px;"> <td width="50%" height="95%" style="padding: 2px;">
<div id="blockly"></div> <div id="blockly"></div>
<div id="blocklyMask"></div>
</td> </td>
<td width="50%" height="95%"> <td width="50%" height="95%">
<table> <table>
@@ -124,6 +142,7 @@
<select id="format"> <select id="format">
<option value="JavaScript">JavaScript</option> <option value="JavaScript">JavaScript</option>
<option value="JSON">JSON</option> <option value="JSON">JSON</option>
<option value="Manual">Manual edit...</option>
</select> </select>
</h3> </h3>
</td> </td>
@@ -131,6 +150,7 @@
<tr> <tr>
<td height="30%"> <td height="30%">
<pre id="languagePre"></pre> <pre id="languagePre"></pre>
<textarea id="languageTA"></textarea>
</td> </td>
</tr> </tr>
<tr> <tr>