diff --git a/accessible/README b/accessible/README index d133fa828..37ea67879 100644 --- a/accessible/README +++ b/accessible/README @@ -25,15 +25,16 @@ the main component to be loaded. This will usually be blocklyApp.AppView, but if you have another component that wraps it, use that one instead. -Customizing the Toolbar ------------------------ +Customizing the Toolbar and Audio +--------------------------------- The Accessible Blockly workspace comes with a customizable toolbar. To customize the toolbar, you will need to declare an ACCESSIBLE_GLOBALS object in the global scope that looks like this: var ACCESSIBLE_GLOBALS = { - toolbarButtonConfig: [] + toolbarButtonConfig: [], + mediaPathPrefix: null }; The value corresponding to 'toolbarButtonConfig' can be modified by adding @@ -43,6 +44,9 @@ two keys: - 'text' (the text to display on the button) - 'action' (the function that gets run when the button is clicked) +In addition, if you want audio to be played, set mediaPathPrefix to the +location of the accessible/media folder. + Limitations ----------- diff --git a/accessible/app.component.js b/accessible/app.component.js index f220af98b..063a5ea50 100644 --- a/accessible/app.component.js +++ b/accessible/app.component.js @@ -64,7 +64,8 @@ blocklyApp.AppView = ng.core // https://www.sitepoint.com/angular-2-components-providers-classes-factories-values/ providers: [ blocklyApp.ClipboardService, blocklyApp.NotificationsService, - blocklyApp.TreeService, blocklyApp.UtilsService] + blocklyApp.TreeService, blocklyApp.UtilsService, + blocklyApp.AudioService] }) .Class({ constructor: [blocklyApp.NotificationsService, function(_notificationsService) { diff --git a/accessible/audio.service.js b/accessible/audio.service.js new file mode 100644 index 000000000..c358c083b --- /dev/null +++ b/accessible/audio.service.js @@ -0,0 +1,57 @@ +/** + * AccessibleBlockly + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Angular2 Service that plays audio files. + * @author sll@google.com (Sean Lip) + */ + +blocklyApp.AudioService = ng.core + .Class({ + constructor: [function() { + // We do not play any audio unless a media path prefix is specified. + this.canPlayAudio = false; + if (ACCESSIBLE_GLOBALS.hasOwnProperty('mediaPathPrefix')) { + this.canPlayAudio = true; + var mediaPathPrefix = ACCESSIBLE_GLOBALS['mediaPathPrefix']; + this.AUDIO_PATHS_ = { + 'connect': mediaPathPrefix + 'click.mp3', + 'delete': mediaPathPrefix + 'delete.mp3' + }; + } + + // TODO(sll): Add ogg and mp3 fallbacks. + this.cachedAudioFiles_ = {}; + }], + play_: function(audioId) { + if (this.canPlayAudio) { + if (!this.cachedAudioFiles_.hasOwnProperty(audioId)) { + this.cachedAudioFiles_[audioId] = new Audio( + this.AUDIO_PATHS_[audioId]); + } + this.cachedAudioFiles_[audioId].play(); + } + }, + playConnectSound: function() { + this.play_('connect'); + }, + playDeleteSound: function() { + this.play_('delete'); + } + }); diff --git a/accessible/clipboard.service.js b/accessible/clipboard.service.js index 84b0b4f52..05b0afe63 100644 --- a/accessible/clipboard.service.js +++ b/accessible/clipboard.service.js @@ -26,7 +26,8 @@ blocklyApp.ClipboardService = ng.core .Class({ constructor: [ blocklyApp.NotificationsService, blocklyApp.UtilsService, - function(_notificationsService, _utilsService) { + blocklyApp.AudioService, + function(_notificationsService, _utilsService, _audioService) { this.clipboardBlockXml_ = null; this.clipboardBlockPreviousConnection_ = null; this.clipboardBlockNextConnection_ = null; @@ -34,6 +35,7 @@ blocklyApp.ClipboardService = ng.core this.markedConnection_ = null; this.notificationsService = _notificationsService; this.utilsService = _utilsService; + this.audioService = _audioService; }], areConnectionsCompatible_: function(blockConnection, connection) { // Check that both connections exist, that it's the right kind of @@ -130,6 +132,7 @@ blocklyApp.ClipboardService = ng.core default: connection.connect(reconstitutedBlock.outputConnection); } + this.audioService.playConnectSound(); this.notificationsService.setStatusMessage( this.utilsService.getBlockDescription(reconstitutedBlock) + ' ' + Blockly.Msg.PASTED_BLOCK_FROM_CLIPBOARD_MSG); @@ -151,6 +154,7 @@ blocklyApp.ClipboardService = ng.core if (this.areConnectionsCompatible_( this.markedConnection_, potentialConnections[i])) { this.markedConnection_.connect(potentialConnections[i]); + this.audioService.playConnectSound(); connectionSuccessful = true; break; } diff --git a/accessible/field.component.js b/accessible/field.component.js index 93a058857..7affe7e53 100644 --- a/accessible/field.component.js +++ b/accessible/field.component.js @@ -30,11 +30,13 @@ blocklyApp.FieldComponent = ng.core template: ` + [attr.aria-label]="disabled ? 'Disabled text field' : 'Press Enter to edit text'" + tabindex="-1"> + [attr.aria-label]="disabled ? 'Disabled number field' : 'Press Enter to edit number'" + tabindex="-1">
@@ -43,7 +45,7 @@ blocklyApp.FieldComponent = ng.core
  • diff --git a/media/accessible.css b/accessible/media/accessible.css similarity index 100% rename from media/accessible.css rename to accessible/media/accessible.css diff --git a/accessible/media/click.mp3 b/accessible/media/click.mp3 new file mode 100644 index 000000000..4534b0ddc Binary files /dev/null and b/accessible/media/click.mp3 differ diff --git a/accessible/media/click.ogg b/accessible/media/click.ogg new file mode 100644 index 000000000..e8ae42a61 Binary files /dev/null and b/accessible/media/click.ogg differ diff --git a/accessible/media/click.wav b/accessible/media/click.wav new file mode 100644 index 000000000..41a50cd76 Binary files /dev/null and b/accessible/media/click.wav differ diff --git a/accessible/media/delete.mp3 b/accessible/media/delete.mp3 new file mode 100644 index 000000000..442bd9c1f Binary files /dev/null and b/accessible/media/delete.mp3 differ diff --git a/accessible/media/delete.ogg b/accessible/media/delete.ogg new file mode 100644 index 000000000..67f84ac19 Binary files /dev/null and b/accessible/media/delete.ogg differ diff --git a/accessible/media/delete.wav b/accessible/media/delete.wav new file mode 100644 index 000000000..18debcf96 Binary files /dev/null and b/accessible/media/delete.wav differ diff --git a/accessible/toolbox-tree.component.js b/accessible/toolbox-tree.component.js index 0bd31ac8f..e5bbb75be 100644 --- a/accessible/toolbox-tree.component.js +++ b/accessible/toolbox-tree.component.js @@ -37,14 +37,14 @@ blocklyApp.ToolboxTreeComponent = ng.core
  • -
  • -
  • @@ -52,7 +52,7 @@ blocklyApp.ToolboxTreeComponent = ng.core [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['sendToSelectedButton'], 'blockly-button', !canBeCopiedToMarkedConnection())" [attr.aria-level]="level + 2"> diff --git a/accessible/toolbox.component.js b/accessible/toolbox.component.js index a20d6c152..a7d5acd93 100644 --- a/accessible/toolbox.component.js +++ b/accessible/toolbox.component.js @@ -30,7 +30,7 @@ blocklyApp.ToolboxComponent = ng.core

    Toolbox

      diff --git a/accessible/tree.service.js b/accessible/tree.service.js index ec175649c..0c463e191 100644 --- a/accessible/tree.service.js +++ b/accessible/tree.service.js @@ -82,21 +82,19 @@ blocklyApp.TreeService = ng.core } return null; }, - focusOnNextTree_: function(treeId) { + getIdOfNextTree_: function(treeId) { var trees = this.getAllTreeNodes_(); for (var i = 0; i < trees.length - 1; i++) { if (trees[i].id == treeId) { - trees[i + 1].focus(); return trees[i + 1].id; } } return null; }, - focusOnPreviousTree_: function(treeId) { + getIdOfPreviousTree_: function(treeId) { var trees = this.getAllTreeNodes_(); for (var i = trees.length - 1; i > 0; i--) { if (trees[i].id == treeId) { - trees[i - 1].focus(); return trees[i - 1].id; } } @@ -190,12 +188,11 @@ blocklyApp.TreeService = ng.core if (e.keyCode == 9) { // Tab key. var destinationTreeId = - e.shiftKey ? this.focusOnPreviousTree_(treeId) : - this.focusOnNextTree_(treeId); - this.notifyUserAboutCurrentTree_(destinationTreeId); - - e.preventDefault(); - e.stopPropagation(); + e.shiftKey ? this.getIdOfPreviousTree_(treeId) : + this.getIdOfNextTree_(treeId); + if (destinationTreeId) { + this.notifyUserAboutCurrentTree_(destinationTreeId); + } } }, isButtonOrFieldNode_: function(node) { @@ -260,16 +257,20 @@ blocklyApp.TreeService = ng.core // For Esc and Tab keys, the focus is removed from the input field. this.focusOnCurrentTree_(treeId); - // In addition, for Tab keys, the user tabs to the previous/next tree. if (e.keyCode == 9) { var destinationTreeId = - e.shiftKey ? this.focusOnPreviousTree_(treeId) : - this.focusOnNextTree_(treeId); - this.notifyUserAboutCurrentTree_(destinationTreeId); + e.shiftKey ? this.getIdOfPreviousTree_(treeId) : + this.getIdOfNextTree_(treeId); + if (destinationTreeId) { + this.notifyUserAboutCurrentTree_(destinationTreeId); + } } - e.preventDefault(); - e.stopPropagation(); + // Allow Tab keypresses to go through. + if (e.keyCode == 27) { + e.preventDefault(); + e.stopPropagation(); + } } } else { // Outside an input field, Enter, Tab and navigation keys are all @@ -302,14 +303,14 @@ blocklyApp.TreeService = ng.core } } } else if (e.keyCode == 9) { - // Tab key. + // Tab key. Note that allowing the event to propagate through is + // intentional. var destinationTreeId = - e.shiftKey ? this.focusOnPreviousTree_(treeId) : - this.focusOnNextTree_(treeId); - this.notifyUserAboutCurrentTree_(destinationTreeId); - - e.preventDefault(); - e.stopPropagation(); + e.shiftKey ? this.getIdOfPreviousTree_(treeId) : + this.getIdOfNextTree_(treeId); + if (destinationTreeId) { + this.notifyUserAboutCurrentTree_(destinationTreeId); + } } else if (e.keyCode >= 35 && e.keyCode <= 40) { // End, home, and arrow keys. if (e.keyCode == 35) { diff --git a/accessible/workspace-tree.component.js b/accessible/workspace-tree.component.js index 8d7d02e7c..b024101d6 100644 --- a/accessible/workspace-tree.component.js +++ b/accessible/workspace-tree.component.js @@ -60,7 +60,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core [attr.aria-level]="level + 2"> @@ -78,7 +78,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[buttonInfo.baseIdKey + 'Button'], 'blockly-button', buttonInfo.isDisabled())" [attr.aria-level]="level + 2"> @@ -102,13 +102,15 @@ blocklyApp.WorkspaceTreeComponent = ng.core constructor: [ blocklyApp.ClipboardService, blocklyApp.NotificationsService, blocklyApp.TreeService, blocklyApp.UtilsService, + blocklyApp.AudioService, function( _clipboardService, _notificationsService, _treeService, - _utilsService) { + _utilsService, _audioService) { this.clipboardService = _clipboardService; this.notificationsService = _notificationsService; this.treeService = _treeService; this.utilsService = _utilsService; + this.audioService = _audioService; }], getBlockDescription: function() { return this.utilsService.getBlockDescription(this.block); @@ -172,6 +174,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core var that = this; this.removeBlockAndSetFocus_(this.block, function() { that.block.dispose(true); + that.audioService.playDeleteSound(); }); setTimeout(function() { diff --git a/accessible/workspace.component.js b/accessible/workspace.component.js index a2c940ec0..255735020 100644 --- a/accessible/workspace.component.js +++ b/accessible/workspace.component.js @@ -47,7 +47,7 @@ blocklyApp.WorkspaceComponent = ng.core
        diff --git a/core/flyout.js b/core/flyout.js index c8bc49b63..3a523251c 100644 --- a/core/flyout.js +++ b/core/flyout.js @@ -657,8 +657,21 @@ Blockly.Flyout.prototype.show = function(xmlList) { contents.push({type: 'block', block: curBlock}); var gap = parseInt(xml.getAttribute('gap'), 10); gaps.push(isNaN(gap) ? this.MARGIN * 3 : gap); - } - else if (tagName == 'BUTTON') { + } else if (xml.tagName.toUpperCase() == 'SEP') { + // Change the gap between two blocks. + // + // The default gap is 24, can be set larger or smaller. + // This overwrites the gap attribute on the previous block. + // Note that a deprecated method is to add a gap to a block. + // + var newGap = parseInt(xml.getAttribute('gap'), 10); + // Ignore gaps before the first block. + if (!isNaN(newGap) && gaps.length > 0) { + gaps[gaps.length - 1] = newGap; + } else { + gaps.push(this.MARGIN * 3); + } + } else if (tagName == 'BUTTON') { var label = xml.getAttribute('text'); var curButton = new Blockly.FlyoutButton(this.workspace_, this.targetWorkspace_, label); diff --git a/core/toolbox.js b/core/toolbox.js index 44661f532..eab72b30e 100644 --- a/core/toolbox.js +++ b/core/toolbox.js @@ -333,10 +333,8 @@ Blockly.Toolbox.prototype.syncTrees_ = function(treeIn, treeOut, pathToMedia) { // Note that a deprecated method is to add a gap to a block. // var newGap = parseFloat(childIn.getAttribute('gap')); - if (!isNaN(newGap)) { - var oldGap = parseFloat(lastElement.getAttribute('gap')); - var gap = isNaN(oldGap) ? newGap : oldGap + newGap; - lastElement.setAttribute('gap', gap); + if (!isNaN(newGap) && lastElement) { + lastElement.setAttribute('gap', newGap); } } } diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 897dd12bf..15ba8433f 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -278,8 +278,9 @@ Blockly.WorkspaceSvg.prototype.dispose = function() { this.zoomControls_ = null; } if (!this.options.parentWorkspace) { - // Top-most workspace. Dispose of the SVG too. - goog.dom.removeNode(this.getParentSvg()); + // Top-most workspace. Dispose of the div that the + // svg is injected into (i.e. injectionDiv). + goog.dom.removeNode(this.getParentSvg().parentNode); } if (this.resizeHandlerWrapper_) { Blockly.unbindEvent_(this.resizeHandlerWrapper_); diff --git a/demos/accessible/index.html b/demos/accessible/index.html index 14bc2c952..af424764d 100644 --- a/demos/accessible/index.html +++ b/demos/accessible/index.html @@ -19,6 +19,7 @@ + @@ -31,7 +32,7 @@ - + -

        Blockly > - Demos > Accessible Blockly

        +

        + Blockly > + Demos > Accessible Blockly +

        This is a simple demo of a version of Blockly designed for screen readers.

        @@ -70,7 +73,9 @@ var ACCESSIBLE_GLOBALS = { // Additional buttons for the workspace toolbar that // go before the "Clear Workspace" button. - toolbarButtonConfig: [] + toolbarButtonConfig: [], + // Prefix of path to sound files. + mediaPathPrefix: '../../accessible/media/' }; document.addEventListener('DOMContentLoaded', function() { ng.platform.browser.bootstrap(blocklyApp.AppView); diff --git a/demos/blocklyfactory/app_controller.js b/demos/blocklyfactory/app_controller.js new file mode 100644 index 000000000..43f66bba6 --- /dev/null +++ b/demos/blocklyfactory/app_controller.js @@ -0,0 +1,485 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview The AppController Class brings together the Block + * Factory, Block Library, and Block Exporter functionality into a single web + * app. + * + * @author quachtina96 (Tina Quach) + */ +goog.provide('AppController'); + +goog.require('BlockFactory'); +goog.require('BlockLibraryController'); +goog.require('BlockExporterController'); +goog.require('goog.dom.classlist'); +goog.require('goog.string'); + +/** + * Controller for the Blockly Factory + * @constructor + */ +AppController = function() { + // Initialize Block Library + this.blockLibraryName = 'blockLibrary'; + this.blockLibraryController = + new BlockLibraryController(this.blockLibraryName); + this.blockLibraryController.populateBlockLibrary(); + + // Initialize Block Exporter + this.exporter = + new BlockExporterController(this.blockLibraryController.storage); + + // Map of tab type to the div element for the tab. + this.tabMap = { + 'BLOCK_FACTORY' : goog.dom.getElement('blockFactory_tab'), + 'WORKSPACE_FACTORY': goog.dom.getElement('workspaceFactory_tab'), + 'EXPORTER' : goog.dom.getElement('blocklibraryExporter_tab') + }; + + // Selected tab. + this.selectedTab = 'BLOCK_FACTORY'; +}; + +/** + * Tied to the 'Import Block Library' button. Imports block library from file to + * Block Factory. Expects user to upload a single file of JSON mapping each + * block type to its xml text representation. + */ +AppController.prototype.importBlockLibraryFromFile = function() { + var self = this; + var files = document.getElementById('files'); + // If the file list is empty, the user likely canceled in the dialog. + if (files.files.length > 0) { + // The input tag doesn't have the "multiple" attribute + // so the user can only choose 1 file. + var file = files.files[0]; + var fileReader = new FileReader(); + + // Create a map of block type to xml text from the file when it has been + // read. + fileReader.addEventListener('load', function(event) { + var fileContents = event.target.result; + // Create empty object to hold the read block library information. + var blockXmlTextMap = Object.create(null); + try { + // Parse the file to get map of block type to xml text. + blockXmlTextMap = self.formatBlockLibForImport_(fileContents); + } catch (e) { + var message = 'Could not load your block library file.\n' + window.alert(message + '\nFile Name: ' + file.name); + return; + } + + // Create a new block library storage object with inputted block library. + var blockLibStorage = new BlockLibraryStorage( + self.blockLibraryName, blockXmlTextMap); + + // Update block library controller with the new block library + // storage. + self.blockLibraryController.setBlockLibStorage(blockLibStorage); + // Update the block library dropdown. + self.blockLibraryController.populateBlockLibrary(); + // Update the exporter's block library storage. + self.exporter.setBlockLibStorage(blockLibStorage); + }); + // Read the file. + fileReader.readAsText(file); + } +}; + +/** + * Tied to the 'Export Block Library' button. Exports block library to file that + * contains JSON mapping each block type to its xml text representation. + */ +AppController.prototype.exportBlockLibraryToFile = function() { + // Get map of block type to xml. + var blockLib = this.blockLibraryController.getBlockLibrary(); + // Concatenate the xmls, each separated by a blank line. + var blockLibText = this.formatBlockLibForExport_(blockLib); + // Get file name. + var filename = prompt('Enter the file name under which to save your block' + + 'library.'); + // Download file if all necessary parameters are provided. + if (filename) { + BlockFactory.createAndDownloadFile_(blockLibText, filename, 'xml'); + } else { + alert('Could not export Block Library without file name under which to ' + + 'save library.'); + } +}; + +/** + * Converts an object mapping block type to xml to text file for output. + * @private + * + * @param {!Object} blockXmlMap - object mapping block type to xml + * @return {string} String of each block's xml separated by a new line. + */ +AppController.prototype.formatBlockLibForExport_ = function(blockXmlMap) { + var blockXmls = []; + for (var blockType in blockXmlMap) { + blockXmls.push(blockXmlMap[blockType]); + } + return blockXmls.join("\n\n"); +}; + +/** + * Converts imported block library to an object mapping block type to block xml. + * @private + * + * @param {string} xmlText - String containing each block's xml optionally + * separated by whitespace. + * @return {!Object} object mapping block type to xml text. + */ +AppController.prototype.formatBlockLibForImport_ = function(xmlText) { + // Get array of xmls. + var xmlText = goog.string.collapseWhitespace(xmlText); + var blockXmls = goog.string.splitLimit(xmlText, '', 500); + + // Create and populate map. + var blockXmlTextMap = Object.create(null); + // The line above is equivalent of {} except that this object is TRULY + // empty. It doesn't have built-in attributes/functions such as length or + // toString. + for (var i = 0, xml; xml = blockXmls[i]; i++) { + var blockType = this.getBlockTypeFromXml_(xml); + blockXmlTextMap[blockType] = xml; + } + + return blockXmlTextMap; +}; + +/** + * Extracts out block type from xml text, the kind that is saved in block + * library storage. + * @private + * + * @param {!string} xmlText - A block's xml text. + * @return {string} The block type that corresponds to the provided xml text. + */ +AppController.prototype.getBlockTypeFromXml_ = function(xmlText) { + var xmlText = Blockly.Options.parseToolboxTree(xmlText); + // Find factory base block. + var factoryBaseBlockXml = xmlText.getElementsByTagName('block')[0]; + // Get field elements from factory base. + var fields = factoryBaseBlockXml.getElementsByTagName('field'); + for (var i = 0; i < fields.length; i++) { + // The field whose name is 'NAME' holds the block type as its value. + if (fields[i].getAttribute('name') == 'NAME') { + return fields[i].childNodes[0].nodeValue; + } + } +}; + +/** + * Updates the Block Factory tab to show selected block when user selects a + * different block in the block library dropdown. Tied to block library dropdown + * in index.html. + * + * @param {!Element} blockLibraryDropdown - HTML select element from which the + * user selects a block to work on. + */ +AppController.prototype.onSelectedBlockChanged = function(blockLibraryDropdown) { + // Get selected block type. + var blockType = this.blockLibraryController.getSelectedBlockType( + blockLibraryDropdown); + // Update Block Factory page by showing the selected block. + this.blockLibraryController.openBlock(blockType); +}; + +/** + * Add click handlers to each tab to allow switching between the Block Factory, + * Workspace Factory, and Block Exporter tab. + * + * @param {!Object} tabMap - Map of tab name to div element that is the tab. + */ +AppController.prototype.addTabHandlers = function(tabMap) { + var self = this; + for (var tabName in tabMap) { + var tab = tabMap[tabName]; + // Use an additional closure to correctly assign the tab callback. + tab.addEventListener('click', self.makeTabClickHandler_(tabName)); + } +}; + +/** + * Set the selected tab. + * @private + * + * @param {string} tabName 'BLOCK_FACTORY', 'WORKSPACE_FACTORY', or 'EXPORTER' + */ +AppController.prototype.setSelected_ = function(tabName) { + this.selectedTab = tabName; +}; + +/** + * Creates the tab click handler specific to the tab specified. + * @private + * + * @param {string} tabName 'BLOCK_FACTORY', 'WORKSPACE_FACTORY', or 'EXPORTER' + * @return {Function} The tab click handler. + */ +AppController.prototype.makeTabClickHandler_ = function(tabName) { + var self = this; + return function() { + self.setSelected_(tabName); + self.onTab(); + }; +}; + +/** + * Called on each tab click. Hides and shows specific content based on which tab + * (Block Factory, Workspace Factory, or Exporter) is selected. + */ +AppController.prototype.onTab = function() { + // Get tab div elements. + var blockFactoryTab = this.tabMap['BLOCK_FACTORY']; + var exporterTab = this.tabMap['EXPORTER']; + var workspaceFactoryTab = this.tabMap['WORKSPACE_FACTORY']; + + // Turn selected tab on and other tabs off. + this.styleTabs_(); + + if (this.selectedTab == 'EXPORTER') { + // Update toolbox to reflect current block library. + this.exporter.updateToolbox(); + + // Show container of exporter. + BlockFactory.show('blockLibraryExporter'); + BlockFactory.hide('workspaceFactoryContent'); + + } else if (this.selectedTab == 'BLOCK_FACTORY') { + // Hide container of exporter. + BlockFactory.hide('blockLibraryExporter'); + BlockFactory.hide('workspaceFactoryContent'); + + } else if (this.selectedTab == 'WORKSPACE_FACTORY') { + // Hide container of exporter. + BlockFactory.hide('blockLibraryExporter'); + // Show workspace factory container. + BlockFactory.show('workspaceFactoryContent'); + } + + // Resize to render workspaces' toolboxes correctly for all tabs. + window.dispatchEvent(new Event('resize')); +}; + +/** + * Called on each tab click. Styles the tabs to reflect which tab is selected. + * @private + */ +AppController.prototype.styleTabs_ = function() { + for (var tabName in this.tabMap) { + if (this.selectedTab == tabName) { + goog.dom.classlist.addRemove(this.tabMap[tabName], 'taboff', 'tabon'); + } else { + goog.dom.classlist.addRemove(this.tabMap[tabName], 'tabon', 'taboff'); + } + } +}; + +/** + * Assign button click handlers for the exporter. + */ +AppController.prototype.assignExporterClickHandlers = function() { + var self = this; + // Export blocks when the user submits the export settings. + document.getElementById('exporterSubmitButton').addEventListener('click', + function() { + self.exporter.export(); + }); + document.getElementById('clearSelectedButton').addEventListener('click', + function() { + self.exporter.clearSelectedBlocks(); + }); + document.getElementById('addAllFromLibButton').addEventListener('click', + function() { + self.exporter.addAllBlocksToWorkspace(); + }); +}; + +/** + * Assign button click handlers for the block library. + */ +AppController.prototype.assignLibraryClickHandlers = function() { + var self = this; + // Assign button click handlers for Block Library. + document.getElementById('saveToBlockLibraryButton').addEventListener('click', + function() { + self.blockLibraryController.saveToBlockLibrary(); + }); + + document.getElementById('removeBlockFromLibraryButton').addEventListener( + 'click', + function() { + self.blockLibraryController.removeFromBlockLibrary(); + }); + + document.getElementById('clearBlockLibraryButton').addEventListener('click', + function() { + self.blockLibraryController.clearBlockLibrary(); + }); + + var dropdown = document.getElementById('blockLibraryDropdown'); + dropdown.addEventListener('change', + function() { + self.onSelectedBlockChanged(dropdown); + }); +}; + +/** + * Assign button click handlers for the block factory. + */ +AppController.prototype.assignFactoryClickHandlers = function() { + var self = this; + // Assign button event handlers for Block Factory. + document.getElementById('localSaveButton') + .addEventListener('click', function() { + self.exportBlockLibraryToFile(); + }); + document.getElementById('helpButton').addEventListener('click', + function() { + open('https://developers.google.com/blockly/custom-blocks/block-factory', + 'BlockFactoryHelp'); + }); + document.getElementById('downloadBlocks').addEventListener('click', + function() { + BlockFactory.downloadTextArea('blocks', 'languagePre'); + }); + document.getElementById('downloadGenerator').addEventListener('click', + function() { + BlockFactory.downloadTextArea('generator', 'generatorPre'); + }); + document.getElementById('files').addEventListener('change', + function() { + // Warn user. + var replace = confirm('This imported block library will ' + + 'replace your current block library.'); + if (replace) { + self.importBlockLibraryFromFile(); + // Clear this so that the change event still fires even if the + // same file is chosen again. If the user re-imports a file, we + // want to reload the workspace with its contents. + this.value = null; + } + }); + document.getElementById('createNewBlockButton') + .addEventListener('click', function() { + BlockFactory.mainWorkspace.clear(); + BlockFactory.showStarterBlock(); + BlockLibraryView.selectDefaultOption('blockLibraryDropdown'); + }); +}; + +/** + * Add event listeners for the block factory. + */ +AppController.prototype.addFactoryEventListeners = function() { + BlockFactory.mainWorkspace.addChangeListener(BlockFactory.updateLanguage); + document.getElementById('direction') + .addEventListener('change', BlockFactory.updatePreview); + document.getElementById('languageTA') + .addEventListener('change', BlockFactory.updatePreview); + document.getElementById('languageTA') + .addEventListener('keyup', BlockFactory.updatePreview); + document.getElementById('format') + .addEventListener('change', BlockFactory.formatChange); + document.getElementById('language') + .addEventListener('change', BlockFactory.updatePreview); +}; + +/** + * Handle Blockly Storage with App Engine. + */ +AppController.prototype.initializeBlocklyStorage = function() { + BlocklyStorage.HTTPREQUEST_ERROR = + 'There was a problem with the request.\n'; + BlocklyStorage.LINK_ALERT = + 'Share your blocks with this link:\n\n%1'; + BlocklyStorage.HASH_ERROR = + 'Sorry, "%1" doesn\'t correspond with any saved Blockly file.'; + BlocklyStorage.XML_ERROR = 'Could not load your saved file.\n' + + 'Perhaps it was created with a different version of Blockly?'; + var linkButton = document.getElementById('linkButton'); + linkButton.style.display = 'inline-block'; + linkButton.addEventListener('click', + function() { + BlocklyStorage.link(BlockFactory.mainWorkspace);}); + BlockFactory.disableEnableLink(); +}; +/** + * Initialize Blockly and layout. Called on page load. + */ +AppController.prototype.init = function() { + // Handle Blockly Storage with App Engine + if ('BlocklyStorage' in window) { + this.initializeBlocklyStorage(); + } + + // Assign click handlers. + this.assignExporterClickHandlers(); + this.assignLibraryClickHandlers(); + this.assignFactoryClickHandlers(); + + // Handle resizing of Block Factory elements. + var expandList = [ + document.getElementById('blockly'), + document.getElementById('blocklyMask'), + document.getElementById('preview'), + document.getElementById('languagePre'), + document.getElementById('languageTA'), + document.getElementById('generatorPre') + ]; + + var onresize = function(e) { + for (var i = 0, expand; expand = expandList[i]; i++) { + expand.style.width = (expand.parentNode.offsetWidth - 2) + 'px'; + expand.style.height = (expand.parentNode.offsetHeight - 2) + 'px'; + } + }; + onresize(); + window.addEventListener('resize', onresize); + + // Inject Block Factory Main Workspace. + var toolbox = document.getElementById('toolbox'); + BlockFactory.mainWorkspace = Blockly.inject('blockly', + {collapse: false, + toolbox: toolbox, + media: '../../media/'}); + + // Add tab handlers for switching between Block Factory and Block Exporter. + this.addTabHandlers(this.tabMap); + + this.exporter.addChangeListenersToSelectorWorkspace(); + + // Create the root block on Block Factory main workspace. + if ('BlocklyStorage' in window && window.location.hash.length > 1) { + BlocklyStorage.retrieveXml(window.location.hash.substring(1), + BlockFactory.mainWorkspace); + } else { + BlockFactory.showStarterBlock(); + } + BlockFactory.mainWorkspace.clearUndo(); + + // Add Block Factory event listeners. + this.addFactoryEventListeners(); +}; diff --git a/demos/blocklyfactory/block_exporter_controller.js b/demos/blocklyfactory/block_exporter_controller.js new file mode 100644 index 000000000..10e7601ce --- /dev/null +++ b/demos/blocklyfactory/block_exporter_controller.js @@ -0,0 +1,300 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Javascript for the Block Exporter Controller class. Allows + * users to export block definitions and generator stubs of their saved blocks + * easily using a visual interface. Depends on Block Exporter View and Block + * Exporter Tools classes. Interacts with Export Settings in the index.html. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockExporterController'); +goog.require('BlockExporterView'); +goog.require('BlockExporterTools'); +goog.require('goog.dom.xml'); + +/** + * BlockExporter Controller Class + * @constructor + * + * @param {!BlockLibrary.Storage} blockLibStorage - Block Library Storage. + */ +BlockExporterController = function(blockLibStorage) { + // BlockLibrary.Storage object containing user's saved blocks + this.blockLibStorage = blockLibStorage; + // Utils for generating code to export + this.tools = new BlockExporterTools(); + // View provides the selector workspace and export settings UI. + this.view = new BlockExporterView( + //Xml representation of the toolbox + this.tools.generateToolboxFromLibrary(this.blockLibStorage)); +}; + +/** + * Set the block library storage object from which exporter exports. + * + * @param {!BlockLibraryStorage} blockLibStorage - Block Library Storage object + * that stores the blocks. + */ +BlockExporterController.prototype.setBlockLibStorage = + function(blockLibStorage) { + this.blockLibStorage = blockLibStorage; +}; + +/** + * Get the block library storage object from which exporter exports. + * + * @return {!BlockLibraryStorage} blockLibStorage - Block Library Storage object + * that stores the blocks. + */ +BlockExporterController.prototype.getBlockLibStorage = + function(blockLibStorage) { + return this.blockLibStorage; +}; + +/** + * Get the selected block types. + * @private + * + * @return {!Array.} Types of blocks in workspace. + */ +BlockExporterController.prototype.getSelectedBlockTypes_ = function() { + var selectedBlocks = this.view.getSelectedBlocks(); + var blockTypes = []; + for (var i = 0, block; block = selectedBlocks[i]; i++) { + blockTypes.push(block.type); + } + return blockTypes; +}; + +/** + * Get selected blocks from selector workspace, pulls info from the Export + * Settings form in Block Exporter, and downloads code accordingly. + * + * TODO(quachtina96): allow export as zip. + */ +BlockExporterController.prototype.export = function() { + // Get selected blocks' information. + var blockTypes = this.getSelectedBlockTypes_(); + var blockXmlMap = this.blockLibStorage.getBlockXmlMap(blockTypes); + + // Pull workspace-related settings from the Export Settings form. + var wantToolbox = document.getElementById('toolboxCheck').checked; + var wantPreloadedWorkspace = + document.getElementById('preloadedWorkspaceCheck').checked; + var wantWorkspaceOptions = + document.getElementById('workspaceOptsCheck').checked; + + // Pull block definition(s) settings from the Export Settings form. + var wantBlockDef = document.getElementById('blockDefCheck').checked; + var definitionFormat = document.getElementById('exportFormat').value; + var blockDef_filename = document.getElementById('blockDef_filename').value; + + // Pull block generator stub(s) settings from the Export Settings form. + var wantGenStub = document.getElementById('genStubCheck').checked; + var language = document.getElementById('exportLanguage').value; + var generatorStub_filename = document.getElementById( + 'generatorStub_filename').value; + + if (wantToolbox) { + // TODO(quachtina96): create and download file once wfactory has been + // integrated. + } + + if (wantPreloadedWorkspace) { + // TODO(quachtina96): create and download file once wfactory has been + // integrated. + } + + if (wantWorkspaceOptions) { + // TODO(quachtina96): create and download file once wfactory has been + // integrated. + } + + if (wantBlockDef) { + // User wants to export selected blocks' definitions. + if (!blockDef_filename) { + // User needs to enter filename. + alert('Please enter a filename for your block definition(s) download.'); + } else { + // Get block definition code in the selected format for the blocks. + var blockDefs = this.tools.getBlockDefs(blockXmlMap, + definitionFormat); + // Download the file. + BlockFactory.createAndDownloadFile_( + blockDefs, blockDef_filename, definitionFormat); + } + } + + if (wantGenStub) { + // User wants to export selected blocks' generator stubs. + if (!generatorStub_filename) { + // User needs to enter filename. + alert('Please enter a filename for your generator stub(s) download.'); + } else { + // Get generator stub code in the selected language for the blocks. + var genStubs = this.tools.getGeneratorCode(blockXmlMap, + language); + // Download the file. + BlockFactory.createAndDownloadFile_( + genStubs, generatorStub_filename, language); + } + } + +}; + +/** + * Update the Exporter's toolbox with either the given toolbox xml or toolbox + * xml generated from blocks stored in block library. + * + * @param {Element} opt_toolboxXml - Xml to define toolbox of the selector + * workspace. + */ +BlockExporterController.prototype.updateToolbox = function(opt_toolboxXml) { + // Use given xml or xml generated from updated block library. + var updatedToolbox = opt_toolboxXml || + this.tools.generateToolboxFromLibrary(this.blockLibStorage); + // Update the view's toolbox. + this.view.setToolbox(updatedToolbox); + // Render the toolbox in the selector workspace. + this.view.renderToolbox(updatedToolbox); + // Disable any selected blocks. + var selectedBlocks = this.getSelectedBlockTypes_(); + for (var i = 0, blockType; blockType = selectedBlocks[i]; i++) { + this.setBlockEnabled(blockType, false); + } +}; + +/** + * Enable or Disable block in selector workspace's toolbox. + * + * @param {!string} blockType - Type of block to disable or enable. + * @param {!boolean} enable - True to enable the block, false to disable block. + */ +BlockExporterController.prototype.setBlockEnabled = + function(blockType, enable) { + // Get toolbox xml, category, and block elements. + var toolboxXml = this.view.toolbox; + var category = goog.dom.xml.selectSingleNode(toolboxXml, + '//category[@name="' + blockType + '"]'); + var block = goog.dom.getFirstElementChild(category); + // Enable block. + goog.dom.xml.setAttributes(block, {disabled: !enable}); +}; + +/** + * Add change listeners to the exporter's selector workspace. + */ +BlockExporterController.prototype.addChangeListenersToSelectorWorkspace + = function() { + // Assign the BlockExporterController to 'self' to be called in the change + // listeners. This keeps it in scope--otherwise, 'this' in the change + // listeners refers to the wrong thing. + var self = this; + var selector = this.view.selectorWorkspace; + selector.addChangeListener( + function(event) { + self.onSelectBlockForExport_(event); + }); + selector.addChangeListener( + function(event) { + self.onDeselectBlockForExport_(event); + }); +}; + +/** + * Callback function for when a user selects a block for export in selector + * workspace. Disables selected block so that the user only exports one + * copy of starter code per block. Attached to the blockly create event in block + * factory expansion's init. + * @private + * + * @param {!Blockly.Events} event - The fired Blockly event. + */ +BlockExporterController.prototype.onSelectBlockForExport_ = function(event) { + // The user created a block in selector workspace. + if (event.type == Blockly.Events.CREATE) { + // Get type of block created. + var block = this.view.selectorWorkspace.getBlockById(event.blockId); + var blockType = block.type; + // Disable the selected block. Users can only export one copy of starter + // code per block. + this.setBlockEnabled(blockType, false); + // Show currently selected blocks in helper text. + this.view.listSelectedBlocks(this.getSelectedBlockTypes_()); + } +}; + +/** + * Callback function for when a user deselects a block in selector + * workspace by deleting it. Re-enables block so that the user may select it for + * export + * @private + * + * @param {!Blockly.Events} event - The fired Blockly event. + */ +BlockExporterController.prototype.onDeselectBlockForExport_ = function(event) { + // The user deleted a block in selector workspace. + if (event.type == Blockly.Events.DELETE) { + // Get type of block created. + var deletedBlockXml = event.oldXml; + var blockType = deletedBlockXml.getAttribute('type'); + // Enable the deselected block. + this.setBlockEnabled(blockType, true); + // Show currently selected blocks in helper text. + this.view.listSelectedBlocks(this.getSelectedBlockTypes_()); + } +}; + +/** + * Tied to the 'Clear Selected Blocks' button in the Block Exporter. + * Deselects all blocks on the selector workspace by deleting them and updating + * text accordingly. + */ +BlockExporterController.prototype.clearSelectedBlocks = function() { + // Clear selector workspace. + this.view.clearSelectorWorkspace(); +}; + +/** + * Tied to the 'Add All Stored Blocks' button in the Block Exporter. + * Adds all blocks stored in block library to the selector workspace. + */ +BlockExporterController.prototype.addAllBlocksToWorkspace = function() { + // Clear selector workspace. + this.view.clearSelectorWorkspace(); + + // Add and evaluate all blocks' definitions. + var allBlockTypes = this.blockLibStorage.getBlockTypes(); + var blockXmlMap = this.blockLibStorage.getBlockXmlMap(allBlockTypes); + this.tools.addBlockDefinitions(blockXmlMap); + + // For every block, render in selector workspace. + for (var i = 0, blockType; blockType = allBlockTypes[i]; i++) { + this.view.addBlock(blockType); + } + + // Clean up workspace. + this.view.cleanUpSelectorWorkspace(); +}; diff --git a/demos/blocklyfactory/block_exporter_tools.js b/demos/blocklyfactory/block_exporter_tools.js new file mode 100644 index 000000000..dc88cb8ea --- /dev/null +++ b/demos/blocklyfactory/block_exporter_tools.js @@ -0,0 +1,223 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Javascript for the BlockExporter Tools class, which generates + * block definitions and generator stubs for given block types. Also generates + * toolbox xml for the exporter's workspace. Depends on the BlockFactory for + * its code generation functions. + * + * @author quachtina96 (Tina Quach) + */ +'use strict'; + +goog.provide('BlockExporterTools'); + +goog.require('BlockFactory'); +goog.require('goog.dom'); +goog.require('goog.dom.xml'); + +/** +* Block Exporter Tools Class +* @constructor +*/ +BlockExporterTools = function() { + // Create container for hidden workspace. + this.container = goog.dom.createDom('div', { + 'id': 'blockExporterTools_hiddenWorkspace' + }, ''); // Empty quotes for empty div. + // Hide hidden workspace. + this.container.style.display = 'none'; + goog.dom.appendChild(document.body, this.container); + /** + * Hidden workspace for the Block Exporter that holds pieces that make + * up the block + * @type {Blockly.Workspace} + */ + this.hiddenWorkspace = Blockly.inject(this.container.id, + {collapse: false, + media: '../../media/'}); +}; + +/** + * Get Blockly Block object from xml that encodes the blocks used to design + * the block. + * @private + * + * @param {!Element} xml - Xml element that encodes the blocks used to design + * the block. For example, the block xmls saved in block library. + * @return {!Blockly.Block} - Root block (factory_base block) which contains + * all information needed to generate block definition or null. + */ +BlockExporterTools.prototype.getRootBlockFromXml_ = function(xml) { + // Render xml in hidden workspace. + this.hiddenWorkspace.clear(); + Blockly.Xml.domToWorkspace(xml, this.hiddenWorkspace); + // Get root block. + var rootBlock = this.hiddenWorkspace.getTopBlocks()[0] || null; + return rootBlock; +}; + +/** + * Get Blockly Block by rendering pre-defined block in workspace. + * @private + * + * @param {!Element} blockType - Type of block. + * @return {!Blockly.Block} the Blockly.Block of desired type. + */ +BlockExporterTools.prototype.getDefinedBlock_ = function(blockType) { + this.hiddenWorkspace.clear(); + return this.hiddenWorkspace.newBlock(blockType); +}; + +/** + * Return the given language code of each block type in an array. + * + * @param {!Object} blockXmlMap - Map of block type to xml. + * @param {string} definitionFormat - 'JSON' or 'JavaScript' + * @return {string} The concatenation of each block's language code in the + * desired format. + */ +BlockExporterTools.prototype.getBlockDefs = + function(blockXmlMap, definitionFormat) { + var blockCode = []; + for (var blockType in blockXmlMap) { + var xml = blockXmlMap[blockType]; + if (xml) { + // Render and get block from hidden workspace. + var rootBlock = this.getRootBlockFromXml_(xml); + if (rootBlock) { + // Generate the block's definition. + var code = BlockFactory.getBlockDefinition(blockType, rootBlock, + definitionFormat, this.hiddenWorkspace); + // Add block's definition to the definitions to return. + } else { + // Append warning comment and write to console. + var code = '// No block definition generated for ' + blockType + + '. Could not find root block in xml stored for this block.'; + console.log('No block definition generated for ' + blockType + + '. Could not find root block in xml stored for this block.'); + } + } else { + // Append warning comment and write to console. + var code = '// No block definition generated for ' + blockType + + '. Block was not found in Block Library Storage.'; + console.log('No block definition generated for ' + blockType + + '. Block was not found in Block Library Storage.'); + } + blockCode.push(code); + } + return blockCode.join("\n\n"); +}; + +/** + * Return the generator code of each block type in an array in a given language. + * + * @param {!Object} blockXmlMap - Map of block type to xml. + * @param {string} generatorLanguage - e.g.'JavaScript', 'Python', 'PHP', 'Lua', + * 'Dart' + * @return {string} The concatenation of each block's generator code in the + * desired format. + */ +BlockExporterTools.prototype.getGeneratorCode = + function(blockXmlMap, generatorLanguage) { + var multiblockCode = []; + // Define the custom blocks in order to be able to create instances of + // them in the exporter workspace. + this.addBlockDefinitions(blockXmlMap); + + for (var blockType in blockXmlMap) { + var xml = blockXmlMap[blockType]; + if (xml) { + // Render the preview block in the hidden workspace. + var tempBlock = this.getDefinedBlock_(blockType); + // Get generator stub for the given block and add to generator code. + var blockGenCode = + BlockFactory.getGeneratorStub(tempBlock, generatorLanguage); + } else { + // Append warning comment and write to console. + var blockGenCode = '// No generator stub generated for ' + blockType + + '. Block was not found in Block Library Storage.'; + console.log('No block generator stub generated for ' + blockType + + '. Block was not found in Block Library Storage.'); + } + multiblockCode.push(blockGenCode); + } + return multiblockCode.join("\n\n"); +}; + +/** + * Evaluates block definition code of each block in given object mapping + * block type to xml. Called in order to be able to create instances of the + * blocks in the exporter workspace. + * + * @param {!Object} blockXmlMap - Map of block type to xml. + */ +BlockExporterTools.prototype.addBlockDefinitions = function(blockXmlMap) { + var blockDefs = this.getBlockDefs(blockXmlMap, 'JavaScript'); + eval(blockDefs); +}; + +/** + * Pulls information about all blocks in the block library to generate xml + * for the selector workpace's toolbox. + * + * @param {!BlockLibraryStorage} blockLibStorage - Block Library Storage object. + * @return {!Element} Xml representation of the toolbox. + */ +BlockExporterTools.prototype.generateToolboxFromLibrary + = function(blockLibStorage) { + // Create DOM for XML. + var xmlDom = goog.dom.createDom('xml', { + 'id' : 'blockExporterTools_toolbox', + 'style' : 'display:none' + }); + + var allBlockTypes = blockLibStorage.getBlockTypes(); + // Object mapping block type to XML. + var blockXmlMap = blockLibStorage.getBlockXmlMap(allBlockTypes); + + // Define the custom blocks in order to be able to create instances of + // them in the exporter workspace. + this.addBlockDefinitions(blockXmlMap); + + for (var blockType in blockXmlMap) { + // Create category DOM element. + var categoryElement = goog.dom.createDom('category'); + categoryElement.setAttribute('name',blockType); + + // Get block. + var block = this.getDefinedBlock_(blockType); + + // Get preview block XML. + var blockChild = Blockly.Xml.blockToDom(block); + blockChild.removeAttribute('id'); + + // Add block to category and category to XML. + categoryElement.appendChild(blockChild); + xmlDom.appendChild(categoryElement); + } + + // If there are no blocks in library, append dummy category. + var categoryElement = goog.dom.createDom('category'); + categoryElement.setAttribute('name','Next Saved Block'); + xmlDom.appendChild(categoryElement); + return xmlDom; +}; diff --git a/demos/blocklyfactory/block_exporter_view.js b/demos/blocklyfactory/block_exporter_view.js new file mode 100644 index 000000000..ee1d76c6c --- /dev/null +++ b/demos/blocklyfactory/block_exporter_view.js @@ -0,0 +1,145 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Javascript for the Block Exporter View class. Takes care of + * generating the selector workspace through which users select blocks to + * export. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockExporterView'); + +goog.require('goog.dom'); + +/** + * BlockExporter View Class + * @constructor + * + * @param {Element} toolbox - Xml for the toolbox of the selector workspace. + */ +BlockExporterView = function(toolbox) { + // Xml representation of the toolbox + if (toolbox.hasChildNodes) { + this.toolbox = toolbox; + } else { + // Toolbox is empty. Append dummy category to toolbox because toolbox + // cannot switch between category and flyout-only mode after injection. + var categoryElement = goog.dom.createDom('category'); + categoryElement.setAttribute('name', 'Next Saved Block'); + toolbox.appendChild(categoryElement); + this.toolbox = toolbox; + } + // Workspace users use to select blocks for export + this.selectorWorkspace = + Blockly.inject('exportSelector', + {collapse: false, + toolbox: this.toolbox, + grid: + {spacing: 20, + length: 3, + colour: '#ccc', + snap: true} + }); +}; + +/** + * Update the toolbox of this instance of BlockExporterView. + * + * @param {Element} toolboxXml - Xml for the toolbox of the selector workspace. + */ +BlockExporterView.prototype.setToolbox = function(toolboxXml) { + // Parse the provided toolbox tree into a consistent DOM format. + this.toolbox = Blockly.Options.parseToolboxTree(toolboxXml); +}; + +/** + * Renders the toolbox in the workspace. Used to update the toolbox upon + * switching between Block Factory tab and Block Exporter Tab. + */ +BlockExporterView.prototype.renderToolbox = function() { + this.selectorWorkspace.updateToolbox(this.toolbox); +}; + +/** + * Updates the helper text. + * + * @param {string} newText - New helper text. + * @param {boolean} opt_append - True if appending to helper Text, false if + * replacing. + */ +BlockExporterView.prototype.updateHelperText = function(newText, opt_append) { + if (opt_append) { + goog.dom.getElement('helperText').textContent = + goog.dom.getElement('helperText').textContent + newText; + } else { + goog.dom.getElement('helperText').textContent = newText; + } +}; + +/** + * Updates the helper text to show list of currently selected blocks. + * + * @param {!Array.} selectedBlockTypes - Array of blocks selected in workspace. + */ +BlockExporterView.prototype.listSelectedBlocks = function(selectedBlockTypes) { + var selectedBlocksText = selectedBlockTypes.join(', '); + this.updateHelperText('Currently Selected: ' + selectedBlocksText); +}; + +/** + * Renders block of given type on selector workspace assuming block has already + * been defined. + * + * @param {string} blockType - Type of block to add to selector workspce. + */ +BlockExporterView.prototype.addBlock = function(blockType) { + var newBlock = this.selectorWorkspace.newBlock(blockType); + newBlock.initSvg(); + newBlock.render(); +}; + +/** + * Clears selector workspace. + */ +BlockExporterView.prototype.clearSelectorWorkspace = function() { + this.selectorWorkspace.clear(); +}; + +/** + * Neatly layout the blocks in selector workspace. + */ +BlockExporterView.prototype.cleanUpSelectorWorkspace = function() { + this.selectorWorkspace.cleanUp_(); +}; + +/** + * Returns array of selected blocks. + * + * @return {Array.} Array of all blocks in selector workspace. + */ +BlockExporterView.prototype.getSelectedBlocks = function() { + return this.selectorWorkspace.getAllBlocks(); +}; + + diff --git a/demos/blocklyfactory/block_library_controller.js b/demos/blocklyfactory/block_library_controller.js new file mode 100644 index 000000000..0b7210118 --- /dev/null +++ b/demos/blocklyfactory/block_library_controller.js @@ -0,0 +1,219 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Contains the code for Block Library Controller, which + * depends on Block Library Storage and Block Library UI. Provides the + * interfaces for the user to + * - save their blocks to the browser + * - re-open and edit saved blocks + * - delete blocks + * - clear their block library + * Depends on BlockFactory functions defined in factory.js. + * + * @author quachtina96 (Tina Quach) + */ +'use strict'; + +goog.provide('BlockLibraryController'); + +goog.require('BlockLibraryStorage'); +goog.require('BlockLibraryView'); +goog.require('BlockFactory'); + +/** + * Block Library Controller Class + * @constructor + * + * @param {string} blockLibraryName - Desired name of Block Library, also used + * to create the key for where it's stored in local storage. + * @param {!BlockLibraryStorage} opt_blockLibraryStorage - optional storage + * object that allows user to import a block library. + */ +BlockLibraryController = function(blockLibraryName, opt_blockLibraryStorage) { + this.name = blockLibraryName; + // Create a new, empty Block Library Storage object, or load existing one. + this.storage = opt_blockLibraryStorage || new BlockLibraryStorage(this.name); +}; + +/** + * Returns the block type of the block the user is building. + * @private + * + * @return {string} The current block's type. + */ +BlockLibraryController.prototype.getCurrentBlockType_ = function() { + var rootBlock = BlockFactory.getRootBlock(BlockFactory.mainWorkspace); + var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase(); + // Replace white space with underscores + return blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1'); +}; + +/** + * Removes current block from Block Library + * + * @param {string} blockType - Type of block. + */ +BlockLibraryController.prototype.removeFromBlockLibrary = function() { + var blockType = this.getCurrentBlockType_(); + this.storage.removeBlock(blockType); + this.storage.saveToLocalStorage(); + this.populateBlockLibrary(); +}; + +/** + * Updates the workspace to show the block user selected from library + * + * @param {string} blockType - Block to edit on block factory. + */ +BlockLibraryController.prototype.openBlock = function(blockType) { + var xml = this.storage.getBlockXml(blockType); + BlockFactory.mainWorkspace.clear(); + Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace); +}; + +/** + * Returns type of block selected from library. + * + * @param {Element} blockLibraryDropdown - The block library dropdown. + * @return {string} Type of block selected. + */ +BlockLibraryController.prototype.getSelectedBlockType = + function(blockLibraryDropdown) { + return BlockLibraryView.getSelected(blockLibraryDropdown); +}; + +/** + * Confirms with user before clearing the block library in local storage and + * updating the dropdown. + */ +BlockLibraryController.prototype.clearBlockLibrary = function() { + var check = confirm( + 'Click OK to clear your block library.'); + if (check) { + // Clear Block Library Storage. + this.storage.clear(); + this.storage.saveToLocalStorage(); + // Update dropdown. + BlockLibraryView.clearOptions('blockLibraryDropdown'); + // Add a default, blank option to dropdown for when no block from library is + // selected. + BlockLibraryView.addDefaultOption('blockLibraryDropdown'); + } +}; + +/** + * Saves current block to local storage and updates dropdown. + */ +BlockLibraryController.prototype.saveToBlockLibrary = function() { + var blockType = this.getCurrentBlockType_(); + // If block under that name already exists, confirm that user wants to replace + // saved block. + if (this.isInBlockLibrary(blockType)) { + var replace = confirm('You already have a block called ' + blockType + + ' in your library. Click OK to replace.'); + if (!replace) { + // Do not save if user doesn't want to replace the saved block. + return; + } + } + + // Save block. + var xmlElement = Blockly.Xml.workspaceToDom(BlockFactory.mainWorkspace); + this.storage.addBlock(blockType, xmlElement); + this.storage.saveToLocalStorage(); + + // Do not add another option to dropdown if replacing. + if (replace) { + return; + } + BlockLibraryView.addOption( + blockType, blockType, 'blockLibraryDropdown', true, true); +}; + +/** + * Checks to see if the given blockType is already in Block Library + * + * @param {string} blockType - Type of block. + * @return {boolean} Boolean indicating whether or not block is in the library. + */ +BlockLibraryController.prototype.isInBlockLibrary = function(blockType) { + var blockLibrary = this.storage.blocks; + return (blockType in blockLibrary && blockLibrary[blockType] != null); +}; + +/** + * Populates the dropdown menu. + */ +BlockLibraryController.prototype.populateBlockLibrary = function() { + BlockLibraryView.clearOptions('blockLibraryDropdown'); + // Add a default, blank option to dropdown for when no block from library is + // selected. + BlockLibraryView.addDefaultOption('blockLibraryDropdown'); + // Add option for each saved block. + var blockLibrary = this.storage.blocks; + for (var block in blockLibrary) { + // Make sure the block wasn't deleted. + if (blockLibrary[block] != null) { + BlockLibraryView.addOption( + block, block, 'blockLibraryDropdown', false, true); + } + } +}; + +/** + * Return block library mapping block type to xml. + * + * @return {Object} Object mapping block type to xml text. + */ +BlockLibraryController.prototype.getBlockLibrary = function() { + return this.storage.getBlockXmlTextMap(); +}; + +/** + * Set the block library storage object from which exporter exports. + * + * @param {!BlockLibraryStorage} blockLibStorage - Block Library Storage + * object. + */ +BlockLibraryController.prototype.setBlockLibStorage + = function(blockLibStorage) { + this.storage = blockLibStorage; +}; + +/** + * Get the block library storage object from which exporter exports. + * + * @return {!BlockLibraryStorage} blockLibStorage - Block Library Storage object + * that stores the blocks. + */ +BlockLibraryController.prototype.getBlockLibStorage = + function(blockLibStorage) { + return this.blockLibStorage; +}; + +/** + * Get the block library storage object from which exporter exports. + * + * @return {boolean} True if the Block Library is empty, false otherwise. + */ +BlockLibraryController.prototype.hasEmptyBlockLib = function() { + return this.storage.isEmpty(); +}; diff --git a/demos/blocklyfactory/block_library_storage.js b/demos/blocklyfactory/block_library_storage.js new file mode 100644 index 000000000..4bca70242 --- /dev/null +++ b/demos/blocklyfactory/block_library_storage.js @@ -0,0 +1,167 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Javascript for Block Library's Storage Class. + * Depends on Block Library for its namespace. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockLibraryStorage'); + +/** + * Represents a block library's storage. + * @constructor + * + * @param {string} blockLibraryName - Desired name of Block Library, also used + * to create the key for where it's stored in local storage. + * @param {Object} opt_blocks - Object mapping block type to xml. + */ +BlockLibraryStorage = function(blockLibraryName, opt_blocks) { + // Add prefix to this.name to avoid collisions in local storage. + this.name = 'BlockLibraryStorage.' + blockLibraryName; + if (!opt_blocks) { + // Initialize this.blocks by loading from local storage. + this.loadFromLocalStorage(); + if (this.blocks == null) { + this.blocks = Object.create(null); + // The line above is equivalent of {} except that this object is TRULY + // empty. It doesn't have built-in attributes/functions such as length or + // toString. + this.saveToLocalStorage(); + } + } else { + this.blocks = opt_blocks; + this.saveToLocalStorage(); + } +}; + +/** + * Reads the named block library from local storage and saves it in this.blocks. + */ +BlockLibraryStorage.prototype.loadFromLocalStorage = function() { + // goog.global is synonymous to window, and allows for flexibility + // between browsers. + var object = goog.global.localStorage[this.name]; + this.blocks = object ? JSON.parse(object) : null; +}; + +/** + * Writes the current block library (this.blocks) to local storage. + */ +BlockLibraryStorage.prototype.saveToLocalStorage = function() { + goog.global.localStorage[this.name] = JSON.stringify(this.blocks); +}; + +/** + * Clears the current block library. + */ +BlockLibraryStorage.prototype.clear = function() { + this.blocks = Object.create(null); + // The line above is equivalent of {} except that this object is TRULY + // empty. It doesn't have built-in attributes/functions such as length or + // toString. +}; + +/** + * Saves block to block library. + * + * @param {string} blockType - Type of block. + * @param {Element} blockXML - The block's XML pulled from workspace. + */ +BlockLibraryStorage.prototype.addBlock = function(blockType, blockXML) { + var prettyXml = Blockly.Xml.domToPrettyText(blockXML); + this.blocks[blockType] = prettyXml; +}; + +/** + * Removes block from current block library (this.blocks). + * + * @param {string} blockType - Type of block. + */ +BlockLibraryStorage.prototype.removeBlock = function(blockType) { + delete this.blocks[blockType]; +}; + +/** + * Returns the xml of given block type stored in current block library + * (this.blocks). + * + * @param {string} blockType - Type of block. + * @return {Element} The xml that represents the block type or null. + */ +BlockLibraryStorage.prototype.getBlockXml = function(blockType) { + var xml = this.blocks[blockType] || null; + if (xml) { + var xml = Blockly.Xml.textToDom(xml); + } + return xml; +}; + + +/** + * Returns map of each block type to its corresponding xml stored in current + * block library (this.blocks). + * + * @param {Array.} blockTypes - Types of blocks. + * @return {!Object} Map of block type to corresponding xml. + */ +BlockLibraryStorage.prototype.getBlockXmlMap = function(blockTypes) { + var blockXmlMap = {}; + for (var i = 0; i < blockTypes.length; i++) { + var blockType = blockTypes[i]; + var xml = this.getBlockXml(blockType); + blockXmlMap[blockType] = xml; + } + return blockXmlMap; +}; + +/** + * Returns array of all block types stored in current block library. + * + * @return {!Array.} Array of block types stored in library. + */ +BlockLibraryStorage.prototype.getBlockTypes = function() { + return Object.keys(this.blocks); +}; + +/** + * Checks to see if block library is empty. + * + * @return {boolean} True if empty, false otherwise. + */ +BlockLibraryStorage.prototype.isEmpty = function() { + for (var blockType in this.blocks) { + return false; + } + return true; +}; + +/** + * Returns array of all block types stored in current block library. + * + * @return {!Array.} Map of block type to corresponding xml text. + */ +BlockLibraryStorage.prototype.getBlockXmlTextMap = function() { + return this.blocks; +}; diff --git a/demos/blocklyfactory/block_library_view.js b/demos/blocklyfactory/block_library_view.js new file mode 100644 index 000000000..151055a42 --- /dev/null +++ b/demos/blocklyfactory/block_library_view.js @@ -0,0 +1,117 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Javascript for Block Library's UI for pulling blocks from the + * Block Library's storage to edit in Block Factory. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockLibraryView'); + +/** + * Creates a node of a given element type and appends to the node with given id. + * + * @param {string} optionIdentifier - String used to identify option. + * @param {string} optionText - Text to display in the dropdown for the option. + * @param {string} dropdownID - ID for HTML select element. + * @param {boolean} selected - Whether or not the option should be selected on + * the dropdown. + * @param {boolean} enabled - Whether or not the option should be enabled. + */ +BlockLibraryView.addOption + = function(optionIdentifier, optionText, dropdownID, selected, enabled) { + var dropdown = document.getElementById(dropdownID); + var option = document.createElement('option'); + // The value attribute of a dropdown's option is not visible in the UI, but is + // useful for identifying different options that may have the same text. + option.value = optionIdentifier; + // The text attribute is what the user sees in the dropdown for the option. + option.text = optionText; + option.selected = selected; + option.disabled = !enabled; + dropdown.add(option); +}; + +/** + * Adds a default, blank option to dropdown for when no block from library is + * selected. + * + * @param {string} dropdownID - ID of HTML select element + */ +BlockLibraryView.addDefaultOption = function(dropdownID) { + BlockLibraryView.addOption( + 'BLOCK_LIBRARY_DEFAULT_BLANK', '', dropdownID, true, false); +}; + +/** + * Selects the default, blank option in dropdown identified by given ID. + * + * @param {string} dropdownID - ID of HTML select element + */ +BlockLibraryView.selectDefaultOption = function(dropdownID) { + var dropdown = document.getElementById(dropdownID); + // Deselect currently selected option. + var index = dropdown.selectedIndex; + dropdown.options[index].selected = false; + // Select default option, always the first in the dropdown. + var defaultOption = dropdown.options[0]; + defaultOption.selected = true; +}; + +/** + * Returns block type of selected block. + * + * @param {Element} dropdown - HTML select element. + * @return {string} Type of block selected. + */ +BlockLibraryView.getSelected = function(dropdown) { + var index = dropdown.selectedIndex; + return dropdown.options[index].value; +}; + +/** + * Removes option currently selected in dropdown from dropdown menu. + * + * @param {string} dropdownID - ID of HTML select element within which to find + * the selected option. + */ +BlockLibraryView.removeSelectedOption = function(dropdownID) { + var dropdown = document.getElementById(dropdownID); + if (dropdown) { + dropdown.remove(dropdown.selectedIndex); + } +}; + +/** + * Removes all options from dropdown. + * + * @param {string} dropdownID - ID of HTML select element to clear options of. + */ +BlockLibraryView.clearOptions = function(dropdownID) { + var dropdown = document.getElementById(dropdownID); + while (dropdown.length > 0) { + dropdown.remove(dropdown.length - 1); + } +}; + diff --git a/demos/blocklyfactory/factory.css b/demos/blocklyfactory/factory.css new file mode 100644 index 000000000..d2a72bc64 --- /dev/null +++ b/demos/blocklyfactory/factory.css @@ -0,0 +1,217 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +html, body { + height: 100%; +} + +body { + background-color: #fff; + font-family: sans-serif; + margin: 0 5px; + overflow: hidden +} + +h1 { + font-weight: normal; + font-size: 140%; +} + +h3 { + margin-top: 5px; + margin-bottom: 0; +} + +table { + height: 100%; + width: 100%; +} + +td { + vertical-align: top; + padding: 0; +} + +p { + display: block; + -webkit-margin-before: 0em; + -webkit-margin-after: 0em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + padding: 5px 0px; +} + + +#blockly { + position: fixed; +} + +#blocklyMask { + background-color: #000; + cursor: not-allowed; + display: none; + position: fixed; + opacity: 0.2; + z-index: 9; +} + +#preview { + position: absolute; +} + +pre, +#languageTA { + border: #ddd 1px solid; + margin-top: 0; + position: absolute; + overflow: scroll; +} + +#languageTA { + display: none; + font: 10pt monospace; +} + +.downloadButton { + padding: 5px; +} + +button:disabled, .buttonStyle:disabled { + opacity: 0.6; +} + +button>*, .buttonStyle>* { + opacity: 1; + vertical-align: text-bottom; +} + +button, .buttonStyle { + border-radius: 4px; + border: 1px solid #ddd; + background-color: #eee; + color: #000; + padding: 10px; + margin: 10px 5px; + font-size: small; +} + +.buttonStyle:hover:not(:disabled), button:hover:not(:disabled) { + box-shadow: 2px 2px 5px #888; +} + +.buttonStyle:hover:not(:disabled)>*, button:hover:not(:disabled)>* { + opacity: 1; +} + +#linkButton { + display: none; +} + +#blockFactoryContent { + height: 87%; +} + +#blockLibraryContainer { + vertical-align: bottom; +} + +#blockLibraryControls { + text-align: right; + vertical-align: middle; +} + +#previewContainer { + vertical-align: bottom; +} + +#buttonContainer { + text-align: right; + vertical-align: middle; +} + +#files { + position: absolute; + visibility: hidden; +} + +#toolbox { + display: none; +} + +#blocklyWorkspaceContainer { + height: 95%; + padding: 2px; + width: 50%; +} + +/* Workspace Factory */ + +#workspaceFactoryContent { + clear: both; + display: none; + height: 100%; +} + +/* Exporter */ + +#blockLibraryExporter { + clear: both; + display: none; + height: 100%; +} + +#exportSelector { + float: left; + height: 75%; + width: 30%; +} + +#exportSettings { + float: left; + padding: 16px; + width: 30%; + overflow: hidden; +} + +#exporterHiddenWorkspace { + display: none; +} + +#exporterPreview { + float: right; + padding: 16px; + overflow: hidden; + background-color: blue; +} + +/* Tabs */ + +.tab { + float: left; + padding: 5px 19px; +} + +.tab.tabon { + background-color: #ddd; +} + +.tab.taboff { + cursor: pointer; +} diff --git a/demos/blocklyfactory/factory.js b/demos/blocklyfactory/factory.js new file mode 100644 index 000000000..12712a0a6 --- /dev/null +++ b/demos/blocklyfactory/factory.js @@ -0,0 +1,989 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @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. + * + * @author fraser@google.com (Neil Fraser), quachtina96 (Tina Quach) + */ +'use strict'; + +/** + * Namespace for Block Factory. + */ +goog.provide('BlockFactory'); + +goog.require('goog.dom.classes'); + +/** + * 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. + */ +BlockFactory.UNNAMED = 'unnamed'; + +/** + * Existing direction ('ltr' vs 'rtl') of preview. + */ +BlockFactory.oldDir = null; + +// UI + +/** + * Inject code into a pre tag, with syntax highlighting. + * Safe from HTML/script injection. + * @param {string} code Lines of code. + * @param {string} id ID of
         element to inject into.
        + */
        +BlockFactory.injectCode = function(code, id) {
        +  var pre = document.getElementById(id);
        +  pre.textContent = code;
        +  code = pre.innerHTML;
        +  code = prettyPrintOne(code, 'js');
        +  pre.innerHTML = code;
        +};
        +
        +// Utils
        +
        +/**
        + * Escape a string.
        + * @param {string} string String to escape.
        + * @return {string} Escaped string surrouned by quotes.
        + */
        +BlockFactory.escapeString = function(string) {
        +  return JSON.stringify(string);
        +};
        +
        +/**
        + * Return the uneditable container block that everything else attaches to in
        + * given workspace
        + *
        + * @param {!Blockly.Workspace} workspace - where the root block lives
        + * @return {Blockly.Block} root block
        + */
        +BlockFactory.getRootBlock = function(workspace) {
        +  var blocks = workspace.getTopBlocks(false);
        +  for (var i = 0, block; block = blocks[i]; i++) {
        +    if (block.type == 'factory_base') {
        +      return block;
        +    }
        +  }
        +  return null;
        +};
        +
        +// Language Code: Block Definitions
        +
        +/**
        + * 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') {
        +    Blockly.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';
        +    BlockFactory.updateLanguage();
        +  }
        +  BlockFactory.disableEnableLink();
        +};
        +
        +/**
        + * Get block definition code for the current block.
        + *
        + * @param {string} blockType - Type of block.
        + * @param {!Blockly.Block} rootBlock - RootBlock from main workspace in which
        + *    user uses Block Factory Blocks to create a custom block.
        + * @param {string} format - 'JSON' or 'JavaScript'.
        + * @param {!Blockly.Workspace} workspace - Where the root block lives.
        + * @return {string} Block definition.
        + */
        +BlockFactory.getBlockDefinition = function(blockType, rootBlock, format, workspace) {
        +  blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1');
        +  switch (format) {
        +    case 'JSON':
        +      var code = BlockFactory.formatJson_(blockType, rootBlock);
        +      break;
        +    case 'JavaScript':
        +      var code = BlockFactory.formatJavaScript_(blockType, rootBlock, workspace);
        +      break;
        +  }
        +  return code;
        +};
        +
        +/**
        + * Update the language code based on constructs made in Blockly.
        + */
        +BlockFactory.updateLanguage = function() {
        +  var rootBlock = BlockFactory.getRootBlock(BlockFactory.mainWorkspace);
        +  if (!rootBlock) {
        +    return;
        +  }
        +  var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase();
        +  if (!blockType) {
        +    blockType = BlockFactory.UNNAMED;
        +  }
        +  var format = document.getElementById('format').value;
        +  var code = BlockFactory.getBlockDefinition(blockType, rootBlock, format,
        +      BlockFactory.mainWorkspace);
        +  BlockFactory.injectCode(code, 'languagePre');
        +  BlockFactory.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
        + */
        +BlockFactory.formatJson_ = function(blockType, rootBlock) {
        +  var JS = {};
        +  // Type is not used by Blockly, but may be used by a loader.
        +  JS.type = blockType;
        +  // Generate inputs.
        +  var message = [];
        +  var args = [];
        +  var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
        +  var lastInput = null;
        +  while (contentsBlock) {
        +    if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
        +      var fields = BlockFactory.getFieldsJson_(
        +          contentsBlock.getInputTargetBlock('FIELDS'));
        +      for (var i = 0; i < fields.length; i++) {
        +        if (typeof fields[i] == 'string') {
        +          message.push(fields[i].replace(/%/g, '%%'));
        +        } else {
        +          args.push(fields[i]);
        +          message.push('%' + args.length);
        +        }
        +      }
        +
        +      var input = {type: contentsBlock.type};
        +      // Dummy inputs don't have names.  Other inputs do.
        +      if (contentsBlock.type != 'input_dummy') {
        +        input.name = contentsBlock.getFieldValue('INPUTNAME');
        +      }
        +      var check = JSON.parse(
        +          BlockFactory.getOptTypesFrom(contentsBlock, 'TYPE') || 'null');
        +      if (check) {
        +        input.check = check;
        +      }
        +      var align = contentsBlock.getFieldValue('ALIGN');
        +      if (align != 'LEFT') {
        +        input.align = align;
        +      }
        +      args.push(input);
        +      message.push('%' + args.length);
        +      lastInput = contentsBlock;
        +    }
        +    contentsBlock = contentsBlock.nextConnection &&
        +        contentsBlock.nextConnection.targetBlock();
        +  }
        +  // Remove last input if dummy and not empty.
        +  if (lastInput && lastInput.type == 'input_dummy') {
        +    var fields = lastInput.getInputTargetBlock('FIELDS');
        +    if (fields && BlockFactory.getFieldsJson_(fields).join('').trim() != '') {
        +      var align = lastInput.getFieldValue('ALIGN');
        +      if (align != 'LEFT') {
        +        JS.lastDummyAlign0 = align;
        +      }
        +      args.pop();
        +      message.pop();
        +    }
        +  }
        +  JS.message0 = message.join(' ');
        +  if (args.length) {
        +    JS.args0 = args;
        +  }
        +  // Generate inline/external switch.
        +  if (rootBlock.getFieldValue('INLINE') == 'EXT') {
        +    JS.inputsInline = false;
        +  } else if (rootBlock.getFieldValue('INLINE') == 'INT') {
        +    JS.inputsInline = true;
        +  }
        +  // Generate output, or next/previous connections.
        +  switch (rootBlock.getFieldValue('CONNECTIONS')) {
        +    case 'LEFT':
        +      JS.output =
        +          JSON.parse(
        +              BlockFactory.getOptTypesFrom(rootBlock, 'OUTPUTTYPE') || 'null');
        +      break;
        +    case 'BOTH':
        +      JS.previousStatement =
        +          JSON.parse(
        +              BlockFactory.getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
        +      JS.nextStatement =
        +          JSON.parse(
        +              BlockFactory.getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
        +      break;
        +    case 'TOP':
        +      JS.previousStatement =
        +          JSON.parse(
        +              BlockFactory.getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
        +      break;
        +    case 'BOTTOM':
        +      JS.nextStatement =
        +          JSON.parse(
        +              BlockFactory.getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
        +      break;
        +  }
        +  // Generate colour.
        +  var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
        +  if (colourBlock && !colourBlock.disabled) {
        +    var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
        +    JS.colour = hue;
        +  }
        +  JS.tooltip = '';
        +  JS.helpUrl = 'http://www.example.com/';
        +  return JSON.stringify(JS, null, '  ');
        +};
        +
        +/**
        + * Update the language code as JavaScript.
        + * @param {string} blockType Name of block.
        + * @param {!Blockly.Block} rootBlock Factory_base block.
        + * @param {!Blockly.Workspace} workspace - Where the root block lives.
        +
        + * @return {string} Generated language code.
        + * @private
        + */
        +BlockFactory.formatJavaScript_ = function(blockType, rootBlock, workspace) {
        +  var code = [];
        +  code.push("Blockly.Blocks['" + blockType + "'] = {");
        +  code.push("  init: function() {");
        +  // Generate inputs.
        +  var TYPES = {'input_value': 'appendValueInput',
        +               'input_statement': 'appendStatementInput',
        +               'input_dummy': 'appendDummyInput'};
        +  var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
        +  while (contentsBlock) {
        +    if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
        +      var name = '';
        +      // Dummy inputs don't have names.  Other inputs do.
        +      if (contentsBlock.type != 'input_dummy') {
        +        name =
        +            BlockFactory.escapeString(contentsBlock.getFieldValue('INPUTNAME'));
        +      }
        +      code.push('    this.' + TYPES[contentsBlock.type] + '(' + name + ')');
        +      var check = BlockFactory.getOptTypesFrom(contentsBlock, 'TYPE');
        +      if (check) {
        +        code.push('        .setCheck(' + check + ')');
        +      }
        +      var align = contentsBlock.getFieldValue('ALIGN');
        +      if (align != 'LEFT') {
        +        code.push('        .setAlign(Blockly.ALIGN_' + align + ')');
        +      }
        +      var fields = BlockFactory.getFieldsJs_(
        +          contentsBlock.getInputTargetBlock('FIELDS'));
        +      for (var i = 0; i < fields.length; i++) {
        +        code.push('        .appendField(' + fields[i] + ')');
        +      }
        +      // Add semicolon to last line to finish the statement.
        +      code[code.length - 1] += ';';
        +    }
        +    contentsBlock = contentsBlock.nextConnection &&
        +        contentsBlock.nextConnection.targetBlock();
        +  }
        +  // Generate inline/external switch.
        +  if (rootBlock.getFieldValue('INLINE') == 'EXT') {
        +    code.push('    this.setInputsInline(false);');
        +  } else if (rootBlock.getFieldValue('INLINE') == 'INT') {
        +    code.push('    this.setInputsInline(true);');
        +  }
        +  // Generate output, or next/previous connections.
        +  switch (rootBlock.getFieldValue('CONNECTIONS')) {
        +    case 'LEFT':
        +      code.push(BlockFactory.connectionLineJs_('setOutput', 'OUTPUTTYPE', workspace));
        +      break;
        +    case 'BOTH':
        +      code.push(
        +          BlockFactory.connectionLineJs_('setPreviousStatement', 'TOPTYPE', workspace));
        +      code.push(
        +          BlockFactory.connectionLineJs_('setNextStatement', 'BOTTOMTYPE', workspace));
        +      break;
        +    case 'TOP':
        +      code.push(
        +          BlockFactory.connectionLineJs_('setPreviousStatement', 'TOPTYPE', workspace));
        +      break;
        +    case 'BOTTOM':
        +      code.push(
        +          BlockFactory.connectionLineJs_('setNextStatement', 'BOTTOMTYPE', workspace));
        +      break;
        +  }
        +  // Generate colour.
        +  var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
        +  if (colourBlock && !colourBlock.disabled) {
        +    var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
        +    if (!isNaN(hue)) {
        +      code.push('    this.setColour(' + hue + ');');
        +    }
        +  }
        +  code.push("    this.setTooltip('');");
        +  code.push("    this.setHelpUrl('http://www.example.com/');");
        +  code.push('  }');
        +  code.push('};');
        +  return code.join('\n');
        +};
        +
        +/**
        + * Create JS code required to create a top, bottom, or value connection.
        + * @param {string} functionName JavaScript function name.
        + * @param {string} typeName Name of type input.
        + * @param {!Blockly.Workspace} workspace - Where the root block lives.
        + * @return {string} Line of JavaScript code to create connection.
        + * @private
        + */
        +BlockFactory.connectionLineJs_ = function(functionName, typeName, workspace) {
        +  var type = BlockFactory.getOptTypesFrom(
        +      BlockFactory.getRootBlock(workspace), typeName);
        +  if (type) {
        +    type = ', ' + type;
        +  } else {
        +    type = '';
        +  }
        +  return '    this.' + functionName + '(true' + type + ');';
        +};
        +
        +/**
        + * Returns field strings and any config.
        + * @param {!Blockly.Block} block Input block.
        + * @return {!Array.} Field strings.
        + * @private
        + */
        +BlockFactory.getFieldsJs_ = function(block) {
        +  var fields = [];
        +  while (block) {
        +    if (!block.disabled && !block.getInheritedDisabled()) {
        +      switch (block.type) {
        +        case 'field_static':
        +          // Result: 'hello'
        +          fields.push(BlockFactory.escapeString(block.getFieldValue('TEXT')));
        +          break;
        +        case 'field_input':
        +          // Result: new Blockly.FieldTextInput('Hello'), 'GREET'
        +          fields.push('new Blockly.FieldTextInput(' +
        +              BlockFactory.escapeString(block.getFieldValue('TEXT')) + '), ' +
        +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
        +          break;
        +        case 'field_number':
        +          // Result: new Blockly.FieldNumber(10, 0, 100, 1), 'NUMBER'
        +          var args = [
        +            Number(block.getFieldValue('VALUE')),
        +            Number(block.getFieldValue('MIN')),
        +            Number(block.getFieldValue('MAX')),
        +            Number(block.getFieldValue('PRECISION'))
        +          ];
        +          // Remove any trailing arguments that aren't needed.
        +          if (args[3] == 0) {
        +            args.pop();
        +            if (args[2] == Infinity) {
        +              args.pop();
        +              if (args[1] == -Infinity) {
        +                args.pop();
        +              }
        +            }
        +          }
        +          fields.push('new Blockly.FieldNumber(' + args.join(', ') + '), ' +
        +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
        +          break;
        +        case 'field_angle':
        +          // Result: new Blockly.FieldAngle(90), 'ANGLE'
        +          fields.push('new Blockly.FieldAngle(' +
        +              parseFloat(block.getFieldValue('ANGLE')) + '), ' +
        +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
        +          break;
        +        case 'field_checkbox':
        +          // Result: new Blockly.FieldCheckbox('TRUE'), 'CHECK'
        +          fields.push('new Blockly.FieldCheckbox(' +
        +              BlockFactory.escapeString(block.getFieldValue('CHECKED')) +
        +               '), ' +
        +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
        +          break;
        +        case 'field_colour':
        +          // Result: new Blockly.FieldColour('#ff0000'), 'COLOUR'
        +          fields.push('new Blockly.FieldColour(' +
        +              BlockFactory.escapeString(block.getFieldValue('COLOUR')) +
        +              '), ' +
        +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
        +          break;
        +        case 'field_date':
        +          // Result: new Blockly.FieldDate('2015-02-04'), 'DATE'
        +          fields.push('new Blockly.FieldDate(' +
        +              BlockFactory.escapeString(block.getFieldValue('DATE')) + '), ' +
        +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
        +          break;
        +        case 'field_variable':
        +          // Result: new Blockly.FieldVariable('item'), 'VAR'
        +          var varname
        +              = BlockFactory.escapeString(block.getFieldValue('TEXT') || null);
        +          fields.push('new Blockly.FieldVariable(' + varname + '), ' +
        +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
        +          break;
        +        case 'field_dropdown':
        +          // Result:
        +          // new Blockly.FieldDropdown([['yes', '1'], ['no', '0']]), 'TOGGLE'
        +          var options = [];
        +          for (var i = 0; i < block.optionCount_; i++) {
        +            options[i] = '[' +
        +                BlockFactory.escapeString(block.getFieldValue('USER' + i)) +
        +                ', ' +
        +                BlockFactory.escapeString(block.getFieldValue('CPU' + i)) + ']';
        +          }
        +          if (options.length) {
        +            fields.push('new Blockly.FieldDropdown([' +
        +                options.join(', ') + ']), ' +
        +                BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
        +          }
        +          break;
        +        case 'field_image':
        +          // Result: new Blockly.FieldImage('http://...', 80, 60)
        +          var src = BlockFactory.escapeString(block.getFieldValue('SRC'));
        +          var width = Number(block.getFieldValue('WIDTH'));
        +          var height = Number(block.getFieldValue('HEIGHT'));
        +          var alt = BlockFactory.escapeString(block.getFieldValue('ALT'));
        +          fields.push('new Blockly.FieldImage(' +
        +              src + ', ' + width + ', ' + height + ', ' + alt + ')');
        +          break;
        +      }
        +    }
        +    block = block.nextConnection && block.nextConnection.targetBlock();
        +  }
        +  return fields;
        +};
        +
        +/**
        + * Returns field strings and any config.
        + * @param {!Blockly.Block} block Input block.
        + * @return {!Array.} Array of static text and field configs.
        + * @private
        + */
        +BlockFactory.getFieldsJson_ = function(block) {
        +  var fields = [];
        +  while (block) {
        +    if (!block.disabled && !block.getInheritedDisabled()) {
        +      switch (block.type) {
        +        case 'field_static':
        +          // Result: 'hello'
        +          fields.push(block.getFieldValue('TEXT'));
        +          break;
        +        case 'field_input':
        +          fields.push({
        +            type: block.type,
        +            name: block.getFieldValue('FIELDNAME'),
        +            text: block.getFieldValue('TEXT')
        +          });
        +          break;
        +        case 'field_number':
        +          var obj = {
        +            type: block.type,
        +            name: block.getFieldValue('FIELDNAME'),
        +            value: parseFloat(block.getFieldValue('VALUE'))
        +          };
        +          var min = parseFloat(block.getFieldValue('MIN'));
        +          if (min > -Infinity) {
        +            obj.min = min;
        +          }
        +          var max = parseFloat(block.getFieldValue('MAX'));
        +          if (max < Infinity) {
        +            obj.max = max;
        +          }
        +          var precision = parseFloat(block.getFieldValue('PRECISION'));
        +          if (precision) {
        +            obj.precision = precision;
        +          }
        +          fields.push(obj);
        +          break;
        +        case 'field_angle':
        +          fields.push({
        +            type: block.type,
        +            name: block.getFieldValue('FIELDNAME'),
        +            angle: Number(block.getFieldValue('ANGLE'))
        +          });
        +          break;
        +        case 'field_checkbox':
        +          fields.push({
        +            type: block.type,
        +            name: block.getFieldValue('FIELDNAME'),
        +            checked: block.getFieldValue('CHECKED') == 'TRUE'
        +          });
        +          break;
        +        case 'field_colour':
        +          fields.push({
        +            type: block.type,
        +            name: block.getFieldValue('FIELDNAME'),
        +            colour: block.getFieldValue('COLOUR')
        +          });
        +          break;
        +        case 'field_date':
        +          fields.push({
        +            type: block.type,
        +            name: block.getFieldValue('FIELDNAME'),
        +            date: block.getFieldValue('DATE')
        +          });
        +          break;
        +        case 'field_variable':
        +          fields.push({
        +            type: block.type,
        +            name: block.getFieldValue('FIELDNAME'),
        +            variable: block.getFieldValue('TEXT') || null
        +          });
        +          break;
        +        case 'field_dropdown':
        +          var options = [];
        +          for (var i = 0; i < block.optionCount_; i++) {
        +            options[i] = [block.getFieldValue('USER' + i),
        +                block.getFieldValue('CPU' + i)];
        +          }
        +          if (options.length) {
        +            fields.push({
        +              type: block.type,
        +              name: block.getFieldValue('FIELDNAME'),
        +              options: options
        +            });
        +          }
        +          break;
        +        case 'field_image':
        +          fields.push({
        +            type: block.type,
        +            src: block.getFieldValue('SRC'),
        +            width: Number(block.getFieldValue('WIDTH')),
        +            height: Number(block.getFieldValue('HEIGHT')),
        +            alt: block.getFieldValue('ALT')
        +          });
        +          break;
        +      }
        +    }
        +    block = block.nextConnection && block.nextConnection.targetBlock();
        +  }
        +  return fields;
        +};
        +
        +/**
        + * Fetch the type(s) defined in the given input.
        + * Format as a string for appending to the generated code.
        + * @param {!Blockly.Block} block Block with input.
        + * @param {string} name Name of the input.
        + * @return {?string} String defining the types.
        + */
        +BlockFactory.getOptTypesFrom = function(block, name) {
        +  var types = BlockFactory.getTypesFrom_(block, name);
        +  if (types.length == 0) {
        +    return undefined;
        +  } else if (types.indexOf('null') != -1) {
        +    return 'null';
        +  } else if (types.length == 1) {
        +    return types[0];
        +  } else {
        +    return '[' + types.join(', ') + ']';
        +  }
        +};
        +
        +/**
        + * Fetch the type(s) defined in the given input.
        + * @param {!Blockly.Block} block Block with input.
        + * @param {string} name Name of the input.
        + * @return {!Array.} List of types.
        + * @private
        + */
        +BlockFactory.getTypesFrom_ = function(block, name) {
        +  var typeBlock = block.getInputTargetBlock(name);
        +  var types;
        +  if (!typeBlock || typeBlock.disabled) {
        +    types = [];
        +  } else if (typeBlock.type == 'type_other') {
        +    types = [BlockFactory.escapeString(typeBlock.getFieldValue('TYPE'))];
        +  } else if (typeBlock.type == 'type_group') {
        +    types = [];
        +    for (var n = 0; n < typeBlock.typeCount_; n++) {
        +      types = types.concat(BlockFactory.getTypesFrom_(typeBlock, 'TYPE' + n));
        +    }
        +    // Remove duplicates.
        +    var hash = Object.create(null);
        +    for (var n = types.length - 1; n >= 0; n--) {
        +      if (hash[types[n]]) {
        +        types.splice(n, 1);
        +      }
        +      hash[types[n]] = true;
        +    }
        +  } else {
        +    types = [BlockFactory.escapeString(typeBlock.valueType)];
        +  }
        +  return types;
        +};
        +
        +// Generator Code
        +
        +/**
        + * Get the generator code for a given block.
        + *
        + * @param {!Blockly.Block} block - Rendered block in preview workspace.
        + * @param {string} generatorLanguage - 'JavaScript', 'Python', 'PHP', 'Lua',
        + *     'Dart'.
        + * @return {string} Generator code for multiple blocks.
        + */
        +BlockFactory.getGeneratorStub = function(block, generatorLanguage) {
        +  function makeVar(root, name) {
        +    name = name.toLowerCase().replace(/\W/g, '_');
        +    return '  var ' + root + '_' + name;
        +  }
        +  // The makevar function lives in the original update generator.
        +  var language = generatorLanguage;
        +  var code = [];
        +  code.push("Blockly." + language + "['" + block.type +
        +            "'] = function(block) {");
        +
        +  // 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;
        +      }
        +      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.FieldNumber) {
        +        code.push(makeVar('number', name) +
        +                  " = block.getFieldValue('" + name + "');");
        +      } else if (field instanceof Blockly.FieldTextInput) {
        +        code.push(makeVar('text', name) +
        +                  " = block.getFieldValue('" + name + "');");
        +      }
        +    }
        +    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 + "');");
        +      }
        +    }
        +  }
        +  // Most languages end lines with a semicolon.  Python does not.
        +  var lineEnd = {
        +    'JavaScript': ';',
        +    'Python': '',
        +    'PHP': ';',
        +    'Dart': ';'
        +  };
        +  code.push("  // TODO: Assemble " + language + " into code variable.");
        +  if (block.outputConnection) {
        +    code.push("  var code = '...';");
        +    code.push("  // TODO: Change ORDER_NONE to the correct strength.");
        +    code.push("  return [code, Blockly." + language + ".ORDER_NONE];");
        +  } else {
        +    code.push("  var code = '..." + (lineEnd[language] || '') + "\\n';");
        +    code.push("  return code;");
        +  }
        +  code.push("};");
        +
        +  return code.join('\n');
        +};
        +
        +/**
        + * 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 = BlockFactory.getGeneratorStub(block, language);
        +  BlockFactory.injectCode(generatorStub, 'generatorPre');
        +};
        +
        +// Preview Block
        +
        +/**
        + * 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();
        +
        +  // 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;
        +  }
        +  if (!code.trim()) {
        +    // Nothing to render.  Happens while cloud storage is loading.
        +    return;
        +  }
        +
        +  // 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;
        +  console.log(backupBlocks);
        +  try {
        +    // Make a shallow copy.
        +    Blockly.Blocks = Object.create(null);
        +    for (var prop in backupBlocks) {
        +      Blockly.Blocks[prop] = backupBlocks[prop];
        +    }
        +
        +    if (format == 'JSON') {
        +      var json = JSON.parse(code);
        +      Blockly.Blocks[json.type || BlockFactory.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;
        +    console.log('Blockly Blocks types');
        +    for (var type in Blockly.Blocks) {
        +      console.log(type);
        +      if (typeof Blockly.Blocks[type].init == 'function' &&
        +          Blockly.Blocks[type] != backupBlocks[type]) {
        +        blockType = type;
        +        console.log('found non matching type');
        +        console.log(blockType);
        +        break;
        +      }
        +    }
        +    if (!blockType) {
        +      console.log('non matching type NOT FOUND');
        +      return;
        +    }
        +
        +    // 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);
        +  } finally {
        +    Blockly.Blocks = backupBlocks;
        +  }
        +};
        +
        +// File Import, Creation, Download
        +
        +/**
        + * Generate a file from the contents of a given text area and
        + * download that file.
        + * @param {string} filename The name of the file to create.
        + * @param {string} id The text area to download.
        +*/
        +BlockFactory.downloadTextArea = function(filename, id) {
        +  var code = document.getElementById(id).textContent;
        +  BlockFactory.createAndDownloadFile_(code, filename, 'plain');
        +};
        +
        +/**
        + * Create a file with the given attributes and download it.
        + * @param {string} contents - The contents of the file.
        + * @param {string} filename - The name of the file to save to.
        + * @param {string} fileType - The type of the file to save.
        + * @private
        + */
        +BlockFactory.createAndDownloadFile_ = function(contents, filename, fileType) {
        +  var data = new Blob([contents], {type: 'text/' + fileType});
        +  var clickEvent = new MouseEvent("click", {
        +    "view": window,
        +    "bubbles": true,
        +    "cancelable": false
        +  });
        +
        +  var a = document.createElement('a');
        +  a.href = window.URL.createObjectURL(data);
        +  a.download = filename;
        +  a.textContent = 'Download file!';
        +  a.dispatchEvent(clickEvent);
        +};
        +
        +/**
        + * Save the workspace's xml representation to a file.
        + * @private
        + */
        +BlockFactory.saveWorkspaceToFile = function() {
        +  var xmlElement = Blockly.Xml.workspaceToDom(BlockFactory.mainWorkspace);
        +  var prettyXml = Blockly.Xml.domToPrettyText(xmlElement);
        +  BlockFactory.createAndDownloadFile_(prettyXml, 'blockXml', 'xml');
        +};
        +
        +/**
        + * Imports xml file for a block to the workspace.
        + */
        +BlockFactory.importBlockFromFile = function() {
        +  var files = document.getElementById('files');
        +  // If the file list is empty, they user likely canceled in the dialog.
        +  if (files.files.length > 0) {
        +    // The input tag doesn't have the "mulitple" attribute
        +    // so the user can only choose 1 file.
        +    var file = files.files[0];
        +    var fileReader = new FileReader();
        +    fileReader.addEventListener('load', function(event) {
        +      var fileContents = event.target.result;
        +      var xml = '';
        +      try {
        +        xml = Blockly.Xml.textToDom(fileContents);
        +      } catch (e) {
        +        var message = 'Could not load your saved file.\n'+
        +          'Perhaps it was created with a different version of Blockly?';
        +        window.alert(message + '\nXML: ' + fileContents);
        +        return;
        +      }
        +      BlockFactory.mainWorkspace.clear();
        +      Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace);
        +    });
        +
        +    fileReader.readAsText(file);
        +  }
        +};
        +
        +/**
        + * 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 == 'Manual';
        +  linkButton.disabled = disabled;
        +  saveBlockButton.disabled = disabled;
        +  saveToLibButton.disabled = disabled;
        +};
        +
        +// Block Factory Expansion View Utils
        +
        +/**
        + * Render starter block (factory_base).
        + */
        + BlockFactory.showStarterBlock = function() {
        +    var xml = '';
        +    Blockly.Xml.domToWorkspace(
        +        Blockly.Xml.textToDom(xml), BlockFactory.mainWorkspace);
        +};
        +
        +/**
        + * Hides element so that it's invisible and doesn't take up space.
        + *
        + * @param {string} elementID - ID of element to hide.
        + */
        +BlockFactory.hide = function(elementID) {
        +  document.getElementById(elementID).style.display = 'none';
        +};
        +
        +/**
        + * Un-hides an element.
        + *
        + * @param {string} elementID - ID of element to hide.
        + */
        +BlockFactory.show = function(elementID) {
        +  document.getElementById(elementID).style.display = 'block';
        +};
        +
        +/**
        + * Hides element so that it's invisible but still takes up space.
        + *
        + * @param {string} elementID - ID of element to hide.
        + */
        +BlockFactory.makeInvisible = function(elementID) {
        +  document.getElementById(elementID).visibility = 'hidden';
        +};
        +
        +/**
        + * Makes element visible.
        + *
        + * @param {string} elementID - ID of element to hide.
        + */
        +BlockFactory.makeVisible = function(elementID) {
        +  document.getElementById(elementID).visibility = 'visible';
        +};
        +
        diff --git a/demos/blocklyfactory/index.html b/demos/blocklyfactory/index.html
        new file mode 100644
        index 000000000..86c2ef442
        --- /dev/null
        +++ b/demos/blocklyfactory/index.html
        @@ -0,0 +1,282 @@
        +
        +
        +
        +
        +
        +  
        +  
        +  Blockly Demo: Blockly Factory
        +  
        +  
        +  
        +  
        +  
        +  
        +  
        +  
        +  
        +  
        +  
        +  
        +  
        +  
        +  
        +  
        +  
        +
        +
        +  

        Blockly > + Demos > Blockly Factory

        + +
        +
        Block Factory
        +
        +
        Workspace Factory
        +
        +
        Exporter
        +
        + +
        +

        Block Selector

        +
        +

        Drag blocks into your workspace to select them for download.

        +
        +
        + + + +
        + +
        + +
        +

        Export Settings

        +
        +
        + Toolbox Xml: + +
        + Pre-loaded Workspace: + +
        + Workspace Option(s): + +
        +
        + Block Definition(s): +
        + Language code: +
        + Block Definition(s) File Name:
        + +
        +
        + Generator Stub(s): +
        +
        + Block Generator Stub(s) File Name:
        +
        +
        +
        + +
        +
        + +
        +
        + + + + + + + + + +
        + + + + + +
        + +

        Block Library:

        + +
        +
        + + + +
        +
        + + + + + +
        +

        Preview: + +

        +
        + + + + + + + +
        +
        +
        +
        +
        + + + + + + + + + + + + + + + + +
        +
        +
        +

        Language code: + + +

        +
        +
        
        +              
        +            
        +

        Generator stub: + + +

        +
        +
        
        +            
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 20 + 65 + 120 + 160 + 210 + 230 + 260 + 290 + 330 + + + + \ No newline at end of file diff --git a/demos/blocklyfactory/link.png b/demos/blocklyfactory/link.png new file mode 100644 index 000000000..11dfd8284 Binary files /dev/null and b/demos/blocklyfactory/link.png differ diff --git a/demos/blocklyfactory/workspacefactory/index.html b/demos/blocklyfactory/workspacefactory/index.html new file mode 100644 index 000000000..061749f59 --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/index.html @@ -0,0 +1,682 @@ + + +Blockly Workspace Factory + + + + + + + + + + + + + + + + + + + + + +
        +

        Blockly‏ > + Demos‏ > + Workspace Factory +

        +
        +

        + + + + + +

        +
        + +
        +

        Drag blocks into your toolbox:

        +
        +
        +
        +
        + +
        + + + + + + diff --git a/demos/blocklyfactory/workspacefactory/standard_categories.js b/demos/blocklyfactory/workspacefactory/standard_categories.js new file mode 100644 index 000000000..5112c5ee2 --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/standard_categories.js @@ -0,0 +1,375 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Contains a map of standard Blockly categories used to load + * standard Blockly categories into the user's toolbox. The map is keyed by + * the lower case name of the category, and contains the Category object for + * that particular category. + * + * @author Emma Dauterman (evd2014) + */ + +FactoryController.prototype.standardCategories = Object.create(null); + +FactoryController.prototype.standardCategories['logic'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Logic'); +FactoryController.prototype.standardCategories['logic'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['logic'].color = '#5C81A6'; + +FactoryController.prototype.standardCategories['loops'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Loops'); +FactoryController.prototype.standardCategories['loops'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '10' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '10' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['loops'].color = '#5CA65C'; + +FactoryController.prototype.standardCategories['math'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Math'); +FactoryController.prototype.standardCategories['math'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '' + + '' + + '9' + + '' + + '' + + '' + + '' + + '' + + '' + + '45' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '0' + + '' + + '' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '' + + '' + + '3.1' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '64' + + '' + + '' + + '' + + '' + + '10'+ + '' + + '' + + '' + + '' + + '' + + '' + + '50' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '100' + + '' + + '' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '100' + + '' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['math'].color = '#5C68A6'; + +FactoryController.prototype.standardCategories['text'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Text'); +FactoryController.prototype.standardCategories['text'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + 'text' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'text' + + '' + + '' + + '' + + '' + + '' + + '' + + 'text' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['text'].color = '#5CA68D'; + +FactoryController.prototype.standardCategories['lists'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Lists'); +FactoryController.prototype.standardCategories['lists'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '5' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + ',' + + '' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['lists'].color = '#745CA6'; + +FactoryController.prototype.standardCategories['colour'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Colour'); +FactoryController.prototype.standardCategories['colour'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '100' + + '' + + '' + + '' + + '' + + '50' + + '' + + '' + + '' + + '' + + '0' + + '' + + '' + + '' + + '' + + '' + + '' + + '#ff0000' + + '' + + '' + + '' + + '' + + '#3333ff' + + '' + + '' + + '' + + '' + + '0.5' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['colour'].color = '#A6745C'; + +FactoryController.prototype.standardCategories['functions'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Functions'); +FactoryController.prototype.standardCategories['functions'].color = '#9A5CA6' +FactoryController.prototype.standardCategories['functions'].custom = + 'PROCEDURE'; + +FactoryController.prototype.standardCategories['variables'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Variables'); +FactoryController.prototype.standardCategories['variables'].color = '#A65C81'; +FactoryController.prototype.standardCategories['variables'].custom = 'VARIABLE'; diff --git a/demos/blocklyfactory/workspacefactory/style.css b/demos/blocklyfactory/workspacefactory/style.css new file mode 100644 index 000000000..efa5897fa --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/style.css @@ -0,0 +1,238 @@ +body { + background-color: #fff; + font-family: sans-serif; +} + +h1 { + font-weight: normal; + font-size: 140%; +} + +section { + float: left; +} + +aside { + float: right; +} + +#categoryTable>table { + border: 1px solid #ccc; + border-bottom: none; +} + +td.tabon { + border-bottom-color: #ddd !important; + background-color: #ddd; + padding: 5px 19px; +} + +td.taboff { + cursor: pointer; + padding: 5px 19px; +} + +td.taboff:hover { + background-color: #eee; +} + +button { + border-radius: 4px; + border: 1px solid #ddd; + background-color: #eee; + color: #000; + font-size: large; + margin: 0 5px; + padding: 10px; +} + +button:hover:not(:disabled) { + box-shadow: 2px 2px 5px #888; +} + +button:disabled { + opacity: .6; +} + +button>* { + opacity: .6; + vertical-align: text-bottom; +} + +button:hover:not(:disabled)>* { + opacity: 1; +} + +label { + border-radius: 4px; + border: 1px solid #ddd; + background-color: #eee; + color: #000; + font-size: large; + margin: 0 5px; + padding: 10px; +} + +label:hover:not(:disabled) { + box-shadow: 2px 2px 5px #888; +} + +label:disabled { + opacity: .6; +} + +label>* { + opacity: .6; + vertical-align: text-bottom; +} + +label:hover:not(:disabled)>* { + opacity: 1; +} + +table { + border: none; + border-collapse: collapse; + margin: 0; + padding: 0; +} + +td { + padding: 0; + vertical-align: top; +} + +.inputfile { + height: 0; + opacity: 0; + overflow: hidden; + position: absolute; + width: 0; + z-index: -1; +} + +#toolbox_section { + height: 480px; + width: 80%; + position: relative; +} + +#toolbox_blocks { + height: 100%; + width: 100%; +} + +#preview_blocks { + height: 300px; + width: 100%; +} + +#createDiv { + width: 70%; +} + +#previewDiv { + width: 30%; +} + +#category_section { + width: 20%; +} + +#disable_div { + background-color: white; + height: 100%; + left: 0; + opacity: .5; + position: absolute; + top: 0; + width: 100%; + z-index: -1; /* Start behind workspace */ +} + +/* Rules for Closure popup color picker */ +.goog-palette { + outline: none; + cursor: default; +} + +.goog-palette-cell { + height: 13px; + width: 15px; + margin: 0; + border: 0; + text-align: center; + vertical-align: middle; + border-right: 1px solid #000000; + font-size: 1px; +} + +.goog-palette-colorswatch { + border: 1px solid #000000; + height: 13px; + position: relative; + width: 15px; +} + +.goog-palette-cell-hover .goog-palette-colorswatch { + border: 1px solid #FFF; +} + +.goog-palette-cell-selected .goog-palette-colorswatch { + border: 1px solid #000; + color: #fff; +} + +.goog-palette-table { + border: 1px solid #000; + border-collapse: collapse; +} + +.goog-popupcolorpicker { + position: absolute; +} + +/* The container
        - needed to position the dropdown content */ +.dropdown { + position: relative; + display: inline-block; +} + +/* Dropdown Content (Hidden by Default) */ +.dropdown-content { + background-color: #f9f9f9; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,.2); + display: none; + min-width: 170px; + opacity: 1; + position: absolute; + z-index: 1; +} + +/* Links inside the dropdown */ +.dropdown-content a { + color: black; + display: block; + padding: 12px 16px; + text-decoration: none; +} + +/* Change color of dropdown links on hover */ +.dropdown-content a:hover { + background-color: #f1f1f1 +} + +/* Show the dropdown menu */ +.show { + display: block; +} + +.shadowBlock>.blocklyPath { + fill-opacity: .5; + stroke-opacity: .5; +} + +.shadowBlock>.blocklyPathLight, +.shadowBlock>.blocklyPathDark { + display: none; +} diff --git a/demos/blocklyfactory/workspacefactory/wfactory_controller.js b/demos/blocklyfactory/workspacefactory/wfactory_controller.js new file mode 100644 index 000000000..994f0442e --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/wfactory_controller.js @@ -0,0 +1,704 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Contains the controller code for workspace factory. Depends + * on the model and view objects (created as internal variables) and interacts + * with previewWorkspace and toolboxWorkspace (internal references stored to + * both). Also depends on standard_categories.js for standard Blockly + * categories. Provides the functionality for the actions the user can initiate: + * - adding and removing categories + * - switching between categories + * - printing and downloading configuration xml + * - updating the preview workspace + * - changing a category name + * - moving the position of a category. + * + * @author Emma Dauterman (evd2014) + */ + +/** + * Class for a FactoryController + * @constructor + * @param {!Blockly.workspace} toolboxWorkspace workspace where blocks are + * dragged into corresponding categories + * @param {!Blockly.workspace} previewWorkspace workspace that shows preview + * of what workspace would look like using generated XML + */ +FactoryController = function(toolboxWorkspace, previewWorkspace) { + // Workspace for user to drag blocks in for a certain category. + this.toolboxWorkspace = toolboxWorkspace; + // Workspace for user to preview their changes. + this.previewWorkspace = previewWorkspace; + // Model to keep track of categories and blocks. + this.model = new FactoryModel(); + // Updates the category tabs. + this.view = new FactoryView(); + // Generates XML for categories. + this.generator = new FactoryGenerator(this.model); +}; + +/** + * Currently prompts the user for a name, checking that it's valid (not used + * before), and then creates a tab and switches to it. + */ +FactoryController.prototype.addCategory = function() { + // Check if it's the first category added. + var firstCategory = !this.model.hasToolbox(); + // Give the option to save blocks if their workspace is not empty and they + // are creating their first category. + if (firstCategory && this.toolboxWorkspace.getAllBlocks().length > 0) { + var confirmCreate = confirm('Do you want to save your work in another ' + + 'category? If you don\'t, the blocks in your workspace will be ' + + 'deleted.'); + // Create a new category for current blocks. + if (confirmCreate) { + var name = prompt('Enter the name of the category for your ' + + 'current blocks: '); + if (!name) { // Exit if cancelled. + return; + } + this.createCategory(name, true); + this.model.setSelectedById(this.model.getCategoryIdByName(name)); + } + } + // After possibly creating a category, check again if it's the first category. + firstCategory = !this.model.hasToolbox(); + // Get name from user. + name = this.promptForNewCategoryName('Enter the name of your new category: '); + if (!name) { //Exit if cancelled. + return; + } + // Create category. + this.createCategory(name, firstCategory); + // Switch to category. + this.switchElement(this.model.getCategoryIdByName(name)); + // Update preview. + this.updatePreview(); +}; + +/** + * Helper method for addCategory. Adds a category to the view given a name, ID, + * and a boolean for if it's the first category created. Assumes the category + * has already been created in the model. Does not switch to category. + * + * @param {!string} name Name of category being added. + * @param {!string} id The ID of the category being added. + * @param {boolean} firstCategory True if it's the first category created, + * false otherwise. + */ +FactoryController.prototype.createCategory = function(name, firstCategory) { + // Create empty category + var category = new ListElement(ListElement.TYPE_CATEGORY, name); + this.model.addElementToList(category); + // Create new category. + var tab = this.view.addCategoryRow(name, category.id, firstCategory); + this.addClickToSwitch(tab, category.id); +}; + +/** + * Given a tab and a ID to be associated to that tab, adds a listener to + * that tab so that when the user clicks on the tab, it switches to the + * element associated with that ID. + * + * @param {!Element} tab The DOM element to add the listener to. + * @param {!string} id The ID of the element to switch to when tab is clicked. + */ +FactoryController.prototype.addClickToSwitch = function(tab, id) { + var self = this; + var clickFunction = function(id) { // Keep this in scope for switchElement + return function() { + self.switchElement(id); + }; + }; + this.view.bindClick(tab, clickFunction(id)); +}; + +/** + * Attached to "-" button. Checks if the user wants to delete + * the current element. Removes the element and switches to another element. + * When the last element is removed, it switches to a single flyout mode. + * + */ +FactoryController.prototype.removeElement = function() { + // Check that there is a currently selected category to remove. + if (!this.model.getSelected()) { + return; + } + // Check if user wants to remove current category. + var check = confirm('Are you sure you want to delete the currently selected ' + + this.model.getSelected().type + '?'); + if (!check) { // If cancelled, exit. + return; + } + var selectedId = this.model.getSelectedId(); + var selectedIndex = this.model.getIndexByElementId(selectedId); + // Delete element visually. + this.view.deleteElementRow(selectedId, selectedIndex); + // Delete element in model. + this.model.deleteElementFromList(selectedIndex); + // Find next logical element to switch to. + var next = this.model.getElementByIndex(selectedIndex); + if (!next && this.model.hasToolbox()) { + next = this.model.getElementByIndex(selectedIndex - 1); + } + var nextId = next ? next.id : null; + // Open next element. + this.clearAndLoadElement(nextId); + if (!nextId) { + alert('You currently have no categories or separators. All your blocks' + + ' will be displayed in a single flyout.'); + } + // Update preview. + this.updatePreview(); +}; + +/** + * Gets a valid name for a new category from the user. + * + * @param {!string} promptString Prompt for the user to enter a name. + * @return {string} Valid name for a new category, or null if cancelled. + */ +FactoryController.prototype.promptForNewCategoryName = function(promptString) { + do { + var name = prompt(promptString); + if (!name) { // If cancelled. + return null; + } + } while (this.model.hasCategoryByName(name)); + return name; +} + +/** + * Switches to a new tab for the element given by ID. Stores XML and blocks + * to reload later, updates selected accordingly, and clears the workspace + * and clears undo, then loads the new element. + * + * @param {!string} id ID of tab to be opened, must be valid element ID. + */ +FactoryController.prototype.switchElement = function(id) { + // Disables events while switching so that Blockly delete and create events + // don't update the preview repeatedly. + Blockly.Events.disable(); + // Caches information to reload or generate xml if switching to/from element. + // Only saves if a category is selected. + if (this.model.getSelectedId() != null && id != null) { + this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace); + } + // Load element. + this.clearAndLoadElement(id); + // Enable Blockly events again. + Blockly.Events.enable(); +}; + +/** + * Switches to a new tab for the element by ID. Helper for switchElement. + * Updates selected, clears the workspace and clears undo, loads a new element. + * + * @param {!string} id ID of category to load + */ +FactoryController.prototype.clearAndLoadElement = function(id) { + // Unselect current tab if switching to and from an element. + if (this.model.getSelectedId() != null && id != null) { + this.view.setCategoryTabSelection(this.model.getSelectedId(), false); + } + + // If switching from a separator, enable workspace in view. + if (this.model.getSelectedId() != null && this.model.getSelected().type == + ListElement.TYPE_SEPARATOR) { + this.view.disableWorkspace(false); + } + + // Set next category. + this.model.setSelectedById(id); + + // Clear workspace. + this.toolboxWorkspace.clear(); + this.toolboxWorkspace.clearUndo(); + + // Loads next category if switching to an element. + if (id != null) { + this.view.setCategoryTabSelection(id, true); + Blockly.Xml.domToWorkspace(this.model.getSelectedXml(), + this.toolboxWorkspace); + // Disable workspace if switching to a separator. + if (this.model.getSelected().type == ListElement.TYPE_SEPARATOR) { + this.view.disableWorkspace(true); + } + } + + // Mark all shadow blocks laoded and order blocks as if shown in a flyout. + this.view.markShadowBlocks(this.model.getShadowBlocksInWorkspace + (toolboxWorkspace.getAllBlocks())); + this.toolboxWorkspace.cleanUp_(); + + // Update category editing buttons. + this.view.updateState(this.model.getIndexByElementId + (this.model.getSelectedId()), this.model.getSelected()); +}; + +/** + * Tied to "Export Config" button. Gets a file name from the user and downloads + * the corresponding configuration xml to that file. + */ +FactoryController.prototype.exportConfig = function() { + // Generate XML. + var configXml = Blockly.Xml.domToPrettyText + (this.generator.generateConfigXml(this.toolboxWorkspace)); + // Get file name. + var fileName = prompt("File Name: "); + if (!fileName) { // If cancelled + return; + } + // Download file. + var data = new Blob([configXml], {type: 'text/xml'}); + this.view.createAndDownloadFile(fileName, data); + }; + +/** + * Tied to "Print Config" button. Mainly used for debugging purposes. Prints + * the configuration XML to the console. + */ +FactoryController.prototype.printConfig = function() { + window.console.log(Blockly.Xml.domToPrettyText + (this.generator.generateConfigXml(this.toolboxWorkspace))); +}; + +/** + * Updates the preview workspace based on the toolbox workspace. If switching + * from no categories to categories or categories to no categories, reinjects + * Blockly with reinjectPreview, otherwise just updates without reinjecting. + * Called whenever a list element is created, removed, or modified and when + * Blockly move and delete events are fired. Do not call on create events + * or disabling will cause the user to "drop" their current blocks. + */ +FactoryController.prototype.updatePreview = function() { + // Disable events to stop updatePreview from recursively calling itself + // through event handlers. + Blockly.Events.disable(); + var tree = Blockly.Options.parseToolboxTree + (this.generator.generateConfigXml(this.toolboxWorkspace)); + // No categories, creates a simple flyout. + if (tree.getElementsByTagName('category').length == 0) { + if (this.previewWorkspace.toolbox_) { + this.reinjectPreview(tree); // Switch to simple flyout, more expensive. + } else { + this.previewWorkspace.flyout_.show(tree.childNodes); + } + // Uses categories, creates a toolbox. + } else { + if (!previewWorkspace.toolbox_) { + this.reinjectPreview(tree); // Create a toolbox, more expensive. + } else { + this.previewWorkspace.toolbox_.populate_(tree); + } + } + // Reenable events. + Blockly.Events.enable(); +}; + +/** + * Used to completely reinject the preview workspace. This should be used only + * when switching from simple flyout to categories, or categories to simple + * flyout. More expensive than simply updating the flyout or toolbox. + * + * @param {!Element} tree of xml elements + * @package + */ +FactoryController.prototype.reinjectPreview = function(tree) { + this.previewWorkspace.dispose(); + previewToolbox = Blockly.Xml.domToPrettyText(tree); + this.previewWorkspace = Blockly.inject('preview_blocks', + {grid: + {spacing: 25, + length: 3, + colour: '#ccc', + snap: true}, + media: '../../../media/', + toolbox: previewToolbox, + zoom: + {controls: true, + wheel: true} + }); +}; + +/** + * Tied to "change name" button. Changes the name of the selected category. + * Continues prompting the user until they input a category name that is not + * currently in use, exits if user presses cancel. + */ +FactoryController.prototype.changeCategoryName = function() { + // Return if no category selected or element a separator. + if (!this.model.getSelected() || + this.model.getSelected().type == ListElement.TYPE_SEPARATOR) { + return; + } + // Get new name from user. + var newName = this.promptForNewCategoryName('What do you want to change this' + + ' category\'s name to?'); + if (!newName) { // If cancelled. + return; + } + // Change category name. + this.model.getSelected().changeName(newName); + this.view.updateCategoryName(newName, this.model.getSelectedId()); + // Update preview. + this.updatePreview(); +}; + +/** + * Tied to arrow up and arrow down buttons. Swaps with the element above or + * below the currently selected element (offset categories away from the + * current element). Updates state to enable the correct element editing + * buttons. + * + * @param {int} offset The index offset from the currently selected element + * to swap with. Positive if the element to be swapped with is below, negative + * if the element to be swapped with is above. + */ +FactoryController.prototype.moveElement = function(offset) { + var curr = this.model.getSelected(); + if (!curr) { // Return if no selected element. + return; + } + var currIndex = this.model.getIndexByElementId(curr.id); + var swapIndex = this.model.getIndexByElementId(curr.id) + offset; + var swap = this.model.getElementByIndex(swapIndex); + if (!swap) { // Return if cannot swap in that direction. + return; + } + // Move currently selected element to index of other element. + // Indexes must be valid because confirmed that curr and swap exist. + this.moveElementToIndex(curr, swapIndex, currIndex); + // Update element editing buttons. + this.view.updateState(swapIndex, this.model.getSelected()); + // Update preview. + this.updatePreview(); +}; + +/** + * Moves a element to a specified index and updates the model and view + * accordingly. Helper functions throw an error if indexes are out of bounds. + * + * @param {!Element} element The element to move. + * @param {int} newIndex The index to insert the element at. + * @param {int} oldIndex The index the element is currently at. + */ +FactoryController.prototype.moveElementToIndex = function(element, newIndex, + oldIndex) { + this.model.moveElementToIndex(element, newIndex, oldIndex); + this.view.moveTabToIndex(element.id, newIndex, oldIndex); +}; + +/** + * Changes the color of the selected category. Return if selected element is + * a separator. + * + * @param {!string} color The color to change the selected category. Must be + * a valid CSS string. + */ +FactoryController.prototype.changeSelectedCategoryColor = function(color) { + // Return if no category selected or element a separator. + if (!this.model.getSelected() || + this.model.getSelected().type == ListElement.TYPE_SEPARATOR) { + return; + } + // Change color of selected category. + this.model.getSelected().changeColor(color); + this.view.setBorderColor(this.model.getSelectedId(), color); + this.updatePreview(); +}; + +/** + * Tied to the "Standard Category" dropdown option, this function prompts + * the user for a name of a standard Blockly category (case insensitive) and + * loads it as a new category and switches to it. Leverages standardCategories + * map in standard_categories.js. + */ +FactoryController.prototype.loadCategory = function() { + // Prompt user for the name of the standard category to load. + do { + var name = prompt('Enter the name of the category you would like to import ' + + '(Logic, Loops, Math, Text, Lists, Colour, Variables, or Functions)'); + if (!name) { + return; // Exit if cancelled. + } + } while (!this.isStandardCategoryName(name)); + + // Check if the user can create that standard category. + if (this.model.hasVariables() && name.toLowerCase() == 'variables') { + alert('A Variables category already exists. You cannot create multiple' + + ' variables categories.'); + return; + } + if (this.model.hasProcedures() && name.toLowerCase() == 'functions') { + alert('A Functions category already exists. You cannot create multiple' + + ' functions categories.'); + return; + } + // Check if the user can create a category with that name. + var standardCategory = this.standardCategories[name.toLowerCase()] + if (this.model.hasCategoryByName(standardCategory.name)) { + alert('You already have a category with the name ' + standardCategory.name + + '. Rename your category and try again.'); + return; + } + + // Copy the standard category in the model. + var copy = standardCategory.copy(); + + // Add the copy in the view. + var tab = this.view.addCategoryRow(copy.name, copy.id, + !this.model.hasToolbox()); + + // Add it to the model. + this.model.addElementToList(copy); + + // Update the view. + this.addClickToSwitch(tab, copy.id); + // Color the category tab in the view. + if (copy.color) { + this.view.setBorderColor(copy.id, copy.color); + } + // Switch to loaded category. + this.switchElement(copy.id); + // Convert actual shadow blocks to user-generated shadow blocks. + this.convertShadowBlocks(); + // Update preview. + this.updatePreview(); +}; + +/** + * Given the name of a category, determines if it's the name of a standard + * category (case insensitive). + * + * @param {string} name The name of the category that should be checked if it's + * in standardCategories + * @return {boolean} True if name is a standard category name, false otherwise. + */ +FactoryController.prototype.isStandardCategoryName = function(name) { + for (var category in this.standardCategories) { + if (name.toLowerCase() == category) { + return true; + } + } + return false; +}; + +/** + * Connected to the "add separator" dropdown option. If categories already + * exist, adds a separator to the model and view. Does not switch to select + * the separator, and updates the preview. + */ +FactoryController.prototype.addSeparator = function() { + // Don't allow the user to add a separator if a category has not been created. + if (!this.model.hasToolbox()) { + alert('Add a category before adding a separator.'); + return; + } + // Create the separator in the model. + var separator = new ListElement(ListElement.TYPE_SEPARATOR); + this.model.addElementToList(separator); + // Create the separator in the view. + var tab = this.view.addSeparatorTab(separator.id); + this.addClickToSwitch(tab, separator.id); + // Switch to the separator and update the preview. + this.switchElement(separator.id); + this.updatePreview(); +}; + +/** + * Connected to the import button. Given the file path inputted by the user + * from file input, this function loads that toolbox XML to the workspace, + * creating category and separator tabs as necessary. This allows the user + * to be able to edit toolboxes given their XML form. Catches errors from + * file reading and prints an error message alerting the user. + * + * @param {string} file The path for the file to be imported into the workspace. + * Should contain valid toolbox XML. + */ +FactoryController.prototype.importFile = function(file) { + // Exit if cancelled. + if (!file) { + return; + } + + var reader = new FileReader(); + // To be executed when the reader has read the file. + reader.onload = function() { + // Try to parse XML from file and load it into toolbox editing area. + // Print error message if fail. + try { + var tree = Blockly.Xml.textToDom(reader.result); + controller.importFromTree_(tree); + } catch(e) { + alert('Cannot load XML from file.'); + console.log(e); + } + } + + // Read the file. + reader.readAsText(file); +}; + +/** + * Given a XML DOM tree, loads it into the toolbox editing area so that the + * user can continue editing their work. Assumes that tree is in valid toolbox + * XML format. + * @private + * + * @param {!Element} tree XML tree to be loaded to toolbox editing area. + */ +FactoryController.prototype.importFromTree_ = function(tree) { + // Clear current editing area. + this.model.clearToolboxList(); + this.view.clearToolboxTabs(); + + if (tree.getElementsByTagName('category').length == 0) { + // No categories present. + // Load all the blocks into a single category evenly spaced. + Blockly.Xml.domToWorkspace(tree, this.toolboxWorkspace); + this.toolboxWorkspace.cleanUp_(); + + // Convert actual shadow blocks to user-generated shadow blocks. + this.convertShadowBlocks(); + + // Add message to denote empty category. + this.view.addEmptyCategoryMessage(); + } else { + // Categories/separators present. + for (var i = 0, item; item = tree.children[i]; i++) { + if (item.tagName == 'category') { + // If the element is a category, create a new category and switch to it. + this.createCategory(item.getAttribute('name'), false); + var category = this.model.getElementByIndex(i); + this.switchElement(category.id); + + // Load all blocks in that category to the workspace to be evenly + // spaced and saved to that category. + for (var j = 0, blockXml; blockXml = item.children[j]; j++) { + Blockly.Xml.domToBlock(blockXml, this.toolboxWorkspace); + } + + // Evenly space the blocks. + // TODO(evd2014): Change to cleanUp once cleanUp_ is made public in + // master. + this.toolboxWorkspace.cleanUp_(); + + // Convert actual shadow blocks to user-generated shadow blocks. + this.convertShadowBlocks(); + + // Set category color. + if (item.getAttribute('colour')) { + category.changeColor(item.getAttribute('colour')); + this.view.setBorderColor(category.id, category.color); + } + // Set any custom tags. + if (item.getAttribute('custom')) { + this.model.addCustomTag(category, item.getAttribute('custom')); + } + } else { + // If the element is a separator, add the separator and switch to it. + this.addSeparator(); + this.switchElement(this.model.getElementByIndex(i).id); + } + } + } + this.view.updateState(this.model.getIndexByElementId + (this.model.getSelectedId()), this.model.getSelected()); + this.updatePreview(); +}; + +/** + * Clears the toolbox editing area completely, deleting all categories and all + * blocks in the model and view. + */ +FactoryController.prototype.clear = function() { + this.model.clearToolboxList(); + this.view.clearToolboxTabs(); + this.view.addEmptyCategoryMessage(); + this.view.updateState(-1, null); + this.toolboxWorkspace.clear(); + this.toolboxWorkspace.clearUndo(); + this.updatePreview(); +}; + +/* + * Makes the currently selected block a user-generated shadow block. These + * blocks are not made into real shadow blocks, but recorded in the model + * and visually marked as shadow blocks, allowing the user to move and edit + * them (which would be impossible with actual shadow blocks). Updates the + * preview when done. + * + */ +FactoryController.prototype.addShadow = function() { + // No block selected to make a shadow block. + if (!Blockly.selected) { + return; + } + this.view.markShadowBlock(Blockly.selected); + this.model.addShadowBlock(Blockly.selected.id); + this.updatePreview(); +}; + +/** + * If the currently selected block is a user-generated shadow block, this + * function makes it a normal block again, removing it from the list of + * shadow blocks and loading the workspace again. Updates the preview again. + * + */ +FactoryController.prototype.removeShadow = function() { + // No block selected to modify. + if (!Blockly.selected) { + return; + } + this.model.removeShadowBlock(Blockly.selected.id); + this.view.unmarkShadowBlock(Blockly.selected); + this.updatePreview(); +}; + +/** + * Given a unique block ID, uses the model to determine if a block is a + * user-generated shadow block. + * + * @param {!string} blockId The unique ID of the block to examine. + * @return {boolean} True if the block is a user-generated shadow block, false + * otherwise. + */ +FactoryController.prototype.isUserGenShadowBlock = function(blockId) { + return this.model.isShadowBlock(blockId); +} + +/** + * Call when importing XML containing real shadow blocks. This function turns + * all real shadow blocks loaded in the workspace into user-generated shadow + * blocks, meaning they are marked as shadow blocks by the model and appear as + * shadow blocks in the view but are still editable and movable. + */ +FactoryController.prototype.convertShadowBlocks = function() { + var blocks = this.toolboxWorkspace.getAllBlocks(); + for (var i = 0, block; block = blocks[i]; i++) { + if (block.isShadow()) { + block.setShadow(false); + this.model.addShadowBlock(block.id); + this.view.markShadowBlock(block); + } + } +}; diff --git a/demos/blocklyfactory/workspacefactory/wfactory_generator.js b/demos/blocklyfactory/workspacefactory/wfactory_generator.js new file mode 100644 index 000000000..2de701e80 --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/wfactory_generator.js @@ -0,0 +1,159 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Generates the configuration xml used to update the preview + * workspace or print to the console or download to a file. Leverages + * Blockly.Xml and depends on information in the model (holds a reference). + * Depends on a hidden workspace created in the generator to load saved XML in + * order to generate toolbox XML. + * + * @author Emma Dauterman (evd2014) + */ + +/** + * Class for a FactoryGenerator + * @constructor + */ +FactoryGenerator = function(model) { + // Model to share information about categories and shadow blocks. + this.model = model; + // Create hidden workspace to load saved XML to generate toolbox XML. + var hiddenBlocks = document.createElement('div'); + // Generate a globally unique ID for the hidden div element to avoid + // collisions. + var hiddenBlocksId = Blockly.genUid(); + hiddenBlocks.id = hiddenBlocksId; + hiddenBlocks.style.display = 'none'; + document.body.appendChild(hiddenBlocks); + this.hiddenWorkspace = Blockly.inject(hiddenBlocksId); +}; + +/** + * Generates the xml for the toolbox or flyout with information from + * toolboxWorkspace and the model. Uses the hiddenWorkspace to generate XML. + * + * @param {!Blockly.workspace} toolboxWorkspace Toolbox editing workspace where + * blocks are added by user to be part of the toolbox. + * @return {!Element} XML element representing toolbox or flyout corresponding + * to toolbox workspace. + */ +FactoryGenerator.prototype.generateConfigXml = function(toolboxWorkspace) { + // Create DOM for XML. + var xmlDom = goog.dom.createDom('xml', + { + 'id' : 'toolbox', + 'style' : 'display:none' + }); + if (!this.model.hasToolbox()) { + // Toolbox has no categories. Use XML directly from workspace. + this.loadToHiddenWorkspaceAndSave_ + (Blockly.Xml.workspaceToDom(toolboxWorkspace), xmlDom); + } else { + // Toolbox has categories. + // Assert that selected != null + if (!this.model.getSelected()) { + throw new Error('Selected is null when the toolbox is empty.'); + } + + // Capture any changes made by user before generating XML. + this.model.getSelected().saveFromWorkspace(toolboxWorkspace); + var xml = this.model.getSelectedXml(); + var toolboxList = this.model.getToolboxList(); + + // Iterate through each category to generate XML for each using the + // hidden workspace. Load each category to the hidden workspace to make sure + // that all the blocks that are not top blocks are also captured as block + // groups in the flyout. + for (var i = 0; i < toolboxList.length; i++) { + var element = toolboxList[i]; + if (element.type == ListElement.TYPE_SEPARATOR) { + // If the next element is a separator. + var nextElement = goog.dom.createDom('sep'); + } else { + // If the next element is a category. + var nextElement = goog.dom.createDom('category'); + nextElement.setAttribute('name', element.name); + // Add a colour attribute if one exists. + if (element.color != null) { + nextElement.setAttribute('colour', element.color); + } + // Add a custom attribute if one exists. + if (element.custom != null) { + nextElement.setAttribute('custom', element.custom); + } + // Load that category to hidden workspace, setting user-generated shadow + // blocks as real shadow blocks. + this.loadToHiddenWorkspaceAndSave_(element.xml, nextElement); + } + xmlDom.appendChild(nextElement); + } + } + return xmlDom; + }; + +/** + * Load the given XML to the hidden workspace, set any user-generated shadow + * blocks to be actual shadow blocks, then append the XML from the workspace + * to the DOM element passed in. + * @private + * + * @param {!Element} xml The XML to be loaded to the hidden workspace. + * @param {!Element} dom The DOM element to append the generated XML to. + */ +FactoryGenerator.prototype.loadToHiddenWorkspaceAndSave_ = function(xml, dom) { + this.hiddenWorkspace.clear(); + Blockly.Xml.domToWorkspace(xml, this.hiddenWorkspace); + this.setShadowBlocksInHiddenWorkspace_(); + this.appendHiddenWorkspaceToDom_(dom); +} + + /** + * Encodes blocks in the hidden workspace in a XML DOM element. Very + * similar to workspaceToDom, but doesn't capture IDs. Uses the top-level + * blocks loaded in hiddenWorkspace. + * @private + * + * @param {!Element} xmlDom Tree of XML elements to be appended to. + */ +FactoryGenerator.prototype.appendHiddenWorkspaceToDom_ = function(xmlDom) { + var blocks = this.hiddenWorkspace.getTopBlocks(); + for (var i = 0, block; block = blocks[i]; i++) { + var blockChild = Blockly.Xml.blockToDom(block); + blockChild.removeAttribute('id'); + xmlDom.appendChild(blockChild); + } +}; + +/** + * Sets the user-generated shadow blocks loaded into hiddenWorkspace to be + * actual shadow blocks. This is done so that blockToDom records them as + * shadow blocks instead of regular blocks. + * @private + * + */ +FactoryGenerator.prototype.setShadowBlocksInHiddenWorkspace_ = function() { + var blocks = this.hiddenWorkspace.getAllBlocks(); + for (var i = 0; i < blocks.length; i++) { + if (this.model.isShadowBlock(blocks[i].id)) { + blocks[i].setShadow(true); + } + } +}; diff --git a/demos/blocklyfactory/workspacefactory/wfactory_model.js b/demos/blocklyfactory/workspacefactory/wfactory_model.js new file mode 100644 index 000000000..3d8585c9a --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/wfactory_model.js @@ -0,0 +1,450 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Stores and updates information about state and categories + * in workspace factory. Each list element is either a separator or a category, + * and each category stores its name, XML to load that category, color, + * custom tags, and a unique ID making it possible to change category names and + * move categories easily. Keeps track of the currently selected list + * element. Also keeps track of all the user-created shadow blocks and + * manipulates them as necessary. + * + * @author Emma Dauterman (evd2014) + */ + +/** + * Class for a FactoryModel + * @constructor + */ +FactoryModel = function() { + // Ordered list of ListElement objects. + this.toolboxList = []; + // Array of block IDs for all user created shadow blocks. + this.shadowBlocks = []; + // String name of current selected list element, null if no list elements. + this.selected = null; + // Boolean for if a Variable category has been added. + this.hasVariableCategory = false; + // Boolean for if a Procedure category has been added. + this.hasProcedureCategory = false; +}; + +// String name of current selected list element, null if no list elements. +FactoryModel.prototype.selected = null; + +/** + * Given a name, determines if it is the name of a category already present. + * Used when getting a valid category name from the user. + * + * @param {string} name String name to be compared against. + * @return {boolean} True if string is a used category name, false otherwise. + */ +FactoryModel.prototype.hasCategoryByName = function(name) { + for (var i = 0; i < this.toolboxList.length; i++) { + if (this.toolboxList[i].type == ListElement.TYPE_CATEGORY && + this.toolboxList[i].name == name) { + return true; + } + } + return false; +}; + +/** + * Determines if a category with the 'VARIABLE' tag exists. + * + * @return {boolean} True if there exists a category with the Variables tag, + * false otherwise. + */ +FactoryModel.prototype.hasVariables = function() { + return this.hasVariableCategory; +}; + +/** + * Determines if a category with the 'PROCEDURE' tag exists. + * + * @return {boolean} True if there exists a category with the Procedures tag, + * false otherwise. + */ +FactoryModel.prototype.hasProcedures = function() { + return this.hasFunctionCategory; +}; + +/** + * Determines if the user has any elements in the toolbox. Uses the length of + * toolboxList. + * + * @return {boolean} True if categories exist, false otherwise. + */ +FactoryModel.prototype.hasToolbox = function() { + return this.toolboxList.length > 0; +}; + +/** + * Given a ListElement, adds it to the toolbox list. + * + * @param {!ListElement} element The element to be added to the list. + */ +FactoryModel.prototype.addElementToList = function(element) { + // Update state if the copied category has a custom tag. + this.hasVariableCategory = element.custom == 'VARIABLE' ? true : + this.hasVariableCategory; + this.hasProcedureCategory = element.custom == 'PROCEDURE' ? true : + this.hasProcedureCategory; + // Add element to toolboxList. + this.toolboxList.push(element); +}; + +/** + * Given an index, deletes a list element and all associated data. + * + * @param {int} index The index of the list element to delete. + */ +FactoryModel.prototype.deleteElementFromList = function(index) { + // Check if index is out of bounds. + if (index < 0 || index >= this.toolboxList.length) { + return; // No entry to delete. + } + // Check if need to update flags. + this.hasVariableCategory = this.toolboxList[index].custom == 'VARIABLE' ? + false : this.hasVariableCategory; + this.hasProcedureCategory = this.toolboxList[index].custom == 'PROCEDURE' ? + false : this.hasProcedureCategory; + // Remove element. + this.toolboxList.splice(index, 1); +}; + +/** + * Moves a list element to a certain position in toolboxList by removing it + * and then inserting it at the correct index. Checks that indices are in + * bounds (throws error if not), but assumes that oldIndex is the correct index + * for list element. + * + * @param {!ListElement} element The element to move in toolboxList. + * @param {int} newIndex The index to insert the element at. + * @param {int} oldIndex The index the element is currently at. + */ +FactoryModel.prototype.moveElementToIndex = function(element, newIndex, + oldIndex) { + // Check that indexes are in bounds. + if (newIndex < 0 || newIndex >= this.toolboxList.length || oldIndex < 0 || + oldIndex >= this.toolboxList.length) { + throw new Error('Index out of bounds when moving element in the model.'); + } + this.deleteElementFromList(oldIndex); + this.toolboxList.splice(newIndex, 0, element); +} + +/** + * Returns the ID of the currently selected element. Returns null if there are + * no categories (if selected == null). + * + * @return {string} The ID of the element currently selected. + */ +FactoryModel.prototype.getSelectedId = function() { + return this.selected ? this.selected.id : null; +}; + +/** + * Returns the name of the currently selected category. Returns null if there + * are no categories (if selected == null) or the selected element is not + * a category (in which case its name is null). + * + * @return {string} The name of the category currently selected. + */ +FactoryModel.prototype.getSelectedName = function() { + return this.selected ? this.selected.name : null; +}; + +/** + * Returns the currently selected list element object. + * + * @return {ListElement} The currently selected ListElement + */ +FactoryModel.prototype.getSelected = function() { + return this.selected; +}; + +/** + * Sets list element currently selected by id. + * + * @param {string} id ID of list element that should now be selected. + */ +FactoryModel.prototype.setSelectedById = function(id) { + this.selected = this.getElementById(id); +}; + +/** + * Given an ID of a list element, returns the index of that list element in + * toolboxList. Returns -1 if ID is not present. + * + * @param {!string} id The ID of list element to search for. + * @return {int} The index of the list element in toolboxList, or -1 if it + * doesn't exist. + */ + +FactoryModel.prototype.getIndexByElementId = function(id) { + for (var i = 0; i < this.toolboxList.length; i++) { + if (this.toolboxList[i].id == id) { + return i; + } + } + return -1; // ID not present in toolboxList. +}; + +/** + * Given the ID of a list element, returns that ListElement object. + * + * @param {!string} id The ID of element to search for. + * @return {ListElement} Corresponding ListElement object in toolboxList, or + * null if that element does not exist. + */ +FactoryModel.prototype.getElementById = function(id) { + for (var i = 0; i < this.toolboxList.length; i++) { + if (this.toolboxList[i].id == id) { + return this.toolboxList[i]; + } + } + return null; // ID not present in toolboxList. +}; + +/** + * Given the index of a list element in toolboxList, returns that ListElement + * object. + * + * @param {int} index The index of the element to return. + * @return {ListElement} The corresponding ListElement object in toolboxList. + */ +FactoryModel.prototype.getElementByIndex = function(index) { + if (index < 0 || index >= this.toolboxList.length) { + return null; + } + return this.toolboxList[index]; +}; + +/** + * Returns the xml to load the selected element. + * + * @return {!Element} The XML of the selected element, or null if there is + * no selected element. + */ +FactoryModel.prototype.getSelectedXml = function() { + return this.selected ? this.selected.xml : null; +}; + +/** + * Return ordered list of ListElement objects. + * + * @return {!Array} ordered list of ListElement objects + */ +FactoryModel.prototype.getToolboxList = function() { + return this.toolboxList; +}; + +/** + * Gets the ID of a category given its name. + * + * @param {string} name Name of category. + * @return {int} ID of category + */ +FactoryModel.prototype.getCategoryIdByName = function(name) { + for (var i = 0; i < this.toolboxList.length; i++) { + if (this.toolboxList[i].name == name) { + return this.toolboxList[i].id; + } + } + return null; // Name not present in toolboxList. +}; + +/** + * Clears the toolbox list, deleting all ListElements. + */ +FactoryModel.prototype.clearToolboxList = function() { + this.toolboxList = []; + this.hasVariableCategory = false; + this.hasVariableCategory = false; + // TODO(evd2014): When merge changes, also clear shadowList. +}; + +/** + * Class for a ListElement + * Adds a shadow block to the list of shadow blocks. + * + * @param {!string} blockId The unique ID of block to be added. + */ +FactoryModel.prototype.addShadowBlock = function(blockId) { + this.shadowBlocks.push(blockId); +}; + +/** + * Removes a shadow block ID from the list of shadow block IDs if that ID is + * in the list. + * + * @param {!string} blockId The unique ID of block to be removed. + */ +FactoryModel.prototype.removeShadowBlock = function(blockId) { + for (var i = 0; i < this.shadowBlocks.length; i++) { + if (this.shadowBlocks[i] == blockId) { + this.shadowBlocks.splice(i, 1); + return; + } + } +}; + +/** + * Determines if a block is a shadow block given a unique block ID. + * + * @param {!string} blockId The unique ID of the block to examine. + * @return {boolean} True if the block is a user-generated shadow block, false + * otherwise. + */ +FactoryModel.prototype.isShadowBlock = function(blockId) { + for (var i = 0; i < this.shadowBlocks.length; i++) { + if (this.shadowBlocks[i] == blockId) { + return true; + } + } + return false; +}; + +/** + * Given a set of blocks currently loaded, returns all blocks in the workspace + * that are user generated shadow blocks. + * + * @param {!} blocks Array of blocks currently loaded. + * @return {!} Array of user-generated shadow blocks currently + * loaded. + */ +FactoryModel.prototype.getShadowBlocksInWorkspace = function(workspaceBlocks) { + var shadowsInWorkspace = []; + for (var i = 0; i < workspaceBlocks.length; i++) { + if (this.isShadowBlock(workspaceBlocks[i].id)) { + shadowsInWorkspace.push(workspaceBlocks[i]); + } + } + return shadowsInWorkspace; +}; + +/** + * Adds a custom tag to a category, updating state variables accordingly. + * Only accepts 'VARIABLE' and 'PROCEDURE' tags. + * + * @param {!ListElement} category The category to add the tag to. + * @param {!string} tag The custom tag to add to the category. + */ +FactoryModel.prototype.addCustomTag = function(category, tag) { + // Only update list elements that are categories. + if (category.type != ListElement.TYPE_CATEGORY) { + return; + } + // Only update the tag to be 'VARIABLE' or 'PROCEDURE'. + if (tag == 'VARIABLE') { + this.hasVariableCategory = true; + category.custom = 'VARIABLE'; + } else if (tag == 'PROCEDURE') { + this.hasProcedureCategory = true; + category.custom = 'PROCEDURE'; + } +}; + + +/** + * Class for a ListElement. + * @constructor + */ +ListElement = function(type, opt_name) { + this.type = type; + // XML DOM element to load the element. + this.xml = Blockly.Xml.textToDom(''); + // Name of category. Can be changed by user. Null if separator. + this.name = opt_name ? opt_name : null; + // Unique ID of element. Does not change. + this.id = Blockly.genUid(); + // Color of category. Default is no color. Null if separator. + this.color = null; + // Stores a custom tag, if necessary. Null if no custom tag or separator. + this.custom = null; +}; + +// List element types. +ListElement.TYPE_CATEGORY = 'category'; +ListElement.TYPE_SEPARATOR = 'separator'; + +/** + * Saves a category by updating its XML (does not save XML for + * elements that are not categories). + * + * @param {!Blockly.workspace} workspace The workspace to save category entry + * from. + */ +ListElement.prototype.saveFromWorkspace = function(workspace) { + // Only save list elements that are categories. + if (this.type != ListElement.TYPE_CATEGORY) { + return; + } + this.xml = Blockly.Xml.workspaceToDom(workspace); +}; + + +/** + * Changes the name of a category object given a new name. Returns if + * not a category. + * + * @param {string} name New name of category. + */ +ListElement.prototype.changeName = function (name) { + // Only update list elements that are categories. + if (this.type != ListElement.TYPE_CATEGORY) { + return; + } + this.name = name; +}; + +/** + * Sets the color of a category. If tries to set the color of something other + * than a category, returns. + * + * @param {!string} color The color that should be used for that category. + */ +ListElement.prototype.changeColor = function (color) { + if (this.type != ListElement.TYPE_CATEGORY) { + return; + } + this.color = color; +}; + +/** + * Makes a copy of the original element and returns it. Everything about the + * copy is identical except for its ID. + * + * @return {!ListElement} The copy of the ListElement. + */ +ListElement.prototype.copy = function() { + copy = new ListElement(this.type); + // Generate a unique ID for the element. + copy.id = Blockly.genUid(); + // Copy all attributes except ID. + copy.name = this.name; + copy.xml = this.xml; + copy.color = this.color; + copy.custom = this.custom; + // Return copy. + return copy; +}; diff --git a/demos/blocklyfactory/workspacefactory/wfactory_view.js b/demos/blocklyfactory/workspacefactory/wfactory_view.js new file mode 100644 index 000000000..6ad6bf51d --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/wfactory_view.js @@ -0,0 +1,341 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Controls the UI elements for workspace factory, mainly the category tabs. + * Also includes downloading files because that interacts directly with the DOM. + * Depends on FactoryController (for adding mouse listeners). Tabs for each + * category are stored in tab map, which associates a unique ID for a + * category with a particular tab. + * + * @author Emma Dauterman (edauterman) + */ + + /** + * Class for a FactoryView + * @constructor + */ + +FactoryView = function() { + // For each tab, maps ID of a ListElement to the td DOM element. + this.tabMap = Object.create(null); +}; + +/** + * Adds a category tab to the UI, and updates tabMap accordingly. + * + * @param {!string} name The name of the category being created + * @param {!string} id ID of category being created + * @param {boolean} firstCategory true if it's the first category, false + * otherwise + * @return {!Element} DOM element created for tab + */ +FactoryView.prototype.addCategoryRow = function(name, id, firstCategory) { + var table = document.getElementById('categoryTable'); + // Delete help label and enable category buttons if it's the first category. + if (firstCategory) { + table.deleteRow(0); + } + // Create tab. + var count = table.rows.length; + var row = table.insertRow(count); + var nextEntry = row.insertCell(0); + // Configure tab. + nextEntry.id = this.createCategoryIdName(name); + nextEntry.textContent = name; + // Store tab. + this.tabMap[id] = table.rows[count].cells[0]; + // Return tab. + return nextEntry; +}; + +/** + * Deletes a category tab from the UI and updates tabMap accordingly. + * + * @param {!string} id ID of category to be deleted. + * @param {!string} name The name of the category to be deleted. + */ +FactoryView.prototype.deleteElementRow = function(id, index) { + // Delete tab entry. + delete this.tabMap[id]; + // Delete tab row. + var table = document.getElementById('categoryTable'); + var count = table.rows.length; + table.deleteRow(index); + + // If last category removed, add category help text and disable category + // buttons. + this.addEmptyCategoryMessage(); +}; + +/** + * If there are no toolbox elements created, adds a help message to show + * where categories will appear. Should be called when deleting list elements + * in case the last element is deleted. + */ +FactoryView.prototype.addEmptyCategoryMessage = function() { + var table = document.getElementById('categoryTable'); + if (table.rows.length == 0) { + var row = table.insertRow(0); + row.textContent = 'Your categories will appear here'; + } +} + +/** + * Given the index of the currently selected element, updates the state of + * the buttons that allow the user to edit the list elements. Updates the edit + * and arrow buttons. Should be called when adding or removing elements + * or when changing to a new element or when swapping to a different element. + * + * TODO(evd2014): Switch to using CSS to add/remove styles. + * + * @param {int} selectedIndex The index of the currently selected category, + * -1 if no categories created. + * @param {ListElement} selected The selected ListElement. + */ +FactoryView.prototype.updateState = function(selectedIndex, selected) { + // Disable/enable editing buttons as necessary. + document.getElementById('button_editCategory').disabled = selectedIndex < 0 || + selected.type != ListElement.TYPE_CATEGORY; + document.getElementById('button_remove').disabled = selectedIndex < 0; + document.getElementById('button_up').disabled = + selectedIndex <= 0 ? true : false; + var table = document.getElementById('categoryTable'); + document.getElementById('button_down').disabled = selectedIndex >= + table.rows.length - 1 || selectedIndex < 0 ? true : false; + // Disable/enable the workspace as necessary. + this.disableWorkspace(this.shouldDisableWorkspace(selected)); +}; + +/** + * Determines the DOM id for a category given its name. + * + * @param {!string} name Name of category + * @return {!string} ID of category tab + */ +FactoryView.prototype.createCategoryIdName = function(name) { + return 'tab_' + name; +}; + +/** + * Switches a tab on or off. + * + * @param {!string} id ID of the tab to switch on or off. + * @param {boolean} selected True if tab should be on, false if tab should be + * off. + */ +FactoryView.prototype.setCategoryTabSelection = function(id, selected) { + if (!this.tabMap[id]) { + return; // Exit if tab does not exist. + } + this.tabMap[id].className = selected ? 'tabon' : 'taboff'; +}; + +/** + * Used to bind a click to a certain DOM element (used for category tabs). + * Taken directly from code.js + * + * @param {string|!Element} e1 tab element or corresponding id string + * @param {!Function} func Function to be executed on click + */ +FactoryView.prototype.bindClick = function(el, func) { + if (typeof el == 'string') { + el = document.getElementById(el); + } + el.addEventListener('click', func, true); + el.addEventListener('touchend', func, true); +}; + +/** + * Creates a file and downloads it. In some browsers downloads, and in other + * browsers, opens new tab with contents. + * + * @param {!string} filename Name of file + * @param {!Blob} data Blob containing contents to download + */ +FactoryView.prototype.createAndDownloadFile = function(filename, data) { + var clickEvent = new MouseEvent("click", { + "view": window, + "bubbles": true, + "cancelable": false + }); + var a = document.createElement('a'); + a.href = window.URL.createObjectURL(data); + a.download = filename; + a.textContent = 'Download file!'; + a.dispatchEvent(clickEvent); + }; + +/** + * Given the ID of a certain category, updates the corresponding tab in + * the DOM to show a new name. + * + * @param {!string} newName Name of string to be displayed on tab + * @param {!string} id ID of category to be updated + * + */ +FactoryView.prototype.updateCategoryName = function(newName, id) { + this.tabMap[id].textContent = newName; + this.tabMap[id].id = this.createCategoryIdName(newName); +}; + +/** + * Moves a tab from one index to another. Adjusts index inserting before + * based on if inserting before or after. Checks that the indexes are in + * bounds, throws error if not. + * + * @param {!string} id The ID of the category to move. + * @param {int} newIndex The index to move the category to. + * @param {int} oldIndex The index the category is currently at. + */ +FactoryView.prototype.moveTabToIndex = function(id, newIndex, oldIndex) { + var table = document.getElementById('categoryTable'); + // Check that indexes are in bounds + if (newIndex < 0 || newIndex >= table.rows.length || oldIndex < 0 || + oldIndex >= table.rows.length) { + throw new Error('Index out of bounds when moving tab in the view.'); + } + if (newIndex < oldIndex) { // Inserting before. + var row = table.insertRow(newIndex); + row.appendChild(this.tabMap[id]); + table.deleteRow(oldIndex + 1); + } else { // Inserting after. + var row = table.insertRow(newIndex + 1); + row.appendChild(this.tabMap[id]); + table.deleteRow(oldIndex); + } +}; + +/** + * Given a category ID and color, use that color to color the left border of the + * tab for that category. + * + * @param {!string} id The ID of the category to color. + * @param {!string} color The color for to be used for the border of the tab. + * Must be a valid CSS string. + */ +FactoryView.prototype.setBorderColor = function(id, color) { + var tab = this.tabMap[id]; + tab.style.borderLeftWidth = "8px"; + tab.style.borderLeftStyle = "solid"; + tab.style.borderColor = color; +}; + +/** + * Given a separator ID, creates a corresponding tab in the view, updates + * tab map, and returns the tab. + * + * @param {!string} id The ID of the separator. + * @param {!Element} The td DOM element representing the separator. + */ +FactoryView.prototype.addSeparatorTab = function(id) { + // Create separator. + var table = document.getElementById('categoryTable'); + var count = table.rows.length; + var row = table.insertRow(count); + var nextEntry = row.insertCell(0); + // Configure separator. + nextEntry.style.height = '10px'; + // Store and return separator. + this.tabMap[id] = table.rows[count].cells[0]; + return nextEntry; +}; + +/** + * Disables or enables the workspace by putting a div over or under the + * toolbox workspace, depending on the value of disable. Used when switching + * to/from separators where the user shouldn't be able to drag blocks into + * the workspace. + * + * @param {boolean} disable True if the workspace should be disabled, false + * if it should be enabled. + */ +FactoryView.prototype.disableWorkspace = function(disable) { + document.getElementById('disable_div').style.zIndex = disable ? 1 : -1; +}; + +/** + * Determines if the workspace should be disabled. The workspace should be + * disabled if category is a separator or has VARIABLE or PROCEDURE tags. + * + * @return {boolean} True if the workspace should be disabled, false otherwise. + */ +FactoryView.prototype.shouldDisableWorkspace = function(category) { + return category != null && (category.type == ListElement.TYPE_SEPARATOR || + category.custom == 'VARIABLE' || category.custom == 'PROCEDURE'); +}; + +/* + * Removes all categories and separators in the view. Clears the tabMap to + * reflect this. + */ +FactoryView.prototype.clearToolboxTabs = function() { + this.tabMap = []; + var oldCategoryTable = document.getElementById('categoryTable'); + var newCategoryTable = document.createElement('table'); + newCategoryTable.id = 'categoryTable'; + oldCategoryTable.parentElement.replaceChild(newCategoryTable, + oldCategoryTable); +}; + +/** + * Given a set of blocks currently loaded user-generated shadow blocks, visually + * marks them without making them actual shadow blocks (allowing them to still + * be editable and movable). + * + * @param {!} blocks Array of user-generated shadow blocks + * currently loaded. + */ +FactoryView.prototype.markShadowBlocks = function(blocks) { + for (var i = 0; i < blocks.length; i++) { + this.markShadowBlock(blocks[i]); + } +}; + +/** + * Visually marks a user-generated shadow block as a shadow block in the + * workspace without making the block an actual shadow block (allowing it + * to be moved and edited). + * + * @param {!Blockly.Block} block The block that should be marked as a shadow + * block (must be rendered). + */ +FactoryView.prototype.markShadowBlock = function(block) { + // Add Blockly CSS for user-generated shadow blocks. + Blockly.addClass_(block.svgGroup_, 'shadowBlock'); + // If not a valid shadow block, add a warning message. + if (!block.getSurroundParent()) { + block.setWarningText('Shadow blocks must be nested inside' + + ' other blocks to be displayed.'); + } +}; + +/** + * Removes visual marking for a shadow block given a rendered block. + * + * @param {!Blockly.Block} block The block that should be unmarked as a shadow + * block (must be rendered). + */ +FactoryView.prototype.unmarkShadowBlock = function(block) { + // Remove Blockly CSS for user-generated shadow blocks. + if (Blockly.hasClass_(block.svgGroup_, 'shadowBlock')) { + Blockly.removeClass_(block.svgGroup_, 'shadowBlock'); + } +}; diff --git a/tests/jsunit/xml_test.js b/tests/jsunit/xml_test.js index 7487b6f3e..25e2655ea 100644 --- a/tests/jsunit/xml_test.js +++ b/tests/jsunit/xml_test.js @@ -59,6 +59,29 @@ function test_domToText() { text.replace(/\s+/g, '')); } +function test_domToWorkspace() { + Blockly.Blocks.test_block = { + init: function() { + this.jsonInit({ + message0: 'test', + }); + } + }; + + try { + var dom = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ''); + var workspace = new Blockly.Workspace(); + Blockly.Xml.domToWorkspace(dom, workspace); + assertEquals('Block count', 1, workspace.getAllBlocks().length); + } finally { + delete Blockly.Blocks.test_block; + } +} + function test_domToPrettyText() { var dom = Blockly.Xml.textToDom(XML_TEXT); var text = Blockly.Xml.domToPrettyText(dom);