Blockly Python Code Generator(2)
前回(Blockly Python Code Generator)では、Pythonコードを表示するのみでしたが、今回はそれにファイルに保存する機能を追加します。
File API
File APIはHTML5で追加された機能です。
ウェブアプリケーションからのファイルの使用
作成したオブジェクトをファイルとしてダウンロードするためのURLを作成することができます。
const objectURL = window.URL.createObjectURL(fileObj);
上記で取得したURLは下記関数で開放します。
URL.revokeObjectURL(objectURL);
Blob
JavaScriptで生データを使用するためのオブジェクトとして、Blobが準備されています。
Blob
テキストを保存する場合は、以下のように設定します。
const blob = new Blob([txt], { type: 'text/plain' });
このデータはFile APIに渡すことができます。
const blob = new Blob([txt], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
File 保存
File APIで作成した一時的なURLに対してクリックイベントを起こすことにより、ファイル保存のトリガを実行します。
ただし、JavaScriptで直接ローカルにファイル保存することはできません。
ユーザー確認が必要になります。
具体的には以下のような処理になります。
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.download = 'sample.py';
a.href = url;
a.click();
a.remove();
URL.revokeObjectURL(url);
まとめ
前回(Blockly Python Code Generator)のファイルをベースに変更します。
ファイル保存用のボタンを追加します。
(今回は機能を実装していませんが、XMLのSave/Loadボタンも作っています。)
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Blockly Sample</title>
<link rel="stylesheet" href="./styles.css">
<link rel="stylesheet" href="./code.css">
<script src="../blockly/blockly_compressed.js"></script>
<script src="../blockly/blocks_compressed.js"></script>
<script src="../blockly/msg/js/en.js"></script>
<script src="../blockly/python_compressed.js"></script>
<script src="./code.js"></script>
</head>
<body>
<table>
<tr>
<td>
<h1>
<p>Blockly Code Generator</p>
</h1>
</td>
</tr>
<tr>
<td>
<table>
<tr id="tabRow" height="1em">
<td id="tab_blocks" class="tabon">Blocks</td>
<td class="tabmin tab_collapse"> </td>
<td id="tab_python" class="taboff">Python</td>
<td class="tabmin tab_collapse"> </td>
<td id="tab_xml" class="taboff">XML</td>
<td class="tabmax">
<button id="trashButton" class="notext" title="...">
<img src='../blockly/media/1x1.gif' class="trash icon21">
</button>
<button id="saveButton" class="text" title="srcSave">
Save
</button>
<button id="loadButton" class="text" title="srcLoad">
Load
</button>
<button id="writeButton" class="text" title="writePython">
Write
</button>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="99%" colspan=2 id="content_area">
</td>
</tr>
</table>
<div id="blocklyDiv"></div>
<div id="content_blocks" class="content"></div>
<pre id="content_python" class="content prettyprint lang-py"></pre>
<textarea id="content_xml" class="content" wrap="off"></textarea>
</body>
</html>
JavaScript
ボタン機能を追加し、ファイル保存の機能を追加しました。
var Code = {};
/**
* Blockly's main workspace.
* @type {Blockly.WorkspaceSvg}
*/
Code.workspace = null;
Code.loadBlocks = function (defaultXml) {
try {
var loadOnce = window.sessionStorage.loadOnceBlocks;
} catch (e) {
// Firefox sometimes throws a SecurityError when accessing sessionStorage.
// Restarting Firefox fixes this, so it looks like a bug.
var loadOnce = null;
}
if ('BlocklyStorage' in window && window.location.hash.length > 1) {
// An href with #key trigers an AJAX call to retrieve saved blocks.
BlocklyStorage.retrieveXml(window.location.hash.substring(1));
} else if (loadOnce) {
// Language switching stores the blocks during the reload.
delete window.sessionStorage.loadOnceBlocks;
var xml = Blockly.Xml.textToDom(loadOnce);
Blockly.Xml.domToWorkspace(xml, Code.workspace);
} else if (defaultXml) {
// Load the editor with default starting blocks.
var xml = Blockly.Xml.textToDom(defaultXml);
Blockly.Xml.domToWorkspace(xml, Code.workspace);
} else if ('BlocklyStorage' in window) {
// Restore saved blocks in a separate thread so that subsequent
// initialization is not affected from a failed load.
window.setTimeout(BlocklyStorage.restoreBlocks, 0);
}
};
Code.bindClick = function (el, func) {
if (typeof el == 'string') {
el = document.getElementById(el);
}
el.addEventListener('click', func, true);
el.addEventListener('touchend', func, true);
};
Code.getBBox_ = function (element) {
var height = element.offsetHeight;
var width = element.offsetWidth;
var x = 0;
var y = 0;
do {
x += element.offsetLeft;
y += element.offsetTop;
element = element.offsetParent;
} while (element);
return {
height: height,
width: width,
x: x,
y: y
};
};
Code.TABS_ = ['blocks', 'python', 'xml'];
Code.TABS_DISPLAY_ = [
'Blocks', 'Python', 'XML',
];
Code.selected = 'blocks';
Code.tabClick = function (clickedName) {
// If the XML tab was open, save and render the content.
if (document.getElementById('tab_xml').classList.contains('tabon')) {
var xmlTextarea = document.getElementById('content_xml');
var xmlText = xmlTextarea.value;
var xmlDom = null;
try {
xmlDom = Blockly.Xml.textToDom(xmlText);
} catch (e) {
var badXml = "XML のエラーです:\n%1\n\nXML の変更をやめるには「OK」、編集を続けるには「キャンセル」を選んでください。"
var q =
window.confirm(badXml.replace('%1', e));
if (!q) {
// Leave the user on the XML tab.
return;
}
}
if (xmlDom) {
Code.workspace.clear();
Blockly.Xml.domToWorkspace(xmlDom, Code.workspace);
}
}
// Deselect all tabs and hide all panes.
for (var i = 0; i < Code.TABS_.length; i++) {
var name = Code.TABS_[i];
var tab = document.getElementById('tab_' + name);
tab.classList.add('taboff');
tab.classList.remove('tabon');
document.getElementById('content_' + name).style.visibility = 'hidden';
}
// Select the active tab.
Code.selected = clickedName;
var selectedTab = document.getElementById('tab_' + clickedName);
selectedTab.classList.remove('taboff');
selectedTab.classList.add('tabon');
// Show the selected pane.
document.getElementById('content_' + clickedName).style.visibility =
'visible';
Code.renderContent();
Blockly.svgResize(Code.workspace);
};
Code.renderContent = function () {
var content = document.getElementById('content_' + Code.selected);
var saveButton = document.getElementById('saveButton');
var loadButton = document.getElementById('loadButton');
var writeButton = document.getElementById('writeButton');
saveButton.style.display = "none";
loadButton.style.display = "none";
writeButton.style.display = "none";
// Initialize the pane.
if (content.id == 'content_xml') {
saveButton.style.display = "";
loadButton.style.display = "";
var xmlTextarea = document.getElementById('content_xml');
var xmlDom = Blockly.Xml.workspaceToDom(Code.workspace);
var xmlText = Blockly.Xml.domToPrettyText(xmlDom);
xmlTextarea.value = xmlText;
xmlTextarea.focus();
} else if (content.id == 'content_python') {
writeButton.style.display = "";
Code.attemptCodeGeneration(Blockly.Python);
}
else {
}
};
/**
* Attempt to generate the code and display it in the UI, pretty printed.
* @param generator {!Blockly.Generator} The generator to use.
*/
Code.attemptCodeGeneration = function (generator) {
var content = document.getElementById('content_' + Code.selected);
content.textContent = '';
if (Code.checkAllGeneratorFunctionsDefined(generator)) {
var code = generator.workspaceToCode(Code.workspace);
content.textContent = code;
}
};
/**
* Check whether all blocks in use have generator functions.
* @param generator {!Blockly.Generator} The generator to use.
*/
Code.checkAllGeneratorFunctionsDefined = function (generator) {
var blocks = Code.workspace.getAllBlocks(false);
var missingBlockGenerators = [];
for (var i = 0; i < blocks.length; i++) {
var blockType = blocks[i].type;
if (!generator[blockType]) {
if (missingBlockGenerators.indexOf(blockType) == -1) {
missingBlockGenerators.push(blockType);
}
}
}
var valid = missingBlockGenerators.length == 0;
if (!valid) {
var msg = 'The generator code for the following blocks not specified for ' +
generator.name_ + ':\n - ' + missingBlockGenerators.join('\n - ');
Blockly.alert(msg); // Assuming synchronous. No callback.
}
return valid;
};
var onClickWrite = function () {
var pythonArea = document.getElementById('content_python');
const blob = new Blob([pythonArea.textContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.download = 'sample.py';
a.href = url;
a.click();
a.remove();
URL.revokeObjectURL(url);
}
Code.init = function () {
var container = document.getElementById('content_area');
var onresize = function (e) {
var bBox = Code.getBBox_(container);
for (var i = 0; i < Code.TABS_.length; i++) {
var el = document.getElementById('content_' + Code.TABS_[i]);
el.style.top = bBox.y + 'px';
el.style.left = bBox.x + 'px';
// Height and width need to be set, read back, then set again to
// compensate for scrollbars.
el.style.height = bBox.height + 'px';
el.style.height = (2 * bBox.height - el.offsetHeight) + 'px';
el.style.width = bBox.width + 'px';
el.style.width = (2 * bBox.width - el.offsetWidth) + 'px';
}
// Make the 'Blocks' tab line up with the toolbox.
if (Code.workspace && Code.workspace.getToolbox().width) {
document.getElementById('tab_blocks').style.minWidth =
(Code.workspace.getToolbox().width - 38) + 'px';
// Account for the 19 pixel margin and on each side.
}
};
window.addEventListener('resize', onresize, false);
var writeButton = document.getElementById('writeButton');
writeButton.addEventListener('click', onClickWrite, false);
var toolbox = document.getElementById("toolbox");
var options = {
toolbox: toolbox,
// scrollbars: true,
grid:
{
spacing: 25,
length: 3,
colour: '#ccc',
snap: true
},
media: '../Blockly/media/',
zoom:
{
controls: true,
wheel: true
}
};
Code.workspace = Blockly.inject('content_blocks', options);
Code.loadBlocks('');
Code.tabClick(Code.selected);
Code.bindClick('trashButton',
function () { Code.discard(); Code.renderContent(); });
for (var i = 0; i < Code.TABS_.length; i++) {
var name = Code.TABS_[i];
Code.bindClick('tab_' + name,
function (name_) { return function () { Code.tabClick(name_); }; }(name));
}
onresize();
Blockly.svgResize(Code.workspace);
}
/**
* Discard all blocks from the workspace.
*/
Code.discard = function () {
var count = Code.workspace.getAllBlocks(false).length;
if (count < 2 ||
window.confirm(Blockly.Msg['DELETE_ALL_BLOCKS'].replace('%1', count))) {
Code.workspace.clear();
if (window.location.hash) {
window.location.hash = '';
}
}
};
Promise.all(
["toolbox.xml"].map(async file => {
return fetch(file).then(
(res) => {
return res.text();
}
);
})
).then((xmls) => {
xmls.forEach((xml) => {
var parser = new DOMParser();
var doc = parser.parseFromString(xml, "application/xml");
document.body.appendChild(doc.documentElement);
});
}).then(() => {
Code.init();
});
作成したPythonコードを保存するところまで実装できました。
XMLのSave/Loadを実装することで、ブロックの保存もできるようになると思います。