Workspace Factory (#522)

Workspace factory helps developers configure their workspace by allowing them to drag blocks into the workspace to add them to their toolbox. 
Current features:

supports categories or a single flyout of blocks
updates a preview workspace automatically
imports toolbox XML already written
exports toolbox XML to a file
prints toolbox XML to the console
imports a standard Blockly category
supports shadow blocks (allowing the user to move shadow blocks and toggle between shadow blocks and normal blocks), disabled blocks, block groups
allows the user to add/move/delete/rename/color categories and separators.
This commit is contained in:
Emma Dauterman
2016-08-10 11:03:11 -07:00
committed by rachel-fenichel
parent 9819d677a9
commit 8211bb30f4
7 changed files with 2949 additions and 0 deletions

View File

@@ -0,0 +1,682 @@
<!DOCTYPE html>
<html>
<title>Blockly Workspace Factory</title>
<script src="../../../blockly_compressed.js"></script>
<script src="../../../javascript_compressed.js"></script>
<script src="../../../msg/messages.js"></script>
<script src="../../../blocks_compressed.js"></script>
<script src="wfactory_model.js"></script>
<script src="wfactory_controller.js"></script>
<script src="wfactory_view.js"></script>
<script src="wfactory_generator.js"></script>
<script src="standard_categories.js"></script>
<script src="../../../../closure-library/closure/goog/base.js"></script>
<link rel="stylesheet" href="style.css">
<script>
goog.require('goog.ui.PopupColorPicker');
goog.require('goog.ui.ColorPicker');
</script>
<table width="100%" height="100%">
<tr>
<td>
<h1><a href="https://developers.google.com/blockly/">Blockly</a>&rlm; &gt;
<a href="../index.html">Demos</a>&rlm; &gt;
<span id="title">Workspace Factory</span>
</h1>
</td>
</tr>
<tr>
<td>
<p>
<input type="file" id="input_import" class="inputfile"></input>
<label for="input_import">Import</label>
<button id="button_export">Export</button>
<button id="button_print">Print</button>
<button id="button_clear">Clear</button>
</p>
</td>
</tr>
</table>
<section id="createDiv">
<p>Drag blocks into your toolbox:</p>
<section id="toolbox_section">
<div id="toolbox_blocks" class="content"></div>
<div id='disable_div'></div>
</section>
<aside id="category_section">
<table id="categoryTable">
<td id="tab_help">Your categories will appear here</td>
</table>
<p>&nbsp;</p>
<div class='dropdown'>
<button id="button_add">+</button>
<div id="dropdownDiv_add" class="dropdown-content">
<a id='dropdown_newCategory'>New Category</a>
<a id='dropdown_loadCategory'>Standard Category</a>
<a id='dropdown_separator'>Separator</a>
</div>
</div>
<button id="button_remove">-</button>
<button id="button_up">&#8593;</button>
<button id="button_down">&#8595;</button>
<p>&nbsp;</p>
<div class='dropdown'>
<button id="button_editCategory">Edit Category</button>
<div id="dropdownDiv_editCategory" class="dropdown-content">
<a id='dropdown_name'>Name</a>
<a id='dropdown_color'>Color</a>
</div>
</div>
<div class='dropdown'>
<button id="button_editShadow">Edit Block</button>
<div id="dropdownDiv_editShadowAdd" class="dropdown-content">
<a id='dropdown_addShadow'>Add Shadow</a>
</div>
<div id="dropdownDiv_editShadowRemove" class="dropdown-content">
<a id='dropdown_removeShadow'>Remove Shadow</a>
</div>
</div>
</aside>
</section>
<aside id="previewDiv">
<p>Preview your workspace:</p>
<div id="preview_blocks" class="content"></div>
</aside>
<xml id="toolbox" style="display: none">
<category name="Logic" colour="210">
<block type="controls_if"></block>
<block type="logic_compare"></block>
<block type="logic_operation"></block>
<block type="logic_negate"></block>
<block type="logic_boolean"></block>
<block type="logic_null"></block>
<block type="logic_ternary"></block>
</category>
<category name="Loops" colour="120">
<block type="controls_repeat_ext">
<value name="TIMES">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="controls_whileUntil"></block>
<block type="controls_for">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
<value name="BY">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="controls_forEach"></block>
<block type="controls_flow_statements"></block>
</category>
<category name="Math" colour="230">
<block type="math_number"></block>
<block type="math_arithmetic">
<value name="A">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="B">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="math_single">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">9</field>
</shadow>
</value>
</block>
<block type="math_trig">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">45</field>
</shadow>
</value>
</block>
<block type="math_constant"></block>
<block type="math_number_property">
<value name="NUMBER_TO_CHECK">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="math_change">
<value name="DELTA">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="math_round">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">3.1</field>
</shadow>
</value>
</block>
<block type="math_on_list"></block>
<block type="math_modulo">
<value name="DIVIDEND">
<shadow type="math_number">
<field name="NUM">64</field>
</shadow>
</value>
<value name="DIVISOR">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="math_constrain">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
<value name="LOW">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="HIGH">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="math_random_int">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="math_random_float"></block>
</category>
<category name="Text" colour="160">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_append">
<value name="TEXT">
<shadow type="text"></shadow>
</value>
</block>
<block type="text_length">
<value name="VALUE">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_isEmpty">
<value name="VALUE">
<shadow type="text">
<field name="TEXT"></field>
</shadow>
</value>
</block>
<block type="text_indexOf">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">text</field>
</block>
</value>
<value name="FIND">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_charAt">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">text</field>
</block>
</value>
</block>
<block type="text_getSubstring">
<value name="STRING">
<block type="variables_get">
<field name="VAR">text</field>
</block>
</value>
</block>
<block type="text_changeCase">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_trim">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_print">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_prompt_ext">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
</category>
<category name="Lists" colour="260">
<block type="lists_create_with">
<mutation items="0"></mutation>
</block>
<block type="lists_create_with"></block>
<block type="lists_repeat">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">5</field>
</shadow>
</value>
</block>
<block type="lists_length"></block>
<block type="lists_isEmpty"></block>
<block type="lists_indexOf">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_getIndex">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_setIndex">
<value name="LIST">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_getSublist">
<value name="LIST">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_split">
<value name="DELIM">
<shadow type="text">
<field name="TEXT">,</field>
</shadow>
</value>
</block>
<block type="lists_sort"></block>
</category>
<category name="Colour" colour="20">
<block type="colour_picker"></block>
<block type="colour_random"></block>
<block type="colour_rgb">
<value name="RED">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
<value name="GREEN">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
<value name="BLUE">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="colour_blend">
<value name="COLOUR1">
<shadow type="colour_picker">
<field name="COLOUR">#ff0000</field>
</shadow>
</value>
<value name="COLOUR2">
<shadow type="colour_picker">
<field name="COLOUR">#3333ff</field>
</shadow>
</value>
<value name="RATIO">
<shadow type="math_number">
<field name="NUM">0.5</field>
</shadow>
</value>
</block>
</category>
<sep></sep>
<category name="Variables" colour="330" custom="VARIABLE"></category>
<category name="Functions" colour="290" custom="PROCEDURE"></category>
</xml>
<script type="text/javascript">
// 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 empty workspace for configuring workspace.
var toolboxWorkspace = Blockly.inject('toolbox_blocks',
{grid:
{spacing: 25,
length: 3,
colour: '#ccc',
snap: true},
media: '../../../media/',
toolbox: toolbox,
});
// Create empty workspace for previewing created workspace.
var previewWorkspace = Blockly.inject('preview_blocks',
{grid:
{spacing: 25,
length: 3,
colour: '#ccc',
snap: true},
media: '../../../media/',
toolbox: '<xml></xml>',
zoom:
{controls: true,
wheel: true}
});
var controller = new FactoryController(toolboxWorkspace, previewWorkspace);
// Wrappers to attach buttons to method calls for the controller object
var addWrapper = function() {
document.getElementById('dropdownDiv_add').classList.toggle("show");
};
var newCategoryWrapper = function() {
controller.addCategory();
document.getElementById('dropdownDiv_add').classList.remove("show");
};
var loadCategoryWrapper = function() {
controller.loadCategory();
document.getElementById('dropdownDiv_add').classList.remove("show");
};
var separatorWrapper = function() {
controller.addSeparator();
document.getElementById('dropdownDiv_add').classList.remove("show");
};
var removeWrapper = function() {
controller.removeElement();
};
var exportWrapper = function() {
controller.exportConfig();
};
var printWrapper = function() {
controller.printConfig();
};
var upWrapper = function() {
controller.moveElement(-1);
};
var downWrapper = function() {
controller.moveElement(1);
};
var editCategoryWrapper = function() {
document.getElementById('dropdownDiv_editCategory').classList.toggle("show");
};
var nameWrapper = function() {
controller.changeCategoryName();
document.getElementById('dropdownDiv_editCategory').classList.remove("show");
};
var shadowAddWrapper = function() {
controller.addShadow();
document.getElementById('dropdownDiv_editShadowAdd').classList.remove("show");
};
var shadowRemoveWrapper = 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;
}
};
var editShadowWrapper = 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");
}
}
};
var importWrapper = function(event) {
controller.importFile(event.target.files[0]);
};
var clearWrapper = function() {
controller.clear();
};
document.getElementById('button_add').addEventListener
('click', addWrapper);
document.getElementById('dropdown_newCategory').addEventListener
('click', newCategoryWrapper);
document.getElementById('dropdown_loadCategory').addEventListener
('click', loadCategoryWrapper);
document.getElementById('dropdown_separator').addEventListener
('click', separatorWrapper);
document.getElementById('button_remove').addEventListener
('click', removeWrapper);
document.getElementById('button_export').addEventListener
('click', exportWrapper);
document.getElementById('button_print').addEventListener
('click', printWrapper);
document.getElementById('button_up').addEventListener
('click', upWrapper);
document.getElementById('button_down').addEventListener
('click', downWrapper);
document.getElementById('button_editCategory').addEventListener
('click', editCategoryWrapper);
document.getElementById('button_editShadow').addEventListener
('click', editShadowWrapper);
document.getElementById('dropdown_name').addEventListener
('click', nameWrapper);
document.getElementById('input_import').addEventListener
('change', importWrapper);
document.getElementById('button_clear').addEventListener
('click', clearWrapper);
document.getElementById('dropdown_addShadow').addEventListener
('click', shadowAddWrapper);
document.getElementById('dropdown_removeShadow').addEventListener
('click', shadowRemoveWrapper);
// 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 (e.keyCode == 38) { // Arrow up.
upWrapper();
} else if (e.keyCode == 40) { // Arrow down.
downWrapper();
}
});
// 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");
});
// 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;
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();
}
});
</script>
</html>

View File

@@ -0,0 +1,375 @@
/**
* @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 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.
*
* @author Emma Dauterman (evd2014)
*/
FactoryController.prototype.standardCategories = Object.create(null);
FactoryController.prototype.standardCategories['logic'] =
new ListElement(ListElement.TYPE_CATEGORY, 'Logic');
FactoryController.prototype.standardCategories['logic'].xml =
Blockly.Xml.textToDom(
'<xml>' +
'<block type="controls_if"></block>' +
'<block type="logic_compare"></block>' +
'<block type="logic_operation"></block>' +
'<block type="logic_negate"></block>' +
'<block type="logic_boolean"></block>' +
'<block type="logic_null"></block>' +
'<block type="logic_ternary"></block>' +
'</xml>');
FactoryController.prototype.standardCategories['logic'].color = '#5C81A6';
FactoryController.prototype.standardCategories['loops'] =
new ListElement(ListElement.TYPE_CATEGORY, 'Loops');
FactoryController.prototype.standardCategories['loops'].xml =
Blockly.Xml.textToDom(
'<xml>' +
'<block type="controls_repeat_ext">' +
'<value name="TIMES">' +
'<shadow type="math_number">' +
'<field name="NUM">10</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="controls_whileUntil"></block>' +
'<block type="controls_for">' +
'<value name="FROM">' +
'<shadow type="math_number">' +
'<field name="NUM">1</field>' +
'</shadow>' +
'</value>' +
'<value name="TO">' +
'<shadow type="math_number">' +
'<field name="NUM">10</field>' +
'</shadow>' +
'</value>' +
'<value name="BY">' +
'<shadow type="math_number">' +
'<field name="NUM">1</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="controls_forEach"></block>' +
'<block type="controls_flow_statements"></block>' +
'</xml>');
FactoryController.prototype.standardCategories['loops'].color = '#5CA65C';
FactoryController.prototype.standardCategories['math'] =
new ListElement(ListElement.TYPE_CATEGORY, 'Math');
FactoryController.prototype.standardCategories['math'].xml =
Blockly.Xml.textToDom(
'<xml>' +
'<block type="math_number"></block>' +
'<block type="math_arithmetic">' +
'<value name="A">' +
'<shadow type="math_number">' +
'<field name="NUM">1</field>' +
'</shadow>' +
'</value>' +
'<value name="B">' +
'<shadow type="math_number">' +
'<field name="NUM">1</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="math_single">' +
'<value name="NUM">' +
'<shadow type="math_number">' +
'<field name="NUM">9</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="math_trig">' +
'<value name="NUM">' +
'<shadow type="math_number">' +
'<field name="NUM">45</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="math_constant"></block>' +
'<block type="math_number_property">' +
'<value name="NUMBER_TO_CHECK">' +
'<shadow type="math_number">' +
'<field name="NUM">0</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="math_change">' +
'<value name="DELTA">' +
'<shadow type="math_number">' +
'<field name="NUM">1</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="math_round">' +
'<value name="NUM">' +
'<shadow type="math_number">' +
'<field name="NUM">3.1</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="math_on_list"></block>' +
'<block type="math_modulo">' +
'<value name="DIVIDEND">' +
'<shadow type="math_number">' +
'<field name="NUM">64</field>' +
'</shadow>' +
'</value>' +
'<value name="DIVISOR">' +
'<shadow type="math_number">' +
'<field name="NUM">10</field>'+
'</shadow>' +
'</value>' +
'</block>' +
'<block type="math_constrain">' +
'<value name="VALUE">' +
'<shadow type="math_number">' +
'<field name="NUM">50</field>' +
'</shadow>' +
'</value>' +
'<value name="LOW">' +
'<shadow type="math_number">' +
'<field name="NUM">1</field>' +
'</shadow>' +
'</value>' +
'<value name="HIGH">' +
'<shadow type="math_number">' +
'<field name="NUM">100</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="math_random_int">' +
'<value name="FROM">' +
'<shadow type="math_number">' +
'<field name="NUM">1</field>' +
'</shadow>' +
'</value>' +
'<value name="TO">' +
'<shadow type="math_number">' +
'<field name="NUM">100</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="math_random_float"></block>' +
'</xml>');
FactoryController.prototype.standardCategories['math'].color = '#5C68A6';
FactoryController.prototype.standardCategories['text'] =
new ListElement(ListElement.TYPE_CATEGORY, 'Text');
FactoryController.prototype.standardCategories['text'].xml =
Blockly.Xml.textToDom(
'<xml>' +
'<block type="text"></block>' +
'<block type="text_join"></block>' +
'<block type="text_append">' +
'<value name="TEXT">' +
'<shadow type="text"></shadow>' +
'</value>' +
'</block>' +
'<block type="text_length">' +
'<value name="VALUE">' +
'<shadow type="text">' +
'<field name="TEXT">abc</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="text_isEmpty">' +
'<value name="VALUE">' +
'<shadow type="text">' +
'<field name="TEXT"></field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="text_indexOf">' +
'<value name="VALUE">' +
'<block type="variables_get">' +
'<field name="VAR">text</field>' +
'</block>' +
'</value>' +
'<value name="FIND">' +
'<shadow type="text">' +
'<field name="TEXT">abc</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="text_charAt">' +
'<value name="VALUE">' +
'<block type="variables_get">' +
'<field name="VAR">text</field>' +
'</block>' +
'</value>' +
'</block>' +
'<block type="text_getSubstring">' +
'<value name="STRING">' +
'<block type="variables_get">' +
'<field name="VAR">text</field>' +
'</block>' +
'</value>' +
'</block>' +
'<block type="text_changeCase">' +
'<value name="TEXT">' +
'<shadow type="text">' +
'<field name="TEXT">abc</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="text_trim">' +
'<value name="TEXT">' +
'<shadow type="text">' +
'<field name="TEXT">abc</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="text_print">' +
'<value name="TEXT">' +
'<shadow type="text">' +
'<field name="TEXT">abc</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="text_prompt_ext">' +
'<value name="TEXT">' +
'<shadow type="text">' +
'<field name="TEXT">abc</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'</xml>');
FactoryController.prototype.standardCategories['text'].color = '#5CA68D';
FactoryController.prototype.standardCategories['lists'] =
new ListElement(ListElement.TYPE_CATEGORY, 'Lists');
FactoryController.prototype.standardCategories['lists'].xml =
Blockly.Xml.textToDom(
'<xml>' +
'<block type="lists_create_with">' +
'<mutation items="0"></mutation>' +
'</block>' +
'<block type="lists_create_with"></block>' +
'<block type="lists_repeat">' +
'<value name="NUM">' +
'<shadow type="math_number">' +
'<field name="NUM">5</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="lists_length"></block>' +
'<block type="lists_isEmpty"></block>' +
'<block type="lists_indexOf">' +
'<value name="VALUE">' +
'<block type="variables_get">' +
'<field name="VAR">list</field>' +
'</block>' +
'</value>' +
'</block>' +
'<block type="lists_getIndex">' +
'<value name="VALUE">' +
'<block type="variables_get">' +
'<field name="VAR">list</field>' +
'</block>' +
'</value>' +
'</block>' +
'<block type="lists_setIndex">' +
'<value name="LIST">' +
'<block type="variables_get">' +
'<field name="VAR">list</field>' +
'</block>' +
'</value>' +
'</block>' +
'<block type="lists_getSublist">' +
'<value name="LIST">' +
'<block type="variables_get">' +
'<field name="VAR">list</field>' +
'</block>' +
'</value>' +
'</block>' +
'<block type="lists_split">' +
'<value name="DELIM">' +
'<shadow type="text">' +
'<field name="TEXT">,</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="lists_sort"></block>' +
'</xml>');
FactoryController.prototype.standardCategories['lists'].color = '#745CA6';
FactoryController.prototype.standardCategories['colour'] =
new ListElement(ListElement.TYPE_CATEGORY, 'Colour');
FactoryController.prototype.standardCategories['colour'].xml =
Blockly.Xml.textToDom(
'<xml>' +
'<block type="colour_picker"></block>' +
'<block type="colour_random"></block>' +
'<block type="colour_rgb">' +
'<value name="RED">' +
'<shadow type="math_number">' +
'<field name="NUM">100</field>' +
'</shadow>' +
'</value>' +
'<value name="GREEN">' +
'<shadow type="math_number">' +
'<field name="NUM">50</field>' +
'</shadow>' +
'</value>' +
'<value name="BLUE">' +
'<shadow type="math_number">' +
'<field name="NUM">0</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'<block type="colour_blend">' +
'<value name="COLOUR1">' +
'<shadow type="colour_picker">' +
'<field name="COLOUR">#ff0000</field>' +
'</shadow>' +
'</value>' +
'<value name="COLOUR2">' +
'<shadow type="colour_picker">' +
'<field name="COLOUR">#3333ff</field>' +
'</shadow>' +
'</value>' +
'<value name="RATIO">' +
'<shadow type="math_number">' +
'<field name="NUM">0.5</field>' +
'</shadow>' +
'</value>' +
'</block>' +
'</xml>');
FactoryController.prototype.standardCategories['colour'].color = '#A6745C';
FactoryController.prototype.standardCategories['functions'] =
new ListElement(ListElement.TYPE_CATEGORY, 'Functions');
FactoryController.prototype.standardCategories['functions'].color = '#9A5CA6'
FactoryController.prototype.standardCategories['functions'].custom =
'PROCEDURE';
FactoryController.prototype.standardCategories['variables'] =
new ListElement(ListElement.TYPE_CATEGORY, 'Variables');
FactoryController.prototype.standardCategories['variables'].color = '#A65C81';
FactoryController.prototype.standardCategories['variables'].custom = 'VARIABLE';

View File

@@ -0,0 +1,238 @@
body {
background-color: #fff;
font-family: sans-serif;
}
h1 {
font-weight: normal;
font-size: 140%;
}
section {
float: left;
}
aside {
float: right;
}
#categoryTable>table {
border: 1px solid #ccc;
border-bottom: none;
}
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;
}
button {
border-radius: 4px;
border: 1px solid #ddd;
background-color: #eee;
color: #000;
font-size: large;
margin: 0 5px;
padding: 10px;
}
button:hover:not(:disabled) {
box-shadow: 2px 2px 5px #888;
}
button:disabled {
opacity: .6;
}
button>* {
opacity: .6;
vertical-align: text-bottom;
}
button:hover:not(:disabled)>* {
opacity: 1;
}
label {
border-radius: 4px;
border: 1px solid #ddd;
background-color: #eee;
color: #000;
font-size: large;
margin: 0 5px;
padding: 10px;
}
label:hover:not(:disabled) {
box-shadow: 2px 2px 5px #888;
}
label:disabled {
opacity: .6;
}
label>* {
opacity: .6;
vertical-align: text-bottom;
}
label:hover:not(:disabled)>* {
opacity: 1;
}
table {
border: none;
border-collapse: collapse;
margin: 0;
padding: 0;
}
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 <div> - 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;
}

View File

@@ -0,0 +1,704 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2016 Google Inc.
* https://developers.google.com/blockly/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Contains the controller code for workspace factory. Depends
* on the model and view objects (created as internal variables) and interacts
* with previewWorkspace and toolboxWorkspace (internal references stored to
* both). Also depends on standard_categories.js for standard Blockly
* categories. Provides the functionality for the actions the user can initiate:
* - adding and removing categories
* - switching between categories
* - printing and downloading configuration xml
* - updating the preview workspace
* - changing a category name
* - moving the position of a category.
*
* @author Emma Dauterman (evd2014)
*/
/**
* Class for a FactoryController
* @constructor
* @param {!Blockly.workspace} toolboxWorkspace workspace where blocks are
* dragged into corresponding categories
* @param {!Blockly.workspace} previewWorkspace workspace that shows preview
* of what workspace would look like using generated XML
*/
FactoryController = function(toolboxWorkspace, previewWorkspace) {
// Workspace for user to drag blocks in for a certain category.
this.toolboxWorkspace = toolboxWorkspace;
// Workspace for user to preview their changes.
this.previewWorkspace = previewWorkspace;
// Model to keep track of categories and blocks.
this.model = new FactoryModel();
// Updates the category tabs.
this.view = new FactoryView();
// Generates XML for categories.
this.generator = new FactoryGenerator(this.model);
};
/**
* Currently prompts the user for a name, checking that it's valid (not used
* before), and then creates a tab and switches to it.
*/
FactoryController.prototype.addCategory = function() {
// Check if it's the first category added.
var firstCategory = !this.model.hasToolbox();
// Give the option to save blocks if their workspace is not empty and they
// are creating their first category.
if (firstCategory && this.toolboxWorkspace.getAllBlocks().length > 0) {
var confirmCreate = confirm('Do you want to save your work in another '
+ 'category? If you don\'t, the blocks in your workspace will be ' +
'deleted.');
// Create a new category for current blocks.
if (confirmCreate) {
var name = prompt('Enter the name of the category for your ' +
'current blocks: ');
if (!name) { // Exit if cancelled.
return;
}
this.createCategory(name, true);
this.model.setSelectedById(this.model.getCategoryIdByName(name));
}
}
// After possibly creating a category, check again if it's the first category.
firstCategory = !this.model.hasToolbox();
// Get name from user.
name = this.promptForNewCategoryName('Enter the name of your new category: ');
if (!name) { //Exit if cancelled.
return;
}
// Create category.
this.createCategory(name, firstCategory);
// Switch to category.
this.switchElement(this.model.getCategoryIdByName(name));
// Update preview.
this.updatePreview();
};
/**
* Helper method for addCategory. Adds a category to the view given a name, ID,
* and a boolean for if it's the first category created. Assumes the category
* has already been created in the model. Does not switch to category.
*
* @param {!string} name Name of category being added.
* @param {!string} id The ID of the category being added.
* @param {boolean} firstCategory True if it's the first category created,
* false otherwise.
*/
FactoryController.prototype.createCategory = function(name, firstCategory) {
// Create empty category
var category = new ListElement(ListElement.TYPE_CATEGORY, name);
this.model.addElementToList(category);
// Create new category.
var tab = this.view.addCategoryRow(name, category.id, firstCategory);
this.addClickToSwitch(tab, category.id);
};
/**
* Given a tab and a ID to be associated to that tab, adds a listener to
* that tab so that when the user clicks on the tab, it switches to the
* element associated with that ID.
*
* @param {!Element} tab The DOM element to add the listener to.
* @param {!string} id The ID of the element to switch to when tab is clicked.
*/
FactoryController.prototype.addClickToSwitch = function(tab, id) {
var self = this;
var clickFunction = function(id) { // Keep this in scope for switchElement
return function() {
self.switchElement(id);
};
};
this.view.bindClick(tab, clickFunction(id));
};
/**
* Attached to "-" button. Checks if the user wants to delete
* the current element. Removes the element and switches to another element.
* When the last element is removed, it switches to a single flyout mode.
*
*/
FactoryController.prototype.removeElement = function() {
// Check that there is a currently selected category to remove.
if (!this.model.getSelected()) {
return;
}
// Check if user wants to remove current category.
var check = confirm('Are you sure you want to delete the currently selected '
+ this.model.getSelected().type + '?');
if (!check) { // If cancelled, exit.
return;
}
var selectedId = this.model.getSelectedId();
var selectedIndex = this.model.getIndexByElementId(selectedId);
// Delete element visually.
this.view.deleteElementRow(selectedId, selectedIndex);
// Delete element in model.
this.model.deleteElementFromList(selectedIndex);
// Find next logical element to switch to.
var next = this.model.getElementByIndex(selectedIndex);
if (!next && this.model.hasToolbox()) {
next = this.model.getElementByIndex(selectedIndex - 1);
}
var nextId = next ? next.id : null;
// Open next element.
this.clearAndLoadElement(nextId);
if (!nextId) {
alert('You currently have no categories or separators. All your blocks' +
' will be displayed in a single flyout.');
}
// Update preview.
this.updatePreview();
};
/**
* Gets a valid name for a new category from the user.
*
* @param {!string} promptString Prompt for the user to enter a name.
* @return {string} Valid name for a new category, or null if cancelled.
*/
FactoryController.prototype.promptForNewCategoryName = function(promptString) {
do {
var name = prompt(promptString);
if (!name) { // If cancelled.
return null;
}
} while (this.model.hasCategoryByName(name));
return name;
}
/**
* Switches to a new tab for the element given by ID. Stores XML and blocks
* to reload later, updates selected accordingly, and clears the workspace
* and clears undo, then loads the new element.
*
* @param {!string} id ID of tab to be opened, must be valid element ID.
*/
FactoryController.prototype.switchElement = function(id) {
// Disables events while switching so that Blockly delete and create events
// don't update the preview repeatedly.
Blockly.Events.disable();
// Caches information to reload or generate xml if switching to/from element.
// Only saves if a category is selected.
if (this.model.getSelectedId() != null && id != null) {
this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace);
}
// Load element.
this.clearAndLoadElement(id);
// Enable Blockly events again.
Blockly.Events.enable();
};
/**
* Switches to a new tab for the element by ID. Helper for switchElement.
* Updates selected, clears the workspace and clears undo, loads a new element.
*
* @param {!string} id ID of category to load
*/
FactoryController.prototype.clearAndLoadElement = function(id) {
// Unselect current tab if switching to and from an element.
if (this.model.getSelectedId() != null && id != null) {
this.view.setCategoryTabSelection(this.model.getSelectedId(), false);
}
// If switching from a separator, enable workspace in view.
if (this.model.getSelectedId() != null && this.model.getSelected().type ==
ListElement.TYPE_SEPARATOR) {
this.view.disableWorkspace(false);
}
// Set next category.
this.model.setSelectedById(id);
// Clear workspace.
this.toolboxWorkspace.clear();
this.toolboxWorkspace.clearUndo();
// Loads next category if switching to an element.
if (id != null) {
this.view.setCategoryTabSelection(id, true);
Blockly.Xml.domToWorkspace(this.model.getSelectedXml(),
this.toolboxWorkspace);
// Disable workspace if switching to a separator.
if (this.model.getSelected().type == ListElement.TYPE_SEPARATOR) {
this.view.disableWorkspace(true);
}
}
// Mark all shadow blocks laoded and order blocks as if shown in a flyout.
this.view.markShadowBlocks(this.model.getShadowBlocksInWorkspace
(toolboxWorkspace.getAllBlocks()));
this.toolboxWorkspace.cleanUp_();
// Update category editing buttons.
this.view.updateState(this.model.getIndexByElementId
(this.model.getSelectedId()), this.model.getSelected());
};
/**
* Tied to "Export Config" button. Gets a file name from the user and downloads
* the corresponding configuration xml to that file.
*/
FactoryController.prototype.exportConfig = function() {
// Generate XML.
var configXml = Blockly.Xml.domToPrettyText
(this.generator.generateConfigXml(this.toolboxWorkspace));
// Get file name.
var fileName = prompt("File Name: ");
if (!fileName) { // If cancelled
return;
}
// Download file.
var data = new Blob([configXml], {type: 'text/xml'});
this.view.createAndDownloadFile(fileName, data);
};
/**
* Tied to "Print Config" button. Mainly used for debugging purposes. Prints
* the configuration XML to the console.
*/
FactoryController.prototype.printConfig = function() {
window.console.log(Blockly.Xml.domToPrettyText
(this.generator.generateConfigXml(this.toolboxWorkspace)));
};
/**
* Updates the preview workspace based on the toolbox workspace. If switching
* from no categories to categories or categories to no categories, reinjects
* Blockly with reinjectPreview, otherwise just updates without reinjecting.
* Called whenever a list element is created, removed, or modified and when
* Blockly move and delete events are fired. Do not call on create events
* or disabling will cause the user to "drop" their current blocks.
*/
FactoryController.prototype.updatePreview = function() {
// Disable events to stop updatePreview from recursively calling itself
// through event handlers.
Blockly.Events.disable();
var tree = Blockly.Options.parseToolboxTree
(this.generator.generateConfigXml(this.toolboxWorkspace));
// No categories, creates a simple flyout.
if (tree.getElementsByTagName('category').length == 0) {
if (this.previewWorkspace.toolbox_) {
this.reinjectPreview(tree); // Switch to simple flyout, more expensive.
} else {
this.previewWorkspace.flyout_.show(tree.childNodes);
}
// Uses categories, creates a toolbox.
} else {
if (!previewWorkspace.toolbox_) {
this.reinjectPreview(tree); // Create a toolbox, more expensive.
} else {
this.previewWorkspace.toolbox_.populate_(tree);
}
}
// Reenable events.
Blockly.Events.enable();
};
/**
* Used to completely reinject the preview workspace. This should be used only
* when switching from simple flyout to categories, or categories to simple
* flyout. More expensive than simply updating the flyout or toolbox.
*
* @param {!Element} tree of xml elements
* @package
*/
FactoryController.prototype.reinjectPreview = function(tree) {
this.previewWorkspace.dispose();
previewToolbox = Blockly.Xml.domToPrettyText(tree);
this.previewWorkspace = Blockly.inject('preview_blocks',
{grid:
{spacing: 25,
length: 3,
colour: '#ccc',
snap: true},
media: '../../../media/',
toolbox: previewToolbox,
zoom:
{controls: true,
wheel: true}
});
};
/**
* Tied to "change name" button. Changes the name of the selected category.
* Continues prompting the user until they input a category name that is not
* currently in use, exits if user presses cancel.
*/
FactoryController.prototype.changeCategoryName = function() {
// Return if no category selected or element a separator.
if (!this.model.getSelected() ||
this.model.getSelected().type == ListElement.TYPE_SEPARATOR) {
return;
}
// Get new name from user.
var newName = this.promptForNewCategoryName('What do you want to change this'
+ ' category\'s name to?');
if (!newName) { // If cancelled.
return;
}
// Change category name.
this.model.getSelected().changeName(newName);
this.view.updateCategoryName(newName, this.model.getSelectedId());
// Update preview.
this.updatePreview();
};
/**
* Tied to arrow up and arrow down buttons. Swaps with the element above or
* below the currently selected element (offset categories away from the
* current element). Updates state to enable the correct element editing
* buttons.
*
* @param {int} offset The index offset from the currently selected element
* to swap with. Positive if the element to be swapped with is below, negative
* if the element to be swapped with is above.
*/
FactoryController.prototype.moveElement = function(offset) {
var curr = this.model.getSelected();
if (!curr) { // Return if no selected element.
return;
}
var currIndex = this.model.getIndexByElementId(curr.id);
var swapIndex = this.model.getIndexByElementId(curr.id) + offset;
var swap = this.model.getElementByIndex(swapIndex);
if (!swap) { // Return if cannot swap in that direction.
return;
}
// Move currently selected element to index of other element.
// Indexes must be valid because confirmed that curr and swap exist.
this.moveElementToIndex(curr, swapIndex, currIndex);
// Update element editing buttons.
this.view.updateState(swapIndex, this.model.getSelected());
// Update preview.
this.updatePreview();
};
/**
* Moves a element to a specified index and updates the model and view
* accordingly. Helper functions throw an error if indexes are out of bounds.
*
* @param {!Element} element The element to move.
* @param {int} newIndex The index to insert the element at.
* @param {int} oldIndex The index the element is currently at.
*/
FactoryController.prototype.moveElementToIndex = function(element, newIndex,
oldIndex) {
this.model.moveElementToIndex(element, newIndex, oldIndex);
this.view.moveTabToIndex(element.id, newIndex, oldIndex);
};
/**
* Changes the color of the selected category. Return if selected element is
* a separator.
*
* @param {!string} color The color to change the selected category. Must be
* a valid CSS string.
*/
FactoryController.prototype.changeSelectedCategoryColor = function(color) {
// Return if no category selected or element a separator.
if (!this.model.getSelected() ||
this.model.getSelected().type == ListElement.TYPE_SEPARATOR) {
return;
}
// Change color of selected category.
this.model.getSelected().changeColor(color);
this.view.setBorderColor(this.model.getSelectedId(), color);
this.updatePreview();
};
/**
* Tied to the "Standard Category" dropdown option, this function prompts
* the user for a name of a standard Blockly category (case insensitive) and
* loads it as a new category and switches to it. Leverages standardCategories
* map in standard_categories.js.
*/
FactoryController.prototype.loadCategory = function() {
// Prompt user for the name of the standard category to load.
do {
var name = prompt('Enter the name of the category you would like to import '
+ '(Logic, Loops, Math, Text, Lists, Colour, Variables, or Functions)');
if (!name) {
return; // Exit if cancelled.
}
} while (!this.isStandardCategoryName(name));
// Check if the user can create that standard category.
if (this.model.hasVariables() && name.toLowerCase() == 'variables') {
alert('A Variables category already exists. You cannot create multiple' +
' variables categories.');
return;
}
if (this.model.hasProcedures() && name.toLowerCase() == 'functions') {
alert('A Functions category already exists. You cannot create multiple' +
' functions categories.');
return;
}
// Check if the user can create a category with that name.
var standardCategory = this.standardCategories[name.toLowerCase()]
if (this.model.hasCategoryByName(standardCategory.name)) {
alert('You already have a category with the name ' + standardCategory.name
+ '. Rename your category and try again.');
return;
}
// Copy the standard category in the model.
var copy = standardCategory.copy();
// Add the copy in the view.
var tab = this.view.addCategoryRow(copy.name, copy.id,
!this.model.hasToolbox());
// Add it to the model.
this.model.addElementToList(copy);
// Update the view.
this.addClickToSwitch(tab, copy.id);
// Color the category tab in the view.
if (copy.color) {
this.view.setBorderColor(copy.id, copy.color);
}
// Switch to loaded category.
this.switchElement(copy.id);
// Convert actual shadow blocks to user-generated shadow blocks.
this.convertShadowBlocks();
// Update preview.
this.updatePreview();
};
/**
* Given the name of a category, determines if it's the name of a standard
* category (case insensitive).
*
* @param {string} name The name of the category that should be checked if it's
* in standardCategories
* @return {boolean} True if name is a standard category name, false otherwise.
*/
FactoryController.prototype.isStandardCategoryName = function(name) {
for (var category in this.standardCategories) {
if (name.toLowerCase() == category) {
return true;
}
}
return false;
};
/**
* Connected to the "add separator" dropdown option. If categories already
* exist, adds a separator to the model and view. Does not switch to select
* the separator, and updates the preview.
*/
FactoryController.prototype.addSeparator = function() {
// Don't allow the user to add a separator if a category has not been created.
if (!this.model.hasToolbox()) {
alert('Add a category before adding a separator.');
return;
}
// Create the separator in the model.
var separator = new ListElement(ListElement.TYPE_SEPARATOR);
this.model.addElementToList(separator);
// Create the separator in the view.
var tab = this.view.addSeparatorTab(separator.id);
this.addClickToSwitch(tab, separator.id);
// Switch to the separator and update the preview.
this.switchElement(separator.id);
this.updatePreview();
};
/**
* Connected to the import button. Given the file path inputted by the user
* from file input, this function loads that toolbox XML to the workspace,
* creating category and separator tabs as necessary. This allows the user
* to be able to edit toolboxes given their XML form. Catches errors from
* file reading and prints an error message alerting the user.
*
* @param {string} file The path for the file to be imported into the workspace.
* Should contain valid toolbox XML.
*/
FactoryController.prototype.importFile = function(file) {
// Exit if cancelled.
if (!file) {
return;
}
var reader = new FileReader();
// To be executed when the reader has read the file.
reader.onload = function() {
// Try to parse XML from file and load it into toolbox editing area.
// Print error message if fail.
try {
var tree = Blockly.Xml.textToDom(reader.result);
controller.importFromTree_(tree);
} catch(e) {
alert('Cannot load XML from file.');
console.log(e);
}
}
// Read the file.
reader.readAsText(file);
};
/**
* Given a XML DOM tree, loads it into the toolbox editing area so that the
* user can continue editing their work. Assumes that tree is in valid toolbox
* XML format.
* @private
*
* @param {!Element} tree XML tree to be loaded to toolbox editing area.
*/
FactoryController.prototype.importFromTree_ = function(tree) {
// Clear current editing area.
this.model.clearToolboxList();
this.view.clearToolboxTabs();
if (tree.getElementsByTagName('category').length == 0) {
// No categories present.
// Load all the blocks into a single category evenly spaced.
Blockly.Xml.domToWorkspace(tree, this.toolboxWorkspace);
this.toolboxWorkspace.cleanUp_();
// Convert actual shadow blocks to user-generated shadow blocks.
this.convertShadowBlocks();
// Add message to denote empty category.
this.view.addEmptyCategoryMessage();
} else {
// Categories/separators present.
for (var i = 0, item; item = tree.children[i]; i++) {
if (item.tagName == 'category') {
// If the element is a category, create a new category and switch to it.
this.createCategory(item.getAttribute('name'), false);
var category = this.model.getElementByIndex(i);
this.switchElement(category.id);
// Load all blocks in that category to the workspace to be evenly
// spaced and saved to that category.
for (var j = 0, blockXml; blockXml = item.children[j]; j++) {
Blockly.Xml.domToBlock(blockXml, this.toolboxWorkspace);
}
// Evenly space the blocks.
// TODO(evd2014): Change to cleanUp once cleanUp_ is made public in
// master.
this.toolboxWorkspace.cleanUp_();
// Convert actual shadow blocks to user-generated shadow blocks.
this.convertShadowBlocks();
// Set category color.
if (item.getAttribute('colour')) {
category.changeColor(item.getAttribute('colour'));
this.view.setBorderColor(category.id, category.color);
}
// Set any custom tags.
if (item.getAttribute('custom')) {
this.model.addCustomTag(category, item.getAttribute('custom'));
}
} else {
// If the element is a separator, add the separator and switch to it.
this.addSeparator();
this.switchElement(this.model.getElementByIndex(i).id);
}
}
}
this.view.updateState(this.model.getIndexByElementId
(this.model.getSelectedId()), this.model.getSelected());
this.updatePreview();
};
/**
* Clears the toolbox editing area completely, deleting all categories and all
* blocks in the model and view.
*/
FactoryController.prototype.clear = function() {
this.model.clearToolboxList();
this.view.clearToolboxTabs();
this.view.addEmptyCategoryMessage();
this.view.updateState(-1, null);
this.toolboxWorkspace.clear();
this.toolboxWorkspace.clearUndo();
this.updatePreview();
};
/*
* Makes the currently selected block a user-generated shadow block. These
* blocks are not made into real shadow blocks, but recorded in the model
* and visually marked as shadow blocks, allowing the user to move and edit
* them (which would be impossible with actual shadow blocks). Updates the
* preview when done.
*
*/
FactoryController.prototype.addShadow = function() {
// No block selected to make a shadow block.
if (!Blockly.selected) {
return;
}
this.view.markShadowBlock(Blockly.selected);
this.model.addShadowBlock(Blockly.selected.id);
this.updatePreview();
};
/**
* If the currently selected block is a user-generated shadow block, this
* function makes it a normal block again, removing it from the list of
* shadow blocks and loading the workspace again. Updates the preview again.
*
*/
FactoryController.prototype.removeShadow = function() {
// No block selected to modify.
if (!Blockly.selected) {
return;
}
this.model.removeShadowBlock(Blockly.selected.id);
this.view.unmarkShadowBlock(Blockly.selected);
this.updatePreview();
};
/**
* Given a unique block ID, uses the model to determine if a block is a
* user-generated shadow block.
*
* @param {!string} blockId The unique ID of the block to examine.
* @return {boolean} True if the block is a user-generated shadow block, false
* otherwise.
*/
FactoryController.prototype.isUserGenShadowBlock = function(blockId) {
return this.model.isShadowBlock(blockId);
}
/**
* Call when importing XML containing real shadow blocks. This function turns
* all real shadow blocks loaded in the workspace into user-generated shadow
* blocks, meaning they are marked as shadow blocks by the model and appear as
* shadow blocks in the view but are still editable and movable.
*/
FactoryController.prototype.convertShadowBlocks = function() {
var blocks = this.toolboxWorkspace.getAllBlocks();
for (var i = 0, block; block = blocks[i]; i++) {
if (block.isShadow()) {
block.setShadow(false);
this.model.addShadowBlock(block.id);
this.view.markShadowBlock(block);
}
}
};

View File

@@ -0,0 +1,159 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2016 Google Inc.
* https://developers.google.com/blockly/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Generates the configuration xml used to update the preview
* workspace or print to the console or download to a file. Leverages
* Blockly.Xml and depends on information in the model (holds a reference).
* Depends on a hidden workspace created in the generator to load saved XML in
* order to generate toolbox XML.
*
* @author Emma Dauterman (evd2014)
*/
/**
* Class for a FactoryGenerator
* @constructor
*/
FactoryGenerator = function(model) {
// Model to share information about categories and shadow blocks.
this.model = model;
// Create hidden workspace to load saved XML to generate toolbox XML.
var hiddenBlocks = document.createElement('div');
// Generate a globally unique ID for the hidden div element to avoid
// collisions.
var hiddenBlocksId = Blockly.genUid();
hiddenBlocks.id = hiddenBlocksId;
hiddenBlocks.style.display = 'none';
document.body.appendChild(hiddenBlocks);
this.hiddenWorkspace = Blockly.inject(hiddenBlocksId);
};
/**
* Generates the xml for the toolbox or flyout with information from
* toolboxWorkspace and the model. Uses the hiddenWorkspace to generate XML.
*
* @param {!Blockly.workspace} toolboxWorkspace Toolbox editing workspace where
* blocks are added by user to be part of the toolbox.
* @return {!Element} XML element representing toolbox or flyout corresponding
* to toolbox workspace.
*/
FactoryGenerator.prototype.generateConfigXml = function(toolboxWorkspace) {
// Create DOM for XML.
var xmlDom = goog.dom.createDom('xml',
{
'id' : 'toolbox',
'style' : 'display:none'
});
if (!this.model.hasToolbox()) {
// Toolbox has no categories. Use XML directly from workspace.
this.loadToHiddenWorkspaceAndSave_
(Blockly.Xml.workspaceToDom(toolboxWorkspace), xmlDom);
} else {
// Toolbox has categories.
// Assert that selected != null
if (!this.model.getSelected()) {
throw new Error('Selected is null when the toolbox is empty.');
}
// Capture any changes made by user before generating XML.
this.model.getSelected().saveFromWorkspace(toolboxWorkspace);
var xml = this.model.getSelectedXml();
var toolboxList = this.model.getToolboxList();
// Iterate through each category to generate XML for each using the
// hidden workspace. Load each category to the hidden workspace to make sure
// that all the blocks that are not top blocks are also captured as block
// groups in the flyout.
for (var i = 0; i < toolboxList.length; i++) {
var element = toolboxList[i];
if (element.type == ListElement.TYPE_SEPARATOR) {
// If the next element is a separator.
var nextElement = goog.dom.createDom('sep');
} else {
// If the next element is a category.
var nextElement = goog.dom.createDom('category');
nextElement.setAttribute('name', element.name);
// Add a colour attribute if one exists.
if (element.color != null) {
nextElement.setAttribute('colour', element.color);
}
// Add a custom attribute if one exists.
if (element.custom != null) {
nextElement.setAttribute('custom', element.custom);
}
// Load that category to hidden workspace, setting user-generated shadow
// blocks as real shadow blocks.
this.loadToHiddenWorkspaceAndSave_(element.xml, nextElement);
}
xmlDom.appendChild(nextElement);
}
}
return xmlDom;
};
/**
* Load the given XML to the hidden workspace, set any user-generated shadow
* blocks to be actual shadow blocks, then append the XML from the workspace
* to the DOM element passed in.
* @private
*
* @param {!Element} xml The XML to be loaded to the hidden workspace.
* @param {!Element} dom The DOM element to append the generated XML to.
*/
FactoryGenerator.prototype.loadToHiddenWorkspaceAndSave_ = function(xml, dom) {
this.hiddenWorkspace.clear();
Blockly.Xml.domToWorkspace(xml, this.hiddenWorkspace);
this.setShadowBlocksInHiddenWorkspace_();
this.appendHiddenWorkspaceToDom_(dom);
}
/**
* Encodes blocks in the hidden workspace in a XML DOM element. Very
* similar to workspaceToDom, but doesn't capture IDs. Uses the top-level
* blocks loaded in hiddenWorkspace.
* @private
*
* @param {!Element} xmlDom Tree of XML elements to be appended to.
*/
FactoryGenerator.prototype.appendHiddenWorkspaceToDom_ = function(xmlDom) {
var blocks = this.hiddenWorkspace.getTopBlocks();
for (var i = 0, block; block = blocks[i]; i++) {
var blockChild = Blockly.Xml.blockToDom(block);
blockChild.removeAttribute('id');
xmlDom.appendChild(blockChild);
}
};
/**
* Sets the user-generated shadow blocks loaded into hiddenWorkspace to be
* actual shadow blocks. This is done so that blockToDom records them as
* shadow blocks instead of regular blocks.
* @private
*
*/
FactoryGenerator.prototype.setShadowBlocksInHiddenWorkspace_ = function() {
var blocks = this.hiddenWorkspace.getAllBlocks();
for (var i = 0; i < blocks.length; i++) {
if (this.model.isShadowBlock(blocks[i].id)) {
blocks[i].setShadow(true);
}
}
};

View File

@@ -0,0 +1,450 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2016 Google Inc.
* https://developers.google.com/blockly/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Stores and updates information about state and categories
* in workspace factory. Each list element is either a separator or a category,
* and each category stores its name, XML to load that category, color,
* custom tags, and a unique ID making it possible to change category names and
* move categories easily. Keeps track of the currently selected list
* element. Also keeps track of all the user-created shadow blocks and
* manipulates them as necessary.
*
* @author Emma Dauterman (evd2014)
*/
/**
* Class for a FactoryModel
* @constructor
*/
FactoryModel = function() {
// Ordered list of ListElement objects.
this.toolboxList = [];
// Array of block IDs for all user created shadow blocks.
this.shadowBlocks = [];
// String name of current selected list element, null if no list elements.
this.selected = null;
// Boolean for if a Variable category has been added.
this.hasVariableCategory = false;
// Boolean for if a Procedure category has been added.
this.hasProcedureCategory = false;
};
// String name of current selected list element, null if no list elements.
FactoryModel.prototype.selected = null;
/**
* Given a name, determines if it is the name of a category already present.
* Used when getting a valid category name from the user.
*
* @param {string} name String name to be compared against.
* @return {boolean} True if string is a used category name, false otherwise.
*/
FactoryModel.prototype.hasCategoryByName = function(name) {
for (var i = 0; i < this.toolboxList.length; i++) {
if (this.toolboxList[i].type == ListElement.TYPE_CATEGORY &&
this.toolboxList[i].name == name) {
return true;
}
}
return false;
};
/**
* Determines if a category with the 'VARIABLE' tag exists.
*
* @return {boolean} True if there exists a category with the Variables tag,
* false otherwise.
*/
FactoryModel.prototype.hasVariables = function() {
return this.hasVariableCategory;
};
/**
* Determines if a category with the 'PROCEDURE' tag exists.
*
* @return {boolean} True if there exists a category with the Procedures tag,
* false otherwise.
*/
FactoryModel.prototype.hasProcedures = function() {
return this.hasFunctionCategory;
};
/**
* Determines if the user has any elements in the toolbox. Uses the length of
* toolboxList.
*
* @return {boolean} True if categories exist, false otherwise.
*/
FactoryModel.prototype.hasToolbox = function() {
return this.toolboxList.length > 0;
};
/**
* Given a ListElement, adds it to the toolbox list.
*
* @param {!ListElement} element The element to be added to the list.
*/
FactoryModel.prototype.addElementToList = function(element) {
// Update state if the copied category has a custom tag.
this.hasVariableCategory = element.custom == 'VARIABLE' ? true :
this.hasVariableCategory;
this.hasProcedureCategory = element.custom == 'PROCEDURE' ? true :
this.hasProcedureCategory;
// Add element to toolboxList.
this.toolboxList.push(element);
};
/**
* Given an index, deletes a list element and all associated data.
*
* @param {int} index The index of the list element to delete.
*/
FactoryModel.prototype.deleteElementFromList = function(index) {
// Check if index is out of bounds.
if (index < 0 || index >= this.toolboxList.length) {
return; // No entry to delete.
}
// Check if need to update flags.
this.hasVariableCategory = this.toolboxList[index].custom == 'VARIABLE' ?
false : this.hasVariableCategory;
this.hasProcedureCategory = this.toolboxList[index].custom == 'PROCEDURE' ?
false : this.hasProcedureCategory;
// Remove element.
this.toolboxList.splice(index, 1);
};
/**
* Moves a list element to a certain position in toolboxList by removing it
* and then inserting it at the correct index. Checks that indices are in
* bounds (throws error if not), but assumes that oldIndex is the correct index
* for list element.
*
* @param {!ListElement} element The element to move in toolboxList.
* @param {int} newIndex The index to insert the element at.
* @param {int} oldIndex The index the element is currently at.
*/
FactoryModel.prototype.moveElementToIndex = function(element, newIndex,
oldIndex) {
// Check that indexes are in bounds.
if (newIndex < 0 || newIndex >= this.toolboxList.length || oldIndex < 0 ||
oldIndex >= this.toolboxList.length) {
throw new Error('Index out of bounds when moving element in the model.');
}
this.deleteElementFromList(oldIndex);
this.toolboxList.splice(newIndex, 0, element);
}
/**
* Returns the ID of the currently selected element. Returns null if there are
* no categories (if selected == null).
*
* @return {string} The ID of the element currently selected.
*/
FactoryModel.prototype.getSelectedId = function() {
return this.selected ? this.selected.id : null;
};
/**
* Returns the name of the currently selected category. Returns null if there
* are no categories (if selected == null) or the selected element is not
* a category (in which case its name is null).
*
* @return {string} The name of the category currently selected.
*/
FactoryModel.prototype.getSelectedName = function() {
return this.selected ? this.selected.name : null;
};
/**
* Returns the currently selected list element object.
*
* @return {ListElement} The currently selected ListElement
*/
FactoryModel.prototype.getSelected = function() {
return this.selected;
};
/**
* Sets list element currently selected by id.
*
* @param {string} id ID of list element that should now be selected.
*/
FactoryModel.prototype.setSelectedById = function(id) {
this.selected = this.getElementById(id);
};
/**
* Given an ID of a list element, returns the index of that list element in
* toolboxList. Returns -1 if ID is not present.
*
* @param {!string} id The ID of list element to search for.
* @return {int} The index of the list element in toolboxList, or -1 if it
* doesn't exist.
*/
FactoryModel.prototype.getIndexByElementId = function(id) {
for (var i = 0; i < this.toolboxList.length; i++) {
if (this.toolboxList[i].id == id) {
return i;
}
}
return -1; // ID not present in toolboxList.
};
/**
* Given the ID of a list element, returns that ListElement object.
*
* @param {!string} id The ID of element to search for.
* @return {ListElement} Corresponding ListElement object in toolboxList, or
* null if that element does not exist.
*/
FactoryModel.prototype.getElementById = function(id) {
for (var i = 0; i < this.toolboxList.length; i++) {
if (this.toolboxList[i].id == id) {
return this.toolboxList[i];
}
}
return null; // ID not present in toolboxList.
};
/**
* Given the index of a list element in toolboxList, returns that ListElement
* object.
*
* @param {int} index The index of the element to return.
* @return {ListElement} The corresponding ListElement object in toolboxList.
*/
FactoryModel.prototype.getElementByIndex = function(index) {
if (index < 0 || index >= this.toolboxList.length) {
return null;
}
return this.toolboxList[index];
};
/**
* Returns the xml to load the selected element.
*
* @return {!Element} The XML of the selected element, or null if there is
* no selected element.
*/
FactoryModel.prototype.getSelectedXml = function() {
return this.selected ? this.selected.xml : null;
};
/**
* Return ordered list of ListElement objects.
*
* @return {!Array<!ListElement>} ordered list of ListElement objects
*/
FactoryModel.prototype.getToolboxList = function() {
return this.toolboxList;
};
/**
* Gets the ID of a category given its name.
*
* @param {string} name Name of category.
* @return {int} ID of category
*/
FactoryModel.prototype.getCategoryIdByName = function(name) {
for (var i = 0; i < this.toolboxList.length; i++) {
if (this.toolboxList[i].name == name) {
return this.toolboxList[i].id;
}
}
return null; // Name not present in toolboxList.
};
/**
* Clears the toolbox list, deleting all ListElements.
*/
FactoryModel.prototype.clearToolboxList = function() {
this.toolboxList = [];
this.hasVariableCategory = false;
this.hasVariableCategory = false;
// TODO(evd2014): When merge changes, also clear shadowList.
};
/**
* Class for a ListElement
* Adds a shadow block to the list of shadow blocks.
*
* @param {!string} blockId The unique ID of block to be added.
*/
FactoryModel.prototype.addShadowBlock = function(blockId) {
this.shadowBlocks.push(blockId);
};
/**
* Removes a shadow block ID from the list of shadow block IDs if that ID is
* in the list.
*
* @param {!string} blockId The unique ID of block to be removed.
*/
FactoryModel.prototype.removeShadowBlock = function(blockId) {
for (var i = 0; i < this.shadowBlocks.length; i++) {
if (this.shadowBlocks[i] == blockId) {
this.shadowBlocks.splice(i, 1);
return;
}
}
};
/**
* Determines if a block is a shadow block given a unique block ID.
*
* @param {!string} blockId The unique ID of the block to examine.
* @return {boolean} True if the block is a user-generated shadow block, false
* otherwise.
*/
FactoryModel.prototype.isShadowBlock = function(blockId) {
for (var i = 0; i < this.shadowBlocks.length; i++) {
if (this.shadowBlocks[i] == blockId) {
return true;
}
}
return false;
};
/**
* Given a set of blocks currently loaded, returns all blocks in the workspace
* that are user generated shadow blocks.
*
* @param {!<Blockly.Block>} blocks Array of blocks currently loaded.
* @return {!<Blockly.Block>} Array of user-generated shadow blocks currently
* loaded.
*/
FactoryModel.prototype.getShadowBlocksInWorkspace = function(workspaceBlocks) {
var shadowsInWorkspace = [];
for (var i = 0; i < workspaceBlocks.length; i++) {
if (this.isShadowBlock(workspaceBlocks[i].id)) {
shadowsInWorkspace.push(workspaceBlocks[i]);
}
}
return shadowsInWorkspace;
};
/**
* Adds a custom tag to a category, updating state variables accordingly.
* Only accepts 'VARIABLE' and 'PROCEDURE' tags.
*
* @param {!ListElement} category The category to add the tag to.
* @param {!string} tag The custom tag to add to the category.
*/
FactoryModel.prototype.addCustomTag = function(category, tag) {
// Only update list elements that are categories.
if (category.type != ListElement.TYPE_CATEGORY) {
return;
}
// Only update the tag to be 'VARIABLE' or 'PROCEDURE'.
if (tag == 'VARIABLE') {
this.hasVariableCategory = true;
category.custom = 'VARIABLE';
} else if (tag == 'PROCEDURE') {
this.hasProcedureCategory = true;
category.custom = 'PROCEDURE';
}
};
/**
* Class for a ListElement.
* @constructor
*/
ListElement = function(type, opt_name) {
this.type = type;
// XML DOM element to load the element.
this.xml = Blockly.Xml.textToDom('<xml></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.genUid();
// Color of category. Default is no color. Null if separator.
this.color = null;
// Stores a custom tag, if necessary. Null if no custom tag or separator.
this.custom = null;
};
// List element types.
ListElement.TYPE_CATEGORY = 'category';
ListElement.TYPE_SEPARATOR = 'separator';
/**
* Saves a category by updating its XML (does not save XML for
* elements that are not categories).
*
* @param {!Blockly.workspace} workspace The workspace to save category entry
* from.
*/
ListElement.prototype.saveFromWorkspace = function(workspace) {
// Only save list elements that are categories.
if (this.type != ListElement.TYPE_CATEGORY) {
return;
}
this.xml = Blockly.Xml.workspaceToDom(workspace);
};
/**
* Changes the name of a category object given a new name. Returns if
* not a category.
*
* @param {string} name New name of category.
*/
ListElement.prototype.changeName = function (name) {
// Only update list elements that are categories.
if (this.type != ListElement.TYPE_CATEGORY) {
return;
}
this.name = name;
};
/**
* Sets the color of a category. If tries to set the color of something other
* than a category, returns.
*
* @param {!string} color The color that should be used for that category.
*/
ListElement.prototype.changeColor = function (color) {
if (this.type != ListElement.TYPE_CATEGORY) {
return;
}
this.color = color;
};
/**
* Makes a copy of the original element and returns it. Everything about the
* copy is identical except for its ID.
*
* @return {!ListElement} The copy of the ListElement.
*/
ListElement.prototype.copy = function() {
copy = new ListElement(this.type);
// Generate a unique ID for the element.
copy.id = Blockly.genUid();
// Copy all attributes except ID.
copy.name = this.name;
copy.xml = this.xml;
copy.color = this.color;
copy.custom = this.custom;
// Return copy.
return copy;
};

View File

@@ -0,0 +1,341 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2016 Google Inc.
* https://developers.google.com/blockly/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Controls the UI elements for workspace factory, mainly the category tabs.
* Also includes downloading files because that interacts directly with the DOM.
* Depends on FactoryController (for adding mouse listeners). Tabs for each
* category are stored in tab map, which associates a unique ID for a
* category with a particular tab.
*
* @author Emma Dauterman (edauterman)
*/
/**
* Class for a FactoryView
* @constructor
*/
FactoryView = function() {
// For each tab, maps ID of a ListElement to the td DOM element.
this.tabMap = Object.create(null);
};
/**
* Adds a category tab to the UI, and updates tabMap accordingly.
*
* @param {!string} name The name of the category being created
* @param {!string} id ID of category being created
* @param {boolean} firstCategory true if it's the first category, false
* otherwise
* @return {!Element} DOM element created for tab
*/
FactoryView.prototype.addCategoryRow = function(name, id, firstCategory) {
var table = document.getElementById('categoryTable');
// Delete help label and enable category buttons if it's the first category.
if (firstCategory) {
table.deleteRow(0);
}
// Create tab.
var count = table.rows.length;
var row = table.insertRow(count);
var nextEntry = row.insertCell(0);
// Configure tab.
nextEntry.id = this.createCategoryIdName(name);
nextEntry.textContent = name;
// Store tab.
this.tabMap[id] = table.rows[count].cells[0];
// Return tab.
return nextEntry;
};
/**
* Deletes a category tab from the UI and updates tabMap accordingly.
*
* @param {!string} id ID of category to be deleted.
* @param {!string} name The name of the category to be deleted.
*/
FactoryView.prototype.deleteElementRow = function(id, index) {
// Delete tab entry.
delete this.tabMap[id];
// Delete tab row.
var table = document.getElementById('categoryTable');
var count = table.rows.length;
table.deleteRow(index);
// If last category removed, add category help text and disable category
// buttons.
this.addEmptyCategoryMessage();
};
/**
* If there are no toolbox elements created, adds a help message to show
* where categories will appear. Should be called when deleting list elements
* in case the last element is deleted.
*/
FactoryView.prototype.addEmptyCategoryMessage = function() {
var table = document.getElementById('categoryTable');
if (table.rows.length == 0) {
var row = table.insertRow(0);
row.textContent = 'Your categories will appear here';
}
}
/**
* Given the index of the currently selected element, updates the state of
* the buttons that allow the user to edit the list elements. Updates the edit
* and arrow buttons. Should be called when adding or removing elements
* or when changing to a new element or when swapping to a different element.
*
* TODO(evd2014): Switch to using CSS to add/remove styles.
*
* @param {int} selectedIndex The index of the currently selected category,
* -1 if no categories created.
* @param {ListElement} selected The selected ListElement.
*/
FactoryView.prototype.updateState = function(selectedIndex, selected) {
// Disable/enable editing buttons as necessary.
document.getElementById('button_editCategory').disabled = selectedIndex < 0 ||
selected.type != ListElement.TYPE_CATEGORY;
document.getElementById('button_remove').disabled = selectedIndex < 0;
document.getElementById('button_up').disabled =
selectedIndex <= 0 ? true : false;
var table = document.getElementById('categoryTable');
document.getElementById('button_down').disabled = selectedIndex >=
table.rows.length - 1 || selectedIndex < 0 ? true : false;
// Disable/enable the workspace as necessary.
this.disableWorkspace(this.shouldDisableWorkspace(selected));
};
/**
* Determines the DOM id for a category given its name.
*
* @param {!string} name Name of category
* @return {!string} ID of category tab
*/
FactoryView.prototype.createCategoryIdName = function(name) {
return 'tab_' + name;
};
/**
* Switches a tab on or off.
*
* @param {!string} id ID of the tab to switch on or off.
* @param {boolean} selected True if tab should be on, false if tab should be
* off.
*/
FactoryView.prototype.setCategoryTabSelection = function(id, selected) {
if (!this.tabMap[id]) {
return; // Exit if tab does not exist.
}
this.tabMap[id].className = selected ? 'tabon' : 'taboff';
};
/**
* Used to bind a click to a certain DOM element (used for category tabs).
* Taken directly from code.js
*
* @param {string|!Element} e1 tab element or corresponding id string
* @param {!Function} func Function to be executed on click
*/
FactoryView.prototype.bindClick = function(el, func) {
if (typeof el == 'string') {
el = document.getElementById(el);
}
el.addEventListener('click', func, true);
el.addEventListener('touchend', func, true);
};
/**
* Creates a file and downloads it. In some browsers downloads, and in other
* browsers, opens new tab with contents.
*
* @param {!string} filename Name of file
* @param {!Blob} data Blob containing contents to download
*/
FactoryView.prototype.createAndDownloadFile = function(filename, data) {
var clickEvent = new MouseEvent("click", {
"view": window,
"bubbles": true,
"cancelable": false
});
var a = document.createElement('a');
a.href = window.URL.createObjectURL(data);
a.download = filename;
a.textContent = 'Download file!';
a.dispatchEvent(clickEvent);
};
/**
* Given the ID of a certain category, updates the corresponding tab in
* the DOM to show a new name.
*
* @param {!string} newName Name of string to be displayed on tab
* @param {!string} id ID of category to be updated
*
*/
FactoryView.prototype.updateCategoryName = function(newName, id) {
this.tabMap[id].textContent = newName;
this.tabMap[id].id = this.createCategoryIdName(newName);
};
/**
* Moves a tab from one index to another. Adjusts index inserting before
* based on if inserting before or after. Checks that the indexes are in
* bounds, throws error if not.
*
* @param {!string} id The ID of the category to move.
* @param {int} newIndex The index to move the category to.
* @param {int} oldIndex The index the category is currently at.
*/
FactoryView.prototype.moveTabToIndex = function(id, newIndex, oldIndex) {
var table = document.getElementById('categoryTable');
// Check that indexes are in bounds
if (newIndex < 0 || newIndex >= table.rows.length || oldIndex < 0 ||
oldIndex >= table.rows.length) {
throw new Error('Index out of bounds when moving tab in the view.');
}
if (newIndex < oldIndex) { // Inserting before.
var row = table.insertRow(newIndex);
row.appendChild(this.tabMap[id]);
table.deleteRow(oldIndex + 1);
} else { // Inserting after.
var row = table.insertRow(newIndex + 1);
row.appendChild(this.tabMap[id]);
table.deleteRow(oldIndex);
}
};
/**
* Given a category ID and color, use that color to color the left border of the
* tab for that category.
*
* @param {!string} id The ID of the category to color.
* @param {!string} color The color for to be used for the border of the tab.
* Must be a valid CSS string.
*/
FactoryView.prototype.setBorderColor = function(id, color) {
var tab = this.tabMap[id];
tab.style.borderLeftWidth = "8px";
tab.style.borderLeftStyle = "solid";
tab.style.borderColor = color;
};
/**
* Given a separator ID, creates a corresponding tab in the view, updates
* tab map, and returns the tab.
*
* @param {!string} id The ID of the separator.
* @param {!Element} The td DOM element representing the separator.
*/
FactoryView.prototype.addSeparatorTab = function(id) {
// Create separator.
var table = document.getElementById('categoryTable');
var count = table.rows.length;
var row = table.insertRow(count);
var nextEntry = row.insertCell(0);
// Configure separator.
nextEntry.style.height = '10px';
// Store and return separator.
this.tabMap[id] = table.rows[count].cells[0];
return nextEntry;
};
/**
* Disables or enables the workspace by putting a div over or under the
* toolbox workspace, depending on the value of disable. Used when switching
* to/from separators where the user shouldn't be able to drag blocks into
* the workspace.
*
* @param {boolean} disable True if the workspace should be disabled, false
* if it should be enabled.
*/
FactoryView.prototype.disableWorkspace = function(disable) {
document.getElementById('disable_div').style.zIndex = disable ? 1 : -1;
};
/**
* Determines if the workspace should be disabled. The workspace should be
* disabled if category is a separator or has VARIABLE or PROCEDURE tags.
*
* @return {boolean} True if the workspace should be disabled, false otherwise.
*/
FactoryView.prototype.shouldDisableWorkspace = function(category) {
return category != null && (category.type == ListElement.TYPE_SEPARATOR ||
category.custom == 'VARIABLE' || category.custom == 'PROCEDURE');
};
/*
* Removes all categories and separators in the view. Clears the tabMap to
* reflect this.
*/
FactoryView.prototype.clearToolboxTabs = function() {
this.tabMap = [];
var oldCategoryTable = document.getElementById('categoryTable');
var newCategoryTable = document.createElement('table');
newCategoryTable.id = 'categoryTable';
oldCategoryTable.parentElement.replaceChild(newCategoryTable,
oldCategoryTable);
};
/**
* Given a set of blocks currently loaded user-generated shadow blocks, visually
* marks them without making them actual shadow blocks (allowing them to still
* be editable and movable).
*
* @param {!<Blockly.Block>} blocks Array of user-generated shadow blocks
* currently loaded.
*/
FactoryView.prototype.markShadowBlocks = function(blocks) {
for (var i = 0; i < blocks.length; i++) {
this.markShadowBlock(blocks[i]);
}
};
/**
* Visually marks a user-generated shadow block as a shadow block in the
* workspace without making the block an actual shadow block (allowing it
* to be moved and edited).
*
* @param {!Blockly.Block} block The block that should be marked as a shadow
* block (must be rendered).
*/
FactoryView.prototype.markShadowBlock = function(block) {
// Add Blockly CSS for user-generated shadow blocks.
Blockly.addClass_(block.svgGroup_, 'shadowBlock');
// If not a valid shadow block, add a warning message.
if (!block.getSurroundParent()) {
block.setWarningText('Shadow blocks must be nested inside' +
' other blocks to be displayed.');
}
};
/**
* Removes visual marking for a shadow block given a rendered block.
*
* @param {!Blockly.Block} block The block that should be unmarked as a shadow
* block (must be rendered).
*/
FactoryView.prototype.unmarkShadowBlock = function(block) {
// Remove Blockly CSS for user-generated shadow blocks.
if (Blockly.hasClass_(block.svgGroup_, 'shadowBlock')) {
Blockly.removeClass_(block.svgGroup_, 'shadowBlock');
}
};