From 8ec5745611b0b217b04c68ff93435aef821c538b Mon Sep 17 00:00:00 2001 From: Emma Dauterman Date: Tue, 16 Aug 2016 15:52:12 -0700 Subject: [PATCH] Add Workspace Factory to Blockly Factory Tab (#538) * Starting to integrate workspacefactory * Committing before switching branches * Tab for workspace factory working * Committing before switching branches * Refactored to have FactoryInit namespace and move logic out of AppController * Nit typo fix. * Fixed bugs from rebasing * Nit fix in factory.css * Added this. to previewWorkspace --- demos/blocklyfactory/app_controller.js | 25 +- demos/blocklyfactory/block_exporter_view.js | 11 +- demos/blocklyfactory/factory.css | 188 ++++++++- demos/blocklyfactory/index.html | 387 +++++++++++++++++- .../workspacefactory/wfactory_controller.js | 55 ++- .../workspacefactory/wfactory_init.js | 364 ++++++++++++++++ 6 files changed, 997 insertions(+), 33 deletions(-) create mode 100644 demos/blocklyfactory/workspacefactory/wfactory_init.js diff --git a/demos/blocklyfactory/app_controller.js b/demos/blocklyfactory/app_controller.js index 052007e28..1cfe61a88 100644 --- a/demos/blocklyfactory/app_controller.js +++ b/demos/blocklyfactory/app_controller.js @@ -33,6 +33,8 @@ goog.require('BlockLibraryController'); goog.require('BlockExporterController'); goog.require('goog.dom.classlist'); goog.require('goog.string'); +goog.require('goog.ui.PopupColorPicker'); +goog.require('goog.ui.ColorPicker'); /** * Controller for the Blockly Factory @@ -45,6 +47,10 @@ AppController = function() { new BlockLibraryController(this.blockLibraryName); this.blockLibraryController.populateBlockLibrary(); + // Construct Workspace Factory Controller. + this.workspaceFactoryController = new FactoryController + ('workspacefactory_toolbox', 'toolbox_blocks', 'preview_blocks'); + // Initialize Block Exporter this.exporter = new BlockExporterController(this.blockLibraryController.storage); @@ -361,18 +367,20 @@ AppController.prototype.assignLibraryClickHandlers = function() { /** * Assign button click handlers for the block factory. */ -AppController.prototype.assignFactoryClickHandlers = function() { +AppController.prototype.assignBlockFactoryClickHandlers = function() { var self = this; // Assign button event handlers for Block Factory. document.getElementById('localSaveButton') .addEventListener('click', function() { self.exportBlockLibraryToFile(); }); + document.getElementById('helpButton').addEventListener('click', function() { open('https://developers.google.com/blockly/custom-blocks/block-factory', 'BlockFactoryHelp'); }); + document.getElementById('files').addEventListener('change', function() { // Warn user. @@ -386,6 +394,7 @@ AppController.prototype.assignFactoryClickHandlers = function() { this.value = null; } }); + document.getElementById('createNewBlockButton') .addEventListener('click', function() { BlockFactory.showStarterBlock(); @@ -396,7 +405,7 @@ AppController.prototype.assignFactoryClickHandlers = function() { /** * Add event listeners for the block factory. */ -AppController.prototype.addFactoryEventListeners = function() { +AppController.prototype.addBlockFactoryEventListeners = function() { BlockFactory.mainWorkspace.addChangeListener(BlockFactory.updateLanguage); document.getElementById('direction') .addEventListener('change', BlockFactory.updatePreview); @@ -429,6 +438,7 @@ AppController.prototype.initializeBlocklyStorage = function() { BlocklyStorage.link(BlockFactory.mainWorkspace);}); BlockFactory.disableEnableLink(); }; + /** * Initialize Blockly and layout. Called on page load. */ @@ -441,7 +451,7 @@ AppController.prototype.init = function() { // Assign click handlers. this.assignExporterClickHandlers(); this.assignLibraryClickHandlers(); - this.assignFactoryClickHandlers(); + this.assignBlockFactoryClickHandlers(); // Handle resizing of Block Factory elements. var expandList = [ @@ -463,7 +473,7 @@ AppController.prototype.init = function() { window.addEventListener('resize', onresize); // Inject Block Factory Main Workspace. - var toolbox = document.getElementById('toolbox'); + var toolbox = document.getElementById('blockfactory_toolbox'); BlockFactory.mainWorkspace = Blockly.inject('blockly', {collapse: false, toolbox: toolbox, @@ -484,5 +494,10 @@ AppController.prototype.init = function() { BlockFactory.mainWorkspace.clearUndo(); // Add Block Factory event listeners. - this.addFactoryEventListeners(); + this.addBlockFactoryEventListeners(); + + // Workspace Factory init. + FactoryInit.initWorkspaceFactory(this.workspaceFactoryController); }; + + diff --git a/demos/blocklyfactory/block_exporter_view.js b/demos/blocklyfactory/block_exporter_view.js index 413c60fd3..7c3366d10 100644 --- a/demos/blocklyfactory/block_exporter_view.js +++ b/demos/blocklyfactory/block_exporter_view.js @@ -38,17 +38,17 @@ goog.require('goog.dom'); * * @param {Element} toolbox - Xml for the toolbox of the selector workspace. */ -BlockExporterView = function(toolbox) { +BlockExporterView = function(selectorToolbox) { // Xml representation of the toolbox - if (toolbox.hasChildNodes) { - this.toolbox = toolbox; + if (selectorToolbox.hasChildNodes) { + this.toolbox = selectorToolbox; } else { // Toolbox is empty. Append dummy category to toolbox because toolbox // cannot switch between category and flyout-only mode after injection. var categoryElement = goog.dom.createDom('category'); categoryElement.setAttribute('name', 'Next Saved Block'); - toolbox.appendChild(categoryElement); - this.toolbox = toolbox; + selectorToolbox.appendChild(categoryElement); + this.toolbox = selectorToolbox; } // Workspace users use to select blocks for export this.selectorWorkspace = @@ -142,4 +142,3 @@ BlockExporterView.prototype.getSelectedBlocks = function() { return this.selectorWorkspace.getAllBlocks(); }; - diff --git a/demos/blocklyfactory/factory.css b/demos/blocklyfactory/factory.css index bddc7cd33..e74383966 100644 --- a/demos/blocklyfactory/factory.css +++ b/demos/blocklyfactory/factory.css @@ -40,7 +40,11 @@ h3 { } table { + border: none; + border-collapse: collapse; height: 100%; + margin: 0; + padding: 0; width: 100%; } @@ -58,7 +62,6 @@ p { padding: 5px 0px; } - #blockly { position: fixed; } @@ -130,6 +133,12 @@ button, .buttonStyle { #blockFactoryContent { height: 87%; + width: 100%; +} + +#blockFactoryPreview { + height: 100%; + width: 100%; } #blockLibraryContainer { @@ -230,3 +239,180 @@ button, .buttonStyle { display: table; width: 100%; } + +/* Workspace Factory */ + +section { + float: left; +} + +aside { + float: right; +} + +#categoryTable>table { + border: 1px solid #ccc; + border-bottom: none; + width: auto; +} + +td.tabon { + border-bottom-color: #ddd !important; + background-color: #ddd; + padding: 5px 19px; +} + +td.taboff { + cursor: pointer; + padding: 5px 19px; +} + +td.taboff:hover { + background-color: #eee; +} + +.large { + font-size: large; +} + +td { + padding: 0; + vertical-align: top; +} + +.inputfile { + height: 0; + opacity: 0; + overflow: hidden; + position: absolute; + width: 0; + z-index: -1; +} + +#toolbox_section { + height: 480px; + width: 80%; + position: relative; +} + +#toolbox_blocks { + height: 100%; + width: 100%; +} + +#preview_blocks { + height: 300px; + width: 100%; +} + +#createDiv { + width: 70%; +} + +#previewDiv { + width: 30%; +} + +#category_section { + width: 20%; +} + +#disable_div { + background-color: white; + height: 100%; + left: 0; + opacity: .5; + position: absolute; + top: 0; + width: 100%; + z-index: -1; /* Start behind workspace */ +} + +/* Rules for Closure popup color picker */ +.goog-palette { + outline: none; + cursor: default; +} + +.goog-palette-cell { + height: 13px; + width: 15px; + margin: 0; + border: 0; + text-align: center; + vertical-align: middle; + border-right: 1px solid #000000; + font-size: 1px; +} + +.goog-palette-colorswatch { + border: 1px solid #000000; + height: 13px; + position: relative; + width: 15px; +} + +.goog-palette-cell-hover .goog-palette-colorswatch { + border: 1px solid #FFF; +} + +.goog-palette-cell-selected .goog-palette-colorswatch { + border: 1px solid #000; + color: #fff; +} + +.goog-palette-table { + border: 1px solid #000; + border-collapse: collapse; +} + +.goog-popupcolorpicker { + position: absolute; +} + +/* The container
- needed to position the dropdown content */ +.dropdown { + position: relative; + display: inline-block; +} + +/* Dropdown Content (Hidden by Default) */ +.dropdown-content { + background-color: #f9f9f9; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,.2); + display: none; + min-width: 170px; + opacity: 1; + position: absolute; + z-index: 1; +} + +/* Links inside the dropdown */ +.dropdown-content a { + color: black; + display: block; + padding: 12px 16px; + text-decoration: none; +} + +/* Change color of dropdown links on hover */ +.dropdown-content a:hover { + background-color: #f1f1f1; +} + +/* Show the dropdown menu */ +.show { + display: block; +} + +.shadowBlock>.blocklyPath { + fill-opacity: .5; + stroke-opacity: .5; +} + +.shadowBlock>.blocklyPathLight, +.shadowBlock>.blocklyPathDark { + display: none; +} + +>>>>>>> Starting to integrate workspacefactory diff --git a/demos/blocklyfactory/index.html b/demos/blocklyfactory/index.html index b24e899e8..ec740f22e 100644 --- a/demos/blocklyfactory/index.html +++ b/demos/blocklyfactory/index.html @@ -6,9 +6,17 @@ Blockly Demo: Blockly Factory + + + + + + + + + + - - @@ -106,11 +114,77 @@
+
+

Preview Workspace:

+
+
+
+

+ + + + + +

+ +
+

Workspace Editor:

+

Drag blocks into your toolbox.

+
+
+
+
+ +
+ +
@@ -141,7 +215,7 @@ - +

Preview: @@ -225,7 +299,7 @@

- + @@ -276,5 +350,310 @@ 330 + + + \ No newline at end of file diff --git a/demos/blocklyfactory/workspacefactory/wfactory_controller.js b/demos/blocklyfactory/workspacefactory/wfactory_controller.js index 5df156121..88ea321ee 100644 --- a/demos/blocklyfactory/workspacefactory/wfactory_controller.js +++ b/demos/blocklyfactory/workspacefactory/wfactory_controller.js @@ -35,18 +35,41 @@ */ /** - * Class for a FactoryController + * Class for a FactoryController. * @constructor - * @param {!Blockly.workspace} toolboxWorkspace workspace where blocks are - * dragged into corresponding categories - * @param {!Blockly.workspace} previewWorkspace workspace that shows preview - * of what workspace would look like using generated XML + * + * @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. */ -FactoryController = function(toolboxWorkspace, previewWorkspace) { +FactoryController = function(toolboxName, toolboxDiv, previewDiv) { + var toolbox = document.getElementById(toolboxName); + // Workspace for user to drag blocks in for a certain category. - this.toolboxWorkspace = toolboxWorkspace; + this.toolboxWorkspace = Blockly.inject(toolboxDiv, + {grid: + {spacing: 25, + length: 3, + colour: '#ccc', + snap: true}, + media: '../../media/', + toolbox: toolbox, + }); + // Workspace for user to preview their changes. - this.previewWorkspace = previewWorkspace; + 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 FactoryModel(); // Updates the category tabs. @@ -275,11 +298,13 @@ FactoryController.prototype.clearAndLoadElement = function(id) { // Selects the next tab. this.view.setCategoryTabSelection(id, true); - - // Order blocks as if shown in the flyout. - this.toolboxWorkspace.cleanUp_(); } + // Mark all shadow blocks laoded and order blocks as if shown in a flyout. + this.view.markShadowBlocks(this.model.getShadowBlocksInWorkspace + (this.toolboxWorkspace.getAllBlocks())); + this.toolboxWorkspace.cleanUp_(); + // Update category editing buttons. this.view.updateState(this.model.getIndexByElementId (this.model.getSelectedId()), this.model.getSelected()); @@ -380,11 +405,9 @@ FactoryController.prototype.updatePreview = function() { } else { this.previewWorkspace.flyout_.show(tree.childNodes); } - } else { // Uses categories, creates a toolbox. - - if (!previewWorkspace.toolbox_) { + if (!this.previewWorkspace.toolbox_) { this.reinjectPreview(tree); // Create a toolbox, more expensive. } else { this.previewWorkspace.toolbox_.populate_(tree); @@ -397,7 +420,7 @@ FactoryController.prototype.updatePreview = function() { this.previewWorkspace.clear(); Blockly.Xml.domToWorkspace(this.generator.generateWorkspaceXml(), this.previewWorkspace); - } else if (this.selectedMode == FactoryController.MODE_PRELOAD){ + } else if (this.selectedMode == FactoryController.MODE_PRELOAD) { // If currently editing the pre-loaded workspace. this.previewWorkspace.clear(); Blockly.Xml.domToWorkspace(this.generator.generateWorkspaceXml(), @@ -720,8 +743,6 @@ FactoryController.prototype.importToolboxFromTree_ = function(tree) { } // Evenly space the blocks. - // TODO(evd2014): Change to cleanUp once cleanUp_ is made public in - // master. this.toolboxWorkspace.cleanUp_(); // Convert actual shadow blocks to user-generated shadow blocks. diff --git a/demos/blocklyfactory/workspacefactory/wfactory_init.js b/demos/blocklyfactory/workspacefactory/wfactory_init.js new file mode 100644 index 000000000..1b1a59e65 --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/wfactory_init.js @@ -0,0 +1,364 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /** + * @fileoverview Contains the 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 + */ +FactoryInit = {}; + +/** + * Initialization for workspace factory tab. + * + * @param {!FactoryController} controller The controller for the workspace + * factory tab. + */ +FactoryInit.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; + document.getElementById('button_editShadow').disabled = true; + + this.initColorPicker_(controller); + this.addWorkspaceFactoryEventListeners_(controller); + this.assignWorkspaceFactoryClickHandlers_(controller); +}; + +/** + * Initialize the color picker in workspace factory. + * @private + * + * @param {!FactoryController} controller The controller for the workspace + * factory tab. + */ +FactoryInit.initColorPicker_ = function(controller) { + // Array of Blockly category colors, variety of hues with saturation 45% + // and value 65% as specified in Blockly Developer documentation: + // https://developers.google.com/blockly/guides/create-custom-blocks/define-blocks + var colors = ['#A65C5C', + '#A6635C', + '#A66A5C', + '#A6725C', + '#A6795C', + '#A6815C', + '#A6885C', + '#A6905C', + '#A6975C', + '#A69F5C', + '#A6A65C', + '#9FA65C', + '#97A65C', + '#90A65C', + '#88A65C', + '#81A65C', + '#79A65C', + '#6FA65C', + '#66A65C', + '#5EA65C', + '#5CA661', + '#5CA668', + '#5CA66F', + '#5CA677', + '#5CA67E', + '#5CA686', + '#5CA68D', + '#5CA695', + '#5CA69C', + '#5CA6A4', + '#5CA1A6', + '#5C9AA6', + '#5C92A6', + '#5C8BA6', + '#5C83A6', + '#5C7CA6', + '#5C74A6', + '#5C6AA6', + '#5C61A6', + '#5E5CA6', + '#665CA6', + '#6D5CA6', + '#745CA6', + '#7C5CA6', + '#835CA6', + '#8B5CA6', + '#925CA6', + '#9A5CA6', + '#A15CA6', + '#A65CA4', + '#A65C9C', + '#A65C95', + '#A65C8D', + '#A65C86', + '#A65C7E', + '#A65C77', + '#A65C6F', + '#A65C66', + '#A65C61', + '#A65C5E']; + + // Create color picker with specific set of Blockly colors. + var colorPicker = new goog.ui.ColorPicker(); + colorPicker.setColors(colors); + + // Create and render the popup color picker and attach to button. + var popupPicker = new goog.ui.PopupColorPicker(null, colorPicker); + popupPicker.render(); + popupPicker.attach(document.getElementById('dropdown_color')); + popupPicker.setFocusable(true); + goog.events.listen(popupPicker, 'change', function(e) { + controller.changeSelectedCategoryColor(popupPicker.getSelectedColor()); + document.getElementById('dropdownDiv_editCategory').classList.remove + ("show"); + }); +}; + +/** + * Assign click handlers for workspace factory. + * @private + * + * @param {!FactoryController} controller The controller for the workspace + * factory tab. + */ +FactoryInit.assignWorkspaceFactoryClickHandlers_ = function(controller) { + document.getElementById('button_add').addEventListener + ('click', + function() { + document.getElementById('dropdownDiv_add').classList.toggle("show"); + }); + + document.getElementById('dropdown_newCategory').addEventListener + ('click', + function() { + controller.addCategory(); + document.getElementById('dropdownDiv_add').classList.remove("show"); + }); + + document.getElementById('dropdown_loadCategory').addEventListener + ('click', + function() { + controller.loadCategory(); + document.getElementById('dropdownDiv_add').classList.remove("show"); + }); + + document.getElementById('dropdown_separator').addEventListener + ('click', + function() { + controller.addSeparator(); + document.getElementById('dropdownDiv_add').classList.remove("show"); + }); + + document.getElementById('button_remove').addEventListener + ('click', + function() { + controller.removeElement(); + }); + + document.getElementById('button_export').addEventListener + ('click', + function() { + controller.exportConfig(); + }); + + document.getElementById('button_print').addEventListener + ('click', + function() { + controller.printConfig(); + }); + + document.getElementById('button_up').addEventListener + ('click', + function() { + controller.moveElement(-1); + }); + + document.getElementById('button_down').addEventListener + ('click', + function() { + controller.moveElement(1); + }); + + document.getElementById('button_editCategory').addEventListener + ('click', + function() { + document.getElementById('dropdownDiv_editCategory').classList. + toggle("show"); + }); + + document.getElementById('button_editShadow').addEventListener + ('click', + function() { + if (Blockly.selected) { + // Can only edit blocks when a block is selected. + + if (!controller.isUserGenShadowBlock(Blockly.selected.id) && + Blockly.selected.getSurroundParent() != null) { + // If a block is selected that could be a valid shadow block (not a + // shadow block, has a surrounding parent), let the user make it a + // shadow block. Use toggle instead of add so that the user can + // click the button again to make the dropdown disappear without + // clicking one of the options. + document.getElementById('dropdownDiv_editShadowRemove').classList. + remove("show"); + document.getElementById('dropdownDiv_editShadowAdd').classList. + toggle("show"); + } else { + // If the block is a shadow block, let the user make it a normal + // block. + document.getElementById('dropdownDiv_editShadowAdd').classList. + remove("show"); + document.getElementById('dropdownDiv_editShadowRemove').classList. + toggle("show"); + } + } + }); + + document.getElementById('dropdown_name').addEventListener + ('click', + function() { + controller.changeCategoryName(); + document.getElementById('dropdownDiv_editCategory').classList. + remove("show"); + }); + + document.getElementById('input_import').addEventListener + ('change', + function(event) { + controller.importFile(event.target.files[0]); + }); + + document.getElementById('button_clear').addEventListener + ('click', + function() { + controller.clear(); + }); + + document.getElementById('dropdown_addShadow').addEventListener + ('click', + function() { + controller.addShadow(); + document.getElementById('dropdownDiv_editShadowAdd').classList. + remove("show"); + }); + + document.getElementById('dropdown_removeShadow').addEventListener + ('click', + function() { + controller.removeShadow(); + document.getElementById('dropdownDiv_editShadowRemove').classList. + remove("show"); + // If turning invalid shadow block back to normal block, remove + // warning and disable block editing privileges. + Blockly.selected.setWarningText(null); + if (!Blockly.selected.getSurroundParent()) { + document.getElementById('button_editShadow').disabled = true; + } + }); +}; + +/** + * Add event listeners for workspace factory. + * @private + * + * @param {!FactoryController} controller The controller for the workspace + * factory tab. + */ +FactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) { + // Use up and down arrow keys to move categories. + // TODO(evd2014): When merge with next CL for editing preloaded blocks, make + // sure mode is toolbox. + window.addEventListener('keydown', function(e) { + if (this.selectedTab != 'WORKSPACE_FACTORY' && e.keyCode == 38) { + // Arrow up. + controller.moveElement(-1); + } else if (this.selectedTab != 'WORKSPACE_FACTORY' && e.keyCode == 40) { + // Arrow down. + controller.moveElement(1); + } + }); + + // 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.MOVE || e.type == Blockly.Events.DELETE) { + 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.MOVE || (e.type == Blockly.Events.UI && + e.element == 'selected')) { + var selected = Blockly.selected; + + if (selected != null && selected.getSurroundParent() != null) { + + // A valid shadow block is selected. Enable block editing and remove + // warnings. + document.getElementById('button_editShadow').disabled = false; + Blockly.selected.setWarningText(null); + } else { + if (selected != null && + controller.isUserGenShadowBlock(selected.id)) { + + // Provide warning if shadow block is moved and is no longer a valid + // shadow block. + Blockly.selected.setWarningText('Shadow blocks must be nested' + + ' inside other blocks to be displayed.'); + + // Give editing options so that the user can make an invalid shadow + // block a normal block. + document.getElementById('button_editShadow').disabled = false; + } else { + + // No block selected that is a shadow block or could be a valid + // shadow block. + // Disable block editing. + document.getElementById('button_editShadow').disabled = true; + document.getElementById('dropdownDiv_editShadowRemove').classList. + remove("show"); + document.getElementById('dropdownDiv_editShadowAdd').classList. + remove("show"); + } + } + } + + // Convert actual shadow blocks added from the toolbox to user-generated + // shadow blocks. + if (e.type == Blockly.Events.CREATE) { + controller.convertShadowBlocks(); + } + }); +};