Commit 774a76f2 authored by Neil Fraser's avatar Neil Fraser

Allowing manual code editing in Block Factory.

parent 345b0cb6
......@@ -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 rootBlock = getRootBlock();
if (rootBlock) {
name = rootBlock.getFieldValue('NAME');
}
blockType = name.replace(/\W/g, '_').replace(/^(\d)/, '_\\1').toLowerCase();
if (!blockType) {
blockType = 'unnamed';
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();
}
updateLanguage();
updateGenerator();
updatePreview();
disableEnableLink();
}
/**
* Update the language code.
* Update the language code based on constructs made in Blockly.
*/
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;
}
if (!rootBlock) {
return;
}
var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase();
if (!blockType) {
blockType = UNNAMED;
}
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 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.<string>} code Array of lines of code.
* @param {string} code Lines of code.
* @param {string} id ID of <pre> 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);
......@@ -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 @@
<tr>
<td width="50%" height="95%" style="padding: 2px;">
<div id="blockly"></div>
<div id="blocklyMask"></div>
</td>
<td width="50%" height="95%">
<table>
......@@ -124,6 +142,7 @@
<select id="format">
<option value="JavaScript">JavaScript</option>
<option value="JSON">JSON</option>
<option value="Manual">Manual edit...</option>
</select>
</h3>
</td>
......@@ -131,6 +150,7 @@
<tr>
<td height="30%">
<pre id="languagePre"></pre>
<textarea id="languageTA"></textarea>
</td>
</tr>
<tr>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment