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">
0" tabIndex="0"
+ *ngIf="toolboxCategories && toolboxCategories.length > 0" tabindex="0"
[attr.aria-labelledby]="toolboxTitle.id"
[attr.aria-activedescendant]="getActiveDescId()"
(keydown)="treeService.onKeypress($event, tree)">
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">
+ [disabled]="fieldButtonInfo.isDisabled(inputBlock.connection)" tabindex="-1">
{{fieldButtonInfo.translationIdForText|translate}}
@@ -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">
+ [disabled]="buttonInfo.isDisabled()" tabindex="-1">
{{buttonInfo.translationIdForText|translate}}
@@ -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 @@
-
+
-
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- 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);