+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 20
+ 65
+ 120
+ 160
+ 210
+ 230
+ 260
+ 290
+ 330
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+ 1
+
+
+
+
+ 10
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+ 1
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 45
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+ 3.1
+
+
+
+
+
+
+
+ 64
+
+
+
+
+ 10
+
+
+
+
+
+
+ 50
+
+
+
+
+ 1
+
+
+
+
+ 100
+
+
+
+
+
+
+ 1
+
+
+
+
+ 100
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ abc
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text
+
+
+
+
+ abc
+
+
+
+
+
+
+ text
+
+
+
+
+
+
+ text
+
+
+
+
+
+
+ abc
+
+
+
+
+
+
+ abc
+
+
+
+
+
+
+ abc
+
+
+
+
+
+
+ abc
+
+
+
+
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+ list
+
+
+
+
+
+
+ list
+
+
+
+
+
+
+ list
+
+
+
+
+
+
+ list
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+
+
+
+
+ 100
+
+
+
+
+ 50
+
+
+
+
+ 0
+
+
+
+
+
+
+ #ff0000
+
+
+
+
+ #3333ff
+
+
+
+
+ 0.5
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/demos/blockfactory/link.png b/js/demos/blockfactory/link.png
new file mode 100644
index 0000000..11dfd82
Binary files /dev/null and b/js/demos/blockfactory/link.png differ
diff --git a/js/demos/blockfactory/standard_categories.js b/js/demos/blockfactory/standard_categories.js
new file mode 100644
index 0000000..95c3324
--- /dev/null
+++ b/js/demos/blockfactory/standard_categories.js
@@ -0,0 +1,392 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Contains a map of standard Blockly categories used to load
+ * standard Blockly categories into the user's toolbox. The map is keyed by
+ * the lower case name of the category, and contains the Category object for
+ * that particular category. Also has a list of core block types provided
+ * by Blockly.
+ *
+ * @author Emma Dauterman (evd2014)
+ */
+ 'use strict';
+
+/**
+ * Namespace for StandardCategories
+ */
+var StandardCategories = StandardCategories || Object.create(null);
+
+
+// Map of standard category information necessary to add a standard category
+// to the toolbox.
+StandardCategories.categoryMap = Object.create(null);
+
+StandardCategories.categoryMap['logic'] =
+ new ListElement(ListElement.TYPE_CATEGORY, 'Logic');
+StandardCategories.categoryMap['logic'].xml =
+ Blockly.Xml.textToDom(
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '');
+StandardCategories.categoryMap['logic'].hue = 210;
+
+StandardCategories.categoryMap['loops'] =
+ new ListElement(ListElement.TYPE_CATEGORY, 'Loops');
+StandardCategories.categoryMap['loops'].xml =
+ Blockly.Xml.textToDom(
+ '' +
+ '' +
+ '' +
+ '' +
+ '10' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '1' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '10' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '1' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '');
+StandardCategories.categoryMap['loops'].hue = 120;
+
+StandardCategories.categoryMap['math'] =
+ new ListElement(ListElement.TYPE_CATEGORY, 'Math');
+StandardCategories.categoryMap['math'].xml =
+ Blockly.Xml.textToDom(
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '1' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '1' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '9' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '45' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '0' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '3.1' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '64' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '10'+
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '50' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '1' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '100' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '1' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '100' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '');
+StandardCategories.categoryMap['math'].hue = 230;
+
+StandardCategories.categoryMap['text'] =
+ new ListElement(ListElement.TYPE_CATEGORY, 'Text');
+StandardCategories.categoryMap['text'].xml =
+ Blockly.Xml.textToDom(
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'abc' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'text' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'abc' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'text' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'text' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'abc' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'abc' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'abc' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'abc' +
+ '' +
+ '' +
+ '' +
+ '');
+StandardCategories.categoryMap['text'].hue = 160;
+
+StandardCategories.categoryMap['lists'] =
+ new ListElement(ListElement.TYPE_CATEGORY, 'Lists');
+StandardCategories.categoryMap['lists'].xml =
+ Blockly.Xml.textToDom(
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '5' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'list' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'list' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'list' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ 'list' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ ',' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '');
+StandardCategories.categoryMap['lists'].hue = 260;
+
+StandardCategories.categoryMap['colour'] =
+ new ListElement(ListElement.TYPE_CATEGORY, 'Colour');
+StandardCategories.categoryMap['colour'].xml =
+ Blockly.Xml.textToDom(
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '100' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '50' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '0' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '#ff0000' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '#3333ff' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '0.5' +
+ '' +
+ '' +
+ '' +
+ '');
+StandardCategories.categoryMap['colour'].hue = 20;
+
+StandardCategories.categoryMap['functions'] =
+ new ListElement(ListElement.TYPE_CATEGORY, 'Functions');
+StandardCategories.categoryMap['functions'].hue = 290;
+StandardCategories.categoryMap['functions'].custom = 'PROCEDURE';
+
+StandardCategories.categoryMap['variables'] =
+ new ListElement(ListElement.TYPE_CATEGORY, 'Variables');
+StandardCategories.categoryMap['variables'].hue = 330;
+StandardCategories.categoryMap['variables'].custom = 'VARIABLE';
+
+// All standard block types in provided in Blockly core.
+StandardCategories.coreBlockTypes = ["controls_if", "logic_compare",
+ "logic_operation", "logic_negate", "logic_boolean", "logic_null",
+ "logic_ternary", "controls_repeat_ext", "controls_whileUntil",
+ "controls_for", "controls_forEach", "controls_flow_statements",
+ "math_number", "math_arithmetic", "math_single", "math_trig",
+ "math_constant", "math_number_property", "math_change", "math_round",
+ "math_on_list", "math_modulo", "math_constrain", "math_random_int",
+ "math_random_float", "text", "text_join", "text_append", "text_length",
+ "text_isEmpty", "text_indexOf", "variables_get", "text_charAt",
+ "text_getSubstring", "text_changeCase", "text_trim", "text_print",
+ "text_prompt_ext", "colour_picker", "colour_random", "colour_rgb",
+ "colour_blend", "lists_create_with", "lists_repeat", "lists_length",
+ "lists_isEmpty", "lists_indexOf", "lists_getIndex", "lists_setIndex",
+ "lists_getSublist", "lists_split", "lists_sort", "variables_set",
+ "procedures_defreturn", "procedures_ifreturn", "procedures_defnoreturn",
+ "procedures_callreturn"];
diff --git a/js/demos/blockfactory/workspacefactory/wfactory_controller.js b/js/demos/blockfactory/workspacefactory/wfactory_controller.js
new file mode 100644
index 0000000..30f032e
--- /dev/null
+++ b/js/demos/blockfactory/workspacefactory/wfactory_controller.js
@@ -0,0 +1,1348 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ *
+ * 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 WorkspaceFactoryController
+ * @param {string} toolboxName Name of workspace toolbox XML.
+ * @param {string} toolboxDiv Name of div to inject toolbox workspace in.
+ * @param {string} previewDiv Name of div to inject preview workspace in.
+ * @constructor
+ */
+WorkspaceFactoryController = function(toolboxName, toolboxDiv, previewDiv) {
+ // Toolbox XML element for the editing workspace.
+ this.toolbox = document.getElementById(toolboxName);
+
+ // Workspace for user to drag blocks in for a certain category.
+ this.toolboxWorkspace = Blockly.inject(toolboxDiv,
+ {grid:
+ {spacing: 25,
+ length: 3,
+ colour: '#ccc',
+ snap: true},
+ media: '../../media/',
+ toolbox: this.toolbox
+ });
+
+ // Workspace for user to preview their changes.
+ this.previewWorkspace = Blockly.inject(previewDiv,
+ {grid:
+ {spacing: 25,
+ length: 3,
+ colour: '#ccc',
+ snap: true},
+ media: '../../media/',
+ toolbox: '',
+ zoom:
+ {controls: true,
+ wheel: true}
+ });
+
+ // Model to keep track of categories and blocks.
+ this.model = new WorkspaceFactoryModel();
+ // Updates the category tabs.
+ this.view = new WorkspaceFactoryView();
+ // Generates XML for categories.
+ this.generator = new WorkspaceFactoryGenerator(this.model);
+ // Tracks which editing mode the user is in. Toolbox mode on start.
+ this.selectedMode = WorkspaceFactoryController.MODE_TOOLBOX;
+ // True if key events are enabled, false otherwise.
+ this.keyEventsEnabled = true;
+ // True if there are unsaved changes in the toolbox, false otherwise.
+ this.hasUnsavedToolboxChanges = false;
+ // True if there are unsaved changes in the preloaded blocks, false otherwise.
+ this.hasUnsavedPreloadChanges = false;
+};
+
+// Toolbox editing mode. Changes the user makes to the workspace updates the
+// toolbox.
+WorkspaceFactoryController.MODE_TOOLBOX = 'toolbox';
+// Pre-loaded workspace editing mode. Changes the user makes to the workspace
+// udpates the pre-loaded blocks.
+WorkspaceFactoryController.MODE_PRELOAD = 'preload';
+
+/**
+ * Currently prompts the user for a name, checking that it's valid (not used
+ * before), and then creates a tab and switches to it.
+ */
+WorkspaceFactoryController.prototype.addCategory = function() {
+ // Transfers the user's blocks to a flyout if it's the first category created.
+ this.transferFlyoutBlocksToCategory();
+
+ // After possibly creating a category, check again if it's the first category.
+ var isFirstCategory = !this.model.hasElements();
+ // Get name from user.
+ var name = this.promptForNewCategoryName('Enter the name of your new category:');
+ if (!name) { // Exit if cancelled.
+ return;
+ }
+ // Create category.
+ this.createCategory(name);
+ // Switch to category.
+ this.switchElement(this.model.getCategoryIdByName(name));
+
+ // Sets the default options for injecting the workspace
+ // when there are categories if adding the first category.
+ if (isFirstCategory) {
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+ }
+ // 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.
+ */
+WorkspaceFactoryController.prototype.createCategory = function(name) {
+ // 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);
+ 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.
+ */
+WorkspaceFactoryController.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));
+};
+
+/**
+ * Transfers the blocks in the user's flyout to a new category if
+ * the user is creating their first category and their workspace is not
+ * empty. Should be called whenever it is possible to switch from single flyout
+ * to categories (not including importing).
+ */
+WorkspaceFactoryController.prototype.transferFlyoutBlocksToCategory =
+ function() {
+ // Saves the user's blocks from the flyout in a category if there is no
+ // toolbox and the user has dragged in blocks.
+ if (!this.model.hasElements() &&
+ this.toolboxWorkspace.getAllBlocks(false).length > 0) {
+ // Create the new category.
+ this.createCategory('Category 1', true);
+ // Set the new category as selected.
+ var id = this.model.getCategoryIdByName('Category 1');
+ this.model.setSelectedById(id);
+ this.view.setCategoryTabSelection(id, true);
+ // Allow user to use the default options for injecting with categories.
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+ // Update preview here in case exit early.
+ this.updatePreview();
+ }
+};
+
+/**
+ * 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.
+ */
+WorkspaceFactoryController.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.hasElements()) {
+ next = this.model.getElementByIndex(selectedIndex - 1);
+ }
+ var nextId = next ? next.id : null;
+
+ // Open next element.
+ this.clearAndLoadElement(nextId);
+
+ // If no element to switch to, display message, clear the workspace, and
+ // set a default selected element not in toolbox list in the model.
+ if (!nextId) {
+ alert('You currently have no categories or separators. All your blocks' +
+ ' will be displayed in a single flyout.');
+ this.toolboxWorkspace.clear();
+ this.toolboxWorkspace.clearUndo();
+ this.model.createDefaultSelectedIfEmpty();
+ }
+ // 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.
+ * @param {string=} opt_oldName The current name.
+ * @return {string?} Valid name for a new category, or null if cancelled.
+ */
+WorkspaceFactoryController.prototype.promptForNewCategoryName =
+ function(promptString, opt_oldName) {
+ var defaultName = opt_oldName;
+ do {
+ var name = prompt(promptString, defaultName);
+ if (!name) { // If cancelled.
+ return null;
+ }
+ defaultName = name;
+ } 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.
+ */
+WorkspaceFactoryController.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.
+ */
+WorkspaceFactoryController.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 to another category, set category selection in the model and
+ // view.
+ if (id != null) {
+ // Set next category.
+ this.model.setSelectedById(id);
+
+ // Clears workspace and loads next category.
+ this.clearAndLoadXml_(this.model.getSelectedXml());
+
+ // Selects the next tab.
+ this.view.setCategoryTabSelection(id, true);
+
+ // Order blocks as shown in flyout.
+ this.toolboxWorkspace.cleanUp();
+
+ // Update category editing buttons.
+ this.view.updateState(this.model.getIndexByElementId
+ (this.model.getSelectedId()), this.model.getSelected());
+ } else {
+ // Update category editing buttons for no categories.
+ this.view.updateState(-1, null);
+ }
+};
+
+/**
+ * Tied to "Export" button. Gets a file name from the user and downloads
+ * the corresponding configuration XML to that file.
+ * @param {string} exportMode The type of file to export
+ * (WorkspaceFactoryController.MODE_TOOLBOX for the toolbox configuration,
+ * and WorkspaceFactoryController.MODE_PRELOAD for the pre-loaded workspace
+ * configuration)
+ */
+WorkspaceFactoryController.prototype.exportXmlFile = function(exportMode) {
+ // Get file name.
+ if (exportMode == WorkspaceFactoryController.MODE_TOOLBOX) {
+ var fileName = prompt('File Name for toolbox XML:', 'toolbox.xml');
+ } else {
+ var fileName = prompt('File Name for pre-loaded workspace XML:',
+ 'workspace.xml');
+ }
+ if (!fileName) { // If cancelled.
+ return;
+ }
+
+ // Generate XML.
+ if (exportMode == WorkspaceFactoryController.MODE_TOOLBOX) {
+ // Export the toolbox XML.
+ var configXml = Blockly.Xml.domToPrettyText(
+ this.generator.generateToolboxXml());
+ this.hasUnsavedToolboxChanges = false;
+ } else if (exportMode == WorkspaceFactoryController.MODE_PRELOAD) {
+ // Export the pre-loaded block XML.
+ var configXml = Blockly.Xml.domToPrettyText(
+ this.generator.generateWorkspaceXml());
+ this.hasUnsavedPreloadChanges = false;
+ } else {
+ // Unknown mode. Throw error.
+ var msg = 'Unknown export mode: ' + exportMode;
+ BlocklyDevTools.Analytics.onError(msg);
+ throw Error(msg);
+ }
+
+ // Unpack self-closing tags. These tags fail when embedded in HTML.
+ // ->
+ configXml = configXml.replace(/<(\w+)([^<]*)\/>/g, '<$1$2>$1>');
+
+ // Download file.
+ var data = new Blob([configXml], {type: 'text/xml'});
+ this.view.createAndDownloadFile(fileName, data);
+
+ if (exportMode == WorkspaceFactoryController.MODE_TOOLBOX) {
+ BlocklyDevTools.Analytics.onExport(
+ BlocklyDevTools.Analytics.TOOLBOX,
+ { format: BlocklyDevTools.Analytics.FORMAT_XML });
+ } else if (exportMode == WorkspaceFactoryController.MODE_PRELOAD) {
+ BlocklyDevTools.Analytics.onExport(
+ BlocklyDevTools.Analytics.WORKSPACE_CONTENTS,
+ { format: BlocklyDevTools.Analytics.FORMAT_XML });
+ }
+};
+
+/**
+ * Export the options object to be used for the Blockly inject call. Gets a
+ * file name from the user and downloads the options object to that file.
+ */
+WorkspaceFactoryController.prototype.exportInjectFile = function() {
+ var fileName = prompt('File Name for starter Blockly workspace code:',
+ 'workspace.js');
+ if (!fileName) { // If cancelled.
+ return;
+ }
+ // Generate new options to remove toolbox XML from options object (if
+ // necessary).
+ this.generateNewOptions();
+ var printableOptions = this.generator.generateInjectString()
+ var data = new Blob([printableOptions], {type: 'text/javascript'});
+ this.view.createAndDownloadFile(fileName, data);
+
+ BlocklyDevTools.Analytics.onExport(
+ BlocklyDevTools.Analytics.STARTER_CODE,
+ {
+ format: BlocklyDevTools.Analytics.FORMAT_JS,
+ platform: BlocklyDevTools.Analytics.PLATFORM_WEB
+ });
+};
+
+/**
+ * Tied to "Print" button. Mainly used for debugging purposes. Prints
+ * the configuration XML to the console.
+ */
+WorkspaceFactoryController.prototype.printConfig = function() {
+ // Capture any changes made by user before generating XML.
+ this.saveStateFromWorkspace();
+ // Print XML.
+ console.log(Blockly.Xml.domToPrettyText(this.generator.generateToolboxXml()));
+};
+
+/**
+ * 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. Make sure
+ * that no changes have been made to the workspace since updating the model
+ * (if this might be the case, call saveStateFromWorkspace).
+ */
+WorkspaceFactoryController.prototype.updatePreview = function() {
+ // Disable events to stop updatePreview from recursively calling itself
+ // through event handlers.
+ Blockly.Events.disable();
+
+ // Only update the toolbox if not in read only mode.
+ if (!this.model.options['readOnly']) {
+ // Get toolbox XML.
+ var tree = Blockly.Options.parseToolboxTree(
+ this.generator.generateToolboxXml());
+
+ // No categories, creates a simple flyout.
+ if (tree.getElementsByTagName('category').length == 0) {
+ // No categories, creates a simple flyout.
+ if (this.previewWorkspace.toolbox_) {
+ this.reinjectPreview(tree); // Switch to simple flyout, expensive.
+ } else {
+ this.previewWorkspace.updateToolbox(tree);
+ }
+ } else {
+ // Uses categories, creates a toolbox.
+ if (!this.previewWorkspace.toolbox_) {
+ this.reinjectPreview(tree); // Create a toolbox, expensive.
+ } else {
+ // Close the toolbox before updating it so that the user has to reopen
+ // the flyout and see their updated toolbox (open flyout doesn't update)
+ this.previewWorkspace.toolbox_.clearSelection();
+ this.previewWorkspace.updateToolbox(tree);
+ }
+ }
+ }
+
+ // Update pre-loaded blocks in the preview workspace.
+ this.previewWorkspace.clear();
+ Blockly.Xml.domToWorkspace(this.generator.generateWorkspaceXml(),
+ this.previewWorkspace);
+
+ // Reenable events.
+ Blockly.Events.enable();
+};
+
+/**
+ * Saves the state from the workspace depending on the current mode. Should
+ * be called after making changes to the workspace.
+ */
+WorkspaceFactoryController.prototype.saveStateFromWorkspace = function() {
+ if (this.selectedMode == WorkspaceFactoryController.MODE_TOOLBOX) {
+ // If currently editing the toolbox.
+ // Update flags if toolbox has been changed.
+ if (this.model.getSelectedXml() !=
+ Blockly.Xml.workspaceToDom(this.toolboxWorkspace)) {
+ this.hasUnsavedToolboxChanges = true;
+ }
+
+ this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace);
+
+ } else if (this.selectedMode == WorkspaceFactoryController.MODE_PRELOAD) {
+ // If currently editing the pre-loaded workspace.
+ // Update flags if preloaded blocks have been changed.
+ if (this.model.getPreloadXml() !=
+ Blockly.Xml.workspaceToDom(this.toolboxWorkspace)) {
+ this.hasUnsavedPreloadChanges = true;
+ }
+
+ this.model.savePreloadXml(
+ Blockly.Xml.workspaceToDom(this.toolboxWorkspace));
+ }
+};
+
+/**
+ * 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
+ */
+WorkspaceFactoryController.prototype.reinjectPreview = function(tree) {
+ this.previewWorkspace.dispose();
+ var injectOptions = this.readOptions_();
+ injectOptions['toolbox'] = Blockly.Xml.domToPrettyText(tree);
+ this.previewWorkspace = Blockly.inject('preview_blocks', injectOptions);
+ Blockly.Xml.domToWorkspace(this.generator.generateWorkspaceXml(),
+ this.previewWorkspace);
+};
+
+/**
+ * Changes the name and colour of the selected category.
+ * Return if selected element is a separator.
+ * @param {string} name New name for selected category.
+ * @param {?string} colour New colour for selected category, or null if none.
+ * Must be a valid CSS string, or '' for none.
+ */
+WorkspaceFactoryController.prototype.changeSelectedCategory = function(name,
+ colour) {
+ var selected = this.model.getSelected();
+ // Return if a category is not selected.
+ if (selected.type != ListElement.TYPE_CATEGORY) {
+ return;
+ }
+ // Change colour of selected category.
+ selected.changeColor(colour);
+ this.view.setBorderColor(this.model.getSelectedId(), colour);
+ // Change category name.
+ selected.changeName(name);
+ this.view.updateCategoryName(name, 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 {number} 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.
+ */
+WorkspaceFactoryController.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 {number} newIndex The index to insert the element at.
+ * @param {number} oldIndex The index the element is currently at.
+ */
+WorkspaceFactoryController.prototype.moveElementToIndex = function(element,
+ newIndex, oldIndex) {
+ this.model.moveElementToIndex(element, newIndex, oldIndex);
+ this.view.moveTabToIndex(element.id, newIndex, oldIndex);
+};
+
+/**
+ * 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.
+ */
+WorkspaceFactoryController.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));
+
+ // Load category.
+ this.loadCategoryByName(name);
+};
+
+/**
+ * Loads a Standard Category by name and switches to it. Leverages
+ * StandardCategories. Returns if cannot load standard category.
+ * @param {string} name Name of the standard category to load.
+ */
+WorkspaceFactoryController.prototype.loadCategoryByName = function(name) {
+ // Check if the user can load that standard category.
+ if (!this.isStandardCategoryName(name)) {
+ return;
+ }
+ 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 = StandardCategories.categoryMap[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;
+ }
+ if (!standardCategory.color && standardCategory.hue !== undefined) {
+ // Calculate the hex colour based on the hue.
+ standardCategory.color = Blockly.hueToHex(standardCategory.hue);
+ }
+ // Transfers current flyout blocks to a category if it's the first category
+ // created.
+ this.transferFlyoutBlocksToCategory();
+
+ var isFirstCategory = !this.model.hasElements();
+ // Copy the standard category in the model.
+ var copy = standardCategory.copy();
+
+ // Add it to the model.
+ this.model.addElementToList(copy);
+
+ // Update the copy in the view.
+ var tab = this.view.addCategoryRow(copy.name, copy.id);
+ 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();
+ // Save state from workspace before updating preview.
+ this.saveStateFromWorkspace();
+ if (isFirstCategory) {
+ // Allow the user to use the default options for injecting the workspace
+ // when there are categories.
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+ }
+ // Update preview.
+ this.updatePreview();
+};
+
+/**
+ * Loads the standard Blockly toolbox into the editing space. Should only
+ * be called when the mode is set to toolbox.
+ */
+WorkspaceFactoryController.prototype.loadStandardToolbox = function() {
+ this.loadCategoryByName('Logic');
+ this.loadCategoryByName('Loops');
+ this.loadCategoryByName('Math');
+ this.loadCategoryByName('Text');
+ this.loadCategoryByName('Lists');
+ this.loadCategoryByName('Colour');
+ this.addSeparator();
+ this.loadCategoryByName('Variables');
+ this.loadCategoryByName('Functions');
+};
+
+/**
+ * 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 categoryMap
+ * @return {boolean} True if name is a standard category name, false otherwise.
+ */
+WorkspaceFactoryController.prototype.isStandardCategoryName = function(name) {
+ return !!StandardCategories.categoryMap[name.toLowerCase()];
+};
+
+/**
+ * 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.
+ */
+WorkspaceFactoryController.prototype.addSeparator = function() {
+ // If adding the first element in the toolbox, transfers the user's blocks
+ // in a flyout to a category.
+ this.transferFlyoutBlocksToCategory();
+ // 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, if the import mode is for the toolbox, this function loads
+ * that toolbox XML to the workspace, creating category and separator tabs as
+ * necessary. If the import mode is for pre-loaded blocks in the workspace,
+ * this function loads that XML to the workspace to be edited further. This
+ * function switches mode to whatever the import mode is. 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.
+ * @param {string} importMode The mode corresponding to the type of file the
+ * user is importing (WorkspaceFactoryController.MODE_TOOLBOX or
+ * WorkspaceFactoryController.MODE_PRELOAD).
+ */
+WorkspaceFactoryController.prototype.importFile = function(file, importMode) {
+ // Exit if cancelled.
+ if (!file) {
+ return;
+ }
+
+ Blockly.Events.disable();
+ var controller = this;
+ 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);
+ if (importMode == WorkspaceFactoryController.MODE_TOOLBOX) {
+ // Switch mode.
+ controller.setMode(WorkspaceFactoryController.MODE_TOOLBOX);
+
+ // Confirm that the user wants to override their current toolbox.
+ var hasToolboxElements = controller.model.hasElements() ||
+ controller.toolboxWorkspace.getAllBlocks(false).length > 0;
+ if (hasToolboxElements) {
+ var msg = 'Are you sure you want to import? You will lose your ' +
+ 'current toolbox.';
+ BlocklyDevTools.Analytics.onWarning(msg);
+ var continueAnyway = confirm();
+ if (!continueAnyway) {
+ return;
+ }
+ }
+ // Import toolbox XML.
+ controller.importToolboxFromTree_(tree);
+ BlocklyDevTools.Analytics.onImport('Toolbox.xml');
+
+ } else if (importMode == WorkspaceFactoryController.MODE_PRELOAD) {
+ // Switch mode.
+ controller.setMode(WorkspaceFactoryController.MODE_PRELOAD);
+
+ // Confirm that the user wants to override their current blocks.
+ if (controller.toolboxWorkspace.getAllBlocks(false).length > 0) {
+ var msg = 'Are you sure you want to import? You will lose your ' +
+ 'current workspace blocks.';
+ var continueAnyway = confirm(msg);
+ BlocklyDevTools.Analytics.onWarning(msg);
+ if (!continueAnyway) {
+ return;
+ }
+ }
+
+ // Import pre-loaded workspace XML.
+ controller.importPreloadFromTree_(tree);
+ BlocklyDevTools.Analytics.onImport('WorkspaceContents.xml');
+ } else {
+ // Throw error if invalid mode.
+ throw Error('Unknown import mode: ' + importMode);
+ }
+ } catch(e) {
+ var msg = 'Cannot load XML from file.';
+ alert(msg);
+ BlocklyDevTools.Analytics.onError(msg);
+ console.log(e);
+ } finally {
+ Blockly.Events.enable();
+ }
+ }
+
+ // Read the file asynchronously.
+ 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. Assumes that the mode is MODE_TOOLBOX.
+ * @param {!Element} tree XML tree to be loaded to toolbox editing area.
+ * @private
+ */
+WorkspaceFactoryController.prototype.importToolboxFromTree_ = 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.
+ 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.saveStateFromWorkspace();
+
+ // Set default configuration options for a single flyout or multiple
+ // categories.
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+
+ this.updatePreview();
+};
+
+/**
+ * Given a XML DOM tree, loads it into the pre-loaded workspace editing area.
+ * Assumes that tree is in valid XML format and that the selected mode is
+ * MODE_PRELOAD.
+ * @param {!Element} tree XML tree to be loaded to pre-loaded block editing
+ * area.
+ */
+WorkspaceFactoryController.prototype.importPreloadFromTree_ = function(tree) {
+ this.clearAndLoadXml_(tree);
+ this.model.savePreloadXml(tree);
+ this.updatePreview();
+};
+
+/**
+ * Given a XML DOM tree, loads it into the pre-loaded workspace editing area.
+ * Assumes that tree is in valid XML format and that the selected mode is
+ * MODE_PRELOAD.
+ * @param {!Element} tree XML tree to be loaded to pre-loaded block editing
+ * area.
+ */
+WorkspaceFactoryController.prototype.importPreloadFromTree_ = function(tree) {
+ this.clearAndLoadXml_(tree);
+ this.model.savePreloadXml(tree);
+ this.saveStateFromWorkspace();
+ this.updatePreview();
+};
+
+/**
+ * Given a XML DOM tree, loads it into the pre-loaded workspace editing area.
+ * Assumes that tree is in valid XML format and that the selected mode is
+ * MODE_PRELOAD.
+ * @param {!Element} tree XML tree to be loaded to pre-loaded block editing
+ * area.
+ */
+WorkspaceFactoryController.prototype.importPreloadFromTree_ = function(tree) {
+ this.clearAndLoadXml_(tree);
+ this.model.savePreloadXml(tree);
+ this.saveStateFromWorkspace();
+ this.updatePreview();
+};
+
+/**
+ * Clears the editing area completely, deleting all categories and all
+ * blocks in the model and view and all pre-loaded blocks. Tied to the
+ * "Clear" button.
+ */
+WorkspaceFactoryController.prototype.clearAll = function() {
+ var msg = 'Are you sure you want to clear all of your work in Workspace' +
+ ' Factory?';
+ BlocklyDevTools.Analytics.onWarning(msg);
+ if (!confirm(msg)) {
+ return;
+ }
+ var hasCategories = this.model.hasElements();
+ this.model.clearToolboxList();
+ this.view.clearToolboxTabs();
+ this.model.savePreloadXml(Blockly.utils.xml.createElement('xml'));
+ this.view.addEmptyCategoryMessage();
+ this.view.updateState(-1, null);
+ this.toolboxWorkspace.clear();
+ this.toolboxWorkspace.clearUndo();
+ this.saveStateFromWorkspace();
+ this.hasUnsavedToolboxChanges = false;
+ this.hasUnsavedPreloadChanges = false;
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+ 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.
+ */
+WorkspaceFactoryController.prototype.addShadow = function() {
+ // No block selected to make a shadow block.
+ if (!Blockly.selected) {
+ return;
+ }
+ // Clear any previous warnings on the block (would only have warnings on
+ // a non-shadow block if it was nested inside another shadow block).
+ Blockly.selected.setWarningText(null);
+ // Set selected block and all children as shadow blocks.
+ this.addShadowForBlockAndChildren_(Blockly.selected);
+
+ // Save and update the preview.
+ this.saveStateFromWorkspace();
+ this.updatePreview();
+};
+
+/**
+ * Sets a block and all of its children to be user-generated shadow blocks,
+ * both in the model and view.
+ * @param {!Blockly.Block} block The block to be converted to a user-generated
+ * shadow block.
+ * @private
+ */
+WorkspaceFactoryController.prototype.addShadowForBlockAndChildren_ =
+ function(block) {
+ // Convert to shadow block.
+ this.view.markShadowBlock(block);
+ this.model.addShadowBlock(block.id);
+
+ if (FactoryUtils.hasVariableField(block)) {
+ block.setWarningText('Cannot make variable blocks shadow blocks.');
+ }
+
+ // Convert all children to shadow blocks recursively.
+ var children = block.getChildren();
+ for (var i = 0; i < children.length; i++) {
+ this.addShadowForBlockAndChildren_(children[i]);
+ }
+};
+
+/**
+ * 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.
+ */
+WorkspaceFactoryController.prototype.removeShadow = function() {
+ // No block selected to modify.
+ if (!Blockly.selected) {
+ return;
+ }
+ this.model.removeShadowBlock(Blockly.selected.id);
+ this.view.unmarkShadowBlock(Blockly.selected);
+
+ // If turning invalid shadow block back to normal block, remove warning.
+ Blockly.selected.setWarningText(null);
+
+ this.saveStateFromWorkspace();
+ 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.
+ */
+WorkspaceFactoryController.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.
+ */
+WorkspaceFactoryController.prototype.convertShadowBlocks = function() {
+ var blocks = this.toolboxWorkspace.getAllBlocks(false);
+ for (var i = 0, block; block = blocks[i]; i++) {
+ if (block.isShadow()) {
+ block.setShadow(false);
+ // Delete the shadow DOM attached to the block so that the shadow block
+ // does not respawn. Dependent on implementation details.
+ var parentConnection = block.outputConnection ?
+ block.outputConnection.targetConnection :
+ block.previousConnection.targetConnection;
+ if (parentConnection) {
+ parentConnection.setShadowDom(null);
+ }
+ this.model.addShadowBlock(block.id);
+ this.view.markShadowBlock(block);
+ }
+ }
+};
+
+/**
+ * Sets the currently selected mode that determines what the toolbox workspace
+ * is being used to edit. Updates the view and then saves and loads XML
+ * to and from the toolbox and updates the help text.
+ * @param {string} tab The type of tab being switched to
+ * (WorkspaceFactoryController.MODE_TOOLBOX or
+ * WorkspaceFactoryController.MODE_PRELOAD).
+ */
+WorkspaceFactoryController.prototype.setMode = function(mode) {
+ // No work to change mode that's currently set.
+ if (this.selectedMode == mode) {
+ return;
+ }
+
+ // No work to change mode that's currently set.
+ if (this.selectedMode == mode) {
+ return;
+ }
+
+ // Set tab selection and display appropriate tab.
+ this.view.setModeSelection(mode);
+
+ // Update selected tab.
+ this.selectedMode = mode;
+
+ // Update help text above workspace.
+ this.view.updateHelpText(mode);
+
+ if (mode == WorkspaceFactoryController.MODE_TOOLBOX) {
+ // Open the toolbox editing space.
+ this.model.savePreloadXml
+ (Blockly.Xml.workspaceToDom(this.toolboxWorkspace));
+ this.clearAndLoadXml_(this.model.getSelectedXml());
+ this.view.disableWorkspace(this.view.shouldDisableWorkspace
+ (this.model.getSelected()));
+ } else {
+ // Open the pre-loaded workspace editing space.
+ if (this.model.getSelected()) {
+ this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace);
+ }
+ this.clearAndLoadXml_(this.model.getPreloadXml());
+ this.view.disableWorkspace(false);
+ }
+};
+
+/**
+ * Clears the toolbox workspace and loads XML to it, marking shadow blocks
+ * as necessary.
+ * @private
+ * @param {!Element} xml The XML to be loaded to the workspace.
+ */
+WorkspaceFactoryController.prototype.clearAndLoadXml_ = function(xml) {
+ this.toolboxWorkspace.clear();
+ this.toolboxWorkspace.clearUndo();
+ Blockly.Xml.domToWorkspace(xml, this.toolboxWorkspace);
+ this.view.markShadowBlocks(this.model.getShadowBlocksInWorkspace
+ (this.toolboxWorkspace.getAllBlocks(false)));
+ this.warnForUndefinedBlocks_();
+};
+
+/**
+ * Sets the standard default options for the options object and updates
+ * the preview workspace. The default values depends on if categories are
+ * present.
+ */
+WorkspaceFactoryController.prototype.setStandardOptionsAndUpdate = function() {
+ this.view.setBaseOptions();
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+};
+
+/**
+ * Generates a new options object for injecting a Blockly workspace based
+ * on user input. Should be called every time a change has been made to
+ * an input field. Updates the model and reinjects the preview workspace.
+ */
+WorkspaceFactoryController.prototype.generateNewOptions = function() {
+ this.model.setOptions(this.readOptions_());
+
+ this.reinjectPreview(Blockly.Options.parseToolboxTree(
+ this.generator.generateToolboxXml()));
+};
+
+/**
+ * Generates a new options object for injecting a Blockly workspace based on
+ * user input.
+ * @return {!Object} Blockly injection options object.
+ * @private
+ */
+WorkspaceFactoryController.prototype.readOptions_ = function() {
+ var optionsObj = Object.create(null);
+
+ // Add all standard options to the options object.
+ // Use parse int to get numbers from value inputs.
+ var readonly = document.getElementById('option_readOnly_checkbox').checked;
+ if (readonly) {
+ optionsObj['readOnly'] = true;
+ } else {
+ optionsObj['collapse'] =
+ document.getElementById('option_collapse_checkbox').checked;
+ optionsObj['comments'] =
+ document.getElementById('option_comments_checkbox').checked;
+ optionsObj['disable'] =
+ document.getElementById('option_disable_checkbox').checked;
+ if (document.getElementById('option_infiniteBlocks_checkbox').checked) {
+ optionsObj['maxBlocks'] = Infinity;
+ } else {
+ var maxBlocksValue =
+ document.getElementById('option_maxBlocks_number').value;
+ optionsObj['maxBlocks'] = typeof maxBlocksValue == 'string' ?
+ parseInt(maxBlocksValue) : maxBlocksValue;
+ }
+ optionsObj['trashcan'] =
+ document.getElementById('option_trashcan_checkbox').checked;
+ optionsObj['horizontalLayout'] =
+ document.getElementById('option_horizontalLayout_checkbox').checked;
+ optionsObj['toolboxPosition'] =
+ document.getElementById('option_toolboxPosition_checkbox').checked ?
+ 'end' : 'start';
+ }
+
+ optionsObj['css'] = document.getElementById('option_css_checkbox').checked;
+ optionsObj['media'] = document.getElementById('option_media_text').value;
+ optionsObj['rtl'] = document.getElementById('option_rtl_checkbox').checked;
+ optionsObj['scrollbars'] =
+ document.getElementById('option_scrollbars_checkbox').checked;
+ optionsObj['sounds'] =
+ document.getElementById('option_sounds_checkbox').checked;
+ optionsObj['oneBasedIndex'] =
+ document.getElementById('option_oneBasedIndex_checkbox').checked;
+
+ // If using a grid, add all grid options.
+ if (document.getElementById('option_grid_checkbox').checked) {
+ var grid = Object.create(null);
+ var spacingValue =
+ document.getElementById('gridOption_spacing_number').value;
+ grid['spacing'] = typeof spacingValue == 'string' ?
+ parseInt(spacingValue) : spacingValue;
+ var lengthValue = document.getElementById('gridOption_length_number').value;
+ grid['length'] = typeof lengthValue == 'string' ?
+ parseInt(lengthValue) : lengthValue;
+ grid['colour'] = document.getElementById('gridOption_colour_text').value;
+ if (!readonly) {
+ grid['snap'] =
+ document.getElementById('gridOption_snap_checkbox').checked;
+ }
+ optionsObj['grid'] = grid;
+ }
+
+ // If using zoom, add all zoom options.
+ if (document.getElementById('option_zoom_checkbox').checked) {
+ var zoom = Object.create(null);
+ zoom['controls'] =
+ document.getElementById('zoomOption_controls_checkbox').checked;
+ zoom['wheel'] =
+ document.getElementById('zoomOption_wheel_checkbox').checked;
+ var startScaleValue =
+ document.getElementById('zoomOption_startScale_number').value;
+ zoom['startScale'] = typeof startScaleValue == 'string' ?
+ Number(startScaleValue) : startScaleValue;
+ var maxScaleValue =
+ document.getElementById('zoomOption_maxScale_number').value;
+ zoom['maxScale'] = typeof maxScaleValue == 'string' ?
+ Number(maxScaleValue) : maxScaleValue;
+ var minScaleValue =
+ document.getElementById('zoomOption_minScale_number').value;
+ zoom['minScale'] = typeof minScaleValue == 'string' ?
+ Number(minScaleValue) : minScaleValue;
+ var scaleSpeedValue =
+ document.getElementById('zoomOption_scaleSpeed_number').value;
+ zoom['scaleSpeed'] = typeof scaleSpeedValue == 'string' ?
+ Number(scaleSpeedValue) : scaleSpeedValue;
+ optionsObj['zoom'] = zoom;
+ }
+
+ return optionsObj;
+};
+
+/**
+ * Imports blocks from a file, generating a category in the toolbox workspace
+ * to allow the user to use imported blocks in the toolbox and in pre-loaded
+ * blocks.
+ * @param {!File} file File object for the blocks to import.
+ * @param {string} format The format of the file to import, either 'JSON' or
+ * 'JavaScript'.
+ */
+WorkspaceFactoryController.prototype.importBlocks = function(file, format) {
+ // Generate category name from file name.
+ var categoryName = file.name;
+
+ var controller = this;
+ var reader = new FileReader();
+
+ // To be executed when the reader has read the file.
+ reader.onload = function() {
+ try {
+ // Define blocks using block types from file.
+ var blockTypes = FactoryUtils.defineAndGetBlockTypes(reader.result,
+ format);
+
+ // If an imported block type is already defined, check if the user wants
+ // to override the current block definition.
+ if (controller.model.hasDefinedBlockTypes(blockTypes)) {
+ var msg = 'An imported block uses the same name as a block '
+ + 'already in your toolbox. Are you sure you want to override the '
+ + 'currently defined block?';
+ var continueAnyway = confirm(msg);
+ BlocklyDevTools.Analytics.onWarning(msg);
+ if (!continueAnyway) {
+ return;
+ }
+ }
+
+ var blocks = controller.generator.getDefinedBlocks(blockTypes);
+ // Generate category XML and append to toolbox.
+ var categoryXml = FactoryUtils.generateCategoryXml(blocks, categoryName);
+ // Get random color for category between 0 and 360. Gives each imported
+ // category a different color.
+ var randomColor = Math.floor(Math.random() * 360);
+ categoryXml.setAttribute('colour', randomColor);
+ controller.toolbox.appendChild(categoryXml);
+ controller.toolboxWorkspace.updateToolbox(controller.toolbox);
+ // Update imported block types.
+ controller.model.addImportedBlockTypes(blockTypes);
+ // Reload current category to possibly reflect any newly defined blocks.
+ controller.clearAndLoadXml_
+ (Blockly.Xml.workspaceToDom(controller.toolboxWorkspace));
+
+ BlocklyDevTools.Analytics.onImport('BlockDefinitions' +
+ (format == 'JSON' ? '.json' : '.js'));
+ } catch (e) {
+ msg = 'Cannot read blocks from file.';
+ alert(msg);
+ BlocklyDevTools.Analytics.onError(msg);
+ window.console.log(e);
+ }
+ }
+
+ // Read the file asynchronously.
+ reader.readAsText(file);
+};
+
+/**
+ * Updates the block library category in the toolbox workspace toolbox.
+ * @param {!Element} categoryXml XML for the block library category.
+ * @param {!Array.} libBlockTypes Array of block types from the block
+ * library.
+ */
+WorkspaceFactoryController.prototype.setBlockLibCategory =
+ function(categoryXml, libBlockTypes) {
+ var blockLibCategory = document.getElementById('blockLibCategory');
+
+ // Set category ID so that it can be easily replaced, and set a standard,
+ // arbitrary block library color.
+ categoryXml.id = 'blockLibCategory';
+ categoryXml.setAttribute('colour', 260);
+
+ // Update the toolbox and toolboxWorkspace.
+ this.toolbox.replaceChild(categoryXml, blockLibCategory);
+ this.toolboxWorkspace.toolbox_.clearSelection();
+ this.toolboxWorkspace.updateToolbox(this.toolbox);
+
+ // Update the block library types.
+ this.model.updateLibBlockTypes(libBlockTypes);
+
+ // Reload XML on page to account for blocks now defined or undefined in block
+ // library.
+ this.clearAndLoadXml_(Blockly.Xml.workspaceToDom(this.toolboxWorkspace));
+};
+
+/**
+ * Return the block types used in the custom toolbox and pre-loaded workspace.
+ * @return {!Array.} Block types used in the custom toolbox and
+ * pre-loaded workspace.
+ */
+WorkspaceFactoryController.prototype.getAllUsedBlockTypes = function() {
+ return this.model.getAllUsedBlockTypes();
+};
+
+/**
+ * Determines if a block loaded in the workspace has a definition (if it
+ * is a standard block, is defined in the block library, or has a definition
+ * imported).
+ * @param {!Blockly.Block} block The block to examine.
+ */
+WorkspaceFactoryController.prototype.isDefinedBlock = function(block) {
+ return this.model.isDefinedBlockType(block.type);
+};
+
+/**
+ * Sets a warning on blocks loaded to the workspace that are not defined.
+ * @private
+ */
+WorkspaceFactoryController.prototype.warnForUndefinedBlocks_ = function() {
+ var blocks = this.toolboxWorkspace.getAllBlocks(false);
+ for (var i = 0, block; block = blocks[i]; i++) {
+ if (!this.isDefinedBlock(block)) {
+ block.setWarningText(block.type + ' is not defined (it is not a ' +
+ 'standard block,\nin your block library, or an imported block)');
+ }
+ }
+};
+
+/**
+ * Determines if a standard variable category is in the custom toolbox.
+ * @return {boolean} True if a variables category is in use, false otherwise.
+ */
+WorkspaceFactoryController.prototype.hasVariablesCategory = function() {
+ return this.model.hasVariables();
+};
+
+/**
+ * Determines if a standard procedures category is in the custom toolbox.
+ * @return {boolean} True if a procedures category is in use, false otherwise.
+ */
+WorkspaceFactoryController.prototype.hasProceduresCategory = function() {
+ return this.model.hasProcedures();
+};
+
+/**
+ * Determines if there are any unsaved changes in workspace factory.
+ * @return {boolean} True if there are unsaved changes, false otherwise.
+ */
+WorkspaceFactoryController.prototype.hasUnsavedChanges = function() {
+ return this.hasUnsavedToolboxChanges || this.hasUnsavedPreloadChanges;
+};
diff --git a/js/demos/blockfactory/workspacefactory/wfactory_generator.js b/js/demos/blockfactory/workspacefactory/wfactory_generator.js
new file mode 100644
index 0000000..a58c565
--- /dev/null
+++ b/js/demos/blockfactory/workspacefactory/wfactory_generator.js
@@ -0,0 +1,237 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ *
+ * 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 WorkspaceFactoryGenerator
+ * @constructor
+ */
+WorkspaceFactoryGenerator = 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.utils.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.
+ * Save state of workspace in model (saveFromWorkspace) before calling if
+ * changes might have been made to the selected category.
+ * @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.
+ */
+WorkspaceFactoryGenerator.prototype.generateToolboxXml = function() {
+ // Create DOM for XML.
+ var xmlDom = Blockly.utils.xml.createElement('xml');
+ xmlDom.id = 'toolbox';
+ xmlDom.setAttribute('style', 'display: none');
+
+ if (!this.model.hasElements()) {
+ // Toolbox has no categories. Use XML directly from workspace.
+ this.loadToHiddenWorkspace_(this.model.getSelectedXml());
+ this.appendHiddenWorkspaceToDom_(xmlDom);
+ } else {
+ // Toolbox has categories.
+ // Assert that selected != null
+ if (!this.model.getSelected()) {
+ throw Error('Selected is null when the toolbox is empty.');
+ }
+
+ 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 = Blockly.utils.xml.createElement('sep');
+ } else if (element.type == ListElement.TYPE_CATEGORY) {
+ // If the next element is a category.
+ var nextElement = Blockly.utils.xml.createElement('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.loadToHiddenWorkspace_(element.xml);
+ this.appendHiddenWorkspaceToDom_(nextElement);
+ }
+ xmlDom.appendChild(nextElement);
+ }
+ }
+ return xmlDom;
+ };
+
+
+ /**
+ * Generates XML for the workspace (different from generateConfigXml in that
+ * it includes XY and ID attributes). Uses a workspace and converts user
+ * generated shadow blocks to actual shadow blocks.
+ * @return {!Element} XML element representing toolbox or flyout corresponding
+ * to toolbox workspace.
+ */
+WorkspaceFactoryGenerator.prototype.generateWorkspaceXml = function() {
+ // Load workspace XML to hidden workspace with user-generated shadow blocks
+ // as actual shadow blocks.
+ this.hiddenWorkspace.clear();
+ Blockly.Xml.domToWorkspace(this.model.getPreloadXml(), this.hiddenWorkspace);
+ this.setShadowBlocksInHiddenWorkspace_();
+
+ // Generate XML and set attributes.
+ var xmlDom = Blockly.Xml.workspaceToDom(this.hiddenWorkspace);
+ xmlDom.id = 'workspaceBlocks';
+ xmlDom.setAttribute('style', 'display: none');
+ return xmlDom;
+};
+
+/**
+ * Generates a string representation of the options object for injecting the
+ * workspace and starter code.
+ * @return {string} String representation of starter code for injecting.
+ */
+WorkspaceFactoryGenerator.prototype.generateInjectString = function() {
+ var addAttributes = function(obj, tabChar) {
+ if (!obj) {
+ return '{}\n';
+ }
+ var str = '';
+ for (var key in obj) {
+ if (key == 'grid' || key == 'zoom') {
+ var temp = tabChar + key + ' : {\n' + addAttributes(obj[key],
+ tabChar + '\t') + tabChar + '}, \n';
+ } else if (typeof obj[key] == 'string') {
+ var temp = tabChar + key + ' : \'' + obj[key] + '\', \n';
+ } else {
+ var temp = tabChar + key + ' : ' + obj[key] + ', \n';
+ }
+ str += temp;
+ }
+ var lastCommaIndex = str.lastIndexOf(',');
+ str = str.slice(0, lastCommaIndex) + '\n';
+ return str;
+ };
+
+ var attributes = addAttributes(this.model.options, '\t');
+ if (!this.model.options['readOnly']) {
+ attributes = '\ttoolbox : toolbox, \n' +
+ attributes;
+ }
+ var finalStr = '/* TODO: Change toolbox XML ID if necessary. Can export ' +
+ 'toolbox XML from Workspace Factory. */\n' +
+ 'var toolbox = document.getElementById("toolbox");\n\n';
+ finalStr += 'var options = { \n' + attributes + '};';
+ finalStr += '\n\n/* Inject your workspace */ \nvar workspace = Blockly.' +
+ 'inject(/* TODO: Add ID of div to inject Blockly into */, options);';
+ finalStr += '\n\n/* Load Workspace Blocks from XML to workspace. ' +
+ 'Remove all code below if no blocks to load */\n\n' +
+ '/* TODO: Change workspace blocks XML ID if necessary. Can export' +
+ ' workspace blocks XML from Workspace Factory. */\n' +
+ 'var workspaceBlocks = document.getElementById("workspaceBlocks"); \n\n' +
+ '/* Load blocks to workspace. */\n' +
+ 'Blockly.Xml.domToWorkspace(workspaceBlocks, workspace);';
+ return finalStr;
+};
+
+/**
+ * Loads the given XML to the hidden workspace and sets any user-generated
+ * shadow blocks to be actual shadow blocks.
+ * @param {!Element} xml The XML to be loaded to the hidden workspace.
+ * @private
+ */
+WorkspaceFactoryGenerator.prototype.loadToHiddenWorkspace_ = function(xml) {
+ this.hiddenWorkspace.clear();
+ Blockly.Xml.domToWorkspace(xml, this.hiddenWorkspace);
+ this.setShadowBlocksInHiddenWorkspace_();
+};
+
+/**
+ * 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.
+ */
+WorkspaceFactoryGenerator.prototype.appendHiddenWorkspaceToDom_ =
+ function(xmlDom) {
+ var blocks = this.hiddenWorkspace.getTopBlocks();
+ for (var i = 0, block; block = blocks[i]; i++) {
+ var blockChild = Blockly.Xml.blockToDom(block, /* opt_noId */ true);
+ 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
+ */
+WorkspaceFactoryGenerator.prototype.setShadowBlocksInHiddenWorkspace_ =
+ function() {
+ var blocks = this.hiddenWorkspace.getAllBlocks(false);
+ for (var i = 0; i < blocks.length; i++) {
+ if (this.model.isShadowBlock(blocks[i].id)) {
+ blocks[i].setShadow(true);
+ }
+ }
+};
+
+/**
+ * Given a set of block types, gets the Blockly.Block objects for each block
+ * type.
+ * @param {!Array.} blockTypes Array of blocks that have been defined.
+ * @return {!Array.} Array of Blockly.Block objects corresponding
+ * to the array of blockTypes.
+ */
+WorkspaceFactoryGenerator.prototype.getDefinedBlocks = function(blockTypes) {
+ var blocks = [];
+ for (var i = 0; i < blockTypes.length ; i++) {
+ blocks.push(FactoryUtils.getDefinedBlock(blockTypes[i],
+ this.hiddenWorkspace));
+ }
+ return blocks;
+};
diff --git a/js/demos/blockfactory/workspacefactory/wfactory_init.js b/js/demos/blockfactory/workspacefactory/wfactory_init.js
new file mode 100644
index 0000000..f99e856
--- /dev/null
+++ b/js/demos/blockfactory/workspacefactory/wfactory_init.js
@@ -0,0 +1,554 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ *
+ * 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 init functions for the workspace factory tab.
+ * Adds click handlers to buttons and dropdowns, adds event listeners for
+ * keydown events and Blockly events, and configures the initial setup of
+ * the page.
+ *
+ * @author Emma Dauterman (evd2014)
+ */
+
+/**
+ * Namespace for workspace factory initialization methods.
+ * @namespace
+ */
+WorkspaceFactoryInit = {};
+
+/**
+ * Initialization for workspace factory tab.
+ * @param {!FactoryController} controller The controller for the workspace
+ * factory tab.
+ */
+WorkspaceFactoryInit.initWorkspaceFactory = function(controller) {
+ // Disable category editing buttons until categories are created.
+ document.getElementById('button_remove').disabled = true;
+ document.getElementById('button_up').disabled = true;
+ document.getElementById('button_down').disabled = true;
+ document.getElementById('button_editCategory').disabled = true;
+
+ this.initColourPicker_(controller);
+ this.addWorkspaceFactoryEventListeners_(controller);
+ this.assignWorkspaceFactoryClickHandlers_(controller);
+ this.addWorkspaceFactoryOptionsListeners_(controller);
+
+ // Check standard options and apply the changes to update the view.
+ controller.setStandardOptionsAndUpdate();
+};
+
+/**
+ * Initialize the colour picker in workspace factory.
+ * @param {!FactoryController} controller The controller for the workspace
+ * factory tab.
+ * @private
+ */
+WorkspaceFactoryInit.initColourPicker_ = function(controller) {
+ // Array of Blockly category colours, consistent with the colour defaults.
+ var colours = [20, 65, 120, 160, 210, 230, 260, 290, 330, ''];
+ // Convert hue numbers to RRGGBB strings.
+ for (var i = 0; i < colours.length; i++) {
+ if (colours[i] !== '') {
+ colours[i] = Blockly.hueToHex(colours[i]).substring(1);
+ }
+ }
+ // Convert to 2D array.
+ var maxCols = Math.ceil(Math.sqrt(colours.length));
+ var grid = [];
+ var row = [];
+ for (var i = 0; i < colours.length; i++) {
+ row.push(colours[i]);
+ if (row.length == maxCols) {
+ grid.push(row);
+ row = [];
+ }
+ }
+ if (row.length) {
+ grid.push(row);
+ }
+
+ // Override the default colours.
+ cp_grid = grid;
+};
+
+/**
+ * Assign click handlers for workspace factory.
+ * @param {!FactoryController} controller The controller for the workspace
+ * factory tab.
+ * @private
+ */
+WorkspaceFactoryInit.assignWorkspaceFactoryClickHandlers_ =
+ function(controller) {
+
+ // Import Custom Blocks button.
+ document.getElementById('button_importBlocks').addEventListener
+ ('click',
+ function() {
+ blocklyFactory.openModal('dropdownDiv_importBlocks');
+ });
+ document.getElementById('input_importBlocksJson').addEventListener
+ ('change',
+ function() {
+ controller.importBlocks(event.target.files[0], 'JSON');
+ });
+ document.getElementById('input_importBlocksJson').addEventListener
+ ('click', function() {blocklyFactory.closeModal()});
+ document.getElementById('input_importBlocksJs').addEventListener
+ ('change',
+ function() {
+ controller.importBlocks(event.target.files[0], 'JavaScript');
+ });
+ document.getElementById('input_importBlocksJs').addEventListener
+ ('click', function() {blocklyFactory.closeModal()});
+
+ // Load to Edit button.
+ document.getElementById('button_load').addEventListener
+ ('click',
+ function() {
+ blocklyFactory.openModal('dropdownDiv_load');
+ });
+ document.getElementById('input_loadToolbox').addEventListener
+ ('change',
+ function() {
+ controller.importFile(event.target.files[0],
+ WorkspaceFactoryController.MODE_TOOLBOX);
+ });
+ document.getElementById('input_loadToolbox').addEventListener
+ ('click', function() {blocklyFactory.closeModal()});
+ document.getElementById('input_loadPreload').addEventListener
+ ('change',
+ function() {
+ controller.importFile(event.target.files[0],
+ WorkspaceFactoryController.MODE_PRELOAD);
+ });
+ document.getElementById('input_loadPreload').addEventListener
+ ('click', function() {blocklyFactory.closeModal()});
+
+ // Export button.
+ document.getElementById('dropdown_exportOptions').addEventListener
+ ('click',
+ function() {
+ controller.exportInjectFile();
+ blocklyFactory.closeModal();
+ });
+ document.getElementById('dropdown_exportToolbox').addEventListener
+ ('click',
+ function() {
+ controller.exportXmlFile(WorkspaceFactoryController.MODE_TOOLBOX);
+ blocklyFactory.closeModal();
+ });
+ document.getElementById('dropdown_exportPreload').addEventListener
+ ('click',
+ function() {
+ controller.exportXmlFile(WorkspaceFactoryController.MODE_PRELOAD);
+ blocklyFactory.closeModal();
+ });
+ document.getElementById('dropdown_exportAll').addEventListener
+ ('click',
+ function() {
+ controller.exportInjectFile();
+ controller.exportXmlFile(WorkspaceFactoryController.MODE_TOOLBOX);
+ controller.exportXmlFile(WorkspaceFactoryController.MODE_PRELOAD);
+ blocklyFactory.closeModal();
+ });
+ document.getElementById('button_export').addEventListener
+ ('click',
+ function() {
+ blocklyFactory.openModal('dropdownDiv_export');
+ });
+
+ // Clear button.
+ document.getElementById('button_clear').addEventListener
+ ('click',
+ function() {
+ controller.clearAll();
+ });
+
+ // Toolbox and Workspace tabs.
+ document.getElementById('tab_toolbox').addEventListener
+ ('click',
+ function() {
+ controller.setMode(WorkspaceFactoryController.MODE_TOOLBOX);
+ });
+ document.getElementById('tab_preload').addEventListener
+ ('click',
+ function() {
+ controller.setMode(WorkspaceFactoryController.MODE_PRELOAD);
+ });
+
+ // '+' button.
+ document.getElementById('button_add').addEventListener
+ ('click',
+ function() {
+ blocklyFactory.openModal('dropdownDiv_add');
+ });
+ document.getElementById('dropdown_newCategory').addEventListener
+ ('click',
+ function() {
+ controller.addCategory();
+ blocklyFactory.closeModal();
+ });
+ document.getElementById('dropdown_loadCategory').addEventListener
+ ('click',
+ function() {
+ controller.loadCategory();
+ blocklyFactory.closeModal();
+ });
+ document.getElementById('dropdown_separator').addEventListener
+ ('click',
+ function() {
+ controller.addSeparator();
+ blocklyFactory.closeModal();
+ });
+ document.getElementById('dropdown_loadStandardToolbox').addEventListener
+ ('click',
+ function() {
+ controller.loadStandardToolbox();
+ blocklyFactory.closeModal();
+ });
+
+ // '-' button.
+ document.getElementById('button_remove').addEventListener
+ ('click',
+ function() {
+ controller.removeElement();
+ });
+
+ // Up/Down buttons.
+ document.getElementById('button_up').addEventListener
+ ('click',
+ function() {
+ controller.moveElement(-1);
+ });
+ document.getElementById('button_down').addEventListener
+ ('click',
+ function() {
+ controller.moveElement(1);
+ });
+
+ // Edit Category button.
+ document.getElementById('button_editCategory').addEventListener
+ ('click',
+ function() {
+ var selected = controller.model.getSelected();
+ // Return if a category is not selected.
+ if (selected.type != ListElement.TYPE_CATEGORY) {
+ return;
+ }
+ document.getElementById('categoryName').value = selected.name;
+ document.getElementById('categoryColour').value = selected.color ?
+ selected.color.substring(1).toLowerCase() : '';
+ console.log(document.getElementById('categoryColour').value);
+ // Link the colour picker to the field.
+ cp_init('categoryColour');
+ blocklyFactory.openModal('dropdownDiv_editCategory');
+ });
+
+ document.getElementById('categorySave').addEventListener
+ ('click',
+ function() {
+ var name = document.getElementById('categoryName').value.trim();
+ var colour = document.getElementById('categoryColour').value;
+ colour = colour ? '#' + colour : null;
+ controller.changeSelectedCategory(name, colour);
+ blocklyFactory.closeModal();
+ });
+
+ // Make/Remove Shadow buttons.
+ document.getElementById('button_addShadow').addEventListener
+ ('click',
+ function() {
+ controller.addShadow();
+ WorkspaceFactoryInit.displayAddShadow_(false);
+ WorkspaceFactoryInit.displayRemoveShadow_(true);
+ });
+ document.getElementById('button_removeShadow').addEventListener
+ ('click',
+ function() {
+ controller.removeShadow();
+ WorkspaceFactoryInit.displayAddShadow_(true);
+ WorkspaceFactoryInit.displayRemoveShadow_(false);
+
+ // Disable shadow editing button if turning invalid shadow block back
+ // to normal block.
+ if (!Blockly.selected.getSurroundParent()) {
+ document.getElementById('button_addShadow').disabled = true;
+ }
+ });
+
+ // Help button on workspace tab.
+ document.getElementById('button_optionsHelp').addEventListener
+ ('click', function() {
+ open('https://developers.google.com/blockly/guides/get-started/web#configuration');
+ });
+
+ // Reset to Default button on workspace tab.
+ document.getElementById('button_standardOptions').addEventListener
+ ('click', function() {
+ controller.setStandardOptionsAndUpdate();
+ });
+};
+
+/**
+ * Add event listeners for workspace factory.
+ * @param {!FactoryController} controller The controller for the workspace
+ * factory tab.
+ * @private
+ */
+WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) {
+ // Use up and down arrow keys to move categories.
+ window.addEventListener('keydown', function(e) {
+ // Don't let arrow keys have any effect if not in Workspace Factory
+ // editing the toolbox.
+ if (!(controller.keyEventsEnabled && controller.selectedMode
+ == WorkspaceFactoryController.MODE_TOOLBOX)) {
+ return;
+ }
+
+ if (e.keyCode == 38) {
+ // Arrow up.
+ controller.moveElement(-1);
+ } else if (e.keyCode == 40) {
+ // Arrow down.
+ controller.moveElement(1);
+ }
+ });
+
+ // Determines if a block breaks shadow block placement rules.
+ // Breaks rules if (1) a shadow block no longer has a valid
+ // parent, or (2) a normal block is inside of a shadow block.
+ var isInvalidBlockPlacement = function(block) {
+ return ((controller.isUserGenShadowBlock(block.id) &&
+ !block.getSurroundParent()) ||
+ (!controller.isUserGenShadowBlock(block.id) &&
+ block.getSurroundParent() &&
+ controller.isUserGenShadowBlock(block.getSurroundParent().id)));
+ };
+
+ // Add change listeners for toolbox workspace in workspace factory.
+ controller.toolboxWorkspace.addChangeListener(function(e) {
+ // Listen for Blockly move and delete events to update preview.
+ // Not listening for Blockly create events because causes the user to drop
+ // blocks when dragging them into workspace. Could cause problems if ever
+ // load blocks into workspace directly without calling updatePreview.
+ if (e.type == Blockly.Events.BLOCK_MOVE ||
+ e.type == Blockly.Events.BLOCK_DELETE ||
+ e.type == Blockly.Events.BLOCK_CHANGE) {
+ controller.saveStateFromWorkspace();
+ controller.updatePreview();
+ }
+
+ // Listen for Blockly UI events to correctly enable the "Edit Block" button.
+ // Only enable "Edit Block" when a block is selected and it has a
+ // surrounding parent, meaning it is nested in another block (blocks that
+ // are not nested in parents cannot be shadow blocks).
+ if (e.type == Blockly.Events.BLOCK_MOVE || (e.type == Blockly.Events.UI &&
+ e.element == 'selected')) {
+ var selected = Blockly.selected;
+
+ // Show shadow button if a block is selected. Show "Add Shadow" if
+ // a block is not a shadow block, show "Remove Shadow" if it is a
+ // shadow block.
+ if (selected) {
+ var isShadow = controller.isUserGenShadowBlock(selected.id);
+ WorkspaceFactoryInit.displayAddShadow_(!isShadow);
+ WorkspaceFactoryInit.displayRemoveShadow_(isShadow);
+ } else {
+ WorkspaceFactoryInit.displayAddShadow_(false);
+ WorkspaceFactoryInit.displayRemoveShadow_(false);
+ }
+
+ if (selected != null && selected.getSurroundParent() != null &&
+ !controller.isUserGenShadowBlock(selected.getSurroundParent().id)) {
+ // Selected block is a valid shadow block or could be a valid shadow
+ // block.
+
+ // Enable block editing and remove warnings if the block is not a
+ // variable user-generated shadow block.
+ document.getElementById('button_addShadow').disabled = false;
+ document.getElementById('button_removeShadow').disabled = false;
+
+ if (!FactoryUtils.hasVariableField(selected) &&
+ controller.isDefinedBlock(selected)) {
+ selected.setWarningText(null);
+ }
+ } else {
+ // Selected block cannot be a valid shadow block.
+
+ if (selected != null && isInvalidBlockPlacement(selected)) {
+ // Selected block breaks shadow block rules.
+ // Invalid shadow block if (1) a shadow block no longer has a valid
+ // parent, or (2) a normal block is inside of a shadow block.
+
+ if (!controller.isUserGenShadowBlock(selected.id)) {
+ // Warn if a non-shadow block is nested inside a shadow block.
+ selected.setWarningText('Only shadow blocks can be nested inside\n'
+ + 'other shadow blocks.');
+ } else if (!FactoryUtils.hasVariableField(selected)) {
+ // Warn if a shadow block is invalid only if not replacing
+ // warning for variables.
+ selected.setWarningText('Shadow blocks must be nested inside other'
+ + ' blocks.')
+ }
+
+ // Give editing options so that the user can make an invalid shadow
+ // block a normal block.
+ document.getElementById('button_removeShadow').disabled = false;
+ document.getElementById('button_addShadow').disabled = true;
+ } else {
+ // Selected block does not break any shadow block rules, but cannot
+ // be a shadow block.
+
+ // Remove possible 'invalid shadow block placement' warning.
+ if (selected != null && controller.isDefinedBlock(selected) &&
+ (!FactoryUtils.hasVariableField(selected) ||
+ !controller.isUserGenShadowBlock(selected.id))) {
+ selected.setWarningText(null);
+ }
+
+ // No block selected that is a shadow block or could be a valid shadow
+ // block. Disable block editing.
+ document.getElementById('button_addShadow').disabled = true;
+ document.getElementById('button_removeShadow').disabled = true;
+ }
+ }
+ }
+
+ // Convert actual shadow blocks added from the toolbox to user-generated
+ // shadow blocks.
+ if (e.type == Blockly.Events.BLOCK_CREATE) {
+ controller.convertShadowBlocks();
+
+ // Let the user create a Variables or Functions category if they use
+ // blocks from either category.
+
+ // Get all children of a block and add them to childList.
+ var getAllChildren = function(block, childList) {
+ childList.push(block);
+ var children = block.getChildren();
+ for (var i = 0, child; child = children[i]; i++) {
+ getAllChildren(child, childList);
+ }
+ };
+
+ var newBaseBlock = controller.toolboxWorkspace.getBlockById(e.blockId);
+ var allNewBlocks = [];
+ getAllChildren(newBaseBlock, allNewBlocks);
+ var variableCreated = false;
+ var procedureCreated = false;
+
+ // Check if the newly created block or any of its children are variable
+ // or procedure blocks.
+ for (var i = 0, block; block = allNewBlocks[i]; i++) {
+ if (FactoryUtils.hasVariableField(block)) {
+ variableCreated = true;
+ } else if (FactoryUtils.isProcedureBlock(block)) {
+ procedureCreated = true;
+ }
+ }
+
+ // If any of the newly created blocks are variable or procedure blocks,
+ // prompt the user to create the corresponding standard category.
+ if (variableCreated && !controller.hasVariablesCategory()) {
+ if (confirm('Your new block has a variables field. To use this block '
+ + 'fully, you will need a Variables category. Do you want to add '
+ + 'a Variables category to your custom toolbox?')) {
+ controller.setMode(WorkspaceFactoryController.MODE_TOOLBOX);
+ controller.loadCategoryByName('variables');
+ }
+ }
+
+ if (procedureCreated && !controller.hasProceduresCategory()) {
+ if (confirm('Your new block is a function block. To use this block '
+ + 'fully, you will need a Functions category. Do you want to add '
+ + 'a Functions category to your custom toolbox?')) {
+ controller.setMode(WorkspaceFactoryController.MODE_TOOLBOX);
+ controller.loadCategoryByName('functions');
+ }
+ }
+ }
+ });
+};
+
+/**
+ * Display or hide the add shadow button.
+ * @param {boolean} show True if the add shadow button should be shown, false
+ * otherwise.
+ */
+WorkspaceFactoryInit.displayAddShadow_ = function(show) {
+ document.getElementById('button_addShadow').style.display =
+ show ? 'inline-block' : 'none';
+};
+
+/**
+ * Display or hide the remove shadow button.
+ * @param {boolean} show True if the remove shadow button should be shown, false
+ * otherwise.
+ */
+WorkspaceFactoryInit.displayRemoveShadow_ = function(show) {
+ document.getElementById('button_removeShadow').style.display =
+ show ? 'inline-block' : 'none';
+};
+
+/**
+ * Add listeners for workspace factory options input elements.
+ * @param {!FactoryController} controller The controller for the workspace
+ * factory tab.
+ * @private
+ */
+WorkspaceFactoryInit.addWorkspaceFactoryOptionsListeners_ =
+ function(controller) {
+ // Checking the grid checkbox displays grid options.
+ document.getElementById('option_grid_checkbox').addEventListener('change',
+ function(e) {
+ document.getElementById('grid_options').style.display =
+ document.getElementById('option_grid_checkbox').checked ?
+ 'block' : 'none';
+ });
+
+ // Checking the zoom checkbox displays zoom options.
+ document.getElementById('option_zoom_checkbox').addEventListener('change',
+ function(e) {
+ document.getElementById('zoom_options').style.display =
+ document.getElementById('option_zoom_checkbox').checked ?
+ 'block' : 'none';
+ });
+
+ // Checking the readonly checkbox enables/disables other options.
+ document.getElementById('option_readOnly_checkbox').addEventListener('change',
+ function(e) {
+ var checkbox = document.getElementById('option_readOnly_checkbox');
+ blocklyFactory.ifCheckedEnable(!checkbox.checked,
+ ['readonly1', 'readonly2']);
+ });
+
+ document.getElementById('option_infiniteBlocks_checkbox').addEventListener('change',
+ function(e) {
+ document.getElementById('maxBlockNumber_option').style.display =
+ document.getElementById('option_infiniteBlocks_checkbox').checked ?
+ 'none' : 'block';
+ });
+
+ // Generate new options every time an options input is updated.
+ var div = document.getElementById('workspace_options');
+ var options = div.getElementsByTagName('input');
+ for (var i = 0, option; option = options[i]; i++) {
+ option.addEventListener('change', function() {
+ controller.generateNewOptions();
+ });
+ }
+};
diff --git a/js/demos/blockfactory/workspacefactory/wfactory_model.js b/js/demos/blockfactory/workspacefactory/wfactory_model.js
new file mode 100644
index 0000000..ff9eaff
--- /dev/null
+++ b/js/demos/blockfactory/workspacefactory/wfactory_model.js
@@ -0,0 +1,561 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ *
+ * 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 WorkspaceFactoryModel
+ * @constructor
+ */
+WorkspaceFactoryModel = function() {
+ // Ordered list of ListElement objects. Empty if there is a single flyout.
+ this.toolboxList = [];
+ // ListElement for blocks in a single flyout. Null if a toolbox exists.
+ this.flyout = new ListElement(ListElement.TYPE_FLYOUT);
+ // Array of block IDs for all user created shadow blocks.
+ this.shadowBlocks = [];
+ // Reference to currently selected ListElement. Stored in this.toolboxList if
+ // there are categories, or in this.flyout if blocks are displayed in a single
+ // flyout.
+ this.selected = this.flyout;
+ // Boolean for if a Variable category has been added.
+ this.hasVariableCategory = false;
+ // Boolean for if a Procedure category has been added.
+ this.hasProcedureCategory = false;
+ // XML to be pre-loaded to workspace. Empty on default;
+ this.preloadXml = Blockly.utils.xml.createElement('xml');
+ // Options object to be configured for Blockly inject call.
+ this.options = new Object(null);
+ // Block Library block types.
+ this.libBlockTypes = [];
+ // Imported block types.
+ this.importedBlockTypes = [];
+ //
+};
+
+/**
+ * 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.
+ */
+WorkspaceFactoryModel.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.
+ */
+WorkspaceFactoryModel.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.
+ */
+WorkspaceFactoryModel.prototype.hasProcedures = function() {
+ return this.hasProcedureCategory;
+};
+
+/**
+ * Determines if the user has any elements in the toolbox. Uses the length of
+ * toolboxList.
+ * @return {boolean} True if elements exist, false otherwise.
+ */
+WorkspaceFactoryModel.prototype.hasElements = 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.
+ */
+WorkspaceFactoryModel.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);
+ // Empty single flyout.
+ this.flyout = null;
+};
+
+/**
+ * Given an index, deletes a list element and all associated data.
+ * @param {number} index The index of the list element to delete.
+ */
+WorkspaceFactoryModel.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);
+};
+
+/**
+ * Sets selected to be an empty category not in toolbox list if toolbox list
+ * is empty. Should be called when removing the last element from toolbox list.
+ * If the toolbox list is empty, selected stores the XML for the single flyout
+ * of blocks displayed.
+ */
+WorkspaceFactoryModel.prototype.createDefaultSelectedIfEmpty = function() {
+ if (this.toolboxList.length == 0) {
+ this.flyout = new ListElement(ListElement.TYPE_FLYOUT);
+ this.selected = this.flyout;
+ }
+};
+
+/**
+ * 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 {number} newIndex The index to insert the element at.
+ * @param {number} oldIndex The index the element is currently at.
+ */
+WorkspaceFactoryModel.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 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.
+ */
+WorkspaceFactoryModel.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.
+ */
+WorkspaceFactoryModel.prototype.getSelectedName = function() {
+ return this.selected ? this.selected.name : null;
+};
+
+/**
+ * Returns the currently selected list element object.
+ * @return {ListElement} The currently selected ListElement
+ */
+WorkspaceFactoryModel.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.
+ */
+WorkspaceFactoryModel.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 {number} The index of the list element in toolboxList, or -1 if it
+ * doesn't exist.
+ */
+WorkspaceFactoryModel.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.
+ */
+WorkspaceFactoryModel.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 {number} index The index of the element to return.
+ * @return {ListElement} The corresponding ListElement object in toolboxList.
+ */
+WorkspaceFactoryModel.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.
+ */
+WorkspaceFactoryModel.prototype.getSelectedXml = function() {
+ return this.selected ? this.selected.xml : null;
+};
+
+/**
+ * Return ordered list of ListElement objects.
+ * @return {!Array.} ordered list of ListElement objects
+ */
+WorkspaceFactoryModel.prototype.getToolboxList = function() {
+ return this.toolboxList;
+};
+
+/**
+ * Gets the ID of a category given its name.
+ * @param {string} name Name of category.
+ * @return {number} ID of category
+ */
+WorkspaceFactoryModel.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.
+ */
+WorkspaceFactoryModel.prototype.clearToolboxList = function() {
+ this.toolboxList = [];
+ this.hasVariableCategory = false;
+ this.hasProcedureCategory = false;
+ this.shadowBlocks = [];
+ this.selected.xml = Blockly.utils.xml.createElement('xml');
+};
+
+/**
+ * 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.
+ */
+WorkspaceFactoryModel.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.
+ */
+WorkspaceFactoryModel.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.
+ */
+WorkspaceFactoryModel.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.
+ */
+WorkspaceFactoryModel.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.
+ */
+WorkspaceFactoryModel.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';
+ }
+};
+
+/**
+ * Have basic pre-loaded workspace working
+ * Saves XML as XML to be pre-loaded into the workspace.
+ * @param {!Element} xml The XML to be saved.
+ */
+WorkspaceFactoryModel.prototype.savePreloadXml = function(xml) {
+ this.preloadXml = xml
+};
+
+/**
+ * Gets the XML to be pre-loaded into the workspace.
+ * @return {!Element} The XML for the workspace.
+ */
+WorkspaceFactoryModel.prototype.getPreloadXml = function() {
+ return this.preloadXml;
+};
+
+/**
+ * Sets a new options object for injecting a Blockly workspace.
+ * @param {Object} options Options object for injecting a Blockly workspace.
+ */
+WorkspaceFactoryModel.prototype.setOptions = function(options) {
+ this.options = options;
+};
+
+/**
+ * Returns an array of all the block types currently being used in the toolbox
+ * and the pre-loaded blocks. No duplicates.
+ * TODO(evd2014): Move pushBlockTypesToList to FactoryUtils.
+ * @return {!Array.} Array of block types currently being used.
+ */
+WorkspaceFactoryModel.prototype.getAllUsedBlockTypes = function() {
+ var blockTypeList = [];
+
+ // Given XML for the workspace, adds all block types included in the XML
+ // to the list, not including duplicates.
+ var pushBlockTypesToList = function(xml, list) {
+ // Get all block XML nodes.
+ var blocks = xml.getElementsByTagName('block');
+
+ // Add block types if not already in list.
+ for (var i = 0; i < blocks.length; i++) {
+ var type = blocks[i].getAttribute('type');
+ if (list.indexOf(type) == -1) {
+ list.push(type);
+ }
+ }
+ };
+
+ if (this.flyout) {
+ // If has a single flyout, add block types for the single flyout.
+ pushBlockTypesToList(this.getSelectedXml(), blockTypeList);
+ } else {
+ // If has categories, add block types for each category.
+
+ for (var i = 0, category; category = this.toolboxList[i]; i++) {
+ if (category.type == ListElement.TYPE_CATEGORY) {
+ pushBlockTypesToList(category.xml, blockTypeList);
+ }
+ }
+ }
+
+ // Add the block types from any pre-loaded blocks.
+ pushBlockTypesToList(this.getPreloadXml(), blockTypeList);
+
+ return blockTypeList;
+};
+
+/**
+ * Adds new imported block types to the list of current imported block types.
+ * @param {!Array.} blockTypes Array of block types imported.
+ */
+WorkspaceFactoryModel.prototype.addImportedBlockTypes = function(blockTypes) {
+ this.importedBlockTypes = this.importedBlockTypes.concat(blockTypes);
+};
+
+/**
+ * Updates block types in block library.
+ * @param {!Array.} blockTypes Array of block types in block library.
+ */
+WorkspaceFactoryModel.prototype.updateLibBlockTypes = function(blockTypes) {
+ this.libBlockTypes = blockTypes;
+};
+
+/**
+ * Determines if a block type is defined as a standard block, in the block
+ * library, or as an imported block.
+ * @param {string} blockType Block type to check.
+ * @return {boolean} True if blockType is defined, false otherwise.
+ */
+WorkspaceFactoryModel.prototype.isDefinedBlockType = function(blockType) {
+ var isStandardBlock = StandardCategories.coreBlockTypes.indexOf(blockType)
+ != -1;
+ var isLibBlock = this.libBlockTypes.indexOf(blockType) != -1;
+ var isImportedBlock = this.importedBlockTypes.indexOf(blockType) != -1;
+ return (isStandardBlock || isLibBlock || isImportedBlock);
+};
+
+/**
+ * Checks if any of the block types are already defined.
+ * @param {!Array.} blockTypes Array of block types.
+ * @return {boolean} True if a block type in the array is already defined,
+ * false if none of the blocks are already defined.
+ */
+WorkspaceFactoryModel.prototype.hasDefinedBlockTypes = function(blockTypes) {
+ for (var i = 0, blockType; blockType = blockTypes[i]; i++) {
+ if (this.isDefinedBlockType(blockType)) {
+ return true;
+ }
+ }
+ return false;
+};
+
+/**
+ * Class for a ListElement.
+ * @constructor
+ */
+ListElement = function(type, opt_name) {
+ this.type = type;
+ // XML DOM element to load the element.
+ this.xml = Blockly.utils.xml.createElement('xml');
+ // 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.utils.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';
+ListElement.TYPE_FLYOUT = 'flyout';
+
+/**
+ * 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 XML for categories and flyouts.
+ if (this.type == ListElement.TYPE_FLYOUT ||
+ this.type == ListElement.TYPE_CATEGORY) {
+ 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,
+ * or null if none.
+ */
+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.utils.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/js/demos/blockfactory/workspacefactory/wfactory_view.js b/js/demos/blockfactory/workspacefactory/wfactory_view.js
new file mode 100644
index 0000000..287fb3d
--- /dev/null
+++ b/js/demos/blockfactory/workspacefactory/wfactory_view.js
@@ -0,0 +1,438 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ *
+ * 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 WorkspaceFactoryController (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 WorkspaceFactoryView
+ * @constructor
+ */
+WorkspaceFactoryView = 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
+ * @return {!Element} DOM element created for tab
+ */
+WorkspaceFactoryView.prototype.addCategoryRow = function(name, id) {
+ var table = document.getElementById('categoryTable');
+ var count = table.rows.length;
+
+ // Delete help label and enable category buttons if it's the first category.
+ if (count == 0) {
+ document.getElementById('categoryHeader').textContent = 'Your categories:';
+ }
+
+ // Create tab.
+ 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.
+ */
+WorkspaceFactoryView.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.
+ */
+WorkspaceFactoryView.prototype.addEmptyCategoryMessage = function() {
+ var table = document.getElementById('categoryTable');
+ if (!table.rows.length) {
+ document.getElementById('categoryHeader').textContent =
+ 'You currently have no categories.';
+ }
+};
+
+/**
+ * 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 {number} selectedIndex The index of the currently selected category,
+ * -1 if no categories created.
+ * @param {ListElement} selected The selected ListElement.
+ */
+WorkspaceFactoryView.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;
+ var table = document.getElementById('categoryTable');
+ document.getElementById('button_down').disabled = selectedIndex >=
+ table.rows.length - 1 || selectedIndex < 0;
+ // 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
+ */
+WorkspaceFactoryView.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.
+ */
+WorkspaceFactoryView.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.
+ */
+WorkspaceFactoryView.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
+ */
+WorkspaceFactoryView.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
+ */
+WorkspaceFactoryView.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 {number} newIndex The index to move the category to.
+ * @param {number} oldIndex The index the category is currently at.
+ */
+WorkspaceFactoryView.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 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} colour The colour for to be used for the border of the tab,
+ * or null if none. Must be a valid CSS string.
+ */
+WorkspaceFactoryView.prototype.setBorderColor = function(id, colour) {
+ var style = this.tabMap[id].style;
+ if (colour) {
+ style.borderLeftWidth = '8px';
+ style.borderLeftStyle = 'solid';
+ style.borderColor = colour;
+ } else {
+ style.borderLeftWidth = '';
+ style.borderLeftStyle = '';
+ style.borderColor = '';
+ }
+};
+
+/**
+ * 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.
+ */
+WorkspaceFactoryView.prototype.addSeparatorTab = function(id) {
+ var table = document.getElementById('categoryTable');
+ var count = table.rows.length;
+
+ if (count == 0) {
+ document.getElementById('categoryHeader').textContent = 'Your categories:';
+ }
+ // Create separator.
+ 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.
+ */
+WorkspaceFactoryView.prototype.disableWorkspace = function(disable) {
+ if (disable) {
+ document.getElementById('toolbox_section').className = 'disabled';
+ document.getElementById('toolbox_blocks').style.pointerEvents = 'none';
+ } else {
+ document.getElementById('toolbox_section').className = '';
+ document.getElementById('toolbox_blocks').style.pointerEvents = 'auto';
+ }
+
+};
+
+/**
+ * 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.
+ */
+WorkspaceFactoryView.prototype.shouldDisableWorkspace = function(category) {
+ return category != null && category.type != ListElement.TYPE_FLYOUT &&
+ (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.
+ */
+WorkspaceFactoryView.prototype.clearToolboxTabs = function() {
+ this.tabMap = [];
+ var oldCategoryTable = document.getElementById('categoryTable');
+ var newCategoryTable = document.createElement('table');
+ newCategoryTable.id = 'categoryTable';
+ newCategoryTable.style.width = 'auto';
+ 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 {!Array.} blocks Array of user-generated shadow blocks
+ * currently loaded.
+ */
+WorkspaceFactoryView.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).
+ */
+WorkspaceFactoryView.prototype.markShadowBlock = function(block) {
+ // Add Blockly CSS for user-generated shadow blocks.
+ Blockly.utils.dom.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.');
+ }
+ if (FactoryUtils.hasVariableField(block)) {
+ block.setWarningText('Cannot make variable blocks shadow blocks.');
+ }
+};
+
+/**
+ * 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).
+ */
+WorkspaceFactoryView.prototype.unmarkShadowBlock = function(block) {
+ // Remove Blockly CSS for user-generated shadow blocks.
+ Blockly.utils.dom.removeClass(block.svgGroup_, 'shadowBlock');
+};
+
+/**
+ * Sets the tabs for modes according to which mode the user is currenly
+ * editing in.
+ * @param {string} mode The mode being switched to
+ * (WorkspaceFactoryController.MODE_TOOLBOX or WorkspaceFactoryController.MODE_PRELOAD).
+ */
+WorkspaceFactoryView.prototype.setModeSelection = function(mode) {
+ document.getElementById('tab_preload').className = mode ==
+ WorkspaceFactoryController.MODE_PRELOAD ? 'tabon' : 'taboff';
+ document.getElementById('preload_div').style.display = mode ==
+ WorkspaceFactoryController.MODE_PRELOAD ? 'block' : 'none';
+ document.getElementById('tab_toolbox').className = mode ==
+ WorkspaceFactoryController.MODE_TOOLBOX ? 'tabon' : 'taboff';
+ document.getElementById('toolbox_div').style.display = mode ==
+ WorkspaceFactoryController.MODE_TOOLBOX ? 'block' : 'none';
+};
+
+/**
+ * Updates the help text above the workspace depending on the selected mode.
+ * @param {string} mode The selected mode (WorkspaceFactoryController.MODE_TOOLBOX or
+ * WorkspaceFactoryController.MODE_PRELOAD).
+ */
+WorkspaceFactoryView.prototype.updateHelpText = function(mode) {
+ if (mode == WorkspaceFactoryController.MODE_TOOLBOX) {
+ var helpText = 'Drag blocks into the workspace to configure the toolbox ' +
+ 'in your custom workspace.';
+ } else {
+ var helpText = 'Drag blocks into the workspace to pre-load them in your ' +
+ 'custom workspace.'
+ }
+ document.getElementById('editHelpText').textContent = helpText;
+};
+
+/**
+ * Sets the basic options that are not dependent on if there are categories
+ * or a single flyout of blocks. Updates checkboxes and text fields.
+ */
+WorkspaceFactoryView.prototype.setBaseOptions = function() {
+ // Readonly mode.
+ document.getElementById('option_readOnly_checkbox').checked = false;
+ blocklyFactory.ifCheckedEnable(true, ['readonly1', 'readonly2']);
+
+ // Set basic options.
+ document.getElementById('option_css_checkbox').checked = true;
+ document.getElementById('option_maxBlocks_number').value = 100;
+ document.getElementById('option_media_text').value =
+ 'https://blockly-demo.appspot.com/static/media/';
+ document.getElementById('option_rtl_checkbox').checked = false;
+ document.getElementById('option_sounds_checkbox').checked = true;
+ document.getElementById('option_oneBasedIndex_checkbox').checked = true;
+ document.getElementById('option_horizontalLayout_checkbox').checked = false;
+ document.getElementById('option_toolboxPosition_checkbox').checked = false;
+
+ // Check infinite blocks and hide suboption.
+ document.getElementById('option_infiniteBlocks_checkbox').checked = true;
+ document.getElementById('maxBlockNumber_option').style.display =
+ 'none';
+
+ // Uncheck grid and zoom options and hide suboptions.
+ document.getElementById('option_grid_checkbox').checked = false;
+ document.getElementById('grid_options').style.display = 'none';
+ document.getElementById('option_zoom_checkbox').checked = false;
+ document.getElementById('zoom_options').style.display = 'none';
+
+ // Set grid options.
+ document.getElementById('gridOption_spacing_number').value = 20;
+ document.getElementById('gridOption_length_number').value = 1;
+ document.getElementById('gridOption_colour_text').value = '#888';
+ document.getElementById('gridOption_snap_checkbox').checked = false;
+
+ // Set zoom options.
+ document.getElementById('zoomOption_controls_checkbox').checked = true;
+ document.getElementById('zoomOption_wheel_checkbox').checked = true;
+ document.getElementById('zoomOption_startScale_number').value = 1.0;
+ document.getElementById('zoomOption_maxScale_number').value = 3;
+ document.getElementById('zoomOption_minScale_number').value = 0.3;
+ document.getElementById('zoomOption_scaleSpeed_number').value = 1.2;
+};
+
+/**
+ * Updates category specific options depending on if there are categories
+ * currently present. Updates checkboxes and text fields in the view.
+ * @param {boolean} hasCategories True if categories are present, false if all
+ * blocks are displayed in a single flyout.
+ */
+WorkspaceFactoryView.prototype.setCategoryOptions = function(hasCategories) {
+ document.getElementById('option_collapse_checkbox').checked = hasCategories;
+ document.getElementById('option_comments_checkbox').checked = hasCategories;
+ document.getElementById('option_disable_checkbox').checked = hasCategories;
+ document.getElementById('option_scrollbars_checkbox').checked = hasCategories;
+ document.getElementById('option_trashcan_checkbox').checked = hasCategories;
+};
diff --git a/js/demos/blockfactory_old/blocks.js b/js/demos/blockfactory_old/blocks.js
new file mode 100644
index 0000000..58c7ca3
--- /dev/null
+++ b/js/demos/blockfactory_old/blocks.js
@@ -0,0 +1,824 @@
+/**
+ * @license
+ * Copyright 2012 Google LLC
+ *
+ * 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 Blocks for Blockly's Block Factory application.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+Blockly.Blocks['factory_base'] = {
+ // Base of new block.
+ init: function() {
+ this.setColour(120);
+ this.appendDummyInput()
+ .appendField('name')
+ .appendField(new Blockly.FieldTextInput('block_type'), 'NAME');
+ this.appendStatementInput('INPUTS')
+ .setCheck('Input')
+ .appendField('inputs');
+ var dropdown = new Blockly.FieldDropdown([
+ ['automatic inputs', 'AUTO'],
+ ['external inputs', 'EXT'],
+ ['inline inputs', 'INT']]);
+ this.appendDummyInput()
+ .appendField(dropdown, 'INLINE');
+ dropdown = new Blockly.FieldDropdown([
+ ['no connections', 'NONE'],
+ ['← left output', 'LEFT'],
+ ['↕ top+bottom connections', 'BOTH'],
+ ['↑ top connection', 'TOP'],
+ ['↓ bottom connection', 'BOTTOM']],
+ function(option) {
+ this.sourceBlock_.updateShape_(option);
+ // Connect a shadow block to this new input.
+ this.sourceBlock_.spawnOutputShadow_(option);
+ });
+ this.appendDummyInput()
+ .appendField(dropdown, 'CONNECTIONS');
+ this.appendValueInput('COLOUR')
+ .setCheck('Colour')
+ .appendField('colour');
+ this.setTooltip('Build a custom block by plugging\n' +
+ 'fields, inputs and other blocks here.');
+ this.setHelpUrl(
+ 'https://developers.google.com/blockly/guides/create-custom-blocks/block-factory');
+ },
+ mutationToDom: function() {
+ var container = Blockly.utils.xml.createElement('mutation');
+ container.setAttribute('connections', this.getFieldValue('CONNECTIONS'));
+ return container;
+ },
+ domToMutation: function(xmlElement) {
+ var connections = xmlElement.getAttribute('connections');
+ this.updateShape_(connections);
+ },
+ spawnOutputShadow_: function(option) {
+ // Helper method for deciding which type of outputs this block needs
+ // to attach shaddow blocks to.
+ switch (option) {
+ case 'LEFT':
+ this.connectOutputShadow_('OUTPUTTYPE');
+ break;
+ case 'TOP':
+ this.connectOutputShadow_('TOPTYPE');
+ break;
+ case 'BOTTOM':
+ this.connectOutputShadow_('BOTTOMTYPE');
+ break;
+ case 'BOTH':
+ this.connectOutputShadow_('TOPTYPE');
+ this.connectOutputShadow_('BOTTOMTYPE');
+ break;
+ }
+ },
+ connectOutputShadow_: function(outputType) {
+ // Helper method to create & connect shadow block.
+ var type = this.workspace.newBlock('type_null');
+ type.setShadow(true);
+ type.outputConnection.connect(this.getInput(outputType).connection);
+ type.initSvg();
+ type.render();
+ },
+ updateShape_: function(option) {
+ var outputExists = this.getInput('OUTPUTTYPE');
+ var topExists = this.getInput('TOPTYPE');
+ var bottomExists = this.getInput('BOTTOMTYPE');
+ if (option == 'LEFT') {
+ if (!outputExists) {
+ this.addTypeInput_('OUTPUTTYPE', 'output type');
+ }
+ } else if (outputExists) {
+ this.removeInput('OUTPUTTYPE');
+ }
+ if (option == 'TOP' || option == 'BOTH') {
+ if (!topExists) {
+ this.addTypeInput_('TOPTYPE', 'top type');
+ }
+ } else if (topExists) {
+ this.removeInput('TOPTYPE');
+ }
+ if (option == 'BOTTOM' || option == 'BOTH') {
+ if (!bottomExists) {
+ this.addTypeInput_('BOTTOMTYPE', 'bottom type');
+ }
+ } else if (bottomExists) {
+ this.removeInput('BOTTOMTYPE');
+ }
+ },
+ addTypeInput_: function(name, label) {
+ this.appendValueInput(name)
+ .setCheck('Type')
+ .appendField(label);
+ this.moveInputBefore(name, 'COLOUR');
+ }
+};
+
+var FIELD_MESSAGE = 'fields %1 %2';
+var FIELD_ARGS = [
+ {
+ "type": "field_dropdown",
+ "name": "ALIGN",
+ "options": [['left', 'LEFT'], ['right', 'RIGHT'], ['centre', 'CENTRE']],
+ },
+ {
+ "type": "input_statement",
+ "name": "FIELDS",
+ "check": "Field"
+ }
+];
+
+var TYPE_MESSAGE = 'type %1';
+var TYPE_ARGS = [
+ {
+ "type": "input_value",
+ "name": "TYPE",
+ "check": "Type",
+ "align": "RIGHT"
+ }
+];
+
+Blockly.Blocks['input_value'] = {
+ // Value input.
+ init: function() {
+ this.jsonInit({
+ "message0": "value input %1 %2",
+ "args0": [
+ {
+ "type": "field_input",
+ "name": "INPUTNAME",
+ "text": "NAME"
+ },
+ {
+ "type": "input_dummy"
+ }
+ ],
+ "message1": FIELD_MESSAGE,
+ "args1": FIELD_ARGS,
+ "message2": TYPE_MESSAGE,
+ "args2": TYPE_ARGS,
+ "previousStatement": "Input",
+ "nextStatement": "Input",
+ "colour": 210,
+ "tooltip": "A value socket for horizontal connections.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=71"
+ });
+ },
+ onchange: function() {
+ inputNameCheck(this);
+ }
+};
+
+Blockly.Blocks['input_statement'] = {
+ // Statement input.
+ init: function() {
+ this.jsonInit({
+ "message0": "statement input %1 %2",
+ "args0": [
+ {
+ "type": "field_input",
+ "name": "INPUTNAME",
+ "text": "NAME"
+ },
+ {
+ "type": "input_dummy"
+ },
+ ],
+ "message1": FIELD_MESSAGE,
+ "args1": FIELD_ARGS,
+ "message2": TYPE_MESSAGE,
+ "args2": TYPE_ARGS,
+ "previousStatement": "Input",
+ "nextStatement": "Input",
+ "colour": 210,
+ "tooltip": "A statement socket for enclosed vertical stacks.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=246"
+ });
+ },
+ onchange: function() {
+ inputNameCheck(this);
+ }
+};
+
+Blockly.Blocks['input_dummy'] = {
+ // Dummy input.
+ init: function() {
+ this.jsonInit({
+ "message0": "dummy input",
+ "message1": FIELD_MESSAGE,
+ "args1": FIELD_ARGS,
+ "previousStatement": "Input",
+ "nextStatement": "Input",
+ "colour": 210,
+ "tooltip": "For adding fields on a separate row with no " +
+ "connections. Alignment options (left, right, centre) " +
+ "apply only to multi-line fields.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=293"
+ });
+ }
+};
+
+Blockly.Blocks['field_static'] = {
+ // Text value.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('text')
+ .appendField(new Blockly.FieldTextInput(''), 'TEXT');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Static text that serves as a label.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=88');
+ }
+};
+
+Blockly.Blocks['field_input'] = {
+ // Text input.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('text input')
+ .appendField(new Blockly.FieldTextInput('default'), 'TEXT')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('An input field for the user to enter text.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=319');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_number'] = {
+ // Numeric input.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('numeric input')
+ .appendField(new Blockly.FieldNumber(0), 'VALUE')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.appendDummyInput()
+ .appendField('min')
+ .appendField(new Blockly.FieldNumber(-Infinity), 'MIN')
+ .appendField('max')
+ .appendField(new Blockly.FieldNumber(Infinity), 'MAX')
+ .appendField('precision')
+ .appendField(new Blockly.FieldNumber(0, 0), 'PRECISION');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('An input field for the user to enter a number.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=319');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_angle'] = {
+ // Angle input.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('angle input')
+ .appendField(new Blockly.FieldAngle('90'), 'ANGLE')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('An input field for the user to enter an angle.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=372');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_dropdown'] = {
+ // Dropdown menu.
+ init: function() {
+ this.appendDummyInput()
+ .appendField('dropdown')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.optionCount_ = 3;
+ this.updateShape_();
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setMutator(new Blockly.Mutator(['field_dropdown_option']));
+ this.setColour(160);
+ this.setTooltip('Dropdown menu with a list of options.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386');
+ },
+ mutationToDom: function(workspace) {
+ // Create XML to represent menu options.
+ var container = Blockly.utils.xml.createElement('mutation');
+ container.setAttribute('options', this.optionCount_);
+ return container;
+ },
+ domToMutation: function(container) {
+ // Parse XML to restore the menu options.
+ this.optionCount_ = parseInt(container.getAttribute('options'), 10);
+ this.updateShape_();
+ },
+ decompose: function(workspace) {
+ // Populate the mutator's dialog with this block's components.
+ var containerBlock = workspace.newBlock('field_dropdown_container');
+ containerBlock.initSvg();
+ var connection = containerBlock.getInput('STACK').connection;
+ for (var i = 0; i < this.optionCount_; i++) {
+ var optionBlock = workspace.newBlock('field_dropdown_option');
+ optionBlock.initSvg();
+ connection.connect(optionBlock.previousConnection);
+ connection = optionBlock.nextConnection;
+ }
+ return containerBlock;
+ },
+ compose: function(containerBlock) {
+ // Reconfigure this block based on the mutator dialog's components.
+ var optionBlock = containerBlock.getInputTargetBlock('STACK');
+ // Count number of inputs.
+ var data = [];
+ while (optionBlock) {
+ data.push([optionBlock.userData_, optionBlock.cpuData_]);
+ optionBlock = optionBlock.nextConnection &&
+ optionBlock.nextConnection.targetBlock();
+ }
+ this.optionCount_ = data.length;
+ this.updateShape_();
+ // Restore any data.
+ for (var i = 0; i < this.optionCount_; i++) {
+ this.setFieldValue(data[i][0] || 'option', 'USER' + i);
+ this.setFieldValue(data[i][1] || 'OPTIONNAME', 'CPU' + i);
+ }
+ },
+ saveConnections: function(containerBlock) {
+ // Store names and values for each option.
+ var optionBlock = containerBlock.getInputTargetBlock('STACK');
+ var i = 0;
+ while (optionBlock) {
+ optionBlock.userData_ = this.getFieldValue('USER' + i);
+ optionBlock.cpuData_ = this.getFieldValue('CPU' + i);
+ i++;
+ optionBlock = optionBlock.nextConnection &&
+ optionBlock.nextConnection.targetBlock();
+ }
+ },
+ updateShape_: function() {
+ // Modify this block to have the correct number of options.
+ // Add new options.
+ for (var i = 0; i < this.optionCount_; i++) {
+ if (!this.getInput('OPTION' + i)) {
+ this.appendDummyInput('OPTION' + i)
+ .appendField(new Blockly.FieldTextInput('option'), 'USER' + i)
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('OPTIONNAME'), 'CPU' + i);
+ }
+ }
+ // Remove deleted options.
+ while (this.getInput('OPTION' + i)) {
+ this.removeInput('OPTION' + i);
+ i++;
+ }
+ },
+ onchange: function() {
+ if (this.workspace && this.optionCount_ < 1) {
+ this.setWarningText('Drop down menu must\nhave at least one option.');
+ } else {
+ fieldNameCheck(this);
+ }
+ }
+};
+
+Blockly.Blocks['field_dropdown_container'] = {
+ // Container.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('add options');
+ this.appendStatementInput('STACK');
+ this.setTooltip('Add, remove, or reorder options\n' +
+ 'to reconfigure this dropdown menu.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386');
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['field_dropdown_option'] = {
+ // Add option.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('option');
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setTooltip('Add a new option to the dropdown menu.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386');
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['field_checkbox'] = {
+ // Checkbox.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('checkbox')
+ .appendField(new Blockly.FieldCheckbox('TRUE'), 'CHECKED')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Checkbox field.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=485');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_colour'] = {
+ // Colour input.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('colour')
+ .appendField(new Blockly.FieldColour('#ff0000'), 'COLOUR')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Colour input field.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=495');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_date'] = {
+ // Date input.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('date')
+ .appendField(new Blockly.FieldDate(), 'DATE')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Date input field.');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_variable'] = {
+ // Dropdown for variables.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('variable')
+ .appendField(new Blockly.FieldTextInput('item'), 'TEXT')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Dropdown menu for variable names.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=510');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_image'] = {
+ // Image.
+ init: function() {
+ this.setColour(160);
+ var src = 'https://www.gstatic.com/codesite/ph/images/star_on.gif';
+ this.appendDummyInput()
+ .appendField('image')
+ .appendField(new Blockly.FieldTextInput(src), 'SRC');
+ this.appendDummyInput()
+ .appendField('width')
+ .appendField(new Blockly.FieldNumber('15', 0, NaN, 1), 'WIDTH')
+ .appendField('height')
+ .appendField(new Blockly.FieldNumber('15', 0, NaN, 1), 'HEIGHT')
+ .appendField('alt text')
+ .appendField(new Blockly.FieldTextInput('*'), 'ALT');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Static image (JPEG, PNG, GIF, SVG, BMP).\n' +
+ 'Retains aspect ratio regardless of height and width.\n' +
+ 'Alt text is for when collapsed.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=567');
+ }
+};
+
+Blockly.Blocks['type_group'] = {
+ // Group of types.
+ init: function() {
+ this.typeCount_ = 2;
+ this.updateShape_();
+ this.setOutput(true, 'Type');
+ this.setMutator(new Blockly.Mutator(['type_group_item']));
+ this.setColour(230);
+ this.setTooltip('Allows more than one type to be accepted.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677');
+ },
+ mutationToDom: function(workspace) {
+ // Create XML to represent a group of types.
+ var container = Blockly.utils.xml.createElement('mutation');
+ container.setAttribute('types', this.typeCount_);
+ return container;
+ },
+ domToMutation: function(container) {
+ // Parse XML to restore the group of types.
+ this.typeCount_ = parseInt(container.getAttribute('types'), 10);
+ this.updateShape_();
+ for (var i = 0; i < this.typeCount_; i++) {
+ this.removeInput('TYPE' + i);
+ }
+ for (var i = 0; i < this.typeCount_; i++) {
+ var input = this.appendValueInput('TYPE' + i)
+ .setCheck('Type');
+ if (i == 0) {
+ input.appendField('any of');
+ }
+ }
+ },
+ decompose: function(workspace) {
+ // Populate the mutator's dialog with this block's components.
+ var containerBlock = workspace.newBlock('type_group_container');
+ containerBlock.initSvg();
+ var connection = containerBlock.getInput('STACK').connection;
+ for (var i = 0; i < this.typeCount_; i++) {
+ var typeBlock = workspace.newBlock('type_group_item');
+ typeBlock.initSvg();
+ connection.connect(typeBlock.previousConnection);
+ connection = typeBlock.nextConnection;
+ }
+ return containerBlock;
+ },
+ compose: function(containerBlock) {
+ // Reconfigure this block based on the mutator dialog's components.
+ var typeBlock = containerBlock.getInputTargetBlock('STACK');
+ // Count number of inputs.
+ var connections = [];
+ while (typeBlock) {
+ connections.push(typeBlock.valueConnection_);
+ typeBlock = typeBlock.nextConnection &&
+ typeBlock.nextConnection.targetBlock();
+ }
+ // Disconnect any children that don't belong.
+ for (var i = 0; i < this.typeCount_; i++) {
+ var connection = this.getInput('TYPE' + i).connection.targetConnection;
+ if (connection && connections.indexOf(connection) == -1) {
+ connection.disconnect();
+ }
+ }
+ this.typeCount_ = connections.length;
+ this.updateShape_();
+ // Reconnect any child blocks.
+ for (var i = 0; i < this.typeCount_; i++) {
+ Blockly.Mutator.reconnect(connections[i], this, 'TYPE' + i);
+ }
+ },
+ saveConnections: function(containerBlock) {
+ // Store a pointer to any connected child blocks.
+ var typeBlock = containerBlock.getInputTargetBlock('STACK');
+ var i = 0;
+ while (typeBlock) {
+ var input = this.getInput('TYPE' + i);
+ typeBlock.valueConnection_ = input && input.connection.targetConnection;
+ i++;
+ typeBlock = typeBlock.nextConnection &&
+ typeBlock.nextConnection.targetBlock();
+ }
+ },
+ updateShape_: function() {
+ // Modify this block to have the correct number of inputs.
+ // Add new inputs.
+ for (var i = 0; i < this.typeCount_; i++) {
+ if (!this.getInput('TYPE' + i)) {
+ var input = this.appendValueInput('TYPE' + i);
+ if (i == 0) {
+ input.appendField('any of');
+ }
+ }
+ }
+ // Remove deleted inputs.
+ while (this.getInput('TYPE' + i)) {
+ this.removeInput('TYPE' + i);
+ i++;
+ }
+ }
+};
+
+Blockly.Blocks['type_group_container'] = {
+ // Container.
+ init: function() {
+ this.jsonInit({
+ "message0": "add types %1 %2",
+ "args0": [
+ {"type": "input_dummy"},
+ {"type": "input_statement", "name": "STACK"}
+ ],
+ "colour": 230,
+ "tooltip": "Add, or remove allowed type.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677"
+ });
+ }
+};
+
+Blockly.Blocks['type_group_item'] = {
+ // Add type.
+ init: function() {
+ this.jsonInit({
+ "message0": "type",
+ "previousStatement": null,
+ "nextStatement": null,
+ "colour": 230,
+ "tooltip": "Add a new allowed type.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677"
+ });
+ }
+};
+
+Blockly.Blocks['type_null'] = {
+ // Null type.
+ valueType: null,
+ init: function() {
+ this.jsonInit({
+ "message0": "any",
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Any type is allowed.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602"
+ });
+ }
+};
+
+Blockly.Blocks['type_boolean'] = {
+ // Boolean type.
+ valueType: 'Boolean',
+ init: function() {
+ this.jsonInit({
+ "message0": "Boolean",
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Booleans (true/false) are allowed.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602"
+ });
+ }
+};
+
+Blockly.Blocks['type_number'] = {
+ // Number type.
+ valueType: 'Number',
+ init: function() {
+ this.jsonInit({
+ "message0": "Number",
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Numbers (int/float) are allowed.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602"
+ });
+ }
+};
+
+Blockly.Blocks['type_string'] = {
+ // String type.
+ valueType: 'String',
+ init: function() {
+ this.jsonInit({
+ "message0": "String",
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Strings (text) are allowed.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602"
+ });
+ }
+};
+
+Blockly.Blocks['type_list'] = {
+ // List type.
+ valueType: 'Array',
+ init: function() {
+ this.jsonInit({
+ "message0": "Array",
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Arrays (lists) are allowed.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602"
+ });
+ }
+};
+
+Blockly.Blocks['type_other'] = {
+ // Other type.
+ init: function() {
+ this.jsonInit({
+ "message0": "other %1",
+ "args0": [{"type": "field_input", "name": "TYPE", "text": ""}],
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Custom type to allow.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=702"
+ });
+ }
+};
+
+Blockly.Blocks['colour_hue'] = {
+ // Set the colour of the block.
+ init: function() {
+ this.appendDummyInput()
+ .appendField('hue:')
+ .appendField(new Blockly.FieldAngle('0', this.validator), 'HUE');
+ this.setOutput(true, 'Colour');
+ this.setTooltip('Paint the block with this colour.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=55');
+ },
+ validator: function(text) {
+ // Update the current block's colour to match.
+ var hue = parseInt(text, 10);
+ if (!isNaN(hue)) {
+ this.sourceBlock_.setColour(hue);
+ }
+ },
+ mutationToDom: function(workspace) {
+ var container = Blockly.utils.xml.createElement('mutation');
+ container.setAttribute('colour', this.getColour());
+ return container;
+ },
+ domToMutation: function(container) {
+ this.setColour(container.getAttribute('colour'));
+ }
+};
+
+/**
+ * Check to see if more than one field has this name.
+ * Highly inefficient (On^2), but n is small.
+ * @param {!Blockly.Block} referenceBlock Block to check.
+ */
+function fieldNameCheck(referenceBlock) {
+ if (!referenceBlock.workspace) {
+ // Block has been deleted.
+ return;
+ }
+ var name = referenceBlock.getFieldValue('FIELDNAME').toLowerCase();
+ var count = 0;
+ var blocks = referenceBlock.workspace.getAllBlocks(false);
+ for (var i = 0, block; block = blocks[i]; i++) {
+ var otherName = block.getFieldValue('FIELDNAME');
+ if (!block.disabled && !block.getInheritedDisabled() &&
+ otherName && otherName.toLowerCase() == name) {
+ count++;
+ }
+ }
+ var msg = (count > 1) ?
+ 'There are ' + count + ' field blocks\n with this name.' : null;
+ referenceBlock.setWarningText(msg);
+}
+
+/**
+ * Check to see if more than one input has this name.
+ * Highly inefficient (On^2), but n is small.
+ * @param {!Blockly.Block} referenceBlock Block to check.
+ */
+function inputNameCheck(referenceBlock) {
+ if (!referenceBlock.workspace) {
+ // Block has been deleted.
+ return;
+ }
+ var name = referenceBlock.getFieldValue('INPUTNAME').toLowerCase();
+ var count = 0;
+ var blocks = referenceBlock.workspace.getAllBlocks(false);
+ for (var i = 0, block; block = blocks[i]; i++) {
+ var otherName = block.getFieldValue('INPUTNAME');
+ if (!block.disabled && !block.getInheritedDisabled() &&
+ otherName && otherName.toLowerCase() == name) {
+ count++;
+ }
+ }
+ var msg = (count > 1) ?
+ 'There are ' + count + ' input blocks\n with this name.' : null;
+ referenceBlock.setWarningText(msg);
+}
diff --git a/js/demos/blockfactory_old/factory.js b/js/demos/blockfactory_old/factory.js
new file mode 100644
index 0000000..fce170a
--- /dev/null
+++ b/js/demos/blockfactory_old/factory.js
@@ -0,0 +1,848 @@
+/**
+ * @license
+ * Copyright 2012 Google LLC
+ *
+ * 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.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+/**
+ * Workspace for user to build block.
+ * @type {Blockly.Workspace}
+ */
+var mainWorkspace = null;
+
+/**
+ * Workspace for preview of block.
+ * @type {Blockly.Workspace}
+ */
+var previewWorkspace = null;
+
+/**
+ * Name of block if not named.
+ */
+var UNNAMED = 'unnamed';
+
+/**
+ * Change the language code format.
+ */
+function formatChange() {
+ var mask = document.getElementById('blocklyMask');
+ var languagePre = document.getElementById('languagePre');
+ var languageTA = document.getElementById('languageTA');
+ if (document.getElementById('format').value == 'Manual') {
+ Blockly.hideChaff();
+ mask.style.display = 'block';
+ languagePre.style.display = 'none';
+ languageTA.style.display = 'block';
+ var code = languagePre.textContent.trim();
+ languageTA.value = code;
+ languageTA.focus();
+ updatePreview();
+ } else {
+ mask.style.display = 'none';
+ languageTA.style.display = 'none';
+ languagePre.style.display = 'block';
+ updateLanguage();
+ }
+ disableEnableLink();
+}
+
+/**
+ * Update the language code based on constructs made in Blockly.
+ */
+function updateLanguage() {
+ var rootBlock = getRootBlock();
+ if (!rootBlock) {
+ return;
+ }
+ var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase();
+ if (!blockType) {
+ blockType = UNNAMED;
+ }
+ blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1');
+ switch (document.getElementById('format').value) {
+ case 'JSON':
+ var code = formatJson_(blockType, rootBlock);
+ break;
+ case 'JavaScript':
+ var code = formatJavaScript_(blockType, rootBlock);
+ break;
+ }
+ injectCode(code, 'languagePre');
+ updatePreview();
+}
+
+/**
+ * Update the language code as JSON.
+ * @param {string} blockType Name of block.
+ * @param {!Blockly.Block} rootBlock Factory_base block.
+ * @return {string} Generanted language code.
+ * @private
+ */
+function formatJson_(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 = 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(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 && 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(getOptTypesFrom(rootBlock, 'OUTPUTTYPE') || 'null');
+ break;
+ case 'BOTH':
+ JS.previousStatement =
+ JSON.parse(getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
+ JS.nextStatement =
+ JSON.parse(getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
+ break;
+ case 'TOP':
+ JS.previousStatement =
+ JSON.parse(getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
+ break;
+ case 'BOTTOM':
+ JS.nextStatement =
+ JSON.parse(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.
+ * @return {string} Generanted language code.
+ * @private
+ */
+function formatJavaScript_(blockType, rootBlock) {
+ 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 = escapeString(contentsBlock.getFieldValue('INPUTNAME'));
+ }
+ code.push(' this.' + TYPES[contentsBlock.type] + '(' + name + ')');
+ var check = 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 = 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(connectionLineJs_('setOutput', 'OUTPUTTYPE'));
+ break;
+ case 'BOTH':
+ code.push(connectionLineJs_('setPreviousStatement', 'TOPTYPE'));
+ code.push(connectionLineJs_('setNextStatement', 'BOTTOMTYPE'));
+ break;
+ case 'TOP':
+ code.push(connectionLineJs_('setPreviousStatement', 'TOPTYPE'));
+ break;
+ case 'BOTTOM':
+ code.push(connectionLineJs_('setNextStatement', 'BOTTOMTYPE'));
+ 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.
+ * @return {string} Line of JavaScript code to create connection.
+ * @private
+ */
+function connectionLineJs_(functionName, typeName) {
+ var type = getOptTypesFrom(getRootBlock(), 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
+ */
+function getFieldsJs_(block) {
+ var fields = [];
+ while (block) {
+ if (!block.disabled && !block.getInheritedDisabled()) {
+ switch (block.type) {
+ case 'field_static':
+ // Result: 'hello'
+ fields.push(escapeString(block.getFieldValue('TEXT')));
+ break;
+ case 'field_input':
+ // Result: new Blockly.FieldTextInput('Hello'), 'GREET'
+ fields.push('new Blockly.FieldTextInput(' +
+ escapeString(block.getFieldValue('TEXT')) + '), ' +
+ 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(', ') + '), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ break;
+ case 'field_angle':
+ // Result: new Blockly.FieldAngle(90), 'ANGLE'
+ fields.push('new Blockly.FieldAngle(' +
+ Number(block.getFieldValue('ANGLE')) + '), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ break;
+ case 'field_checkbox':
+ // Result: new Blockly.FieldCheckbox('TRUE'), 'CHECK'
+ fields.push('new Blockly.FieldCheckbox(' +
+ escapeString(block.getFieldValue('CHECKED')) + '), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ break;
+ case 'field_colour':
+ // Result: new Blockly.FieldColour('#ff0000'), 'COLOUR'
+ fields.push('new Blockly.FieldColour(' +
+ escapeString(block.getFieldValue('COLOUR')) + '), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ break;
+ case 'field_date':
+ // Result: new Blockly.FieldDate('2015-02-04'), 'DATE'
+ fields.push('new Blockly.FieldDate(' +
+ escapeString(block.getFieldValue('DATE')) + '), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ break;
+ case 'field_variable':
+ // Result: new Blockly.FieldVariable('item'), 'VAR'
+ var varname = escapeString(block.getFieldValue('TEXT') || null);
+ fields.push('new Blockly.FieldVariable(' + varname + '), ' +
+ 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] = '[' + escapeString(block.getFieldValue('USER' + i)) +
+ ', ' + escapeString(block.getFieldValue('CPU' + i)) + ']';
+ }
+ if (options.length) {
+ fields.push('new Blockly.FieldDropdown([' +
+ options.join(', ') + ']), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ }
+ break;
+ case 'field_image':
+ // Result: new Blockly.FieldImage('http://...', 80, 60, '*')
+ var src = escapeString(block.getFieldValue('SRC'));
+ var width = Number(block.getFieldValue('WIDTH'));
+ var height = Number(block.getFieldValue('HEIGHT'));
+ var alt = 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
+ */
+function getFieldsJson_(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: Number(block.getFieldValue('VALUE'))
+ };
+ var min = Number(block.getFieldValue('MIN'));
+ if (min > -Infinity) {
+ obj.min = min;
+ }
+ var max = Number(block.getFieldValue('MAX'));
+ if (max < Infinity) {
+ obj.max = max;
+ }
+ var precision = Number(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;
+}
+
+/**
+ * Escape a string.
+ * @param {string} string String to escape.
+ * @return {string} Escaped string surrouned by quotes.
+ */
+function escapeString(string) {
+ return JSON.stringify(string);
+}
+
+/**
+ * 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.
+ */
+function getOptTypesFrom(block, name) {
+ var types = 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
+ */
+function getTypesFrom_(block, name) {
+ var typeBlock = block.getInputTargetBlock(name);
+ var types;
+ if (!typeBlock || typeBlock.disabled) {
+ types = [];
+ } else if (typeBlock.type == 'type_other') {
+ types = [escapeString(typeBlock.getFieldValue('TYPE'))];
+ } else if (typeBlock.type == 'type_group') {
+ types = [];
+ for (var i = 0; i < typeBlock.typeCount_; i++) {
+ types = types.concat(getTypesFrom_(typeBlock, 'TYPE' + i));
+ }
+ // 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 = [escapeString(typeBlock.valueType)];
+ }
+ return types;
+}
+
+/**
+ * Update the generator code.
+ * @param {!Blockly.Block} block Rendered block in preview workspace.
+ */
+function updateGenerator(block) {
+ function makeVar(root, name) {
+ name = name.toLowerCase().replace(/\W/g, '_');
+ return ' var ' + root + '_' + name;
+ }
+ var language = document.getElementById('language').value;
+ var code = [];
+ code.push("Blockly." + language + "['" + 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("};");
+
+ injectCode(code.join('\n'), 'generatorPre');
+}
+
+/**
+ * Existing direction ('ltr' vs 'rtl') of preview.
+ */
+var oldDir = null;
+
+/**
+ * Update the preview display.
+ */
+function updatePreview() {
+ // Toggle between LTR/RTL if needed (also used in first display).
+ var newDir = document.getElementById('direction').value;
+ if (oldDir != newDir) {
+ if (previewWorkspace) {
+ previewWorkspace.dispose();
+ }
+ var rtl = newDir == 'rtl';
+ previewWorkspace = Blockly.inject('preview',
+ {rtl: rtl,
+ media: '../../media/',
+ scrollbars: true});
+ oldDir = newDir;
+ }
+ 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;
+ try {
+ // Make a shallow copy.
+ Blockly.Blocks = {};
+ for (var prop in backupBlocks) {
+ Blockly.Blocks[prop] = backupBlocks[prop];
+ }
+
+ if (format == 'JSON') {
+ var json = JSON.parse(code);
+ Blockly.Blocks[json.type || UNNAMED] = {
+ init: function() {
+ this.jsonInit(json);
+ }
+ };
+ } else if (format == 'JavaScript') {
+ eval(code);
+ } else {
+ throw 'Unknown format: ' + format;
+ }
+
+ // Look for a block on Blockly.Blocks that does not match the backup.
+ var blockType = null;
+ for (var type in Blockly.Blocks) {
+ if (typeof Blockly.Blocks[type].init == 'function' &&
+ Blockly.Blocks[type] != backupBlocks[type]) {
+ blockType = type;
+ break;
+ }
+ }
+ if (!blockType) {
+ return;
+ }
+
+ // Create the preview block.
+ var previewBlock = previewWorkspace.newBlock(blockType);
+ previewBlock.initSvg();
+ previewBlock.render();
+ previewBlock.setMovable(false);
+ previewBlock.setDeletable(false);
+ previewBlock.moveBy(15, 10);
+ previewWorkspace.clearUndo();
+
+ updateGenerator(previewBlock);
+ } finally {
+ Blockly.Blocks = backupBlocks;
+ }
+}
+
+/**
+ * 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.
+ */
+function injectCode(code, id) {
+ var pre = document.getElementById(id);
+ pre.textContent = code;
+ // Remove the 'prettyprinted' class, so that Prettify will recalculate.
+ pre.className = pre.className.replace('prettyprinted', '');
+ PR.prettyPrint();
+}
+
+/**
+ * Return the uneditable container block that everything else attaches to.
+ * @return {Blockly.Block}
+ */
+function getRootBlock() {
+ var blocks = mainWorkspace.getTopBlocks(false);
+ for (var i = 0, block; block = blocks[i]; i++) {
+ if (block.type == 'factory_base') {
+ return block;
+ }
+ }
+ return null;
+}
+
+/**
+ * Disable the link button if the format is 'Manual', enable otherwise.
+ */
+function disableEnableLink() {
+ var linkButton = document.getElementById('linkButton');
+ linkButton.disabled = document.getElementById('format').value == 'Manual';
+}
+
+/**
+ * Initialize Blockly and layout. Called on page load.
+ */
+function init() {
+ if ('BlocklyStorage' in window) {
+ 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(mainWorkspace);});
+ disableEnableLink();
+ }
+
+ document.getElementById('helpButton').addEventListener('click',
+ function() {
+ open('https://developers.google.com/blockly/guides/create-custom-blocks/block-factory',
+ 'BlockFactoryHelp');
+ });
+
+ var expandList = [
+ document.getElementById('blockly'),
+ document.getElementById('blocklyMask'),
+ document.getElementById('preview'),
+ document.getElementById('languagePre'),
+ document.getElementById('languageTA'),
+ document.getElementById('generatorPre')
+ ];
+ var onresize = function(e) {
+ 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);
+
+ var toolbox = document.getElementById('toolbox');
+ mainWorkspace = Blockly.inject('blockly',
+ {collapse: false,
+ toolbox: toolbox,
+ media: '../../media/'});
+
+ // Create the root block.
+ if ('BlocklyStorage' in window && window.location.hash.length > 1) {
+ BlocklyStorage.retrieveXml(window.location.hash.substring(1),
+ mainWorkspace);
+ } else {
+ var xml = '';
+ Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(xml), mainWorkspace);
+ }
+ mainWorkspace.clearUndo();
+
+ mainWorkspace.addChangeListener(Blockly.Events.disableOrphans);
+ mainWorkspace.addChangeListener(updateLanguage);
+ document.getElementById('direction')
+ .addEventListener('change', updatePreview);
+ document.getElementById('languageTA')
+ .addEventListener('change', updatePreview);
+ document.getElementById('languageTA')
+ .addEventListener('keyup', updatePreview);
+ document.getElementById('format')
+ .addEventListener('change', formatChange);
+ document.getElementById('language')
+ .addEventListener('change', updatePreview);
+}
+window.addEventListener('load', init);
diff --git a/js/demos/blockfactory_old/icon.png b/js/demos/blockfactory_old/icon.png
new file mode 100644
index 0000000..4f8b72f
Binary files /dev/null and b/js/demos/blockfactory_old/icon.png differ
diff --git a/js/demos/blockfactory_old/index.html b/js/demos/blockfactory_old/index.html
new file mode 100644
index 0000000..b03db91
--- /dev/null
+++ b/js/demos/blockfactory_old/index.html
@@ -0,0 +1,229 @@
+
+
+
+
+
+ Blockly Demo: Block Factory
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/demos/custom-fields/pitch/blocks.js b/js/demos/custom-fields/pitch/blocks.js
new file mode 100644
index 0000000..3e02ac8
--- /dev/null
+++ b/js/demos/custom-fields/pitch/blocks.js
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright 2019 Google LLC
+ *
+ * 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 Pitch field demo blocks.
+ * @author samelh@gmail.com (Sam El-Husseini)
+ */
+
+Blockly.Blocks['test_pitch_field'] = {
+ init: function() {
+ this.appendDummyInput()
+ .appendField('pitch')
+ .appendField(new CustomFields.FieldPitch('7'), 'PITCH');
+ this.setStyle('loop_blocks');
+ }
+};
diff --git a/js/demos/custom-fields/pitch/field_pitch.js b/js/demos/custom-fields/pitch/field_pitch.js
new file mode 100644
index 0000000..cf3b9fd
--- /dev/null
+++ b/js/demos/custom-fields/pitch/field_pitch.js
@@ -0,0 +1,250 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * https://github.com/google/blockly-games
+ *
+ * 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 Music pitch input field. Borrowed from Blockly Games.
+ * @author fraser@google.com (Neil Fraser)
+ * @author samelh@google.com (Sam El-Husseini)
+ */
+'use strict';
+
+goog.provide('CustomFields.FieldPitch');
+
+goog.require('Blockly.FieldTextInput');
+goog.require('Blockly.utils.math');
+goog.require('Blockly.utils.object');
+
+var CustomFields = CustomFields || {};
+
+/**
+ * Class for an editable pitch field.
+ * @param {string} text The initial content of the field.
+ * @extends {Blockly.FieldTextInput}
+ * @constructor
+ */
+CustomFields.FieldPitch = function(text) {
+ CustomFields.FieldPitch.superClass_.constructor.call(this, text);
+
+ /**
+ * Click event data.
+ * @type {?Blockly.EventData}
+ * @private
+ */
+ this.clickWrapper_ = null;
+
+ /**
+ * Move event data.
+ * @type {?Blockly.EventData}
+ * @private
+ */
+ this.moveWrapper_ = null;
+};
+Blockly.utils.object.inherits(CustomFields.FieldPitch, Blockly.FieldTextInput);
+
+/**
+ * Construct a FieldPitch from a JSON arg object.
+ * @param {!Object} options A JSON object with options (pitch).
+ * @return {!CustomFields.FieldPitch} The new field instance.
+ * @package
+ * @nocollapse
+ */
+CustomFields.FieldPitch.fromJson = function(options) {
+ return new CustomFields.FieldPitch(options['pitch']);
+};
+
+/**
+ * All notes available for the picker.
+ */
+CustomFields.FieldPitch.NOTES = 'C3 D3 E3 F3 G3 A3 B3 C4 D4 E4 F4 G4 A4'.split(/ /);
+
+/**
+ * Show the inline free-text editor on top of the text and the note picker.
+ * @private
+ */
+CustomFields.FieldPitch.prototype.showEditor_ = function() {
+ CustomFields.FieldPitch.superClass_.showEditor_.call(this);
+
+ var div = Blockly.WidgetDiv.DIV;
+ if (!div.firstChild) {
+ // Mobile interface uses Blockly.prompt.
+ return;
+ }
+ // Build the DOM.
+ var editor = this.dropdownCreate_();
+ Blockly.DropDownDiv.getContentDiv().appendChild(editor);
+
+ Blockly.DropDownDiv.setColour(this.sourceBlock_.style.colourPrimary,
+ this.sourceBlock_.style.colourTertiary);
+
+ Blockly.DropDownDiv.showPositionedByField(
+ this, this.dropdownDispose_.bind(this));
+
+ // The note picker is different from other fields in that it updates on
+ // mousemove even if it's not in the middle of a drag. In future we may
+ // change this behaviour. For now, using bindEvent_ instead of
+ // bindEventWithChecks_ allows it to work without a mousedown/touchstart.
+ this.clickWrapper_ =
+ Blockly.bindEvent_(this.imageElement_, 'click', this,
+ this.hide_);
+ this.moveWrapper_ =
+ Blockly.bindEvent_(this.imageElement_, 'mousemove', this,
+ this.onMouseMove);
+
+ this.updateGraph_();
+};
+
+/**
+ * Create the pitch editor.
+ * @return {!Element} The newly created pitch picker.
+ * @private
+ */
+CustomFields.FieldPitch.prototype.dropdownCreate_ = function() {
+ this.imageElement_ = document.createElement('div');
+ this.imageElement_.id = 'notePicker';
+
+ return this.imageElement_;
+};
+
+/**
+ * Dispose of events belonging to the pitch editor.
+ * @private
+ */
+CustomFields.FieldPitch.prototype.dropdownDispose_ = function() {
+ if (this.clickWrapper_) {
+ Blockly.unbindEvent_(this.clickWrapper_);
+ this.clickWrapper_ = null;
+ }
+ if (this.moveWrapper_) {
+ Blockly.unbindEvent_(this.moveWrapper_);
+ this.moveWrapper_ = null;
+ }
+ this.imageElement_ = null;
+};
+
+/**
+ * Hide the editor.
+ * @private
+ */
+CustomFields.FieldPitch.prototype.hide_ = function() {
+ Blockly.WidgetDiv.hide();
+ Blockly.DropDownDiv.hideWithoutAnimation();
+};
+
+/**
+ * Set the note to match the mouse's position.
+ * @param {!Event} e Mouse move event.
+ */
+CustomFields.FieldPitch.prototype.onMouseMove = function(e) {
+ var bBox = this.imageElement_.getBoundingClientRect();
+ var dy = e.clientY - bBox.top;
+ var note = Blockly.utils.math.clamp(Math.round(13.5 - dy / 7.5), 0, 12);
+ this.imageElement_.style.backgroundPosition = (-note * 37) + 'px 0';
+ this.setEditorValue_(note);
+};
+
+/**
+ * Convert the machine-readable value (0-12) to human-readable text (C3-A4).
+ * @param {number|string} value The provided value.
+ * @return {string|undefined} The respective note, or undefined if invalid.
+ */
+CustomFields.FieldPitch.prototype.valueToNote = function(value) {
+ return CustomFields.FieldPitch.NOTES[Number(value)];
+};
+
+/**
+ * Convert the human-readable text (C3-A4) to machine-readable value (0-12).
+ * @param {string} text The provided note.
+ * @return {number|undefined} The respective value, or undefined if invalid.
+ */
+CustomFields.FieldPitch.prototype.noteToValue = function(text) {
+ var normalizedText = text.trim().toUpperCase();
+ var i = CustomFields.FieldPitch.NOTES.indexOf(normalizedText);
+ return i > -1 ? i : undefined;
+};
+
+/**
+ * Get the text to be displayed on the field node.
+ * @return {?string} The HTML value if we're editing, otherwise null. Null means
+ * the super class will handle it, likely a string cast of value.
+ * @protected
+ */
+CustomFields.FieldPitch.prototype.getText_ = function() {
+ if (this.isBeingEdited_) {
+ return CustomFields.FieldPitch.superClass_.getText_.call(this);
+ }
+ return this.valueToNote(this.getValue()) || null;
+};
+
+/**
+ * Transform the provided value into a text to show in the HTML input.
+ * @param {*} value The value stored in this field.
+ * @return {string} The text to show on the HTML input.
+ */
+CustomFields.FieldPitch.prototype.getEditorText_ = function(value) {
+ return this.valueToNote(value);
+};
+
+/**
+ * Transform the text received from the HTML input (note) into a value
+ * to store in this field.
+ * @param {string} text Text received from the HTML input.
+ * @return {*} The value to store.
+ */
+CustomFields.FieldPitch.prototype.getValueFromEditorText_ = function(text) {
+ return this.noteToValue(text);
+};
+
+/**
+ * Updates the graph when the field rerenders.
+ * @private
+ * @override
+ */
+CustomFields.FieldPitch.prototype.render_ = function() {
+ CustomFields.FieldPitch.superClass_.render_.call(this);
+ this.updateGraph_();
+};
+
+/**
+ * Redraw the note picker with the current note.
+ * @private
+ */
+CustomFields.FieldPitch.prototype.updateGraph_ = function() {
+ if (!this.imageElement_) {
+ return;
+ }
+ var i = this.getValue();
+ this.imageElement_.style.backgroundPosition = (-i * 37) + 'px 0';
+};
+
+/**
+ * Ensure that only a valid value may be entered.
+ * @param {*} opt_newValue The input value.
+ * @return {*} A valid value, or null if invalid.
+ */
+CustomFields.FieldPitch.prototype.doClassValidation_ = function(opt_newValue) {
+ if (opt_newValue === null || opt_newValue === undefined) {
+ return null;
+ }
+ var note = this.valueToNote(opt_newValue);
+ if (note) {
+ return opt_newValue;
+ }
+ return null;
+};
+
+Blockly.fieldRegistry.register('field_pitch', CustomFields.FieldPitch);
diff --git a/js/demos/custom-fields/pitch/index.html b/js/demos/custom-fields/pitch/index.html
new file mode 100644
index 0000000..642482c
--- /dev/null
+++ b/js/demos/custom-fields/pitch/index.html
@@ -0,0 +1,120 @@
+
+
+
+
+ Blockly Demo: Custom Pitch Field
+
+
+
+
+
+
+
+
+
This is a demo of creating custom block fields. In this case the field
+ is used to select a note pitch.
+
+
+
All of the custom field implementation is in
+ demos/custom-fields/pitch/field_pitch.js, including comments on each required
+ function.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/demos/custom-fields/pitch/media/notes.png b/js/demos/custom-fields/pitch/media/notes.png
new file mode 100644
index 0000000..b9a57b5
Binary files /dev/null and b/js/demos/custom-fields/pitch/media/notes.png differ
diff --git a/js/demos/custom-fields/pitch/pitch.css b/js/demos/custom-fields/pitch/pitch.css
new file mode 100644
index 0000000..586c6cd
--- /dev/null
+++ b/js/demos/custom-fields/pitch/pitch.css
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright 2019 Google LLC
+ *
+ * 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.
+ */
+
+
+#notePicker {
+ background-image: url(media/notes.png);
+ border: 1px solid #ccc;
+ height: 109px;
+ width: 46px;
+}
\ No newline at end of file
diff --git a/js/demos/custom-fields/turtle/blocks.js b/js/demos/custom-fields/turtle/blocks.js
new file mode 100644
index 0000000..ef8b245
--- /dev/null
+++ b/js/demos/custom-fields/turtle/blocks.js
@@ -0,0 +1,103 @@
+/**
+ * @license
+ * Copyright 2019 Google LLC
+ *
+ * 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 Turtle field demo blocks..
+ * @author bekawestberg@gmail.com (Beka Westberg)
+ */
+
+Blockly.Blocks['turtle_basic'] = {
+ init: function() {
+ this.appendDummyInput()
+ .appendField('simple turtle');
+ this.appendDummyInput()
+ .setAlign(Blockly.ALIGN_CENTRE)
+ .appendField(new CustomFields.FieldTurtle(), 'TURTLE');
+ this.setStyle('loop_blocks');
+ this.setCommentText('Demonstrates a turtle field with no validator.');
+ }
+};
+
+Blockly.Blocks['turtle_nullifier'] = {
+ init: function() {
+ this.appendDummyInput()
+ .appendField('no trademarks');
+ this.appendDummyInput()
+ .setAlign(Blockly.ALIGN_CENTRE)
+ .appendField(new CustomFields.FieldTurtle(null, null, null, this.validate)
+ , 'TURTLE');
+ this.setStyle('loop_blocks');
+ this.setCommentText('Validates combinations of names and hats to null' +
+ ' (invalid) if they could be considered infringe-y. This turns the' +
+ ' turtle field red. Infringe-y combinations are: (Leonardo, Mask),' +
+ ' (Yertle, Crown), and (Franklin, Propeller).');
+ },
+
+ validate: function(newValue) {
+ this.cachedValidatedValue_ = {
+ turtleName: newValue.turtleName,
+ pattern: newValue.pattern,
+ hat: newValue.hat,
+ };
+ if ((newValue.turtleName == 'Leonardo' && newValue.hat == 'Mask') ||
+ (newValue.turtleName == 'Yertle' && newValue.hat == 'Crown') ||
+ (newValue.turtleName == 'Franklin') && newValue.hat == 'Propeller') {
+
+ var currentValue = this.getValue();
+ if (newValue.turtleName != currentValue.turtleName) {
+ // Turtle name changed.
+ this.cachedValidatedValue_.turtleName = null;
+ } else {
+ // Hat must have changed.
+ this.cachedValidatedValue_.hat = null;
+ }
+
+ return null;
+ }
+ return newValue;
+ }
+};
+
+Blockly.Blocks['turtle_changer'] = {
+ init: function() {
+ this.appendDummyInput()
+ .setAlign(Blockly.ALIGN_CENTRE)
+ .appendField('force hats');
+ this.appendDummyInput()
+ .appendField(new CustomFields.FieldTurtle(
+ 'Dots', 'Crown', 'Yertle', this.validate), 'TURTLE');
+ this.setStyle('loop_blocks');
+ this.setCommentText('Validates the input so that certain names always' +
+ ' have specific hats. The name-hat combinations are: (Leonardo, Mask),' +
+ ' (Yertle, Crown), (Franklin, Propeller).');
+ },
+
+ validate: function(newValue) {
+ switch(newValue.turtleName) {
+ case 'Leonardo':
+ newValue.hat = 'Mask';
+ break;
+ case 'Yertle':
+ newValue.hat = 'Crown';
+ break;
+ case 'Franklin':
+ newValue.hat = 'Propeller';
+ break;
+ }
+ return newValue;
+ }
+};
diff --git a/js/demos/custom-fields/turtle/field_turtle.js b/js/demos/custom-fields/turtle/field_turtle.js
new file mode 100644
index 0000000..9a787e4
--- /dev/null
+++ b/js/demos/custom-fields/turtle/field_turtle.js
@@ -0,0 +1,753 @@
+/**
+ * @license
+ * Copyright 2019 Google LLC
+ *
+ * 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 A field used to customize a turtle.
+ * @author bekawestberg@gmail.com (Beka Westberg)
+ */
+'use strict';
+
+// You must provide the constructor for your custom field.
+goog.provide('CustomFields.FieldTurtle');
+
+// You must require the abstract field class to inherit from.
+goog.require('Blockly.Field');
+goog.require('Blockly.fieldRegistry');
+goog.require('Blockly.utils');
+goog.require('Blockly.utils.dom');
+goog.require('Blockly.utils.object');
+goog.require('Blockly.utils.Size');
+
+var CustomFields = CustomFields || {};
+
+// Generally field's values should be optional, and have logical defaults.
+// If this is not possible (for example image fields can't have logical
+// defaults) the field should throw a clear error when a value is not provided.
+// Editable fields also generally accept validators, so we will accept a
+// validator.
+CustomFields.FieldTurtle = function(
+ opt_pattern, opt_hat, opt_turtleName, opt_validator) {
+
+ // The turtle field contains an object as its value, so we need to compile
+ // the parameters into an object.
+ var value = {};
+ value.pattern = opt_pattern || CustomFields.FieldTurtle.PATTERNS[0];
+ value.hat = opt_hat || CustomFields.FieldTurtle.HATS[0];
+ value.turtleName = opt_turtleName || CustomFields.FieldTurtle.NAMES[0];
+
+ // A field constructor should always call its parent constructor, because
+ // that helps keep the code organized and DRY.
+ CustomFields.FieldTurtle.superClass_.constructor.call(
+ this, value, opt_validator);
+
+ /**
+ * The size of the area rendered by the field.
+ * @type {Blockly.utils.Size}
+ * @protected
+ * @override
+ */
+ this.size_ = new Blockly.utils.Size(0, 0);
+};
+Blockly.utils.object.inherits(CustomFields.FieldTurtle, Blockly.Field);
+
+// This allows the field to be constructed using a JSON block definition.
+CustomFields.FieldTurtle.fromJson = function(options) {
+ // In this case we simply pass the JSON options along to the constructor,
+ // but you can also use this to get message references, and other such things.
+ return new CustomFields.FieldTurtle(
+ options['pattern'],
+ options['hat'],
+ options['turtleName']);
+};
+
+// Since this field is editable we must also define serializable as true
+// (for backwards compatibility reasons serializable is false by default).
+CustomFields.FieldTurtle.prototype.SERIALIZABLE = true;
+
+// The cursor property defines what the mouse will look like when the user
+// hovers over the field. By default the cursor will be whatever
+// .blocklyDraggable's cursor is defined as (vis. grab). Most fields define
+// this property as 'default'.
+CustomFields.FieldTurtle.prototype.CURSOR = 'pointer';
+
+// How far to move the text to keep it to the right of the turtle.
+// May change if the turtle gets fancy enough.
+CustomFields.FieldTurtle.prototype.TEXT_OFFSET_X = 80;
+
+// These are the different options for our turtle. Being declared this way
+// means they are static, and not translatable. If you want to do something
+// similar, but make it translatable you should set up your options like a
+// dropdown field, with language-neutral keys and human-readable values.
+CustomFields.FieldTurtle.PATTERNS =
+ ['Dots', 'Stripes', 'Hexagons'];
+CustomFields.FieldTurtle.HATS =
+ ['Stovepipe', 'Crown', 'Propeller', 'Mask', 'Fedora'];
+CustomFields.FieldTurtle.NAMES =
+ ['Yertle', 'Franklin', 'Crush', 'Leonardo', 'Bowser', 'Squirtle', 'Oogway'];
+
+// Used to keep track of our editor event listeners, so they can be
+// properly disposed of when the field closes. You can keep track of your
+// listeners however you want, just be sure to dispose of them!
+CustomFields.FieldTurtle.prototype.editorListeners_ = [];
+
+// Used to create the DOM of our field.
+CustomFields.FieldTurtle.prototype.initView = function() {
+ // Because we want to have both a borderRect_ (background) and a
+ // textElement_ (text) we can call the super-function. If we only wanted
+ // one or the other, we could call their individual createX functions.
+ CustomFields.FieldTurtle.superClass_.initView.call(this);
+
+ // Note that the field group is created by the abstract field's init_
+ // function. This means that *all elements* should be children of the
+ // fieldGroup_.
+ this.createView_();
+};
+
+// Updates how the field looks depending on if it is editable or not.
+CustomFields.FieldTurtle.prototype.updateEditable = function() {
+ if (!this.fieldGroup_) {
+ // Not initialized yet.
+ return;
+ }
+ // The default functionality just makes it so the borderRect_ does not
+ // highlight when hovered.
+ Blockly.FieldColour.superClass_.updateEditable.call(this);
+ // Things like this are best applied to the clickTarget_. By default the
+ // click target is the same as getSvgRoot, which by default is the
+ // fieldGroup_.
+ var group = this.getClickTarget_();
+ if (!this.isCurrentlyEditable()) {
+ group.style.cursor = 'not-allowed';
+ } else {
+ group.style.cursor = this.CURSOR;
+ }
+};
+
+// Gets the text to display when the block is collapsed
+CustomFields.FieldTurtle.prototype.getText = function() {
+ var text = this.value_.turtleName + ' wearing a ' + this.value_.hat;
+ if (this.value_.hat == 'Stovepipe' || this.value_.hat == 'Propeller') {
+ text += ' hat';
+ }
+ return text;
+};
+
+// Makes sure new field values (given to setValue) are valid, meaning
+// something this field can legally "hold". Class validators can either change
+// the input value, or return null if the input value is invalid. Called by
+// the setValue() function.
+CustomFields.FieldTurtle.prototype.doClassValidation_ = function(newValue) {
+ // Undefined signals that we want the value to remain unchanged. This is a
+ // special feature of turtle fields, but could be useful for other
+ // multi-part fields.
+ if (newValue.pattern == undefined) {
+ newValue.pattern = this.displayValue_ && this.displayValue_.pattern;
+ // We only want to allow patterns that are part of our pattern list.
+ // Anything else is invalid, so we return null.
+ } else if (CustomFields.FieldTurtle.PATTERNS.indexOf(newValue.pattern) == -1) {
+ newValue.pattern = null;
+ }
+
+ if (newValue.hat == undefined) {
+ newValue.hat = this.displayValue_ && this.displayValue_.hat;
+ } else if (CustomFields.FieldTurtle.HATS.indexOf(newValue.hat) == -1) {
+ newValue.hat = null;
+ }
+
+ if (newValue.turtleName == undefined) {
+ newValue.turtleName = this.displayValue_ && this.displayValue_.turtleName;
+ } else if (CustomFields.FieldTurtle.NAMES.indexOf(newValue.turtleName) == -1) {
+ newValue.turtleName = null;
+ }
+
+ // This is a strategy for dealing with defaults on multi-part values.
+ // The class validator sets individual properties of the object to null
+ // to indicate that they are invalid, and then caches that object to the
+ // cachedValidatedValue_ property. This way the field can, for
+ // example, properly handle an invalid pattern, combined with a valid hat.
+ // This can also be done with local validators.
+ this.cachedValidatedValue_ = newValue;
+
+ // Always be sure to return!
+ if (!newValue.pattern || !newValue.hat || !newValue.turtleName) {
+ return null;
+ }
+ return newValue;
+};
+
+// Saves the new field value. Called by the setValue function.
+CustomFields.FieldTurtle.prototype.doValueUpdate_ = function(newValue) {
+ // The default function sets this field's this.value_ property to the
+ // newValue, and its this.isDirty_ property to true. The isDirty_ property
+ // tells the setValue function whether the field needs to be re-rendered.
+ CustomFields.FieldTurtle.superClass_.doValueUpdate_.call(this, newValue);
+ this.displayValue_ = newValue;
+ // Since this field has custom UI for invalid values, we also want to make
+ // sure it knows it is now valid.
+ this.isValueInvalid_ = false;
+};
+
+// Notifies that the field that the new value was invalid. Called by
+// setValue function. Can either be triggered by the class validator, or the
+// local validator.
+CustomFields.FieldTurtle.prototype.doValueInvalid_ = function(invalidValue) {
+ // By default this function is no-op, meaning if the new value is invalid
+ // the field simply won't be updated. This field has custom UI for invalid
+ // values, so we override this function.
+
+ // We want the value to be displayed like normal.
+ // But we want to flag it as invalid, so the render_ function knows to
+ // make the borderRect_ red.
+ this.displayValue_ = invalidValue;
+ this.isDirty_ = true;
+ this.isValueInvalid_ = true;
+};
+
+// Updates the field's on-block display based on the current display value.
+CustomFields.FieldTurtle.prototype.render_ = function() {
+ var value = this.displayValue_;
+
+ // Always do editor updates inside render. This makes sure the editor
+ // always displays the correct value, even if a validator changes it.
+ if (this.editor_) {
+ this.renderEditor_();
+ }
+
+ this.stovepipe_.style.display = 'none';
+ this.crown_.style.display = 'none';
+ this.mask_.style.display = 'none';
+ this.propeller_.style.display = 'none';
+ this.fedora_.style.display = 'none';
+ switch(value.hat) {
+ case 'Stovepipe':
+ this.stovepipe_.style.display = '';
+ this.turtleGroup_.setAttribute('transform', 'translate(0,12)');
+ this.textElement_.setAttribute(
+ 'transform', 'translate(' + this.TEXT_OFFSET_X + ',20)');
+ break;
+ case 'Crown':
+ this.crown_.style.display = '';
+ this.turtleGroup_.setAttribute('transform', 'translate(0,9)');
+ this.textElement_.setAttribute(
+ 'transform', 'translate(' + this.TEXT_OFFSET_X + ',16)');
+ break;
+ case 'Mask':
+ this.mask_.style.display = '';
+ this.turtleGroup_.setAttribute('transform', 'translate(0,1.2)');
+ this.textElement_.setAttribute('transform',
+ 'translate(' + this.TEXT_OFFSET_X + ',4)');
+ break;
+ case 'Propeller':
+ this.propeller_.style.display = '';
+ this.turtleGroup_.setAttribute('transform', 'translate(0,6)');
+ this.textElement_.setAttribute('transform',
+ 'translate(' + this.TEXT_OFFSET_X + ',12)');
+ break;
+ case 'Fedora':
+ this.fedora_.style.display = '';
+ this.turtleGroup_.setAttribute('transform', 'translate(0,6)');
+ this.textElement_.setAttribute('transform',
+ 'translate(' + this.TEXT_OFFSET_X + ',12)');
+ break;
+ }
+
+ switch(value.pattern) {
+ case 'Dots':
+ this.shellPattern_.setAttribute('fill', 'url(#polkadots)');
+ break;
+ case 'Stripes':
+ this.shellPattern_.setAttribute('fill', 'url(#stripes)');
+ break;
+ case 'Hexagons':
+ this.shellPattern_.setAttribute('fill', 'url(#hexagons)');
+ break;
+ }
+
+ // Always modify the textContent_ rather than the textElement_. This
+ // allows fields to append DOM to the textElement (e.g. the angle field).
+ this.textContent_.nodeValue = value.turtleName;
+
+ if (this.isValueInvalid_) {
+ this.borderRect_.style.fill = '#f99';
+ this.borderRect_.style.fillOpacity = 1;
+ } else {
+ this.borderRect_.style.fill = '#fff';
+ this.borderRect_.style.fillOpacity = 0.6;
+ }
+
+ this.updateSize_();
+};
+
+CustomFields.FieldTurtle.prototype.renderEditor_ = function() {
+ var value = this.displayValue_;
+
+ // .textElement is a property assigned to the element.
+ // It allows the text to be edited without destroying the warning icon.
+ this.editor_.patternText.textElement.nodeValue = value.pattern;
+ this.editor_.hatText.textElement.nodeValue = value.hat;
+ this.editor_.turtleNameText.textElement.nodeValue = value.turtleName;
+
+ this.editor_.patternText.warningIcon.style.display =
+ this.cachedValidatedValue_.pattern ? 'none' : '';
+ this.editor_.hatText.warningIcon.style.display =
+ this.cachedValidatedValue_.hat ? 'none' : '';
+ this.editor_.turtleNameText.warningIcon.style.display =
+ this.cachedValidatedValue_.turtleName ? 'none' : '';
+};
+
+// Used to update the size of the field. This function's logic could be simply
+// included inside render_ (it is not called anywhere else), but it is
+// usually separated to keep code more organized.
+CustomFields.FieldTurtle.prototype.updateSize_ = function() {
+ var bbox = this.movableGroup_.getBBox();
+ var width = bbox.width;
+ var height = bbox.height;
+ if (this.borderRect_) {
+ width += this.constants_.FIELD_BORDER_RECT_X_PADDING * 2;
+ height += this.constants_.FIELD_BORDER_RECT_X_PADDING * 2;
+ this.borderRect_.setAttribute('width', width);
+ this.borderRect_.setAttribute('height', height);
+ }
+ // Note how both the width and the height can be dynamic.
+ this.size_.width = width;
+ this.size_.height = height;
+};
+
+// Called when the field is clicked. It is usually used to show an editor,
+// but it can also be used for other things e.g. the checkbox field uses
+// this function to check/uncheck itself.
+CustomFields.FieldTurtle.prototype.showEditor_ = function() {
+ this.editor_ = this.dropdownCreate_();
+ this.renderEditor_();
+ Blockly.DropDownDiv.getContentDiv().appendChild(this.editor_);
+
+ // These allow us to have the editor match the block's colour.
+ var fillColour = this.sourceBlock_.getColour();
+ Blockly.DropDownDiv.setColour(fillColour,
+ this.sourceBlock_.style.colourTertiary);
+
+ // Always pass the dropdown div a dispose function so that you can clean
+ // up event listeners when the editor closes.
+ Blockly.DropDownDiv.showPositionedByField(
+ this, this.dropdownDispose_.bind(this));
+};
+
+// Creates the UI of the editor, and adds event listeners to it.
+CustomFields.FieldTurtle.prototype.dropdownCreate_ = function() {
+ var createRow = function(table) {
+ var row = table.appendChild(document.createElement('tr'));
+ row.className = 'row';
+ return row;
+ };
+ var createLeftArrow = function(row) {
+ var cell = document.createElement('div');
+ cell.className = 'arrow';
+ var leftArrow = document.createElement('button');
+ leftArrow.setAttribute('type', 'button');
+ leftArrow.textContent = '<';
+ cell.appendChild(leftArrow);
+ row.appendChild(cell);
+ return cell;
+ };
+ var createTextNode = function(row, text) {
+ var cell = document.createElement('div');
+ cell.className = 'text';
+ var text = document.createTextNode(text);
+ cell.appendChild(text);
+ cell.textElement = text;
+ var warning = document.createElement('img');
+ warning.setAttribute('src', 'media/warning.svg');
+ warning.setAttribute('height', '16px');
+ warning.setAttribute('width', '16px');
+ warning.style.marginLeft = '4px';
+ cell.appendChild(warning);
+ cell.warningIcon = warning;
+ row.appendChild(cell);
+ return cell;
+ };
+ var createRightArrow = function(row) {
+ var cell = document.createElement('div');
+ cell.className = 'arrow';
+ var rightArrow = document.createElement('button');
+ rightArrow.setAttribute('type', 'button');
+ rightArrow.textContent = '>';
+ cell.appendChild(rightArrow);
+ row.appendChild(cell);
+ return cell;
+ };
+ var createArrowListener = function(variable, array, direction) {
+ return function() {
+ var currentIndex = array.indexOf(this.displayValue_[variable]);
+ currentIndex += direction;
+ if (currentIndex <= -1) {
+ currentIndex = array.length - 1;
+ } else if (currentIndex >= array.length) {
+ currentIndex = 0;
+ }
+ var value = {};
+ value[variable] = array[currentIndex];
+ this.setValue(value);
+ };
+ };
+
+ var widget = document.createElement('div');
+ widget.className = 'customFieldsTurtleWidget blocklyNonSelectable';
+
+ var table = document.createElement('div');
+ table.className = 'table';
+ widget.appendChild(table);
+
+ var row = createRow(table);
+ var leftArrow = createLeftArrow(row);
+ widget.patternText = createTextNode(row, this.displayValue_.pattern);
+ var rightArrow = createRightArrow(row);
+ this.editorListeners_.push(Blockly.bindEvent_(leftArrow, 'mouseup', this,
+ createArrowListener('pattern', CustomFields.FieldTurtle.PATTERNS, -1)));
+ this.editorListeners_.push(Blockly.bindEvent_(rightArrow, 'mouseup', this,
+ createArrowListener('pattern', CustomFields.FieldTurtle.PATTERNS, 1)));
+
+ row = createRow(table);
+ leftArrow = createLeftArrow(row);
+ widget.hatText = createTextNode(row, this.displayValue_.hat);
+ rightArrow = createRightArrow(row);
+ this.editorListeners_.push(Blockly.bindEvent_(leftArrow, 'mouseup', this,
+ createArrowListener('hat', CustomFields.FieldTurtle.HATS, -1)));
+ this.editorListeners_.push(Blockly.bindEvent_(rightArrow, 'mouseup', this,
+ createArrowListener('hat', CustomFields.FieldTurtle.HATS, 1)));
+
+ row = createRow(table);
+ leftArrow = createLeftArrow(row);
+ widget.turtleNameText = createTextNode(row, this.displayValue_.turtleName);
+ rightArrow = createRightArrow(row);
+ this.editorListeners_.push(Blockly.bindEvent_(leftArrow, 'mouseup', this,
+ createArrowListener('turtleName', CustomFields.FieldTurtle.NAMES, -1)));
+ this.editorListeners_.push(Blockly.bindEvent_(rightArrow, 'mouseup', this,
+ createArrowListener('turtleName', CustomFields.FieldTurtle.NAMES, 1)));
+
+ var randomizeButton = document.createElement('button');
+ randomizeButton.className = 'randomize';
+ randomizeButton.setAttribute('type', 'button');
+ randomizeButton.textContent = 'randomize turtle';
+ this.editorListeners_.push(Blockly.bindEvent_(randomizeButton, 'mouseup', this,
+ function() {
+ var value = {};
+ value.pattern = CustomFields.FieldTurtle.PATTERNS[
+ Math.floor(Math.random() * CustomFields.FieldTurtle.PATTERNS.length)];
+
+ value.hat = CustomFields.FieldTurtle.HATS[
+ Math.floor(Math.random() * CustomFields.FieldTurtle.HATS.length)];
+
+ value.turtleName = CustomFields.FieldTurtle.NAMES[
+ Math.floor(Math.random() * CustomFields.FieldTurtle.NAMES.length)];
+
+ this.setValue(value);
+ }));
+ widget.appendChild(randomizeButton);
+
+ return widget;
+};
+
+// Cleans up any event listeners that were attached to the now hidden editor.
+CustomFields.FieldTurtle.prototype.dropdownDispose_ = function() {
+ for (var i = this.editorListeners_.length, listener;
+ listener = this.editorListeners_[i]; i--) {
+ Blockly.unbindEvent_(listener);
+ this.editorListeners_.pop();
+ }
+};
+
+// Updates the field's colour based on the colour of the block. Called by
+// block.applyColour.
+CustomFields.FieldTurtle.prototype.applyColour = function() {
+ if (!this.sourceBlock_) {
+ return;
+ }
+ // The getColourX functions are the best way to access the colours of a block.
+ var isShadow = this.sourceBlock_.isShadow();
+ var fillColour = isShadow ?
+ this.sourceBlock_.getColourShadow() : this.sourceBlock_.getColour();
+ // This is technically a package function, meaning it could change.
+ var borderColour = isShadow ? fillColour :
+ this.sourceBlock_.style.colourTertiary;
+
+ if (this.turtleGroup_) {
+ var child = this.turtleGroup_.firstChild;
+ while(child) {
+ // If it is a text node, continue.
+ if (child.nodeType == 3) {
+ child = child.nextSibling;
+ continue;
+ }
+ // Or if it is a non-turtle node, continue.
+ var className = child.getAttribute('class');
+ if (!className || className.indexOf('turtleBody') == -1) {
+ child = child.nextSibling;
+ continue;
+ }
+
+ child.style.fill = fillColour;
+ child.style.stroke = borderColour;
+ child = child.nextSibling;
+ }
+ }
+};
+
+// Saves the field's value to an XML node. Allows for custom serialization.
+CustomFields.FieldTurtle.prototype.toXml = function(fieldElement) {
+ // The default implementation of this function creates a node that looks
+ // like this: (where value is returned by getValue())
+ // value
+ // But this doesn't work for our field because it stores an /object/.
+
+ fieldElement.setAttribute('pattern', this.value_.pattern);
+ fieldElement.setAttribute('hat', this.value_.hat);
+ // The textContent usually contains whatever is closest to the field's
+ // 'value'. The textContent doesn't need to contain anything, but saving
+ // something to it does aid in readability.
+ fieldElement.textContent = this.value_.turtleName;
+
+ // Always return the element!
+ return fieldElement;
+};
+
+// Sets the field's value based on an XML node. Allows for custom
+// de-serialization.
+CustomFields.FieldTurtle.prototype.fromXml = function(fieldElement) {
+ // Because we had to do custom serialization for this field, we also need
+ // to do custom de-serialization.
+
+ var value = {};
+ value.pattern = fieldElement.getAttribute('pattern');
+ value.hat = fieldElement.getAttribute('hat');
+ value.turtleName = fieldElement.textContent;
+ // The end goal is to call this.setValue()
+ this.setValue(value);
+};
+
+// Blockly needs to know the JSON name of this field. Usually this is
+// registered at the bottom of the field class.
+Blockly.fieldRegistry.register('field_turtle', CustomFields.FieldTurtle);
+
+// Called by initView to create all of the SVGs. This is just used to keep
+// the code more organized.
+CustomFields.FieldTurtle.prototype.createView_ = function() {
+ this.movableGroup_ = Blockly.utils.dom.createSvgElement('g',
+ {
+ 'transform': 'translate(0,5)'
+ }, this.fieldGroup_);
+ var scaleGroup = Blockly.utils.dom.createSvgElement('g',
+ {
+ 'transform': 'scale(1.5)'
+ }, this.movableGroup_);
+ this.turtleGroup_ = Blockly.utils.dom.createSvgElement('g',
+ {
+ // Makes the smaller turtle graphic align with the hats.
+ 'class': 'turtleBody'
+ }, scaleGroup);
+ var tail = Blockly.utils.dom.createSvgElement('path',
+ {
+ 'class': 'turtleBody',
+ 'd': 'M7,27.5H0.188c3.959-2,6.547-2.708,8.776-5.237',
+ 'transform': 'translate(0.312 -12.994)'
+ }, this.turtleGroup_);
+ var legLeft = Blockly.utils.dom.createSvgElement('rect',
+ {
+ 'class': 'turtleBody',
+ 'x': 8.812,
+ 'y': 12.506,
+ 'width': 4,
+ 'height': 10
+ }, this.turtleGroup_);
+ var legRight = Blockly.utils.dom.createSvgElement('rect',
+ {
+ 'class': 'turtleBody',
+ 'x': 28.812,
+ 'y': 12.506,
+ 'width': 4,
+ 'height': 10
+ }, this.turtleGroup_);
+ var head = Blockly.utils.dom.createSvgElement('path',
+ {
+ 'class': 'turtleBody',
+ 'd': 'M47.991,17.884c0,1.92-2.144,3.477-4.788,3.477a6.262,6.262,0,0,1-2.212-.392c-0.2-.077-1.995,2.343-4.866,3.112a17.019,17.019,0,0,1-6.01.588c-4.413-.053-2.5-3.412-2.745-3.819-0.147-.242,2.232.144,6.126-0.376a7.392,7.392,0,0,0,4.919-2.588c0-1.92,2.144-3.477,4.788-3.477S47.991,15.964,47.991,17.884Z',
+ 'transform': 'translate(0.312 -12.994)'
+ }, this.turtleGroup_);
+ var smile = Blockly.utils.dom.createSvgElement('path',
+ {
+ 'class': 'turtleBody',
+ 'd': 'M42.223,18.668a3.614,3.614,0,0,0,2.728,2.38',
+ 'transform': 'translate(0.312 -12.994)'
+ }, this.turtleGroup_);
+ var sclera = Blockly.utils.dom.createSvgElement('ellipse',
+ {
+ 'cx': 43.435,
+ 'cy': 2.61,
+ 'rx': 2.247,
+ 'ry': 2.61,
+ 'fill': '#fff'
+ }, this.turtleGroup_);
+ var pupil = Blockly.utils.dom.createSvgElement('ellipse',
+ {
+ 'cx': 44.166,
+ 'cy': 3.403,
+ 'rx': 1.318,
+ 'ry': 1.62
+ }, this.turtleGroup_);
+ var shell = Blockly.utils.dom.createSvgElement('path',
+ {
+ 'class': 'turtleBody',
+ 'd': 'M33.4,27.5H7.193c0-6,5.866-13.021,13.1-13.021S33.4,21.5,33.4,27.5Z',
+ 'transform': 'translate(0.312 -12.994)'
+ }, this.turtleGroup_);
+ this.shellPattern_ = Blockly.utils.dom.createSvgElement('path',
+ {
+ 'd': 'M33.4,27.5H7.193c0-6,5.866-13.021,13.1-13.021S33.4,21.5,33.4,27.5Z',
+ 'transform': 'translate(0.312 -12.994)'
+ }, this.turtleGroup_);
+
+ this.stovepipe_ = Blockly.utils.dom.createSvgElement('image',
+ {
+ 'width': '50',
+ 'height': '18'
+ }, scaleGroup);
+ this.stovepipe_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
+ 'media/stovepipe.svg');
+ this.crown_ = Blockly.utils.dom.createSvgElement('image',
+ {
+ 'width': '50',
+ 'height': '15'
+ }, scaleGroup);
+ this.crown_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
+ 'media/crown.svg');
+ this.mask_ = Blockly.utils.dom.createSvgElement('image',
+ {
+ 'width': '50',
+ 'height': '14'
+ }, scaleGroup);
+ this.mask_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
+ 'media/mask.svg');
+ this.propeller_ = Blockly.utils.dom.createSvgElement('image',
+ {
+ 'width': '50',
+ 'height': '11'
+ }, scaleGroup);
+ this.propeller_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
+ 'media/propeller.svg');
+ this.fedora_ = Blockly.utils.dom.createSvgElement('image',
+ {
+ 'width': '50',
+ 'height': '12'
+ }, scaleGroup);
+ this.fedora_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
+ 'media/fedora.svg');
+
+ // Even if we're not going to display it right away, we want to create all
+ // of our DOM elements inside this function.
+ this.crown_.style.display = 'none';
+ this.mask_.style.display = 'none';
+ this.propeller_.style.display = 'none';
+ this.fedora_.style.display = 'none';
+
+ this.movableGroup_.appendChild(this.textElement_);
+ this.textElement_.setAttribute(
+ 'transform', 'translate(' + this.TEXT_OFFSET_X + ',20)');
+
+ this.defs_ = Blockly.utils.dom.createSvgElement('defs', {}, this.fieldGroup_);
+ this.polkadotPattern_ = Blockly.utils.dom.createSvgElement('pattern',
+ {
+ 'id': 'polkadots',
+ 'patternUnits': 'userSpaceOnUse',
+ 'width': 10,
+ 'height': 10
+ }, this.defs_);
+ this.polkadotGroup_ = Blockly.utils.dom.createSvgElement(
+ 'g', {}, this.polkadotPattern_);
+ Blockly.utils.dom.createSvgElement('circle',
+ {
+ 'cx': 2.5,
+ 'cy': 2.5,
+ 'r': 2.5,
+ 'fill': '#000',
+ 'fill-opacity': .3
+ }, this.polkadotGroup_);
+ Blockly.utils.dom.createSvgElement('circle',
+ {
+ 'cx': 7.5,
+ 'cy': 7.5,
+ 'r': 2.5,
+ 'fill': '#000',
+ 'fill-opacity': .3
+ }, this.polkadotGroup_);
+
+ this.hexagonPattern_ = Blockly.utils.dom.createSvgElement('pattern',
+ {
+ 'id': 'hexagons',
+ 'patternUnits': 'userSpaceOnUse',
+ 'width': 10,
+ 'height': 8.68,
+ 'patternTransform': 'translate(2) rotate(45)'
+ }, this.defs_);
+ Blockly.utils.dom.createSvgElement('polygon',
+ {
+ 'id': 'hex',
+ 'points': '4.96,4.4 7.46,5.84 7.46,8.74 4.96,10.18 2.46,8.74 2.46,5.84',
+ 'stroke': '#000',
+ 'stroke-opacity': .3,
+ 'fill-opacity': 0
+ }, this.hexagonPattern_);
+ var use = Blockly.utils.dom.createSvgElement('use',
+ {
+ 'x': 5,
+ }, this.hexagonPattern_);
+ use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#hex');
+ use = Blockly.utils.dom.createSvgElement('use',
+ {
+ 'x': -5,
+ }, this.hexagonPattern_);
+ use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#hex');
+ use = Blockly.utils.dom.createSvgElement('use',
+ {
+ 'x': 2.5,
+ 'y': -4.34
+ }, this.hexagonPattern_);
+ use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#hex');
+ use = Blockly.utils.dom.createSvgElement('use',
+ {
+ 'x': -2.5,
+ 'y': -4.34
+ }, this.hexagonPattern_);
+ use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#hex');
+
+ this.stripesPattern_ = Blockly.utils.dom.createSvgElement('pattern',
+ {
+ 'id': 'stripes',
+ 'patternUnits': 'userSpaceOnUse',
+ 'width': 5,
+ 'height': 10,
+ 'patternTransform': 'rotate(45)'
+ }, this.defs_);
+ Blockly.utils.dom.createSvgElement('line',
+ {
+ 'x1': 0,
+ 'y1': 0,
+ 'x2': 0,
+ 'y2': 10,
+ 'stroke-width': 4,
+ 'stroke': '#000',
+ 'stroke-opacity': .3
+ }, this.stripesPattern_);
+};
diff --git a/js/demos/custom-fields/turtle/icon.png b/js/demos/custom-fields/turtle/icon.png
new file mode 100644
index 0000000..3a7314a
Binary files /dev/null and b/js/demos/custom-fields/turtle/icon.png differ
diff --git a/js/demos/custom-fields/turtle/index.html b/js/demos/custom-fields/turtle/index.html
new file mode 100644
index 0000000..9bc4dfe
--- /dev/null
+++ b/js/demos/custom-fields/turtle/index.html
@@ -0,0 +1,175 @@
+
+
+
+
+ Blockly Demo: Custom Turtle Field
+
+
+
+
+
+
+
+
+
Blockly >
+ Demos > Step Execution with JS Interpreter
+
+
This is a demo of executing code step-by-step with a sandboxed JavaScript interpreter.
+
+
The generator's Blockly.JavaScript.STATEMENT_PREFIX is assigned 'highlightBlock(%1);\n',
+ where %1 is the block id. The call to highlightBlock() will highlight the identified block
+ and set the variable highlightPause to true.
+
+
"Parse JavaScript" will generate the code and load it into the interpreter. Then, each press of the
+ "Step JavaScript" button will run the interpreter one step until the highlightPause is true.
+ That is, until highlightBlock() has highlighted the block that will be executed on the next step.
Keyboard Navigation is our first step towards an accessible Blockly.
+ You can enter accessibility mode by shift clicking anywhere on the
+ workspace or on a block. Some basic commands for moving around are below.
+ More complete documentation is still in progress.
+ Workspace Navigation
+ W: Previous block/field/input at the same level
+ A: Up one level (Field (or input) -> Block -> Input (or field) -> Block ->
+ Stack -> Workspace)
+ S: Next block/field/input at the same level
+ D: Down one level (Workspace -> Stack -> Block -> Input (or field) -> Block
+ -> Field (or input))
+ T: Will open the toolbox. Once in there you can moving around using the WASD keys. And insert a block by hitting Enter
+ X: While on a connection hit X to disconnect the block after the cursor
+
+ Pre Order Traversal
+ Feel free to just play around in accessibility mode or hit the button below to see the demo.
+ The demo uses preorder tree traversal
+ as an alternative way to navigate the blocks,
+ connections, and fields on the workspace.
+
+
+
+
+ Cursor
+ The cursor controls how the user navigates the blocks, inputs, fields and connections on a workspace.
+ This demo shows two different cursors:
+ Default Cursor: Allow the user to go to the previous, next, in or out location.
+ Basic Cursor: Using the pre order traversal allows the user to go to the next and previous location.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Set key mappings below
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+ 123
+
+
+
+
+
+
+
+
+
+
+
+
+
+ list
+ item
+
+
+
+
+ EQ
+
+
+ ADD
+
+
+ 1
+
+
+
+
+ 1
+
+
+
+
+
+
+ ROOT
+
+
+ 9
+
+
+ 123
+
+
+
+
+
+
+
+
+
+ SET
+ FROM_START
+
+
+ list
+
+
+
+
+ item
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/demos/keyboard_nav/line_cursor.js b/js/demos/keyboard_nav/line_cursor.js
new file mode 100644
index 0000000..d197e8a
--- /dev/null
+++ b/js/demos/keyboard_nav/line_cursor.js
@@ -0,0 +1,180 @@
+/**
+ * @license
+ * Copyright 2019 Google LLC
+ *
+ * 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 class representing a line cursor.
+ * A line cursor traverses the blocks as if they were
+ * lines of code in a text editor.
+ * Previous and next go up and down lines. In and out go
+ * through the elements in a line.
+ * @author aschmiedt@google.com (Abby Schmiedt)
+ */
+'use strict';
+
+
+/**
+ * Class for a line cursor.
+ * This will allow the user to get to all nodes in the AST by hitting next or
+ * previous.
+ * @constructor
+ * @extends {Blockly.BasicCursor}
+ */
+Blockly.LineCursor = function() {
+ Blockly.LineCursor.superClass_.constructor.call(this);
+};
+Blockly.utils.object.inherits(Blockly.LineCursor, Blockly.BasicCursor);
+
+
+/**
+ * Find the next node in the pre order traversal.
+ * @return {Blockly.ASTNode} The next node, or null if the current node is
+ * not set or there is no next value.
+ * @override
+ */
+Blockly.LineCursor.prototype.next = function() {
+ var curNode = this.getCurNode();
+ if (!curNode) {
+ return null;
+ }
+ var newNode = this.getNextNode_(curNode, this.validLineNode_);
+
+ // Skip the input or next value if there is a connected block.
+ if (newNode && (newNode.getType() == Blockly.ASTNode.types.INPUT ||
+ newNode.getType() == Blockly.ASTNode.types.NEXT) &&
+ newNode.getLocation().targetBlock()) {
+ newNode = this.getNextNode_(newNode, this.validLineNode_);
+ }
+ if (newNode) {
+ this.setCurNode(newNode);
+ }
+ return newNode;
+};
+
+/**
+ * For a basic cursor we only have the ability to go next and previous, so
+ * in will also allow the user to get to the next node in the pre order traversal.
+ * @return {Blockly.ASTNode} The next node, or null if the current node is
+ * not set or there is no next value.
+ * @override
+ */
+Blockly.LineCursor.prototype.in = function() {
+ var curNode = this.getCurNode();
+ if (!curNode) {
+ return null;
+ }
+ var newNode = this.getNextNode_(curNode, this.validInLineNode_);
+
+ if (newNode) {
+ this.setCurNode(newNode);
+ }
+ return newNode;
+};
+
+/**
+ * Find the previous node in the pre order traversal.
+ * @return {Blockly.ASTNode} The previous node, or null if the current node
+ * is not set or there is no previous value.
+ * @override
+ */
+Blockly.LineCursor.prototype.prev = function() {
+ var curNode = this.getCurNode();
+ if (!curNode) {
+ return null;
+ }
+ var newNode = this.getPreviousNode_(curNode, this.validLineNode_);
+
+ if (newNode && (newNode.getType() == Blockly.ASTNode.types.INPUT ||
+ newNode.getType() == Blockly.ASTNode.types.NEXT) &&
+ newNode.getLocation().targetBlock()) {
+ newNode = this.getPreviousNode_(newNode, this.validLineNode_);
+ }
+
+ if (newNode) {
+ this.setCurNode(newNode);
+ }
+ return newNode;
+};
+
+/**
+ * For a basic cursor we only have the ability to go next and previou, so
+ * out will allow the user to get to the previous node in the pre order traversal.
+ * @return {Blockly.ASTNode} The previous node, or null if the current node is
+ * not set or there is no previous value.
+ * @override
+ */
+Blockly.LineCursor.prototype.out = function() {
+ var curNode = this.getCurNode();
+ if (!curNode) {
+ return null;
+ }
+ var newNode = this.getPreviousNode_(curNode, this.validInLineNode_);
+
+ if (newNode) {
+ this.setCurNode(newNode);
+ }
+ return newNode;
+
+};
+
+/**
+ * Meant to traverse by lines of code. This is blocks, statement inputs and
+ * next connections.
+ * @param {Blockly.ASTNode} node The AST node to check whether it is valid.
+ * @return {boolean} True if the node should be visited, false otherwise.
+ * @private
+ */
+Blockly.LineCursor.prototype.validLineNode_ = function(node) {
+ if (!node) {
+ return false;
+ }
+ var isValid = false;
+ var location = node.getLocation();
+ var type = node && node.getType();
+ if (type == Blockly.ASTNode.types.BLOCK) {
+ if (location.outputConnection === null) {
+ isValid = true;
+ }
+ } else if (type == Blockly.ASTNode.types.INPUT &&
+ location.type == Blockly.NEXT_STATEMENT) {
+ isValid = true;
+ } else if (type == Blockly.ASTNode.types.NEXT) {
+ isValid = true;
+ }
+ return isValid;
+};
+
+/**
+ * Meant to traverse within a block. These are fields and input values.
+ * @param {Blockly.ASTNode} node The AST node to check whether it is valid.
+ * @return {boolean} True if the node should be visited, false otherwise.
+ * @private
+ */
+Blockly.LineCursor.prototype.validInLineNode_ = function(node) {
+ if (!node) {
+ return false;
+ }
+ var isValid = false;
+ var location = node.getLocation();
+ var type = node && node.getType();
+ if (type == Blockly.ASTNode.types.FIELD) {
+ isValid = true;
+ } else if (type == Blockly.ASTNode.types.INPUT &&
+ location.type == Blockly.INPUT_VALUE) {
+ isValid = true;
+ }
+ return isValid;
+};
diff --git a/js/demos/maxBlocks/icon.png b/js/demos/maxBlocks/icon.png
new file mode 100644
index 0000000..b90c796
Binary files /dev/null and b/js/demos/maxBlocks/icon.png differ
diff --git a/js/demos/maxBlocks/index.html b/js/demos/maxBlocks/index.html
new file mode 100644
index 0000000..dc94f2f
--- /dev/null
+++ b/js/demos/maxBlocks/index.html
@@ -0,0 +1,100 @@
+
+
+
+
+ Blockly Demo: Maximum Block Limit
+
+
+
+
+
+
+
This is a simple demo showing how a minimap can be implemented.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123
+
+
+
+
+ i
+ j
+ k
+
+
+
+
+
+
+
diff --git a/js/demos/minimap/minimap.js b/js/demos/minimap/minimap.js
new file mode 100644
index 0000000..05e00bb
--- /dev/null
+++ b/js/demos/minimap/minimap.js
@@ -0,0 +1,312 @@
+/**
+
+ * Copyright 2017 Google LLC
+ *
+ * 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 Minimap demo.
+ * @author karnpurohit@gmail.com (Karan Purohit)
+ */
+'use strict';
+
+/**
+ * Creating a separate namespace for minimap.
+ */
+var Minimap = {};
+
+/**
+ * Initialize the workspace and minimap.
+ * @param {!Workspace} workspace The main workspace of the user.
+ * @param {!Workspace} minimap The workspace that will be used as a minimap.
+ */
+Minimap.init = function(workspace, minimap) {
+ this.workspace = workspace;
+ this.minimap = minimap;
+
+ // Adding scroll callback functionality to vScroll and hScroll just for this demo.
+ // IMPORTANT: This should be changed when there is proper UI event handling
+ // API available and should be handled by workspace's event listeners.
+ this.workspace.scrollbar.vScroll.setHandlePosition = function(newPosition) {
+ this.handlePosition_ = newPosition;
+ this.svgHandle_.setAttribute(this.positionAttribute_, this.handlePosition_);
+
+ // Code above is same as the original setHandlePosition function in core/scrollbar.js.
+ // New code starts from here.
+
+ // Get the absolutePosition.
+ var absolutePosition = (this.handlePosition_ / this.ratio_);
+
+ // Firing the scroll change listener.
+ Minimap.onScrollChange(absolutePosition, this.horizontal_);
+ };
+
+ // Adding call back for horizontal scroll.
+ this.workspace.scrollbar.hScroll.setHandlePosition = function(newPosition) {
+ this.handlePosition_ = newPosition;
+ this.svgHandle_.setAttribute(this.positionAttribute_, this.handlePosition_);
+
+ // Code above is same as the original setHandlePosition function in core/scrollbar.js.
+ // New code starts from here.
+
+ // Get the absolutePosition.
+ var absolutePosition = (this.handlePosition_ / this.ratio_);
+
+ // Firing the scroll change listener.
+ Minimap.onScrollChange(absolutePosition, this.horizontal_);
+ };
+
+
+ // Required to stop a positive feedback loop when user clicks minimap
+ // and the scroll changes, which in turn may change minimap.
+ this.disableScrollChange = false;
+
+ // Listen to events on the main workspace.
+ this.workspace.addChangeListener(Minimap.mirrorEvent);
+
+ //Get rectangle bounding the minimap div.
+ this.rect = document.getElementById('mapDiv').getBoundingClientRect();
+
+ // Create a svg overlay on the top of mapDiv for the minimap.
+ this.svg = Blockly.utils.dom.createSvgElement('svg', {
+ 'xmlns': Blockly.utils.dom.SVG_NS,
+ 'xmlns:html': Blockly.utils.dom.HTML_NS,
+ 'xmlns:xlink': Blockly.utils.dom.XLINK_NS,
+ 'version': '1.1',
+ 'height': this.rect.bottom-this.rect.top,
+ 'width': this.rect.right-this.rect.left,
+ 'class': 'minimap',
+ }, document.getElementById('mapDiv'));
+ this.svg.style.top = this.rect.top + 'px';
+ this.svg.style.left = this.rect.left + 'px';
+
+ // Creating a rectangle in the minimap that represents current view.
+ Blockly.utils.dom.createSvgElement('rect', {
+ 'width': 100,
+ 'height': 100,
+ 'class': 'mapDragger'
+ }, this.svg);
+
+ // Rectangle in the minimap that represents current view.
+ this.mapDragger = this.svg.childNodes[0];
+
+ // Adding mouse events to the rectangle, to make it Draggable.
+ // Using Blockly.bindEvent_ to attach mouse/touch listeners.
+ Blockly.bindEvent_(this.mapDragger, 'mousedown', null, Minimap.mousedown);
+
+ //When the window change, we need to resize the minimap window.
+ window.addEventListener('resize', Minimap.repositionMinimap);
+
+ // Mouse up event for the minimap.
+ this.svg.addEventListener('mouseup', Minimap.updateMapDragger);
+
+ //Boolean to check whether I am dragging the surface or not.
+ this.isDragging = false;
+};
+
+Minimap.mousedown = function(e) {
+ // Using Blockly.bindEvent_ to attach mouse/touch listeners.
+ Minimap.mouseMoveBindData =
+ Blockly.bindEvent_(document, 'mousemove', null, Minimap.mousemove);
+ Minimap.mouseUpBindData =
+ Blockly.bindEvent_(document, 'mouseup', null, Minimap.mouseup);
+
+ Minimap.isDragging = true;
+ e.stopPropagation();
+};
+
+Minimap.mouseup = function(e) {
+ Minimap.isDragging = false;
+ // Removing listeners.
+ Blockly.unbindEvent_(Minimap.mouseUpBindData);
+ Blockly.unbindEvent_(Minimap.mouseMoveBindData);
+ Minimap.updateMapDragger(e);
+ e.stopPropagation();
+};
+
+Minimap.mousemove = function(e) {
+ if (Minimap.isDragging) {
+ Minimap.updateMapDragger(e);
+ e.stopPropagation();
+ }
+};
+
+/**
+ * Run non-UI events from the main workspace on the minimap.
+ * @param {!Event} event Event that triggered in the main workspace.
+ */
+Minimap.mirrorEvent = function(event) {
+ if (event.type == Blockly.Events.UI) {
+ return; // Don't mirror UI events.
+ }
+ // Convert event to JSON. This could then be transmitted across the net.
+ var json = event.toJson();
+ // Convert JSON back into an event, then execute it.
+ var minimapEvent = Blockly.Events.fromJson(json, Minimap.minimap);
+ minimapEvent.run(true);
+ Minimap.scaleMinimap();
+ Minimap.setDraggerHeight();
+ Minimap.setDraggerWidth();
+};
+
+/**
+ * Called when window is resized. Repositions the minimap overlay.
+ */
+Minimap.repositionMinimap = function() {
+ Minimap.rect = document.getElementById('mapDiv').getBoundingClientRect();
+ Minimap.svg.style.top = Minimap.rect.top + 'px';
+ Minimap.svg.style.left = Minimap.rect.left + 'px';
+};
+
+/**
+ * Updates the rectangle's height.
+ */
+Minimap.setDraggerHeight = function() {
+ var workspaceMetrics = Minimap.workspace.getMetrics();
+ var draggerHeight = (workspaceMetrics.viewHeight / Minimap.workspace.scale) *
+ Minimap.minimap.scale;
+ // It's zero when first block is placed.
+ if (draggerHeight == 0) {
+ return;
+ }
+ Minimap.mapDragger.setAttribute('height', draggerHeight);
+};
+
+/**
+ * Updates the rectangle's width.
+ */
+Minimap.setDraggerWidth = function() {
+ var workspaceMetrics = Minimap.workspace.getMetrics();
+ var draggerWidth = (workspaceMetrics.viewWidth / Minimap.workspace.scale) *
+ Minimap.minimap.scale;
+ // It's zero when first block is placed.
+ if (draggerWidth == 0) {
+ return;
+ }
+ Minimap.mapDragger.setAttribute('width', draggerWidth);
+};
+
+
+/**
+ * Updates the overall position of the viewport of the minimap by appropriately
+ * using translate functions.
+ */
+Minimap.scaleMinimap = function() {
+ var minimapBoundingBox = Minimap.minimap.getBlocksBoundingBox();
+ var workspaceBoundingBox = Minimap.workspace.getBlocksBoundingBox();
+ var workspaceMetrics = Minimap.workspace.getMetrics();
+ var minimapMetrics = Minimap.minimap.getMetrics();
+
+ // Scaling the mimimap such that all the blocks can be seen in the viewport.
+ // This padding is default because this is how to scrollbar(in main workspace)
+ // is implemented.
+ var topPadding = (workspaceMetrics.viewHeight) * Minimap.minimap.scale /
+ (2 * Minimap.workspace.scale);
+ var sidePadding = (workspaceMetrics.viewWidth) * Minimap.minimap.scale /
+ (2 * Minimap.workspace.scale);
+
+ // If actual padding is more than half view ports height,
+ // change it to actual padding.
+ if ((workspaceBoundingBox.y * Minimap.workspace.scale -
+ workspaceMetrics.contentTop) *
+ Minimap.minimap.scale / Minimap.workspace.scale > topPadding) {
+ topPadding = (workspaceBoundingBox.y * Minimap.workspace.scale -
+ workspaceMetrics.contentTop) *
+ Minimap.minimap.scale / Minimap.workspace.scale;
+ }
+
+ // If actual padding is more than half view ports height,
+ // change it to actual padding.
+ if ((workspaceBoundingBox.x * Minimap.workspace.scale -
+ workspaceMetrics.contentLeft) *
+ Minimap.minimap.scale / Minimap.workspace.scale > sidePadding) {
+ sidePadding = (workspaceBoundingBox.x * Minimap.workspace.scale -
+ workspaceMetrics.contentLeft) *
+ Minimap.minimap.scale / Minimap.workspace.scale;
+ }
+
+ var scalex = (minimapMetrics.viewWidth - 2 * sidePadding) /
+ minimapBoundingBox.width;
+ var scaley = (minimapMetrics.viewHeight - 2 * topPadding) /
+ minimapBoundingBox.height;
+ Minimap.minimap.setScale(Math.min(scalex, scaley));
+
+ // Translating the minimap.
+ Minimap.minimap.translate(
+ -minimapMetrics.contentLeft * Minimap.minimap.scale + sidePadding,
+ -minimapMetrics.contentTop * Minimap.minimap.scale + topPadding);
+};
+
+/**
+ * Handles the onclick event on the minimapBoundingBox.
+ * Changes mapDraggers position.
+ * @param {!Event} e Event from the mouse click.
+ */
+Minimap.updateMapDragger = function(e) {
+ var y = e.clientY;
+ var x = e.clientX;
+ var draggerHeight = Minimap.mapDragger.getAttribute('height');
+ var draggerWidth = Minimap.mapDragger.getAttribute('width');
+
+ var finalY = y - Minimap.rect.top - draggerHeight / 2;
+ var finalX = x - Minimap.rect.left - draggerWidth / 2;
+
+ var maxValidY = (Minimap.workspace.getMetrics().contentHeight -
+ Minimap.workspace.getMetrics().viewHeight) * Minimap.minimap.scale;
+ var maxValidX = (Minimap.workspace.getMetrics().contentWidth -
+ Minimap.workspace.getMetrics().viewWidth) * Minimap.minimap.scale;
+
+ if (y + draggerHeight / 2 > Minimap.rect.bottom) {
+ finalY = Minimap.rect.bottom - Minimap.rect.top - draggerHeight;
+ } else if (y < Minimap.rect.top + draggerHeight / 2) {
+ finalY = 0;
+ }
+
+ if (x + draggerWidth / 2 > Minimap.rect.right) {
+ finalX = Minimap.rect.right - Minimap.rect.left - draggerWidth;
+ } else if (x < Minimap.rect.left + draggerWidth / 2) {
+ finalX = 0;
+ }
+
+ // Do not go below lower bound of scrollbar.
+ if (finalY > maxValidY) {
+ finalY = maxValidY;
+ }
+ if (finalX > maxValidX) {
+ finalX = maxValidX;
+ }
+ Minimap.mapDragger.setAttribute('y', finalY);
+ Minimap.mapDragger.setAttribute('x', finalX);
+ // Required, otherwise creates a feedback loop.
+ Minimap.disableScrollChange = true;
+ Minimap.workspace.scrollbar.vScroll.set((finalY * Minimap.workspace.scale) /
+ Minimap.minimap.scale);
+ Minimap.workspace.scrollbar.hScroll.set((finalX * Minimap.workspace.scale) /
+ Minimap.minimap.scale);
+ Minimap.disableScrollChange = false;
+};
+
+/**
+ * Handles the onclick event on the minimapBoundingBox, parameters are passed by
+ * the event handler.
+ * @param {number} position This is the absolute position of the scrollbar.
+ * @param {boolean} horizontal Informs if the change event if for
+ * horizontal (true) or vertical (false) scrollbar.
+ */
+Minimap.onScrollChange = function(position, horizontal) {
+ if (!Minimap.disableScrollChange) {
+ Minimap.mapDragger.setAttribute(horizontal ? 'x' : 'y',
+ position * Minimap.minimap.scale / Minimap.workspace.scale);
+ }
+};
diff --git a/js/demos/mirror/icon.png b/js/demos/mirror/icon.png
new file mode 100644
index 0000000..45e2a9a
Binary files /dev/null and b/js/demos/mirror/icon.png differ
diff --git a/js/demos/mirror/index.html b/js/demos/mirror/index.html
new file mode 100644
index 0000000..84762f3
--- /dev/null
+++ b/js/demos/mirror/index.html
@@ -0,0 +1,81 @@
+
+
+
+
+ Blockly Demo: Mirrored Blockly
+
+
+
+
+
+
+
This is a simple demo of a primary Blockly instance that controls a secondary Blockly instance with events.
+ Open the JavaScript console to see the event passing.
+
+
+
+
+
+
+ 123
+
+
+
+
+ i
+ j
+ k
+
+
+
+
+
+
diff --git a/js/demos/mobile/README.md b/js/demos/mobile/README.md
new file mode 100644
index 0000000..bab0960
--- /dev/null
+++ b/js/demos/mobile/README.md
@@ -0,0 +1,53 @@
+# Blockly on Mobile Devices
+
+This directory contains three examples of running the Blockly library on mobile
+devices. The `html/` directory is a example of configuring a webpage for touch
+devices, with a Blockly workspace that fills the screen.
+
+The `mobile/html/` is also the basis for the Android and iOS demos. Each native
+app copies this demo into the app's local resources, and required Blockly
+library files, and hosts them in an embedded WebView.
+
+Thus, developers can quickly iterate within the `mobile/html/` directory, and
+see changes in both the Android and iOS native apps.
+
+## Running the Mobile HTML Demo
+
+Before running the mobile HTML demo, you need to create some symbolic links
+in your local file system. Run the `mobile/html/ln_resources.sh` file from
+the `mobile/html/` directory. This mimicks the relative locations of the
+Blockly files seen when loading the page in a native app's embedded WebView.
+
+After doing this, opening `mobile/html/index.html` should open normally,
+filling the page with one large Blockly workspace.
+
+## The Android App
+
+### Build and Run
+
+Open the `demos/mobile/android/` directory in Android Studio. The project
+files in the directory should be ready to build and run the demo in an emulator
+or connected device.
+
+### Android Copy Tasks
+
+If you edit the `mobile/html/` demo to include new files, you will need to
+update the native app project files to also copy those files.
+
+In the Android project, two Gradle tasks are responsible for the copies.
+In `mobile/android/app/build.gradle`, the tasks `copyBlocklyHtmlFile` and
+`copyBlocklyMoreFiles` configure the copy actions.
+
+## The iOS App
+
+### Build and Run
+
+Open the `demos/mobile/iOS/` directory in XCode. The project files in the
+directory should be ready to build and run the demo in a simulator or connected
+device.
+
+### iOS Copy Script
+
+The XCode project call out to `mobile/ios/cp_resources.sh` to copy the required
+HTML and related files. If you've edited the `mobile/html/` demo to require new
+files, update this script to copy these files, too.
diff --git a/js/demos/mobile/android/.gitignore b/js/demos/mobile/android/.gitignore
new file mode 100644
index 0000000..3ba2b2b
--- /dev/null
+++ b/js/demos/mobile/android/.gitignore
@@ -0,0 +1,27 @@
+/build
+/captures
+/app/src/main/assets/blockly
+.settings
+.project
+
+# Local Settings
+local.properties
+
+# Project files
+*.komodoproject
+.gradle
+*.iml
+.idea
+
+# Build files
+*.pyc
+*.apk
+*.ap_
+*.class
+*.dex
+
+# OSX Files
+.DS_Store
+
+# Windows Files
+Thumb.db
diff --git a/js/demos/mobile/android/README.md b/js/demos/mobile/android/README.md
new file mode 100644
index 0000000..31f968f
--- /dev/null
+++ b/js/demos/mobile/android/README.md
@@ -0,0 +1,45 @@
+# Blockly in an Android WebView
+
+This code demonstrates how to get Blockly running in an Android app by
+embedding it in a WebView.
+
+### BlocklyWebViewFragment
+
+Most of the work is done within the fragment class `BlocklyWebViewFragment`.
+This fragment instantiates the WebView, loads the HTML
+(`assets/blockly/webview.html`, copied from `demos/mobile/html/index.html`),
+and provides a few helper methods.
+
+### Copying web assets with gradle
+
+This android project copies the necessary files from the main Blockly
+repository (i.e., parent directory). In `app/build.gradle`, note the
+`copyBlocklyHtmlFile` and `copyBlocklyMoreFiles` tasks.
+
+In your own project, the HTML and related files can be placed directly in the
+`assets/blockly` directory without the copy step. However, using the copy tasks
+simplifies the synchronization with an iOS app using the same files.
+
+### Loading Block Definitions and Generator functions
+
+The `webview.html` loads the block definitions and generator functions directly
+into the page, without support or coordination with the Android classes. This
+assumes the app will always utilize the same blocks. This does not mean all
+blocks are visible to the user all the time; that is controlled by the toolbox
+and workspace files. This should accommodate almost all applications.
+
+This does mean loading your own block definitions and generators will involve
+editing the HTML, adding you own `
+
+
+
+
+
+
+
+
+
+