/** * @license * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview JavaScript for Blockly's Block Factory application through * which users can build blocks using a visual interface and dynamically * generate a preview block and starter code for the block (block definition and * generator stub. Uses the Block Factory namespace. Depends on the FactoryUtils * for its code generation functions. * */ 'use strict'; /** * Namespace for Block Factory. */ var BlockFactory = BlockFactory || Object.create(null); /** * Workspace for user to build block. * @type {Blockly.Workspace} */ BlockFactory.mainWorkspace = null; /** * Workspace for preview of block. * @type {Blockly.Workspace} */ BlockFactory.previewWorkspace = null; /** * Name of block if not named. * @type string */ BlockFactory.UNNAMED = 'unnamed'; /** * Existing direction ('ltr' vs 'rtl') of preview. * @type string */ BlockFactory.oldDir = null; /** * Flag to signal that an update came from a manual update to the JSON or JavaScript. * definition manually. * @type boolean */ // TODO: Replace global state with parameter passed to functions. BlockFactory.updateBlocksFlag = false; /** * Delayed flag to avoid infinite update after updating the JSON or JavaScript. * definition manually. * @type boolean */ // TODO: Replace global state with parameter passed to functions. BlockFactory.updateBlocksFlagDelayed = false; /** * The starting XML for the Block Factory main workspace. Contains the * unmovable, undeletable factory_base block. */ BlockFactory.STARTER_BLOCK_XML_TEXT = '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '230' + ''; /** * Change the language code format. */ BlockFactory.formatChange = function() { var mask = document.getElementById('blocklyMask'); var languagePre = document.getElementById('languagePre'); var languageTA = document.getElementById('languageTA'); if (document.getElementById('format').value === 'Manual-JSON' || document.getElementById('format').value === 'Manual-JS') { Blockly.common.getMainWorkspace().hideChaff(); mask.style.display = 'block'; languagePre.style.display = 'none'; languageTA.style.display = 'block'; var code = languagePre.textContent.trim(); languageTA.value = code; languageTA.focus(); BlockFactory.updatePreview(); } else { mask.style.display = 'none'; languageTA.style.display = 'none'; languagePre.style.display = 'block'; var code = languagePre.textContent.trim(); languageTA.value = code; BlockFactory.updateLanguage(); } BlockFactory.disableEnableLink(); }; /** * Update the language code based on constructs made in Blockly. */ BlockFactory.updateLanguage = function() { var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); if (!rootBlock) { return; } var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase(); if (!blockType) { blockType = BlockFactory.UNNAMED; } if (!BlockFactory.updateBlocksFlag) { var format = document.getElementById('format').value; if (format === 'Manual-JSON') { format = 'JSON'; } else if (format === 'Manual-JS') { format = 'JavaScript'; } var code = FactoryUtils.getBlockDefinition(blockType, rootBlock, format, BlockFactory.mainWorkspace); FactoryUtils.injectCode(code, 'languagePre'); if (!BlockFactory.updateBlocksFlagDelayed) { var languagePre = document.getElementById('languagePre'); var languageTA = document.getElementById('languageTA'); code = languagePre.innerText.trim(); languageTA.value = code; } } BlockFactory.updatePreview(); }; /** * Update the generator code. * @param {!Blockly.Block} block Rendered block in preview workspace. */ BlockFactory.updateGenerator = function(block) { var language = document.getElementById('language').value; var generatorStub = FactoryUtils.getGeneratorStub(block, language); FactoryUtils.injectCode(generatorStub, 'generatorPre'); }; /** * Update the preview display. */ BlockFactory.updatePreview = function() { // Toggle between LTR/RTL if needed (also used in first display). var newDir = document.getElementById('direction').value; if (BlockFactory.oldDir !== newDir) { if (BlockFactory.previewWorkspace) { BlockFactory.previewWorkspace.dispose(); } var rtl = newDir === 'rtl'; BlockFactory.previewWorkspace = Blockly.inject('preview', {rtl: rtl, media: '../../media/', scrollbars: true}); BlockFactory.oldDir = newDir; } BlockFactory.previewWorkspace.clear(); var format = BlockFactory.getBlockDefinitionFormat(); var code = document.getElementById('languageTA').value; if (!code.trim()) { // Nothing to render. Happens while cloud storage is loading. return; } // Backup Blockly.Blocks definitions so we can delete them all // before instantiating user-defined block. This avoids a collision // between the main workspace and preview if the user creates a // 'factory_base' block, for instance. var originalBlocks = Object.assign(Object.create(null), Blockly.Blocks); try { // Delete existing blocks. for (var key in Blockly.Blocks) { delete Blockly.Blocks[key]; } if (format === 'JSON') { var json = JSON.parse(code); Blockly.Blocks[json.type || BlockFactory.UNNAMED] = { init: function() { this.jsonInit(json); } }; } else if (format === 'JavaScript') { try { eval(code); } catch (e) { // TODO: Display error in the UI console.error("Error while evaluating JavaScript formatted block definition", e); return; } } // Look for newly-created block(s) (ideally just one). var createdTypes = Object.getOwnPropertyNames(Blockly.Blocks); if (createdTypes.length < 1) { return; } else if (createdTypes.length > 1) { console.log('Unexpectedly found more than one block definition'); } var blockType = createdTypes[0]; // Create the preview block. var previewBlock = BlockFactory.previewWorkspace.newBlock(blockType); previewBlock.initSvg(); previewBlock.render(); previewBlock.setMovable(false); previewBlock.setDeletable(false); previewBlock.moveBy(15, 10); BlockFactory.previewWorkspace.clearUndo(); BlockFactory.updateGenerator(previewBlock); // Warn user only if their block type is already exists in Blockly's // standard library. var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); if (StandardCategories.coreBlockTypes.indexOf(blockType) !== -1) { rootBlock.setWarningText('A core Blockly block already exists ' + 'under this name.'); } else if (blockType === 'block_type') { // Warn user to let them know they can't save a block under the default // name 'block_type' rootBlock.setWarningText('You cannot save a block with the default ' + 'name, "block_type"'); } else { rootBlock.setWarningText(null); } } catch(err) { // TODO: Show error on the UI console.log(err); BlockFactory.updateBlocksFlag = false BlockFactory.updateBlocksFlagDelayed = false } finally { // Remove all newly-created block(s). for (var key in Blockly.Blocks) { delete Blockly.Blocks[key]; } // Restore original blocks. Object.assign(Blockly.Blocks, originalBlocks); } }; /** * Gets the format from the Block Definitions' format selector/drop-down. * @return Either 'JavaScript' or 'JSON'. * @throws If selector value is not recognized. */ BlockFactory.getBlockDefinitionFormat = function() { switch (document.getElementById('format').value) { case 'JSON': case 'Manual-JSON': return 'JSON'; case 'JavaScript': case 'Manual-JS': return 'JavaScript'; default: throw 'Unknown format: ' + format; } } /** * Disable link and save buttons if the format is 'Manual', enable otherwise. */ BlockFactory.disableEnableLink = function() { var linkButton = document.getElementById('linkButton'); var saveBlockButton = document.getElementById('localSaveButton'); var saveToLibButton = document.getElementById('saveToBlockLibraryButton'); var disabled = document.getElementById('format').value.substr(0, 6) === 'Manual'; linkButton.disabled = disabled; saveBlockButton.disabled = disabled; saveToLibButton.disabled = disabled; }; /** * Render starter block (factory_base). */ BlockFactory.showStarterBlock = function() { BlockFactory.mainWorkspace.clear(); var xml = Blockly.Xml.textToDom(BlockFactory.STARTER_BLOCK_XML_TEXT); Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace); }; /** * Returns whether or not the current block open is the starter block. */ BlockFactory.isStarterBlock = function() { var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); return rootBlock && !( // The starter block does not have blocks nested into the factory_base block. rootBlock.getChildren().length > 0 || // The starter block's name is the default, 'block_type'. rootBlock.getFieldValue('NAME').trim().toLowerCase() !== 'block_type' || // The starter block has no connections. rootBlock.getFieldValue('CONNECTIONS') !== 'NONE' || // The starter block has automatic inputs. rootBlock.getFieldValue('INLINE') !== 'AUTO' ); }; /** * Updates blocks from the manually edited js or json from their text area. */ BlockFactory.manualEdit = function() { // TODO(#1267): Replace these global state flags with parameters passed to // the right functions. BlockFactory.updateBlocksFlag = true; BlockFactory.updateBlocksFlagDelayed = true; BlockFactory.updateLanguage(); }