Skip to content

Instantly share code, notes, and snippets.

@jollytoad
Last active August 23, 2018 13:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jollytoad/7a7df5272aa7d767463fcb391a215f86 to your computer and use it in GitHub Desktop.
Save jollytoad/7a7df5272aa7d767463fcb391a215f86 to your computer and use it in GitHub Desktop.
Multi-type variable support for Blockly
/**
* Check whether the given variable type is allowed on this field.
* @param {string|Array<string>} type The type (or types) to check.
* @return {boolean} True if the type (or one of the types) is in the list of allowed types.
* @private
*/
Blockly.FieldVariable.prototype.typeIsAllowed_ = function (type) {
var typeList = this.getVariableTypes_()
if (!typeList) {
return true // If it's null, all types are valid.
}
var types = Array.isArray(type) ? type : [type]
// Find any intersection in the type lists.
for (var i = 0; i < types.length; i++) {
if (typeList.indexOf(types[i]) != -1) {
return true
}
}
return false
}
/**
* Parse the optional arguments representing the allowed variable types and the
* default variable type.
* @param {Array.<string>=} opt_variableTypes A list of the types of variables
* to include in the dropdown. If null or undefined, variables of all types
* will be displayed in the dropdown.
* @param {string|Array<string>=} opt_defaultType The type of the variable to create if this
* field's value is not explicitly set. Defaults to ''.
* @private
*/
Blockly.FieldVariable.prototype.setTypes_ = function (opt_variableTypes,
opt_defaultType) {
// If you expected that the default type would be the same as the only entry
// in the variable types array, tell the Blockly team by commenting on #1499.
var defaultTypes = Array.isArray(opt_defaultType) ? opt_defaultType : [opt_defaultType || '']
// Set the allowable variable types. Null means all types on the workspace.
if (opt_variableTypes == null || opt_variableTypes == undefined) {
var variableTypes = null
} else if (Array.isArray(opt_variableTypes)) {
var variableTypes = opt_variableTypes
// Make sure the default type is valid.
var isInArray = false
// Find any intersection in the type lists.
loop:
for (var i = 0; i < variableTypes.length; i++) {
for (var j = 0; j < defaultTypes.length; j++) {
if (variableTypes[i].indexOf(defaultTypes[j]) != -1) {
isInArray = true
break loop;
}
}
}
if (!isInArray) {
throw new Error('Invalid default type \'' + defaultTypes + '\' in ' +
'the definition of a FieldVariable')
}
} else {
throw new Error('\'variableTypes\' was not an array in the definition of ' +
'a FieldVariable')
}
// Only update the field once all checks pass.
this.defaultType_ = opt_defaultType || ''
this.variableTypes = variableTypes
}
/**
* Return a sorted list of variable names for variable dropdown menus.
* Include a special option at the end for creating a new variable name.
* @return {!Array.<string>} Array of variable names.
* @this {Blockly.FieldVariable}
*/
Blockly.FieldVariable.dropdownCreate = function () {
if (!this.variable_) {
throw new Error('Tried to call dropdownCreate on a variable field with no' +
' variable selected.')
}
var name = this.getText()
var workspace = null
if (this.sourceBlock_) {
workspace = this.sourceBlock_.workspace
}
var variableModelList = []
if (workspace) {
var variableTypes = this.getVariableTypes_()
// Get a copy of the list, so that adding rename and new variable options
// doesn't modify the workspace's list.
for (var i = 0; i < variableTypes.length; i++) {
var variableType = variableTypes[i]
var variables = workspace.getVariablesOfType(variableType)
variableModelList = variableModelList.concat(variables)
}
}
variableModelList.sort(Blockly.VariableModel.compareByName)
var options = []
for (var i = 0, prevId = null; i < variableModelList.length; i++) {
var id = variableModelList[i].getId()
if (id !== prevId) {
// Set the UUID as the internal representation of the variable.
options.push([variableModelList[i].name, id])
prevId = id
}
}
options.push([Blockly.Msg['RENAME_VARIABLE'], Blockly.RENAME_VARIABLE_ID])
if (Blockly.Msg['DELETE_VARIABLE']) {
options.push(
[
Blockly.Msg['DELETE_VARIABLE'].replace('%1', name),
Blockly.DELETE_VARIABLE_ID
]
)
}
return options
}
/**
* Rename the given variable by updating its name in the variable map.
* @param {!Blockly.VariableModel} variable Variable to rename.
* @param {string} newName New variable name.
* @package
*/
Blockly.VariableMap.prototype.renameVariable = function (variable, newName) {
var types = Array.isArray(variable.type) ? variable.type : [variable.type]
Blockly.Events.setGroup(true)
try {
for (var i = 0; i < types.length; i++) {
var type = types[i]
var conflictVar = this.getVariable(newName, type)
var blocks = this.workspace.getAllBlocks()
// The IDs may match if the rename is a simple case change (name1 -> Name1).
if (!conflictVar || conflictVar.getId() == variable.getId()) {
this.renameVariableAndUses_(variable, newName, blocks)
} else {
this.renameVariableWithConflict_(variable, newName, conflictVar, blocks)
}
}
} finally {
Blockly.Events.setGroup(false)
}
}
/**
* Create a variable with a given name, optional type(s), and optional ID.
* @param {string} name The name of the variable. This must be unique across
* variables and procedures.
* @param {string|Array<string>=} opt_type The type of the variable like 'int' or 'string'.
* Does not need to be unique. Field_variable can filter variables based on
* their type. This will default to '' which is a specific type.
* @param {string=} opt_id The unique ID of the variable. This will default to
* a UUID.
* @return {Blockly.VariableModel} The newly created variable.
*/
Blockly.VariableMap.prototype.createVariable = function (name, opt_type, opt_id) {
var variable = this.getVariable(name, opt_type)
if (variable) {
if (opt_id && variable.getId() != opt_id) {
throw Error('Variable "' + name + '" is already in use and its id is "' +
variable.getId() + '" which conflicts with the passed in ' +
'id, "' + opt_id + '".')
}
// The variable already exists and has the same ID.
return variable
}
if (opt_id && this.getVariableById(opt_id)) {
throw Error('Variable id, "' + opt_id + '", is already in use.')
}
opt_id = opt_id || Blockly.utils.genUid()
opt_type = opt_type || ''
variable = new Blockly.VariableModel(this.workspace, name, opt_type, opt_id)
this.addVariable_(variable)
return variable
}
/**
* Add the variable to the maps by its type
* @param {Blockly.VariableModel} variable
* @private
*/
Blockly.VariableMap.prototype.addVariable_ = function (variable) {
var types = Array.isArray(variable.type) ? variable.type : [variable.type]
for (var i = 0; i < types.length; i++) {
var type = types[i]
// If opt_type is not a key, create a new list.
if (!this.variableMap_[type]) {
this.variableMap_[type] = [variable]
} else {
// Else append the variable to the preexisting list.
this.variableMap_[type].push(variable)
}
}
}
/* Begin functions for variable deletion. */
/**
* Delete a variable.
* @param {!Blockly.VariableModel} variable Variable to delete.
*/
Blockly.VariableMap.prototype.deleteVariable = function (variable) {
if (this.removeVariable_(variable)) {
Blockly.Events.fire(new Blockly.Events.VarDelete(variable))
}
}
/**
* Remove the variable from the maps by its type
* @param {Blockly.VariableModel} variable
* @return {boolean} True if the variable was removed from at least one of the maps
* @private
*/
Blockly.VariableMap.prototype.removeVariable_ = function (variable) {
var types = Array.isArray(variable.type) ? variable.type : [variable.type]
var removed = false
for (var i = 0; i < types.length; i++) {
var type = types[i]
var variableList = this.variableMap_[type]
for (var j = 0, tempVar; tempVar = variableList[j]; j++) {
if (tempVar.getId() == variable.getId()) {
variableList.splice(j, 1)
removed = true
break
}
}
}
return removed
}
/**
* Find the variable by the given name and type and return it. Return null if
* it is not found.
* @param {string} name The name to check for.
* @param {string|Array<string>=} opt_type The type of the variable. If not provided it
* defaults to the empty string, which is a specific type.
* @return {Blockly.VariableModel} The variable with the given name, or null if
* it was not found.
*/
Blockly.VariableMap.prototype.getVariable = function (name, opt_type) {
var types = Array.isArray(opt_type) ? opt_type : [opt_type || '']
for (var i = 0; i < types.length; i++) {
var type = types[i]
var list = this.variableMap_[type]
if (list) {
for (var j = 0, variable; variable = list[j]; j++) {
if (Blockly.Names.equals(variable.name, name)) {
return variable
}
}
}
}
return null
}
/**
* Find the variable by the given ID and return it. Return null if it is not
* found.
* @param {string} id The ID to check for.
* @return {Blockly.VariableModel} The variable with the given ID.
*/
Blockly.VariableMap.prototype.getVariableById = function (id) {
var keys = Object.keys(this.variableMap_)
for (var i = 0; i < keys.length; i++) {
var key = keys[i]
for (var j = 0, variable; variable = this.variableMap_[key][j]; j++) {
if (variable.getId() == id) {
return variable
}
}
}
return null
}
/**
* Return all variable types. This list always contains the empty string.
* @return {!Array.<string>} List of variable types.
* @package
*/
Blockly.VariableMap.prototype.getVariableTypes = function () {
var types = Object.keys(this.variableMap_)
var hasEmpty = false
for (var i = 0; i < types.length; i++) {
if (types[i] == '') {
hasEmpty = true
}
}
if (!hasEmpty) {
types.push('')
}
return types
}
/**
* Return all variables of all types.
* @return {!Array.<!Blockly.VariableModel>} List of variable models.
*/
Blockly.VariableMap.prototype.getAllVariables = function () {
var variable_ids = {}
var all_variables = []
var keys = Object.keys(this.variableMap_)
for (var i = 0; i < keys.length; i++) {
var variables = this.variableMap_[keys[i]]
for (var j = 0; j < variables.length; j++) {
var variable = variables[j]
if (!variable_ids[variable.getId()]) {
all_variables.push(variable)
variable_ids[variable.getId()] = true
}
}
}
return all_variables
}
// EXTENSION (to allow changing of a variable type)
/**
* Change the type of the given variable by moving it to the appropriate variable map.
* @param {!Blockly.VariableModel} variable Variable to change.
* @param {string|Array<string>=} opt_type New type for the variable.
*/
Blockly.VariableMap.prototype.changeVariableType = function (variable, opt_type) {
var oldType = variable.type
var newType = opt_type || ''
if (String(oldType) !== String(newType)) {
this.removeVariable_(variable)
variable.type = newType
this.addVariable_(variable)
// TODO: consider firing a variable event? may need to be a new event type...
// Blockly.Events.fire(new Blockly.Events.VarTypeChanged(variable, oldType, newType))
}
}
/**
* Decode an XML list of variables and add the variables to the workspace.
* @param {!Element} xmlVariables List of XML variable elements.
* @param {!Blockly.Workspace} workspace The workspace to which the variable
* should be added.
*/
Blockly.Xml.domToVariables = function(xmlVariables, workspace) {
for (var i = 0, xmlChild; xmlChild = xmlVariables.children[i]; i++) {
var type = xmlChild.getAttribute('type');
var id = xmlChild.getAttribute('id');
var name = xmlChild.textContent;
if (type == null) {
throw Error('Variable with id, ' + id + ' is without a type');
}
type = type.indexOf(',') > -1 ? type.split(',') : type
workspace.createVariable(name, type, id);
}
};
/**
* Decode an XML variable field tag and set the value of that field.
* @param {!Blockly.Workspace} workspace The workspace that is currently being
* deserialized.
* @param {!Element} xml The field tag to decode.
* @param {string} text The text content of the XML tag.
* @param {!Blockly.FieldVariable} field The field on which the value will be
* set.
* @private
*/
Blockly.Xml.domToFieldVariable_ = function (workspace, xml, text, field) {
var type = xml.getAttribute('variabletype') || ''
// TODO (fenichel): Does this need to be explicit or not?
if (type == '\'\'') {
type = ''
}
type = type.indexOf(',') > -1 ? type.split(',') : type
var variable = Blockly.Variables.getOrCreateVariablePackage(workspace, xml.id, text, type)
// This should never happen :)
if (type != null && String(type) !== String(variable.type)) {
console.debug('Serialized variable type with id \'' +
variable.getId() + '\' had type ' + variable.type + ', and ' +
'does not match variable field that references it: ' +
Blockly.Xml.domToText(xml) + '.')
}
field.setValue(variable.getId())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment