diff --git a/demos/blocklyfactory/app_controller.js b/demos/blocklyfactory/app_controller.js index 2a6301cbf..a1d884706 100644 --- a/demos/blocklyfactory/app_controller.js +++ b/demos/blocklyfactory/app_controller.js @@ -230,23 +230,6 @@ AppController.prototype.getBlockTypeFromXml_ = function(xmlText) { } }; -/** - * Updates the Block Factory tab to show selected block when user selects a - * different block in the block library dropdown. Tied to block library dropdown - * in index.html. - * - * @param {!Element} blockLibraryDropdown - HTML select element from which the - * user selects a block to work on. - */ -AppController.prototype.onSelectedBlockChanged - = function(blockLibraryDropdown) { - // Get selected block type. - var blockType = this.blockLibraryController.getSelectedBlockType( - blockLibraryDropdown); - // Update Block Factory page by showing the selected block. - this.blockLibraryController.openBlock(blockType); -}; - /** * Add click handlers to each tab to allow switching between the Block Factory, * Workspace Factory, and Block Exporter tab. @@ -303,14 +286,16 @@ AppController.prototype.onTab = function() { // Warn user if they have unsaved changes when leaving Block Factory. if (this.lastSelectedTab == AppController.BLOCK_FACTORY && this.selectedTab != AppController.BLOCK_FACTORY) { - if (!BlockFactory.isStarterBlock() && !this.savedBlockChanges()) { - if (!confirm('You have unsaved changes in Block Factory.')) { - // If the user doesn't want to switch tabs with unsaved changes, - // stay on Block Factory Tab. - this.setSelected_(AppController.BLOCK_FACTORY); - this.lastSelectedTab == AppController.BLOCK_FACTORY; - return; - } + + var hasUnsavedChanges = + !FactoryUtils.savedBlockChanges(this.blockLibraryController); + if (hasUnsavedChanges && + !confirm('You have unsaved changes in Block Factory.')) { + // If the user doesn't want to switch tabs with unsaved changes, + // stay on Block Factory Tab. + this.setSelected_(AppController.BLOCK_FACTORY); + this.lastSelectedTab = AppController.BLOCK_FACTORY; + return; } } @@ -398,16 +383,16 @@ AppController.prototype.assignExporterClickHandlers = function() { document.getElementById('dropdownDiv_setBlocks').classList.remove("show"); }); - document.getElementById('dropdown_clearSelected').addEventListener('click', + document.getElementById('dropdown_addAllFromLib').addEventListener('click', function() { - self.exporter.clearSelectedBlocks(); + self.exporter.selectAllBlocks(); self.exporter.updatePreview(); document.getElementById('dropdownDiv_setBlocks').classList.remove("show"); }); - document.getElementById('dropdown_addAllFromLib').addEventListener('click', + document.getElementById('clearSelectedButton').addEventListener('click', function() { - self.exporter.selectAllBlocks(); + self.exporter.clearSelectedBlocks(); self.exporter.updatePreview(); document.getElementById('dropdownDiv_setBlocks').classList.remove("show"); }); @@ -479,50 +464,35 @@ AppController.prototype.ifCheckedDisplay = function(checkbox, elementArray) { } }; -/** - * Returns whether or not a block's changes has been saved to the Block Library. - * - * @return {boolean} True if all changes made to the block have been saved to - * the Block Library. - */ -AppController.prototype.savedBlockChanges = function() { - var blockType = this.blockLibraryController.getCurrentBlockType(); - var currentXml = Blockly.Xml.workspaceToDom(BlockFactory.mainWorkspace); - - if (this.blockLibraryController.has(blockType)) { - // Block is saved in block library. - var savedXml = this.blockLibraryController.getBlockXml(blockType); - return FactoryUtils.sameBlockXml(savedXml, currentXml); - } - return false; -}; - /** * Assign button click handlers for the block library. */ AppController.prototype.assignLibraryClickHandlers = function() { var self = this; - // Assign button click handlers for Block Library. + + // Button for saving block to library. document.getElementById('saveToBlockLibraryButton').addEventListener('click', function() { self.blockLibraryController.saveToBlockLibrary(); }); + // Button for removing selected block from library. document.getElementById('removeBlockFromLibraryButton').addEventListener( 'click', function() { self.blockLibraryController.removeFromBlockLibrary(); }); + // Button for clearing the block library. document.getElementById('clearBlockLibraryButton').addEventListener('click', function() { self.blockLibraryController.clearBlockLibrary(); }); - var dropdown = document.getElementById('blockLibraryDropdown'); - dropdown.addEventListener('change', + // Hide and show the block library dropdown. + document.getElementById('button_blockLib').addEventListener('click', function() { - self.onSelectedBlockChanged(dropdown); + document.getElementById('dropdownDiv_blockLib').classList.toggle("show"); }); }; @@ -559,26 +529,40 @@ AppController.prototype.assignBlockFactoryClickHandlers = function() { document.getElementById('createNewBlockButton') .addEventListener('click', function() { - // If there are unsaved changes to the block in open in Block Factory, - // warn user that proceeding to create a new block will cause them to lose - // their changes if they don't save. - if (!self.savedBlockChanges()) { - if(!confirm('You have unsaved changes. By proceeding without saving ' + - ' your block first, you will lose these changes.')) { - return; - } + // If there are unsaved changes warn user, check if they'd like to + // proceed with unsaved changes, and act accordingly. + var proceedWithUnsavedChanges = + self.blockLibraryController.warnIfUnsavedChanges(); + if (!proceedWithUnsavedChanges) { + return; } - BlockFactory.showStarterBlock(); - BlockLibraryView.selectDefaultOption('blockLibraryDropdown'); - }); + + BlockFactory.showStarterBlock(); + self.blockLibraryController.setNoneSelected(); + + // Close the Block Library Dropdown. + goog.dom.getElement('dropdownDiv_blockLib').classList.remove("show"); + }); }; /** * Add event listeners for the block factory. */ AppController.prototype.addBlockFactoryEventListeners = function() { + // Update code on changes to block being edited. BlockFactory.mainWorkspace.addChangeListener(BlockFactory.updateLanguage); + + // Disable blocks not attached to the factory_base block. BlockFactory.mainWorkspace.addChangeListener(Blockly.Events.disableOrphans); + + // Update the buttons on the screen based on whether + // changes have been saved. + var self = this; + BlockFactory.mainWorkspace.addChangeListener(function() { + self.blockLibraryController.updateButtons(FactoryUtils.savedBlockChanges( + self.blockLibraryController)); + }); + document.getElementById('direction') .addEventListener('change', BlockFactory.updatePreview); document.getElementById('languageTA') @@ -636,6 +620,22 @@ AppController.prototype.onresize = function(event) { } }; +/** + * Handler for the window's 'onbeforeunload' event. When a user has unsaved + * changes and refreshes or leaves the page, confirm that they want to do so + * before actually refreshing. + */ +AppController.prototype.confirmLeavePage = function() { + if ((!BlockFactory.isStarterBlock() && + !FactoryUtils.savedBlockChanges(this.blockLibraryController)) || + this.workspaceFactoryController.hasUnsavedChanges()) { + // When a string is assigned to the returnValue Event property, a dialog box + // appears, asking the users for confirmation to leave the page. + return 'You will lose any unsaved changes. Are you sure you want ' + + 'to exit this page?'; + } +}; + /** * Initialize Blockly and layout. Called on page load. */ @@ -651,7 +651,7 @@ AppController.prototype.init = function() { this.assignBlockFactoryClickHandlers(); this.onresize(); - self = this; + var self = this; window.addEventListener('resize', function() { self.onresize(); }); diff --git a/demos/blocklyfactory/block_library_controller.js b/demos/blocklyfactory/block_library_controller.js index 201a9e705..1c0345e45 100644 --- a/demos/blocklyfactory/block_library_controller.js +++ b/demos/blocklyfactory/block_library_controller.js @@ -51,6 +51,9 @@ BlockLibraryController = function(blockLibraryName, opt_blockLibraryStorage) { this.name = blockLibraryName; // Create a new, empty Block Library Storage object, or load existing one. this.storage = opt_blockLibraryStorage || new BlockLibraryStorage(this.name); + // The BlockLibraryView object handles the proper updating and formatting of + // the block library dropdown. + this.view = new BlockLibraryView(); }; /** @@ -67,7 +70,8 @@ BlockLibraryController.prototype.getCurrentBlockType = function() { }; /** - * Removes current block from Block Library + * Removes current block from Block Library and updates the save and delete + * buttons so that user may save block to library and but not delete. * * @param {string} blockType - Type of block. */ @@ -76,8 +80,7 @@ BlockLibraryController.prototype.removeFromBlockLibrary = function() { this.storage.removeBlock(blockType); this.storage.saveToLocalStorage(); this.populateBlockLibrary(); - // Show default block. - BlockFactory.showStarterBlock(); + this.view.updateButtons(blockType, false, false); }; /** @@ -86,25 +89,24 @@ BlockLibraryController.prototype.removeFromBlockLibrary = function() { * @param {string} blockType - Block to edit on block factory. */ BlockLibraryController.prototype.openBlock = function(blockType) { - if (blockType =='BLOCK_LIBRARY_DEFAULT_BLANK') { - BlockFactory.showStarterBlock(); - } else { + if (blockType) { var xml = this.storage.getBlockXml(blockType); BlockFactory.mainWorkspace.clear(); Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace); BlockFactory.mainWorkspace.clearUndo(); + } else { + BlockFactory.showStarterBlock(); + this.view.setSelectedBlockType(null); } }; /** * Returns type of block selected from library. * - * @param {Element} blockLibraryDropdown - The block library dropdown. * @return {string} Type of block selected. */ -BlockLibraryController.prototype.getSelectedBlockType = - function(blockLibraryDropdown) { - return BlockLibraryView.getSelected(blockLibraryDropdown); +BlockLibraryController.prototype.getSelectedBlockType = function() { + return this.view.getSelectedBlockType(); }; /** @@ -118,12 +120,12 @@ BlockLibraryController.prototype.clearBlockLibrary = function() { this.storage.clear(); this.storage.saveToLocalStorage(); // Update dropdown. - BlockLibraryView.clearOptions('blockLibraryDropdown'); - // Add a default, blank option to dropdown for when no block from library is - // selected. - BlockLibraryView.addDefaultOption('blockLibraryDropdown'); + this.view.clearOptions(); // Show default block. BlockFactory.showStarterBlock(); + // User may not save the starter block, but will get explicit instructions + // upon clicking the red save button. + this.view.updateButtons(null); } }; @@ -132,15 +134,13 @@ BlockLibraryController.prototype.clearBlockLibrary = function() { */ BlockLibraryController.prototype.saveToBlockLibrary = function() { var blockType = this.getCurrentBlockType(); - // If block under that name already exists, confirm that user wants to replace - // saved block. - if (this.has(blockType)) { - var replace = confirm('You already have a block called "' + blockType + - '" in your library. Replace this block?'); - if (!replace) { - // Do not save if user doesn't want to replace the saved block. - return; - } + // If user has not changed the name of the starter block. + if (blockType == 'block_type') { + // Do not save block if it has the default type, 'block_type'. + alert('You cannot save a block under the name "block_type". Try changing ' + + 'the name before saving. Then, click on the "Block Library" button ' + + 'to view your saved blocks.'); + return; } // Create block xml. @@ -148,6 +148,11 @@ BlockLibraryController.prototype.saveToBlockLibrary = function() { var block = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); xmlElement.appendChild(Blockly.Xml.blockToDomWithXY(block)); + // Do not add option again if block type is already in library. + if (!this.has(blockType)) { + this.view.addOption(blockType, true, true); + } + // Save block. this.storage.addBlock(blockType, xmlElement); this.storage.saveToLocalStorage(); @@ -156,12 +161,8 @@ BlockLibraryController.prototype.saveToBlockLibrary = function() { // main workspace. this.openBlock(blockType); - // Do not add another option to dropdown if replacing. - if (replace) { - return; - } - BlockLibraryView.addOption( - blockType, blockType, 'blockLibraryDropdown', true, true); + // Add select handler to the new option. + this.addOptionSelectHandler(blockType); }; /** @@ -179,19 +180,13 @@ BlockLibraryController.prototype.has = function(blockType) { * Populates the dropdown menu. */ BlockLibraryController.prototype.populateBlockLibrary = function() { - BlockLibraryView.clearOptions('blockLibraryDropdown'); - // Add a default, blank option to dropdown for when no block from library is - // selected. - BlockLibraryView.addDefaultOption('blockLibraryDropdown'); - // Add option for each saved block. + this.view.clearOptions(); + // Add an unselected option for each saved block. var blockLibrary = this.storage.blocks; - for (var block in blockLibrary) { - // Make sure the block wasn't deleted. - if (blockLibrary[block] != null) { - BlockLibraryView.addOption( - block, block, 'blockLibraryDropdown', false, true); - } + for (var blockType in blockLibrary) { + this.view.addOption(blockType, false); } + this.addOptionSelectHandlers(); }; /** @@ -251,3 +246,92 @@ BlockLibraryController.prototype.hasEmptyBlockLibrary = function() { BlockLibraryController.prototype.getStoredBlockTypes = function() { return this.storage.getBlockTypes(); }; + +/** + * Sets the currently selected block option to none. + */ +BlockLibraryController.prototype.setNoneSelected = function() { + this.view.setSelectedBlockType(null); +}; + +/** + * If there are unsaved changes to the block in open in Block Factory + * and the block is not the starter block, check if user wants to proceed, + * knowing that it will cause them to lose their changes. + * + * @return {boolean} Whether or not to proceed. + */ +BlockLibraryController.prototype.warnIfUnsavedChanges = function() { + if (!FactoryUtils.savedBlockChanges(this)) { + return confirm('You have unsaved changes. By proceeding without saving ' + + ' your block first, you will lose these changes.'); + } + return true; +}; + +/** + * Add select handler for an option of a given block type. The handler will to + * update the view and the selected block accordingly. + * + * @param {!string} blockType - The type of block represented by the option is + * for. + */ +BlockLibraryController.prototype.addOptionSelectHandler = function(blockType) { + var self = this; + + // Click handler for a block option. Sets the block option as the selected + // option and opens the block for edit in Block Factory. + var setSelectedAndOpen_ = function(blockOption) { + var blockType = blockOption.textContent; + self.view.setSelectedBlockType(blockType); + self.openBlock(blockType); + // The block is saved in the block library and all changes have been saved + // when the user opens a block from the block library dropdown. + // Thus, the buttons show up as a disabled update button and an enabled + // delete. + self.view.updateButtons(blockType, true, true); + self.view.hide(); + }; + + // Returns a block option select handler. + var makeOptionSelectHandler_ = function(blockOption) { + return function() { + // If there are unsaved changes warn user, check if they'd like to + // proceed with unsaved changes, and act accordingly. + var proceedWithUnsavedChanges = self.warnIfUnsavedChanges(); + if (!proceedWithUnsavedChanges) { + return; + } + setSelectedAndOpen_(blockOption); + }; + }; + + // Assign a click handler to the block option. + var blockOption = this.view.optionMap[blockType]; + // Use an additional closure to correctly assign the tab callback. + blockOption.addEventListener( + 'click', makeOptionSelectHandler_(blockOption)); +}; + +/** + * Add select handlers to each option to update the view and the selected + * blocks accordingly. + */ +BlockLibraryController.prototype.addOptionSelectHandlers = function() { + // Assign a click handler to each block option. + for (var blockType in this.view.optionMap) { + this.addOptionSelectHandler(blockType); + } +}; + +/** + * Update the save and delete buttons based on the current block type of the + * block the user is currently editing. + * + * @param {boolean} Whether changes to the block have been saved. + */ +BlockLibraryController.prototype.updateButtons = function(savedChanges) { + var blockType = this.getCurrentBlockType(); + var isInLibrary = this.has(blockType); + this.view.updateButtons(blockType, isInLibrary, savedChanges); +}; diff --git a/demos/blocklyfactory/block_library_view.js b/demos/blocklyfactory/block_library_view.js index cc5800646..00c676dfa 100644 --- a/demos/blocklyfactory/block_library_view.js +++ b/demos/blocklyfactory/block_library_view.js @@ -19,8 +19,8 @@ */ /** - * @fileoverview Javascript for Block Library's UI for pulling blocks from the - * Block Library's storage to edit in Block Factory. + * @fileoverview Javascript for BlockLibraryView class. It manages the display + * of the Block Library dropdown, save, and delete buttons. * * @author quachtina96 (Tina Quach) */ @@ -29,89 +29,198 @@ goog.provide('BlockLibraryView'); +goog.require('goog.dom'); +goog.require('goog.dom.classlist'); + +/** + * BlockLibraryView Class + * @constructor + */ +var BlockLibraryView = function() { + // Div element to contain the block types to choose from. + // Id of the div that holds the block library view. + this.blockLibraryViewDivID = 'dropdownDiv_blockLib'; + this.dropdown = goog.dom.getElement('dropdownDiv_blockLib'); + // Map of block type to corresponding 'a' element that is the option in the + // dropdown. Used to quickly and easily get a specific option. + this.optionMap = Object.create(null); + // Save and delete buttons. + this.saveButton = goog.dom.getElement('saveToBlockLibraryButton'); + this.deleteButton = goog.dom.getElement('removeBlockFromLibraryButton'); + // Initially, user should not be able to delete a block. They must save a + // block or select a stored block first. + this.deleteButton.disabled = true; +}; + +/** + * Open the Block Library dropdown. + */ +BlockLibraryView.prototype.show = function() { + this.dropdown.classList.add("show"); +}; + +/** + * Close the Block Library dropdown. + */ +BlockLibraryView.prototype.hide = function() { + this.dropdown.classList.remove("show"); +}; + /** * Creates a node of a given element type and appends to the node with given id. * - * @param {string} optionIdentifier - String used to identify option. - * @param {string} optionText - Text to display in the dropdown for the option. - * @param {string} dropdownID - ID for HTML select element. + * @param {!string} blockType - Type of block. * @param {boolean} selected - Whether or not the option should be selected on * the dropdown. - * @param {boolean} enabled - Whether or not the option should be enabled. */ -BlockLibraryView.addOption - = function(optionIdentifier, optionText, dropdownID, selected, enabled) { - var dropdown = document.getElementById(dropdownID); - var option = document.createElement('option'); - // The value attribute of a dropdown's option is not visible in the UI, but is - // useful for identifying different options that may have the same text. - option.value = optionIdentifier; - // The text attribute is what the user sees in the dropdown for the option. - option.text = optionText; - option.selected = selected; - option.disabled = !enabled; - dropdown.add(option); +BlockLibraryView.prototype.addOption = function(blockType, selected) { + // Create option. + var option = goog.dom.createDom('a', { + 'id': 'dropdown_' + blockType, + 'class': 'blockLibOpt' + }, blockType); + + // Add option to dropdown. + this.dropdown.appendChild(option); + this.optionMap[blockType] = option; + + // Select the block. + if (selected) { + this.setSelectedBlockType(blockType); + } }; /** - * Adds a default, blank option to dropdown for when no block from library is - * selected. + * Sets a given block type to selected and all other blocks to deselected. + * If null, deselects all blocks. * - * @param {string} dropdownID - ID of HTML select element + * @param {string} blockTypeToSelect - Type of block to select or null. */ -BlockLibraryView.addDefaultOption = function(dropdownID) { - BlockLibraryView.addOption( - 'BLOCK_LIBRARY_DEFAULT_BLANK', '', dropdownID, true, true); +BlockLibraryView.prototype.setSelectedBlockType = function(blockTypeToSelect) { + // Select given block type and deselect all others. Will deselect all blocks + // if null or invalid block type selected. + for (var blockType in this.optionMap) { + var option = this.optionMap[blockType]; + if (blockType == blockTypeToSelect) { + this.selectOption_(option); + } else { + this.deselectOption_(option); + } + } }; /** - * Selects the default, blank option in dropdown identified by given ID. + * Selects a given option. + * @private * - * @param {string} dropdownID - ID of HTML select element + * @param {!Element} option - HTML 'a' element in the dropdown that represents + * a particular block type. */ -BlockLibraryView.selectDefaultOption = function(dropdownID) { - var dropdown = document.getElementById(dropdownID); - // Deselect currently selected option. - var index = dropdown.selectedIndex; - dropdown.options[index].selected = false; - // Select default option, always the first in the dropdown. - var defaultOption = dropdown.options[0]; - defaultOption.selected = true; +BlockLibraryView.prototype.selectOption_ = function(option) { + goog.dom.classlist.add(option, 'dropdown-content-selected'); +}; + +/** + * Deselects a given option. + * @private + * + * @param {!Element} option - HTML 'a' element in the dropdown that represents + * a particular block type. + */ +BlockLibraryView.prototype.deselectOption_ = function(option) { + goog.dom.classlist.remove(option, 'dropdown-content-selected'); +}; + +/** + * Updates the save and delete buttons to represent how the current block will + * be saved by including the block type in the button text as well as indicating + * whether the block is being saved or updated. + * + * @param {!string} blockType - The type of block being edited. + * @param {boolean} isInLibrary - Whether the block type is in the library. + * @param {boolean} savedChanges - Whether changes to block have been saved. + */ +BlockLibraryView.prototype.updateButtons = + function(blockType, isInLibrary, savedChanges) { + if (blockType) { + // User is editing a block. + + if (!isInLibrary) { + // Block type has not been saved to library yet. Disable the delete button + // and allow user to save. + this.saveButton.textContent = 'Save "' + blockType + '"'; + this.saveButton.disabled = false; + this.deleteButton.disabled = true; + } else { + // Block type has already been saved. Disable the save button unless the + // there are unsaved changes (checked below). + this.saveButton.textContent = 'Update "' + blockType + '"'; + this.saveButton.disabled = true; + this.deleteButton.disabled = false; + } + this.deleteButton.textContent = 'Delete "' + blockType + '"'; + + // If changes to block have been made and are not saved, make button + // green to encourage user to save the block. + if (!savedChanges) { + var buttonFormatClass = 'button_warn'; + + // If block type is the default, 'block_type', make button red to alert + // user. + if (blockType == 'block_type') { + buttonFormatClass = 'button_alert'; + } + goog.dom.classlist.add(this.saveButton, buttonFormatClass); + this.saveButton.disabled = false; + + } else { + // No changes to save. + var classesToRemove = ['button_alert', 'button_warn']; + goog.dom.classlist.removeAll(this.saveButton, classesToRemove); + this.saveButton.disabled = true; + } + + } +}; + +/** + * Removes option currently selected in dropdown from dropdown menu. + */ +BlockLibraryView.prototype.removeSelectedOption = function() { + var selectedOption = this.getSelectedOption(); + this.dropdown.removeNode(selectedOption); }; /** * Returns block type of selected block. * - * @param {Element} dropdown - HTML select element. * @return {string} Type of block selected. */ -BlockLibraryView.getSelected = function(dropdown) { - var index = dropdown.selectedIndex; - return dropdown.options[index].value; +BlockLibraryView.prototype.getSelectedBlockType = function() { + var selectedOption = this.getSelectedOption(); + var blockType = selectedOption.textContent; + return blockType; }; /** - * Removes option currently selected in dropdown from dropdown menu. + * Returns selected option. * - * @param {string} dropdownID - ID of HTML select element within which to find - * the selected option. + * @return {!Element} HTML 'a' element that is the option for a block type. */ -BlockLibraryView.removeSelectedOption = function(dropdownID) { - var dropdown = document.getElementById(dropdownID); - if (dropdown) { - dropdown.remove(dropdown.selectedIndex); - } +BlockLibraryView.prototype.getSelectedOption = function() { + return goog.dom.getElementByClass('dropdown-content-selected', this.dropdown); }; /** * Removes all options from dropdown. - * - * @param {string} dropdownID - ID of HTML select element to clear options of. */ -BlockLibraryView.clearOptions = function(dropdownID) { - var dropdown = document.getElementById(dropdownID); - while (dropdown.length > 0) { - dropdown.remove(dropdown.length - 1); +BlockLibraryView.prototype.clearOptions = function() { + var blockOpts = goog.dom.getElementsByClass('blockLibOpt', this.dropdown); + if (blockOpts) { + for (var i = 0, option; option = blockOpts[i]; i++) { + goog.dom.removeNode(option); + } } }; + diff --git a/demos/blocklyfactory/factory.css b/demos/blocklyfactory/factory.css index 704b8de4d..f212b1d49 100644 --- a/demos/blocklyfactory/factory.css +++ b/demos/blocklyfactory/factory.css @@ -302,6 +302,28 @@ button, .buttonStyle { width: 90%; } +/* Block Library */ + +#dropdownDiv_blockLib { + max-height: 65%; + overflow-y: scroll; +} + +#button_blockLib { + border-color: darkgrey; + font-size: large; +} + +.button_alert { + background-color: #fcc; + border-color: #f99; +} + +.button_warn { + background-color: #aea; + border-color: #5d5; +} + /* Tabs */ .tab { @@ -531,7 +553,7 @@ td { /* Dropdown Content (Hidden by Default) */ .dropdown-content { - background-color: #f9f9f9; + background-color: #FFF; box-shadow: 0px 8px 16px 0px rgba(0,0,0,.2); display: none; min-width: 170px; @@ -549,9 +571,14 @@ td { text-decoration: none; } -/* Change color of dropdown links on hover. */ +/* Change color of dropdown links on hover. */ .dropdown-content a:hover, .dropdown-content label:hover { - background-color: #f1f1f1 + background-color: #EEE; +} + +/* Change color of dropdown links on selected. */ +.dropdown-content-selected { + background-color: #DDD; } /* Show the dropdown menu */ diff --git a/demos/blocklyfactory/factory.js b/demos/blocklyfactory/factory.js index 6ec2cfd00..837d04bc7 100644 --- a/demos/blocklyfactory/factory.js +++ b/demos/blocklyfactory/factory.js @@ -209,11 +209,19 @@ BlockFactory.updatePreview = function() { // standard library. var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); if (StandardCategories.coreBlockTypes.indexOf(blockType) != -1) { - rootBlock.setWarningText('A standard Blockly.Block already exists ' + + rootBlock.setWarningText('A core Blockly block already exists ' + 'under this name.'); + + } else if (blockType == 'block_type') { + // Warn user to let them know they can't save a block under the default + // name 'block_type' + rootBlock.setWarningText('You cannot save a block with the default ' + + 'name, "block_type"'); + } else { rootBlock.setWarningText(null); } + } finally { Blockly.Blocks = backupBlocks; } @@ -248,7 +256,10 @@ BlockFactory.isStarterBlock = function() { var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); // The starter block does not have blocks nested into the factory_base block. return !(rootBlock.getChildren().length > 0 || + // The starter block's name is the default, 'block_type'. rootBlock.getFieldValue('NAME').trim().toLowerCase() != 'block_type' || + // The starter block has no connections. rootBlock.getFieldValue('CONNECTIONS') != 'NONE' || + // The starter block has automatic inputs. rootBlock.getFieldValue('INLINE') != 'AUTO'); }; diff --git a/demos/blocklyfactory/factory_utils.js b/demos/blocklyfactory/factory_utils.js index 89e421663..08c003a95 100644 --- a/demos/blocklyfactory/factory_utils.js +++ b/demos/blocklyfactory/factory_utils.js @@ -33,8 +33,6 @@ */ goog.provide('FactoryUtils'); -goog.require('goog.dom.classes'); - /** * Get block definition code for the current block. * @@ -889,7 +887,7 @@ FactoryUtils.injectCode = function(code, id) { /** * Returns whether or not two blocks are the same based on their xml. Expects - * xml with a single child node that is a factory_base block. The xml found on + * xml with a single child node that is a factory_base block, the xml found on * Block Factory's main workspace. * * @param {!Element} blockXml1 - An xml element with a single child node that @@ -899,18 +897,31 @@ FactoryUtils.injectCode = function(code, id) { * @return {boolean} Whether or not two blocks are the same based on their xml. */ FactoryUtils.sameBlockXml = function(blockXml1, blockXml2) { - // Each block xml has only one child. - var blockXmlText1 = Blockly.Xml.domToText( - blockXml1.getElementsByTagName('block')[0]); - var blockXmlText2 = Blockly.Xml.domToText( - blockXml2.getElementsByTagName('block')[0]); + // Each xml element should contain a single child element with a 'block' tag + if (goog.string.caseInsensitiveCompare(blockXml1.tagName, 'xml') || + goog.string.caseInsensitiveCompare(blockXml2.tagName, 'xml')) { + throw new Error('Expected two xml elements, recieved elements with tag ' + + 'names: ' + blockXml1.tagName + ' and ' + blockXml2.tagName + '.'); + } - // Strip white space. - blockXmlText1 = blockXmlText1.replace(/\s+/g, ''); - blockXmlText2 = blockXmlText2.replace(/\s+/g, ''); + // Compare the block elements directly. The xml tags may include other meta + // information we want to igrore. + var blockElement1 = blockXml1.getElementsByTagName('block')[0]; + var blockElement2 = blockXml2.getElementsByTagName('block')[0]; - // Return whether or not changes have been saved. - return blockXmlText1 == blockXmlText2; + if (!(blockElement1 && blockElement2)) { + throw new Error('Could not get find block element in xml.'); + } + + var blockXmlText1 = Blockly.Xml.domToText(blockElement1); + var blockXmlText2 = Blockly.Xml.domToText(blockElement2); + + // Strip white space. + blockXmlText1 = blockXmlText1.replace(/\s+/g, ''); + blockXmlText2 = blockXmlText2.replace(/\s+/g, ''); + + // Return whether or not changes have been saved. + return blockXmlText1 == blockXmlText2; }; /* @@ -950,3 +961,29 @@ FactoryUtils.isProcedureBlock = function(block) { block.type == 'procedures_callreturn' || block.type == 'procedures_ifreturn'); }; + +/** + * Returns whether or not a modified block's changes has been saved to the + * Block Library. + * TODO(quachtina96): move into the Block Factory Controller once made. + * + * @param {!BlockLibraryController} blockLibraryController - Block Library + * Controller storing custom blocks. + * @return {boolean} True if all changes made to the block have been saved to + * the given Block Library. + */ +FactoryUtils.savedBlockChanges = function(blockLibraryController) { + if (BlockFactory.isStarterBlock()) { + return true; + } + var blockType = blockLibraryController.getCurrentBlockType(); + var currentXml = Blockly.Xml.workspaceToDom(BlockFactory.mainWorkspace); + + if (blockLibraryController.has(blockType)) { + // Block is saved in block library. + var savedXml = blockLibraryController.getBlockXml(blockType); + return FactoryUtils.sameBlockXml(savedXml, currentXml); + } + return false; +}; + diff --git a/demos/blocklyfactory/index.html b/demos/blocklyfactory/index.html index b002b4061..69d18a09b 100644 --- a/demos/blocklyfactory/index.html +++ b/demos/blocklyfactory/index.html @@ -41,7 +41,7 @@ window.addEventListener('load', init); -
+First, select blocks from your block library by dragging them into your workspace. Then, use the Export Settings form to download starter code for selected blocks. +
First, select blocks from your block library by clicking on them. Then, use the Export Settings form to download starter code for selected blocks.
Configure the options for your Blockly inject call.
- +