mirror of
https://github.com/google/blockly.git
synced 2026-01-30 20:20:09 +01:00
Allowing manual code editing in Block Factory.
This commit is contained in:
@@ -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.<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>
|
||||
|
||||
Reference in New Issue
Block a user