Blockly Factory: Prompt User to Add Variables/Functions Category (#589)

* Fixed marking shadow blocks so keeps warnings when switching between categories

* Done with variable and procedure block checks

* Used setShadowDom instead of shadowDom_, and nit changes in wfactory init

* Fixed bug of disable div covering whole screen
This commit is contained in:
Emma Dauterman 2016-08-26 11:35:53 -07:00 committed by picklesrus
parent 6e88d5c035
commit 4192ca6b52
6 changed files with 229 additions and 113 deletions

View file

@ -430,8 +430,9 @@ td {
padding-right: 20px;
}
.test {
border: 1px solid black;
.disabled {
background-color: white;
opacity: 0.5;
}
#toolbox_div {
@ -466,17 +467,6 @@ td {
margin-top: 2%;
}
#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;

View file

@ -913,3 +913,40 @@ FactoryUtils.sameBlockXml = function(blockXml1, blockXml2) {
return blockXmlText1 == blockXmlText2;
};
/*
* Checks if a block has a variable field. Blocks with variable fields cannot
* be shadow blocks.
*
* @param {Blockly.Block} block The block to check if a variable field exists.
* @return {boolean} True if the block has a variable field, false otherwise.
*/
FactoryUtils.hasVariableField = function(block) {
if (!block) {
return false;
}
for (var i = 0, input; input = block.inputList[i]; i++) {
for (var j = 0, field; field = input.fieldRow[j]; j++) {
if (field.name == 'VAR') {
return true;
}
}
}
return false;
};
/**
* Checks if a block is a procedures block. If procedures block names are
* ever updated or expanded, this function should be updated as well (no
* other known markers for procedure blocks beyond name).
*
* @param {Blockly.Block} block The block to check.
* @return {boolean} True if hte block is a procedure block, false otherwise.
*/
FactoryUtils.isProcedureBlock = function(block) {
return block &&
(block.type == 'procedures_defnoreturn' ||
block.type == 'procedures_defreturn' ||
block.type == 'procedures_callnoreturn' ||
block.type == 'procedures_callreturn' ||
block.type == 'procedures_ifreturn');
};

View file

@ -180,7 +180,6 @@
</table>
<section id="toolbox_section">
<div id="toolbox_blocks"></div>
<div id='disable_div'></div>
</section>
<aside id="toolbox_div">
<p id="categoryHeader">Your categories will appear here</p>

View file

@ -98,11 +98,76 @@ WorkspaceFactoryController.MODE_PRELOAD = 'preload';
* before), and then creates a tab and switches to it.
*/
WorkspaceFactoryController.prototype.addCategory = function() {
// Check if it's the first category added.
this.allowToTransferFlyoutBlocksToCategory();
// After possibly creating a category, check again if it's the first category.
var isFirstCategory = !this.model.hasElements();
// Get name from user.
name = this.promptForNewCategoryName('Enter the name of your new category: ');
if (!name) { //Exit if cancelled.
return;
}
// Create category.
this.createCategory(name);
// Switch to category.
this.switchElement(this.model.getCategoryIdByName(name));
// Allow the user to use the default options for injecting the workspace
// when there are categories if adding the first category.
if (isFirstCategory) {
this.allowToSetDefaultOptions();
}
// 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));
};
/**
* Allows the user to transfer blocks in their 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.allowToTransferFlyoutBlocksToCategory =
function() {
// Give the option to save blocks if their workspace is not empty and they
// are creating their first category.
if (isFirstCategory && this.toolboxWorkspace.getAllBlocks().length > 0) {
if (!this.model.hasElements() &&
this.toolboxWorkspace.getAllBlocks().length > 0) {
var confirmCreate = confirm('Do you want to save your work in another '
+ 'category? If you don\'t, the blocks in your workspace will be ' +
'deleted.');
@ -127,64 +192,6 @@ WorkspaceFactoryController.prototype.addCategory = function() {
this.updatePreview();
}
}
// After possibly creating a category, check again if it's the first category.
isFirstCategory = !this.model.hasElements();
// Get name from user.
name = this.promptForNewCategoryName('Enter the name of your new category: ');
if (!name) { //Exit if cancelled.
return;
}
// Create category.
this.createCategory(name, isFirstCategory);
// Switch to category.
this.switchElement(this.model.getCategoryIdByName(name));
// Allow the user to use the default options for injecting the workspace
// when there are categories if adding the first category.
if (isFirstCategory) {
this.allowToSetDefaultOptions();
}
// Update preview.
this.updatePreview();
};
/**
* Helper method for addCategory. Adds a category to the view given a name, ID,
* and a boolean for if it's the first category created. Assumes the category
* has already been created in the model. Does not switch to category.
*
* @param {!string} name Name of category being added.
* @param {!string} id The ID of the category being added.
* @param {boolean} isFirstCategory True if it's the first category created,
* false otherwise.
*/
WorkspaceFactoryController.prototype.createCategory = function(name,
isFirstCategory) {
// 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, isFirstCategory);
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));
};
/**
@ -562,7 +569,21 @@ WorkspaceFactoryController.prototype.loadCategory = function() {
}
} while (!this.isStandardCategoryName(name));
// Check if the user can create that standard category.
// 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.');
@ -580,6 +601,8 @@ WorkspaceFactoryController.prototype.loadCategory = function() {
+ '. Rename your category and try again.');
return;
}
// Allow user to transfer current flyout blocks to a category.
this.allowToTransferFlyoutBlocksToCategory();
var isFirstCategory = !this.model.hasElements();
// Copy the standard category in the model.
@ -589,7 +612,7 @@ WorkspaceFactoryController.prototype.loadCategory = function() {
this.model.addElementToList(copy);
// Update the copy in the view.
var tab = this.view.addCategoryRow(copy.name, copy.id, isFirstCategory);
var tab = this.view.addCategoryRow(copy.name, copy.id);
this.addClickToSwitch(tab, copy.id);
// Color the category tab in the view.
if (copy.color) {
@ -633,11 +656,9 @@ WorkspaceFactoryController.prototype.isStandardCategoryName = function(name) {
* the separator, and updates the preview.
*/
WorkspaceFactoryController.prototype.addSeparator = function() {
// Don't allow the user to add a separator if a category has not been created.
if (!this.model.hasElements()) {
alert('Add a category before adding a separator.');
return;
}
// If adding the first element in the toolbox, allow the user to transfer
// their flyout blocks to a category.
this.allowToTransferFlyoutBlocksToCategory();
// Create the separator in the model.
var separator = new ListElement(ListElement.TYPE_SEPARATOR);
this.model.addElementToList(separator);
@ -881,7 +902,7 @@ WorkspaceFactoryController.prototype.addShadowForBlockAndChildren_ =
this.view.markShadowBlock(block);
this.model.addShadowBlock(block.id);
if (this.hasVariableField(block)) {
if (FactoryUtils.hasVariableField(block)) {
block.setWarningText('Cannot make variable blocks shadow blocks.');
}
@ -892,26 +913,6 @@ WorkspaceFactoryController.prototype.addShadowForBlockAndChildren_ =
}
};
/**
* Checks if a block has a variable field. Blocks with variable fields cannot
* be shadow blocks.
*
* @param {Blockly.Block} block The block to check if a variable field exists.
* @return {boolean} True if the block has a variable field, false otherwise.
*/
WorkspaceFactoryController.prototype.hasVariableField = function(block) {
if (!block) {
return false;
}
for (var i = 0; i < block.inputList.length; i++) {
if (block.inputList[i].fieldRow.length > 0 &&
block.inputList[i].fieldRow[0].name == 'VAR') {
return true;
}
}
return false;
};
/**
* 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
@ -956,6 +957,14 @@ WorkspaceFactoryController.prototype.convertShadowBlocks = function() {
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);
}
@ -1231,3 +1240,21 @@ WorkspaceFactoryController.prototype.warnForUndefinedBlocks_ = function() {
}
}
};
/*
* 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();
};

View file

@ -27,6 +27,8 @@
* @author Emma Dauterman (evd2014)
*/
goog.require('FactoryUtils');
/**
* Namespace for workspace factory initialization methods.
* @namespace
@ -443,7 +445,7 @@ WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) {
// Enable block editing and remove warnings if the block is not a
// variable user-generated shadow block.
document.getElementById('button_editShadow').disabled = false;
if (!controller.hasVariableField(selected) &&
if (!FactoryUtils.hasVariableField(selected) &&
controller.isDefinedBlock(selected)) {
selected.setWarningText(null);
}
@ -459,7 +461,7 @@ WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) {
// Warn if a non-shadow block is nested inside a shadow block.
selected.setWarningText('Only shadow blocks can be nested inside '
+ 'other shadow blocks.');
} else if (!controller.hasVariableField(selected)) {
} 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'
@ -475,7 +477,7 @@ WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) {
// Remove possible 'invalid shadow block placement' warning.
if (selected != null && controller.isDefinedBlock(selected) &&
(!controller.hasVariableField(selected) ||
(!FactoryUtils.hasVariableField(selected) ||
!controller.isUserGenShadowBlock(selected.id))) {
selected.setWarningText(null);
}
@ -495,6 +497,51 @@ WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) {
// shadow blocks.
if (e.type == Blockly.Events.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.loadCategoryByName('variables');
}
} else 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.loadCategoryByName('functions');
}
}
}
});
};

View file

@ -28,6 +28,8 @@
* @author Emma Dauterman (edauterman)
*/
goog.require('FactoryUtils');
/**
* Class for a WorkspaceFactoryView
* @constructor
@ -43,19 +45,18 @@ WorkspaceFactoryView = function() {
*
* @param {!string} name The name of the category being created
* @param {!string} id ID of category being created
* @param {boolean} firstCategory true if it's the first category, false
* otherwise
* @return {!Element} DOM element created for tab
*/
WorkspaceFactoryView.prototype.addCategoryRow =
function(name, id, firstCategory) {
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 (firstCategory) {
if (count == 0) {
document.getElementById('categoryHeader').textContent = 'Your Categories:';
}
// Create tab.
var count = table.rows.length;
var row = table.insertRow(count);
var nextEntry = row.insertCell(0);
// Configure tab.
@ -253,9 +254,13 @@ WorkspaceFactoryView.prototype.setBorderColor = function(id, color) {
* @param {!Element} The td DOM element representing the separator.
*/
WorkspaceFactoryView.prototype.addSeparatorTab = function(id) {
// Create separator.
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.
@ -275,7 +280,14 @@ WorkspaceFactoryView.prototype.addSeparatorTab = function(id) {
* if it should be enabled.
*/
WorkspaceFactoryView.prototype.disableWorkspace = function(disable) {
document.getElementById('disable_div').style.zIndex = disable ? 1 : -1;
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';
}
};
/**
@ -334,6 +346,10 @@ WorkspaceFactoryView.prototype.markShadowBlock = function(block) {
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.');
}
};
/**