mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-08-28 22:10:31 -04:00
* Done adding warning if not using defined block * Nit comments * Nit warning message * Small fix for clear * Fixed merging errors
593 lines
18 KiB
JavaScript
593 lines
18 KiB
JavaScript
/**
|
|
* @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 WorkspaceFactoryModel
|
|
* @constructor
|
|
*/
|
|
WorkspaceFactoryModel = function() {
|
|
// Ordered list of ListElement objects. Empty if there is a single flyout.
|
|
this.toolboxList = [];
|
|
// ListElement for blocks in a single flyout. Null if a toolbox exists.
|
|
this.flyout = new ListElement(ListElement.TYPE_FLYOUT);
|
|
// Array of block IDs for all user created shadow blocks.
|
|
this.shadowBlocks = [];
|
|
// Reference to currently selected ListElement. Stored in this.toolboxList if
|
|
// there are categories, or in this.flyout if blocks are displayed in a single
|
|
// flyout.
|
|
this.selected = this.flyout;
|
|
// Boolean for if a Variable category has been added.
|
|
this.hasVariableCategory = false;
|
|
// Boolean for if a Procedure category has been added.
|
|
this.hasProcedureCategory = false;
|
|
// XML to be pre-loaded to workspace. Empty on default;
|
|
this.preloadXml = Blockly.Xml.textToDom('<xml></xml>');
|
|
// Options object to be configured for Blockly inject call.
|
|
this.options = new Object(null);
|
|
// Block Library block types.
|
|
this.libBlockTypes = [];
|
|
// Imported block types.
|
|
this.importedBlockTypes = [];
|
|
//
|
|
};
|
|
|
|
/**
|
|
* Given a name, determines if it is the name of a category already present.
|
|
* Used when getting a valid category name from the user.
|
|
*
|
|
* @param {string} name String name to be compared against.
|
|
* @return {boolean} True if string is a used category name, false otherwise.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.hasCategoryByName = function(name) {
|
|
for (var i = 0; i < this.toolboxList.length; i++) {
|
|
if (this.toolboxList[i].type == ListElement.TYPE_CATEGORY &&
|
|
this.toolboxList[i].name == name) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Determines if a category with the 'VARIABLE' tag exists.
|
|
*
|
|
* @return {boolean} True if there exists a category with the Variables tag,
|
|
* false otherwise.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.hasVariables = function() {
|
|
return this.hasVariableCategory;
|
|
};
|
|
|
|
/**
|
|
* Determines if a category with the 'PROCEDURE' tag exists.
|
|
*
|
|
* @return {boolean} True if there exists a category with the Procedures tag,
|
|
* false otherwise.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.hasProcedures = function() {
|
|
return this.hasFunctionCategory;
|
|
};
|
|
|
|
/**
|
|
* Determines if the user has any elements in the toolbox. Uses the length of
|
|
* toolboxList.
|
|
*
|
|
* @return {boolean} True if elements exist, false otherwise.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.hasElements = function() {
|
|
return this.toolboxList.length > 0;
|
|
};
|
|
|
|
/**
|
|
* Given a ListElement, adds it to the toolbox list.
|
|
*
|
|
* @param {!ListElement} element The element to be added to the list.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.addElementToList = function(element) {
|
|
// Update state if the copied category has a custom tag.
|
|
this.hasVariableCategory = element.custom == 'VARIABLE' ? true :
|
|
this.hasVariableCategory;
|
|
this.hasProcedureCategory = element.custom == 'PROCEDURE' ? true :
|
|
this.hasProcedureCategory;
|
|
// Add element to toolboxList.
|
|
this.toolboxList.push(element);
|
|
// Empty single flyout.
|
|
this.flyout = null;
|
|
};
|
|
|
|
/**
|
|
* Given an index, deletes a list element and all associated data.
|
|
*
|
|
* @param {int} index The index of the list element to delete.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.deleteElementFromList = function(index) {
|
|
// Check if index is out of bounds.
|
|
if (index < 0 || index >= this.toolboxList.length) {
|
|
return; // No entry to delete.
|
|
}
|
|
// Check if need to update flags.
|
|
this.hasVariableCategory = this.toolboxList[index].custom == 'VARIABLE' ?
|
|
false : this.hasVariableCategory;
|
|
this.hasProcedureCategory = this.toolboxList[index].custom == 'PROCEDURE' ?
|
|
false : this.hasProcedureCategory;
|
|
// Remove element.
|
|
this.toolboxList.splice(index, 1);
|
|
};
|
|
|
|
/**
|
|
* Sets selected to be an empty category not in toolbox list if toolbox list
|
|
* is empty. Should be called when removing the last element from toolbox list.
|
|
* If the toolbox list is empty, selected stores the XML for the single flyout
|
|
* of blocks displayed.
|
|
*
|
|
*/
|
|
WorkspaceFactoryModel.prototype.createDefaultSelectedIfEmpty = function() {
|
|
if (this.toolboxList.length == 0) {
|
|
this.flyout = new ListElement(ListElement.TYPE_FLYOUT);
|
|
this.selected = this.flyout;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves a list element to a certain position in toolboxList by removing it
|
|
* and then inserting it at the correct index. Checks that indices are in
|
|
* bounds (throws error if not), but assumes that oldIndex is the correct index
|
|
* for list element.
|
|
*
|
|
* @param {!ListElement} element The element to move in toolboxList.
|
|
* @param {int} newIndex The index to insert the element at.
|
|
* @param {int} oldIndex The index the element is currently at.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.moveElementToIndex = function(element, newIndex,
|
|
oldIndex) {
|
|
// Check that indexes are in bounds.
|
|
if (newIndex < 0 || newIndex >= this.toolboxList.length || oldIndex < 0 ||
|
|
oldIndex >= this.toolboxList.length) {
|
|
throw 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.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.getSelectedId = function() {
|
|
return this.selected ? this.selected.id : null;
|
|
};
|
|
|
|
/**
|
|
* Returns the name of the currently selected category. Returns null if there
|
|
* are no categories (if selected == null) or the selected element is not
|
|
* a category (in which case its name is null).
|
|
*
|
|
* @return {string} The name of the category currently selected.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.getSelectedName = function() {
|
|
return this.selected ? this.selected.name : null;
|
|
};
|
|
|
|
/**
|
|
* Returns the currently selected list element object.
|
|
*
|
|
* @return {ListElement} The currently selected ListElement
|
|
*/
|
|
WorkspaceFactoryModel.prototype.getSelected = function() {
|
|
return this.selected;
|
|
};
|
|
|
|
/**
|
|
* Sets list element currently selected by id.
|
|
*
|
|
* @param {string} id ID of list element that should now be selected.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.setSelectedById = function(id) {
|
|
this.selected = this.getElementById(id);
|
|
};
|
|
|
|
/**
|
|
* Given an ID of a list element, returns the index of that list element in
|
|
* toolboxList. Returns -1 if ID is not present.
|
|
*
|
|
* @param {!string} id The ID of list element to search for.
|
|
* @return {int} The index of the list element in toolboxList, or -1 if it
|
|
* doesn't exist.
|
|
*/
|
|
|
|
WorkspaceFactoryModel.prototype.getIndexByElementId = function(id) {
|
|
for (var i = 0; i < this.toolboxList.length; i++) {
|
|
if (this.toolboxList[i].id == id) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1; // ID not present in toolboxList.
|
|
};
|
|
|
|
/**
|
|
* Given the ID of a list element, returns that ListElement object.
|
|
*
|
|
* @param {!string} id The ID of element to search for.
|
|
* @return {ListElement} Corresponding ListElement object in toolboxList, or
|
|
* null if that element does not exist.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.getElementById = function(id) {
|
|
for (var i = 0; i < this.toolboxList.length; i++) {
|
|
if (this.toolboxList[i].id == id) {
|
|
return this.toolboxList[i];
|
|
}
|
|
}
|
|
return null; // ID not present in toolboxList.
|
|
};
|
|
|
|
/**
|
|
* Given the index of a list element in toolboxList, returns that ListElement
|
|
* object.
|
|
*
|
|
* @param {int} index The index of the element to return.
|
|
* @return {ListElement} The corresponding ListElement object in toolboxList.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.getElementByIndex = function(index) {
|
|
if (index < 0 || index >= this.toolboxList.length) {
|
|
return null;
|
|
}
|
|
return this.toolboxList[index];
|
|
};
|
|
|
|
/**
|
|
* Returns the xml to load the selected element.
|
|
*
|
|
* @return {!Element} The XML of the selected element, or null if there is
|
|
* no selected element.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.getSelectedXml = function() {
|
|
return this.selected ? this.selected.xml : null;
|
|
};
|
|
|
|
/**
|
|
* Return ordered list of ListElement objects.
|
|
*
|
|
* @return {!Array<!ListElement>} ordered list of ListElement objects
|
|
*/
|
|
WorkspaceFactoryModel.prototype.getToolboxList = function() {
|
|
return this.toolboxList;
|
|
};
|
|
|
|
/**
|
|
* Gets the ID of a category given its name.
|
|
*
|
|
* @param {string} name Name of category.
|
|
* @return {int} ID of category
|
|
*/
|
|
WorkspaceFactoryModel.prototype.getCategoryIdByName = function(name) {
|
|
for (var i = 0; i < this.toolboxList.length; i++) {
|
|
if (this.toolboxList[i].name == name) {
|
|
return this.toolboxList[i].id;
|
|
}
|
|
}
|
|
return null; // Name not present in toolboxList.
|
|
};
|
|
|
|
/**
|
|
* Clears the toolbox list, deleting all ListElements.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.clearToolboxList = function() {
|
|
this.toolboxList = [];
|
|
this.hasVariableCategory = false;
|
|
this.hasProcedureCategory = false;
|
|
this.shadowBlocks = [];
|
|
this.selected.xml = Blockly.Xml.textToDom('<xml></xml>');
|
|
};
|
|
|
|
/**
|
|
* Class for a ListElement
|
|
* Adds a shadow block to the list of shadow blocks.
|
|
*
|
|
* @param {!string} blockId The unique ID of block to be added.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.addShadowBlock = function(blockId) {
|
|
this.shadowBlocks.push(blockId);
|
|
};
|
|
|
|
/**
|
|
* Removes a shadow block ID from the list of shadow block IDs if that ID is
|
|
* in the list.
|
|
*
|
|
* @param {!string} blockId The unique ID of block to be removed.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.removeShadowBlock = function(blockId) {
|
|
for (var i = 0; i < this.shadowBlocks.length; i++) {
|
|
if (this.shadowBlocks[i] == blockId) {
|
|
this.shadowBlocks.splice(i, 1);
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Determines if a block is a shadow block given a unique block ID.
|
|
*
|
|
* @param {!string} blockId The unique ID of the block to examine.
|
|
* @return {boolean} True if the block is a user-generated shadow block, false
|
|
* otherwise.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.isShadowBlock = function(blockId) {
|
|
for (var i = 0; i < this.shadowBlocks.length; i++) {
|
|
if (this.shadowBlocks[i] == blockId) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Given a set of blocks currently loaded, returns all blocks in the workspace
|
|
* that are user generated shadow blocks.
|
|
*
|
|
* @param {!<Blockly.Block>} blocks Array of blocks currently loaded.
|
|
* @return {!<Blockly.Block>} Array of user-generated shadow blocks currently
|
|
* loaded.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.getShadowBlocksInWorkspace =
|
|
function(workspaceBlocks) {
|
|
var shadowsInWorkspace = [];
|
|
for (var i = 0; i < workspaceBlocks.length; i++) {
|
|
if (this.isShadowBlock(workspaceBlocks[i].id)) {
|
|
shadowsInWorkspace.push(workspaceBlocks[i]);
|
|
}
|
|
}
|
|
return shadowsInWorkspace;
|
|
};
|
|
|
|
/**
|
|
* Adds a custom tag to a category, updating state variables accordingly.
|
|
* Only accepts 'VARIABLE' and 'PROCEDURE' tags.
|
|
*
|
|
* @param {!ListElement} category The category to add the tag to.
|
|
* @param {!string} tag The custom tag to add to the category.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.addCustomTag = function(category, tag) {
|
|
// Only update list elements that are categories.
|
|
if (category.type != ListElement.TYPE_CATEGORY) {
|
|
return;
|
|
}
|
|
// Only update the tag to be 'VARIABLE' or 'PROCEDURE'.
|
|
if (tag == 'VARIABLE') {
|
|
this.hasVariableCategory = true;
|
|
category.custom = 'VARIABLE';
|
|
} else if (tag == 'PROCEDURE') {
|
|
this.hasProcedureCategory = true;
|
|
category.custom = 'PROCEDURE';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Have basic pre-loaded workspace working
|
|
* Saves XML as XML to be pre-loaded into the workspace.
|
|
*
|
|
* @param {!Element} xml The XML to be saved.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.savePreloadXml = function(xml) {
|
|
this.preloadXml = xml
|
|
};
|
|
|
|
/**
|
|
* Gets the XML to be pre-loaded into the workspace.
|
|
*
|
|
* @return {!Element} The XML for the workspace.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.getPreloadXml = function() {
|
|
return this.preloadXml;
|
|
};
|
|
|
|
/**
|
|
* Sets a new options object for injecting a Blockly workspace.
|
|
*
|
|
* @param {Object} options Options object for injecting a Blockly workspace.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.setOptions = function(options) {
|
|
this.options = options;
|
|
};
|
|
|
|
/**
|
|
* Sets an attribute of the options object.
|
|
*
|
|
* @param {!string} name Name of the attribute to add.
|
|
* @param {Object} value The value of the attribute to add.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.setOptionsAttribute = function(name, value) {
|
|
this.options[name] = value;
|
|
};
|
|
|
|
/*
|
|
* Returns an array of all the block types currently being used in the toolbox
|
|
* and the pre-loaded blocks. No duplicates.
|
|
* TODO(evd2014): Move pushBlockTypesToList to FactoryUtils.
|
|
*
|
|
* @return {!Array<!string>} Array of block types currently being used.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.getAllUsedBlockTypes = function() {
|
|
var blockTypeList = [];
|
|
|
|
// Given XML for the workspace, adds all block types included in the XML
|
|
// to the list, not including duplicates.
|
|
var pushBlockTypesToList = function(xml, list) {
|
|
// Get all block XML nodes.
|
|
var blocks = xml.getElementsByTagName('block');
|
|
|
|
// Add block types if not already in list.
|
|
for (var i = 0; i < blocks.length; i++) {
|
|
var type = blocks[i].getAttribute('type');
|
|
if (list.indexOf(type) == -1) {
|
|
list.push(type);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (this.flyout) {
|
|
// If has a single flyout, add block types for the single flyout.
|
|
pushBlockTypesToList(this.getSelectedXml(), blockTypeList);
|
|
} else {
|
|
// If has categories, add block types for each category.
|
|
|
|
for (var i = 0, category; category = this.toolboxList[i]; i++) {
|
|
if (category.type == ListElement.TYPE_CATEGORY) {
|
|
pushBlockTypesToList(category.xml, blockTypeList);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the block types from any pre-loaded blocks.
|
|
pushBlockTypesToList(this.getPreloadXml(), blockTypeList);
|
|
|
|
return blockTypeList;
|
|
};
|
|
|
|
/**
|
|
* Adds new imported block types to the list of current imported block types.
|
|
*
|
|
* @param {!Array<!string>} blockTypes Array of block types imported.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.addImportedBlockTypes = function(blockTypes) {
|
|
this.importedBlockTypes = this.importedBlockTypes.concat(blockTypes);
|
|
};
|
|
|
|
/**
|
|
* Updates block types in block library.
|
|
*
|
|
* @param {!Array<!string>} blockTypes Array of block types in block library.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.updateLibBlockTypes = function(blockTypes) {
|
|
this.libBlockTypes = blockTypes;
|
|
};
|
|
|
|
/**
|
|
* Determines if a block type is defined as a standard block, in the block
|
|
* library, or as an imported block.
|
|
*
|
|
* @param {!string} blockType Block type to check.
|
|
* @return {boolean} True if blockType is defined, false otherwise.
|
|
*/
|
|
WorkspaceFactoryModel.prototype.isDefinedBlockType = function(blockType) {
|
|
var isStandardBlock = StandardCategories.coreBlockTypes.indexOf(blockType)
|
|
!= -1;
|
|
var isLibBlock = this.libBlockTypes.indexOf(blockType) != -1;
|
|
var isImportedBlock = this.importedBlockTypes.indexOf(blockType) != -1;
|
|
return (isStandardBlock || isLibBlock || isImportedBlock);
|
|
}
|
|
|
|
/**
|
|
* 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';
|
|
ListElement.TYPE_FLYOUT = 'flyout';
|
|
|
|
/**
|
|
* Saves a category by updating its XML (does not save XML for
|
|
* elements that are not categories).
|
|
*
|
|
* @param {!Blockly.workspace} workspace The workspace to save category entry
|
|
* from.
|
|
*/
|
|
ListElement.prototype.saveFromWorkspace = function(workspace) {
|
|
// Only save XML for categories and flyouts.
|
|
if (this.type == ListElement.TYPE_FLYOUT ||
|
|
this.type == ListElement.TYPE_CATEGORY) {
|
|
this.xml = Blockly.Xml.workspaceToDom(workspace);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Changes the name of a category object given a new name. Returns if
|
|
* not a category.
|
|
*
|
|
* @param {string} name New name of category.
|
|
*/
|
|
ListElement.prototype.changeName = function (name) {
|
|
// Only update list elements that are categories.
|
|
if (this.type != ListElement.TYPE_CATEGORY) {
|
|
return;
|
|
}
|
|
this.name = name;
|
|
};
|
|
|
|
/**
|
|
* Sets the color of a category. If tries to set the color of something other
|
|
* than a category, returns.
|
|
*
|
|
* @param {!string} color The color that should be used for that category.
|
|
*/
|
|
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;
|
|
};
|