[WIP] Move typeblock.js to App Inventor sources

This commit is contained in:
Evan W. Patton
2016-10-01 02:58:47 -04:00
parent 746522297e
commit fc03770ea0

View File

@@ -1,644 +0,0 @@
//Copyright 2013 Massachusetts Institute of Technology. All rights reserved.
/**
* @fileoverview File to handle 'Type Blocking'. When the user starts typing the
* name of a Block in the workspace, a series of suggestions will appear. Upon
* selecting one (enter key), the chosen block will be created in the workspace
* This file needs additional configuration through the inject method.
* @author josmasflores@gmail.com (Jose Dominguez)
*/
'use strict';
goog.provide('Blockly.TypeBlock');
goog.require('Blockly.Xml');
goog.require('goog.events');
goog.require('goog.events.KeyCodes');
goog.require('goog.events.KeyHandler');
goog.require('goog.ui.ac');
goog.require('goog.style');
goog.require('goog.ui.ac.ArrayMatcher');
goog.require('goog.ui.ac.AutoComplete');
goog.require('goog.ui.ac.InputHandler');
goog.require('goog.ui.ac.Renderer');
/**
* Main Type Block function for configuration.
* @param {Object} htmlConfig an object of the type:
{
frame: 'ai_frame',
typeBlockDiv: 'ai_type_block',
inputText: 'ac_input_text'
}
* stating the ids of the attributes to be used in the html enclosing page
* create a new block
*/
Blockly.TypeBlock = function( htmlConfig ){
var frame = htmlConfig['frame'];
Blockly.TypeBlock.typeBlockDiv_ = htmlConfig['typeBlockDiv'];
Blockly.TypeBlock.inputText_ = htmlConfig['inputText'];
Blockly.TypeBlock.docKh_ = new goog.events.KeyHandler(goog.dom.getElement(frame));
Blockly.TypeBlock.inputKh_ = new goog.events.KeyHandler(goog.dom.getElement(Blockly.TypeBlock.inputText_));
Blockly.TypeBlock.handleKey = function(e){
if (e.altKey || e.ctrlKey || e.metaKey || e.keycode === 9) return; // 9 is tab
//We need to duplicate delete handling here from blockly.js
if (e.keyCode === 8 || e.keyCode === 46) {
// Delete or backspace.
// If the panel is showing the panel, just return to allow deletion in the panel itself
if (goog.style.isElementShown(goog.dom.getElement(Blockly.TypeBlock.typeBlockDiv_))) return;
// if id is empty, it is deleting inside a block title
if (e.target.id === '') return;
// only when selected and deletable, actually delete the block
if (Blockly.selected && Blockly.selected.deletable) {
Blockly.hideChaff();
Blockly.selected.dispose(true, true);
}
// Stop the browser from going back to the previous page.
e.preventDefault();
return;
}
if (e.keyCode === 27){ //Dismiss the panel with esc
Blockly.TypeBlock.hide();
return;
}
// A way to know if the user is editing a block or trying to type a new one
if (e.target.id === '') return;
if (goog.style.isElementShown(goog.dom.getElement(Blockly.TypeBlock.typeBlockDiv_))) {
// Enter in the panel makes it select an option
if (e.keyCode === 13) Blockly.TypeBlock.hide();
}
else {
Blockly.TypeBlock.show();
// Can't seem to make Firefox display first character, so keep all browsers from automatically
// displaying the first character and add it manually.
e.preventDefault();
goog.dom.getElement(Blockly.TypeBlock.inputText_).value =
String.fromCharCode(e.charCode != null ? e.charCode : e.keycode);
}
};
goog.events.listen(Blockly.TypeBlock.docKh_, 'key', Blockly.TypeBlock.handleKey);
// Create the auto-complete panel
Blockly.TypeBlock.createAutoComplete_(Blockly.TypeBlock.inputText_);
};
/**
* Div where the type block panel will be rendered
* @private
*/
Blockly.TypeBlock.typeBlockDiv_ = null;
/**
* input text contained in the type block panel used as input
* @private
*/
Blockly.TypeBlock.inputText_ = null;
/**
* Document key handler applied to the frame area, and used to catch keyboard
* events. It is detached when the Type Block panel is shown, and
* re-attached when the Panel is dismissed.
* @private
*/
Blockly.TypeBlock.docKh_ = null;
/**
* Input key handler applied to the Type Block Panel, and used to catch
* keyboard events. It is attached when the Type Block panel is shown, and
* dettached when the Panel is dismissed.
* @private
*/
Blockly.TypeBlock.inputKh_ = null;
/**
* Is the Type Block panel currently showing?
*/
Blockly.TypeBlock.visible = false;
/**
* Mapping of options to show in the auto-complete panel. This maps the
* canonical name of the block, needed to create a new Blockly.Block, with the
* internationalised word or sentence used in typeblocks. Certain blocks do not only need the
* canonical block representation, but also values for dropdowns (name and value)
* - No dropdowns: this.typeblock: [{ translatedName: Blockly.LANG_VAR }]
* - With dropdowns: this.typeblock: [{ translatedName: Blockly.LANG_VAR },
* dropdown: {
* titleName: 'TITLE', value: 'value'
* }]
* - Additional types can be used to mark a block as isProcedure or isGlobalVar. These are only
* used to manage the loading of options in the auto-complete matcher.
* @private
*/
Blockly.TypeBlock.TBOptions_ = {};
/**
* This array contains only the Keys of Blockly.TypeBlock.TBOptions_ to be used
* as options in the autocomplete widget.
* @private
*/
Blockly.TypeBlock.TBOptionsNames_ = [];
/**
* pointer to the automcomplete widget to be able to change its contents when
* the Language tree is modified (additions, renaming, or deletions)
* @private
*/
Blockly.TypeBlock.ac_ = null;
/**
* We keep a listener pointer in case of needing to unlisten to it. We only want
* one listener at a time, and a reload could create a second one, so we
* unlisten first and then listen back
* @private
*/
Blockly.TypeBlock.currentListener_ = null;
/**
* function to hide the autocomplete panel. Also used from hideChaff in
* Blockly.js
*/
Blockly.TypeBlock.hide = function(){
// if (Blockly.TypeBlock.typeBlockDiv_ == null)
// return;
goog.style.showElement(goog.dom.getElement(Blockly.TypeBlock.typeBlockDiv_), false);
goog.events.unlisten(Blockly.TypeBlock.inputKh_, 'key', Blockly.TypeBlock.handleKey);
goog.events.listen(Blockly.TypeBlock.docKh_, 'key', Blockly.TypeBlock.handleKey);
Blockly.TypeBlock.visible = false;
};
/**
* function to show the auto-complete panel to start typing block names
*/
Blockly.TypeBlock.show = function(){
this.lazyLoadOfOptions_();
var panel = goog.dom.getElement(Blockly.TypeBlock.typeBlockDiv_);
goog.style.setStyle(panel, 'top', Blockly.latestClick.y);
goog.style.setStyle(panel, 'left', Blockly.latestClick.x);
goog.style.showElement(panel, true);
goog.dom.getElement(Blockly.TypeBlock.inputText_).focus();
// If the input gets cleaned before adding the handler, all keys are read
// correctly (at times it was missing the first char)
goog.dom.getElement(Blockly.TypeBlock.inputText_).value = '';
goog.events.unlisten(Blockly.TypeBlock.docKh_, 'key', Blockly.TypeBlock.handleKey);
goog.events.listen(Blockly.TypeBlock.inputKh_, 'key', Blockly.TypeBlock.handleKey);
Blockly.TypeBlock.visible = true;
};
/**
* Used as an optimisation trick to avoid reloading components and built-ins unless there is a real
* need to do so. needsReload.components can be set to true when a component changes.
* Defaults to true so that it loads the first time (set to null after loading in lazyLoadOfOptions_())
* @type {{components: boolean}}
*/
Blockly.TypeBlock.needsReload = {
components: true
};
/**
* Lazily loading options because some of them are not available during bootstrapping, and some
* users will never use this functionality, so we avoid having to deal with changes such as handling
* renaming of variables and procedures (leaving it until the moment they are used, if ever).
* @private
*/
Blockly.TypeBlock.lazyLoadOfOptions_ = function () {
// Optimisation to avoid reloading all components and built-in objects unless it is needed.
// needsReload.components is setup when adding/renaming/removing a component in components.js
if (this.needsReload.components){
Blockly.TypeBlock.generateOptions();
this.needsReload.components = null;
}
Blockly.TypeBlock.loadGlobalVariables_();
Blockly.TypeBlock.loadProcedures_();
this.reloadOptionsAfterChanges_();
};
/**
* This function traverses the Language tree and re-creates all the options
* available for type blocking. It's needed in the case of modifying the
* Language tree after its creation (adding or renaming components, for instance).
* It also loads all the built-in blocks.
*
* call 'reloadOptionsAfterChanges_' after calling this. The function lazyLoadOfOptions_ is an
* example of how to call this function.
*/
Blockly.TypeBlock.generateOptions = function() {
var buildListOfOptions = function() {
var listOfOptions = {};
var typeblockArray;
for (var name in Blockly.Blocks) {
var block = Blockly.Blocks[name];
if(block.typeblock){
typeblockArray = block.typeblock;
if(typeof block.typeblock == "function") {
typeblockArray = block.typeblock();
}
createOption(typeblockArray, name);
}
}
function createOption(tb, canonicName){
if (tb){
goog.array.forEach(tb, function(dd){
var dropDownValues = {};
var mutatorAttributes = {};
if (dd.dropDown){
if (dd.dropDown.titleName && dd.dropDown.value){
dropDownValues.titleName = dd.dropDown.titleName;
dropDownValues.value = dd.dropDown.value;
}
else {
throw new Error('TypeBlock not correctly set up for ' + canonicName);
}
}
if(dd.mutatorAttributes) {
mutatorAttributes = dd.mutatorAttributes;
}
listOfOptions[dd.translatedName] = {
canonicName: canonicName,
dropDown: dropDownValues,
mutatorAttributes: mutatorAttributes
};
});
}
}
return listOfOptions;
};
// This is called once on startup, and it will contain all built-in blocks. After that, it can
// be called on demand (for instance in the function lazyLoadOfOptions_)
Blockly.TypeBlock.TBOptions_ = buildListOfOptions();
};
/**
* This function reloads all the latest changes that might have occurred in the language tree or
* the structures containing procedures and variables. It only needs to be called once even if
* different sources are being updated at the same time (call on load proc, load vars, and generate
* options, only needs one call of this function; and example of that is lazyLoadOfOptions_
* @private
*/
Blockly.TypeBlock.reloadOptionsAfterChanges_ = function () {
Blockly.TypeBlock.TBOptionsNames_ = goog.object.getKeys(Blockly.TypeBlock.TBOptions_);
goog.array.sort(Blockly.TypeBlock.TBOptionsNames_);
Blockly.TypeBlock.ac_.matcher_.setRows(Blockly.TypeBlock.TBOptionsNames_);
};
/**
* Loads all procedure names as options for TypeBlocking. It is used lazily from show().
* Call 'reloadOptionsAfterChanges_' after calling this one. The function lazyLoadOfOptions_ is an
* example of how to call this function.
* @private
*/
Blockly.TypeBlock.loadProcedures_ = function(){
// Clean up any previous procedures in the list.
Blockly.TypeBlock.TBOptions_ = goog.object.filter(Blockly.TypeBlock.TBOptions_,
function(opti){ return !opti.isProcedure;});
var procsNoReturn = createTypeBlockForProcedures_(false);
goog.array.forEach(procsNoReturn, function(pro){
Blockly.TypeBlock.TBOptions_[pro.translatedName] = {
canonicName: 'procedures_callnoreturn',
dropDown: pro.dropDown,
isProcedure: true // this attribute is used to clean up before reloading
};
});
var procsReturn = createTypeBlockForProcedures_(true);
goog.array.forEach(procsReturn, function(pro){
Blockly.TypeBlock.TBOptions_[pro.translatedName] = {
canonicName: 'procedures_callreturn',
dropDown: pro.dropDown,
isProcedure: true
};
});
/**
* Procedure names can be collected for both 'with return' and 'no return' varieties from
* getProcedureNames()
* @param {boolean} withReturn indicates if the query us for 'with':true or 'no':false return
* @returns {Array} array of the procedures requested
* @private
*/
function createTypeBlockForProcedures_(withReturn) {
var options = [];
var procNames = Blockly.AIProcedure.getProcedureNames(withReturn);
goog.array.forEach(procNames, function(proc){
options.push(
{
translatedName: Blockly.LANG_PROCEDURES_CALLNORETURN_CALL + ' ' + proc[0],
dropDown: {
titleName: 'PROCNAME',
value: proc[0]
}
}
);
});
return options;
}
};
/**
* Loads all global variable names as options for TypeBlocking. It is used lazily from show().
* Call 'reloadOptionsAfterChanges_' after calling this one. The function lazyLoadOfOptions_ is an
* example of how to call this function.
*/
Blockly.TypeBlock.loadGlobalVariables_ = function () {
//clean up any previous procedures in the list
Blockly.TypeBlock.TBOptions_ = goog.object.filter(Blockly.TypeBlock.TBOptions_,
function(opti){ return !opti.isGlobalvar;});
var globalVarNames = createTypeBlockForVariables_();
goog.array.forEach(globalVarNames, function(varName){
var canonicalN;
if (varName.translatedName.substring(0,3) === 'get')
canonicalN = 'lexical_variable_get';
else
canonicalN = 'lexical_variable_set';
Blockly.TypeBlock.TBOptions_[varName.translatedName] = {
canonicName: canonicalN,
dropDown: varName.dropDown,
isGlobalvar: true
};
});
/**
* Create TypeBlock options for global variables (a setter and a getter for each).
* @returns {Array} array of global var options
*/
function createTypeBlockForVariables_() {
var options = [];
var varNames = Blockly.FieldLexicalVariable.getGlobalNames();
// Make a setter and a getter for each of the names
goog.array.forEach(varNames, function(varName){
options.push(
{
translatedName: 'get global ' + varName,
dropDown: {
titleName: 'VAR',
value: 'global ' + varName
}
}
);
options.push(
{
translatedName: 'set global ' + varName,
dropDown: {
titleName: 'VAR',
value: 'global ' + varName
}
}
);
});
return options;
}
};
/**
* Creates the auto-complete panel, powered by Google Closure's ac widget
* @private
*/
Blockly.TypeBlock.createAutoComplete_ = function(inputText){
Blockly.TypeBlock.TBOptionsNames_ = goog.object.getKeys( Blockly.TypeBlock.TBOptions_ );
goog.array.sort(Blockly.TypeBlock.TBOptionsNames_);
goog.events.unlistenByKey(Blockly.TypeBlock.currentListener_); //if there is a key, unlisten
if (Blockly.TypeBlock.ac_)
Blockly.TypeBlock.ac_.dispose(); //Make sure we only have 1 at a time
// 3 objects needed to create a goog.ui.ac.AutoComplete instance
var matcher = new Blockly.TypeBlock.ac.AIArrayMatcher(Blockly.TypeBlock.TBOptionsNames_, false);
var renderer = new goog.ui.ac.Renderer();
var inputHandler = new goog.ui.ac.InputHandler(null, null, false);
Blockly.TypeBlock.ac_ = new goog.ui.ac.AutoComplete(matcher, renderer, inputHandler);
Blockly.TypeBlock.ac_.setMaxMatches(100); //Renderer has a set height of 294px and a scroll bar.
inputHandler.attachAutoComplete(Blockly.TypeBlock.ac_);
inputHandler.attachInputs(goog.dom.getElement(inputText));
Blockly.TypeBlock.currentListener_ = goog.events.listen(Blockly.TypeBlock.ac_,
goog.ui.ac.AutoComplete.EventType.UPDATE,
function() {
var blockName = goog.dom.getElement(inputText).value;
var blockToCreate = goog.object.get(Blockly.TypeBlock.TBOptions_, blockName);
if (!blockToCreate) {
//If the input passed is not a block, check if it is a number or a pre-populated text block
var numberReg = new RegExp('^-?[0-9]\\d*(\.\\d+)?$', 'g');
var numberMatch = numberReg.exec(blockName);
var textReg = new RegExp('^[\"|\']+', 'g');
var textMatch = textReg.exec(blockName);
if (numberMatch && numberMatch.length > 0){
blockToCreate = {
canonicName: 'math_number',
dropDown: {
titleName: 'NUM',
value: blockName
}
};
}
else if (textMatch && textMatch.length === 1){
blockToCreate = {
canonicName: 'text',
dropDown: {
titleName: 'TEXT',
value: blockName.substring(1)
}
};
}
else
return; // block does not exist: return
}
var blockToCreateName = '';
var block;
if (blockToCreate.dropDown){ //All blocks should have a dropDown property, even if empty
blockToCreateName = blockToCreate.canonicName;
// components have mutator attributes we need to deal with. We can also add these for special blocks
// e.g., this is done for create empty list
if(!goog.object.isEmpty(blockToCreate.mutatorAttributes)) {
//construct xml
var xmlString = '<xml><block type="' + blockToCreateName + '"><mutation ';
for(var attributeName in blockToCreate.mutatorAttributes) {
xmlString += attributeName + '="' + blockToCreate.mutatorAttributes[attributeName] + '" ';
}
xmlString += '>';
xmlString += '</mutation></block></xml>';
var xml = Blockly.Xml.textToDom(xmlString);
block = Blockly.Xml.domToBlock(Blockly.mainWorkspace, xml.firstChild);
} else {
block = new Blockly.Block.obtain(Blockly.mainWorkspace, blockToCreateName);
block.initSvg(); //Need to init the block before doing anything else
if (block.type && (block.type == "procedures_callnoreturn" || block.type == "procedures_callreturn")) {
//Need to make sure Procedure Block inputs are updated
Blockly.FieldProcedure.onChange.call(block.getField_("PROCNAME"), blockToCreate.dropDown.value);
}
}
if (blockToCreate.dropDown.titleName && blockToCreate.dropDown.value){
block.setTitleValue(blockToCreate.dropDown.value, blockToCreate.dropDown.titleName);
// change type checking for split blocks
if(blockToCreate.dropDown.value == 'SPLITATFIRST' || blockToCreate.dropDown.value == 'SPLIT') {
block.getInput("AT").setCheck(Blockly.Blocks.Utilities.YailTypeToBlocklyType("text",Blockly.Blocks.Utilities.INPUT));
} else if(blockToCreate.dropDown.value == 'SPLITATFIRSTOFANY' || blockToCreate.dropDown.value == 'SPLITATANY') {
block.getInput("AT").setCheck(Blockly.Blocks.Utilities.YailTypeToBlocklyType("list",Blockly.Blocks.Utilities.INPUT));
}
}
} else {
throw new Error('Type Block not correctly set up for: ' + blockToCreateName);
}
Blockly.WarningHandler.checkAllBlocksForWarningsAndErrors();
block.render();
var blockSelected = Blockly.selected;
var selectedX, selectedY, selectedXY;
if (blockSelected) {
selectedXY = blockSelected.getRelativeToSurfaceXY();
selectedX = selectedXY.x;
selectedY = selectedXY.y;
Blockly.TypeBlock.connectIfPossible(blockSelected, block);
if(!block.parentBlock_){
//Place it close but a bit out of the way from the one we created.
block.moveBy(Blockly.selected.getRelativeToSurfaceXY().x + 110,
Blockly.selected.getRelativeToSurfaceXY().y + 50);
}
block.select();
}
else {
//calculate positions relative to the view and the latest click
var left = Blockly.mainWorkspace.getMetrics().viewLeft +
Blockly.latestClick.x;
var top = Blockly.mainWorkspace.getMetrics().viewTop +
Blockly.latestClick.y;
block.moveBy(left, top);
block.select();
}
Blockly.TypeBlock.hide();
}
);
};
/**
* Blocks connect in different ways; a block with an outputConnection such as
* a number will connect in one of its parent's input connection (inputLis). .
* A block with no outputConnection could be connected to its parent's next
* connection.
*/
Blockly.TypeBlock.connectIfPossible = function(blockSelected, createdBlock) {
var i = 0,
inputList = blockSelected.inputList,
ilLength = inputList.length;
//If createdBlock has an output connection, we need to:
// connect to parent (eg: connect equals into if)
//else we need to:
// connect its previousConnection to parent (eg: connect if to if)
for (i = 0; i < ilLength; i++){
try {
if (createdBlock.outputConnection != null){
//Check for type validity (connect does not do it)
if ( inputList[i].connection.checkType_(createdBlock.outputConnection) ){
if (!inputList[i].connection.targetConnection){ // is connection empty?
createdBlock.outputConnection.connect(inputList[i].connection);
break;
}
}
}
else {
createdBlock.previousConnection.connect(inputList[i].connection);
}
} catch(e) {
//We can ignore these exceptions; they happen when connecting two blocks
//that should not be connected.
}
}
if (createdBlock.parentBlock_ !== null) return; //Already connected --> return
// Are both blocks statement blocks? If so, connect created block below the selected block
if (blockSelected.outputConnection == null && createdBlock.outputConnection == null) {
createdBlock.previousConnection.connect(blockSelected.nextConnection);
return;
}
// No connections? Try the parent (if it exists)
if (blockSelected.parentBlock_) {
//Is the parent block a statement?
if (blockSelected.parentBlock_.outputConnection == null) {
//Is the created block a statment? If so, connect it below the parent block,
// which is a statement
if(createdBlock.outputConnection == null) {
blockSelected.parentBlock_.nextConnection.connect(createdBlock.previousConnection);
return;
//If it's not, no connections should be made
} else return;
}
else {
//try the parent for other connections
Blockly.TypeBlock.connectIfPossible(blockSelected.parentBlock_, createdBlock);
//recursive call: creates the inner functions again, but should not be much
//overhead; if it is, optimise!
}
}
};
//--------------------------------------
// A custom matcher for the auto-complete widget that can handle numbers as well as the default
// functionality of goog.ui.ac.ArrayMatcher
goog.provide('Blockly.TypeBlock.ac.AIArrayMatcher');
goog.require('goog.iter');
goog.require('goog.string');
/**
* Extension of goog.ui.ac.ArrayMatcher so that it can handle any number typed in.
* @constructor
* @param {Array} rows Dictionary of items to match. Can be objects if they
* have a toString method that returns the value to match against.
* @param {boolean=} opt_noSimilar if true, do not do similarity matches for the
* input token against the dictionary.
* @extends {goog.ui.ac.ArrayMatcher}
*/
Blockly.TypeBlock.ac.AIArrayMatcher = function(rows, opt_noSimilar) {
goog.ui.ac.ArrayMatcher.call(rows, opt_noSimilar);
this.rows_ = rows;
this.useSimilar_ = !opt_noSimilar;
};
goog.inherits(Blockly.TypeBlock.ac.AIArrayMatcher, goog.ui.ac.ArrayMatcher);
/**
* @inheritDoc
*/
Blockly.TypeBlock.ac.AIArrayMatcher.prototype.requestMatchingRows = function(token, maxMatches,
matchHandler, opt_fullString) {
var matches = this.getPrefixMatches(token, maxMatches);
//Because we allow for similar matches, Button.Text will always appear before Text
//So we handle the 'text' case as a special case here
if (token === 'text' || token === 'Text'){
goog.array.remove(matches, 'Text');
goog.array.insertAt(matches, 'Text', 0);
}
// Added code to handle any number typed in the widget (including negatives and decimals)
var reg = new RegExp('^-?[0-9]\\d*(\.\\d+)?$', 'g');
var match = reg.exec(token);
if (match && match.length > 0){
matches.push(token);
}
// Added code to handle default values for text fields (they start with " or ')
var textReg = new RegExp('^[\"|\']+', 'g');
var textMatch = textReg.exec(token);
if (textMatch && textMatch.length === 1){
matches.push(token);
}
if (matches.length === 0 && this.useSimilar_) {
matches = this.getSimilarRows(token, maxMatches);
}
matchHandler(token, matches);
};