Merge from Upstream Blockly (#776)

* WIP merge from upstream google/blockly

* fixing some merging bugs. Getting the drag surface to work, updating some function calls, etc.

* make the trash can lid animate again and fix the cursor to show an x when things will be removed.

* Fix text rendering logic in fields

* Hand apply some more changes from blockly flyout.js to flyout_base.js.

Also revert 08efd1381c
in scratch-blocks since it breaks variables and there is more work fenichel is planning
to do here.

* Fix drop-down field rendering

* Resolve issue with text input field widths on init

* Remove unused blockly (built) files

* Remove unused language files

* Resolve lint issues and rebuild

* Add blockly build targets to cleanup script

* Return 0 if 'this.arrow_' does not exist in a dropdown field

* Remove unused / irrelevant comments
This commit is contained in:
Andrew Sliwinski 2017-02-02 14:17:43 -05:00 committed by GitHub
parent ccd2da9173
commit be9d5fefed
59 changed files with 4804 additions and 1167 deletions

841
blocks/lists.js Normal file
View file

@ -0,0 +1,841 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 List blocks for Blockly.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Blocks.lists');
goog.require('Blockly.Blocks');
/**
* Common HSV hue for all blocks in this category.
*/
Blockly.Blocks.lists.HUE = 260;
Blockly.Blocks['lists_create_empty'] = {
/**
* Block for creating an empty list.
* The 'list_create_with' block is preferred as it is more flexible.
* <block type="lists_create_with">
* <mutation items="0"></mutation>
* </block>
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": Blockly.Msg.LISTS_CREATE_EMPTY_TITLE,
"output": "Array",
"colour": Blockly.Blocks.lists.HUE,
"tooltip": Blockly.Msg.LISTS_CREATE_EMPTY_TOOLTIP,
"helpUrl": Blockly.Msg.LISTS_CREATE_EMPTY_HELPURL
});
}
};
Blockly.Blocks['lists_create_with'] = {
/**
* Block for creating a list with any number of elements of any type.
* @this Blockly.Block
*/
init: function() {
this.setHelpUrl(Blockly.Msg.LISTS_CREATE_WITH_HELPURL);
this.setColour(Blockly.Blocks.lists.HUE);
this.itemCount_ = 3;
this.updateShape_();
this.setOutput(true, 'Array');
this.setMutator(new Blockly.Mutator(['lists_create_with_item']));
this.setTooltip(Blockly.Msg.LISTS_CREATE_WITH_TOOLTIP);
},
/**
* Create XML to represent list inputs.
* @return {!Element} XML storage element.
* @this Blockly.Block
*/
mutationToDom: function() {
var container = document.createElement('mutation');
container.setAttribute('items', this.itemCount_);
return container;
},
/**
* Parse XML to restore the list inputs.
* @param {!Element} xmlElement XML storage element.
* @this Blockly.Block
*/
domToMutation: function(xmlElement) {
this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10);
this.updateShape_();
},
/**
* Populate the mutator's dialog with this block's components.
* @param {!Blockly.Workspace} workspace Mutator's workspace.
* @return {!Blockly.Block} Root block in mutator.
* @this Blockly.Block
*/
decompose: function(workspace) {
var containerBlock = workspace.newBlock('lists_create_with_container');
containerBlock.initSvg();
var connection = containerBlock.getInput('STACK').connection;
for (var i = 0; i < this.itemCount_; i++) {
var itemBlock = workspace.newBlock('lists_create_with_item');
itemBlock.initSvg();
connection.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection;
}
return containerBlock;
},
/**
* Reconfigure this block based on the mutator dialog's components.
* @param {!Blockly.Block} containerBlock Root block in mutator.
* @this Blockly.Block
*/
compose: function(containerBlock) {
var itemBlock = containerBlock.getInputTargetBlock('STACK');
// Count number of inputs.
var connections = [];
while (itemBlock) {
connections.push(itemBlock.valueConnection_);
itemBlock = itemBlock.nextConnection &&
itemBlock.nextConnection.targetBlock();
}
// Disconnect any children that don't belong.
for (var i = 0; i < this.itemCount_; i++) {
var connection = this.getInput('ADD' + i).connection.targetConnection;
if (connection && connections.indexOf(connection) == -1) {
connection.disconnect();
}
}
this.itemCount_ = connections.length;
this.updateShape_();
// Reconnect any child blocks.
for (var i = 0; i < this.itemCount_; i++) {
Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i);
}
},
/**
* Store pointers to any connected child blocks.
* @param {!Blockly.Block} containerBlock Root block in mutator.
* @this Blockly.Block
*/
saveConnections: function(containerBlock) {
var itemBlock = containerBlock.getInputTargetBlock('STACK');
var i = 0;
while (itemBlock) {
var input = this.getInput('ADD' + i);
itemBlock.valueConnection_ = input && input.connection.targetConnection;
i++;
itemBlock = itemBlock.nextConnection &&
itemBlock.nextConnection.targetBlock();
}
},
/**
* Modify this block to have the correct number of inputs.
* @private
* @this Blockly.Block
*/
updateShape_: function() {
if (this.itemCount_ && this.getInput('EMPTY')) {
this.removeInput('EMPTY');
} else if (!this.itemCount_ && !this.getInput('EMPTY')) {
this.appendDummyInput('EMPTY')
.appendField(Blockly.Msg.LISTS_CREATE_EMPTY_TITLE);
}
// Add new inputs.
for (var i = 0; i < this.itemCount_; i++) {
if (!this.getInput('ADD' + i)) {
var input = this.appendValueInput('ADD' + i);
if (i == 0) {
input.appendField(Blockly.Msg.LISTS_CREATE_WITH_INPUT_WITH);
}
}
}
// Remove deleted inputs.
while (this.getInput('ADD' + i)) {
this.removeInput('ADD' + i);
i++;
}
}
};
Blockly.Blocks['lists_create_with_container'] = {
/**
* Mutator block for list container.
* @this Blockly.Block
*/
init: function() {
this.setColour(Blockly.Blocks.lists.HUE);
this.appendDummyInput()
.appendField(Blockly.Msg.LISTS_CREATE_WITH_CONTAINER_TITLE_ADD);
this.appendStatementInput('STACK');
this.setTooltip(Blockly.Msg.LISTS_CREATE_WITH_CONTAINER_TOOLTIP);
this.contextMenu = false;
}
};
Blockly.Blocks['lists_create_with_item'] = {
/**
* Mutator block for adding items.
* @this Blockly.Block
*/
init: function() {
this.setColour(Blockly.Blocks.lists.HUE);
this.appendDummyInput()
.appendField(Blockly.Msg.LISTS_CREATE_WITH_ITEM_TITLE);
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setTooltip(Blockly.Msg.LISTS_CREATE_WITH_ITEM_TOOLTIP);
this.contextMenu = false;
}
};
Blockly.Blocks['lists_repeat'] = {
/**
* Block for creating a list with one element repeated.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": Blockly.Msg.LISTS_REPEAT_TITLE,
"args0": [
{
"type": "input_value",
"name": "ITEM"
},
{
"type": "input_value",
"name": "NUM",
"check": "Number"
}
],
"output": "Array",
"colour": Blockly.Blocks.lists.HUE,
"tooltip": Blockly.Msg.LISTS_REPEAT_TOOLTIP,
"helpUrl": Blockly.Msg.LISTS_REPEAT_HELPURL
});
}
};
Blockly.Blocks['lists_length'] = {
/**
* Block for list length.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": Blockly.Msg.LISTS_LENGTH_TITLE,
"args0": [
{
"type": "input_value",
"name": "VALUE",
"check": ['String', 'Array']
}
],
"output": 'Number',
"colour": Blockly.Blocks.lists.HUE,
"tooltip": Blockly.Msg.LISTS_LENGTH_TOOLTIP,
"helpUrl": Blockly.Msg.LISTS_LENGTH_HELPURL
});
}
};
Blockly.Blocks['lists_isEmpty'] = {
/**
* Block for is the list empty?
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": Blockly.Msg.LISTS_ISEMPTY_TITLE,
"args0": [
{
"type": "input_value",
"name": "VALUE",
"check": ['String', 'Array']
}
],
"output": 'Boolean',
"colour": Blockly.Blocks.lists.HUE,
"tooltip": Blockly.Msg.LISTS_ISEMPTY_TOOLTIP,
"helpUrl": Blockly.Msg.LISTS_ISEMPTY_HELPURL
});
}
};
Blockly.Blocks['lists_indexOf'] = {
/**
* Block for finding an item in the list.
* @this Blockly.Block
*/
init: function() {
var OPERATORS =
[[Blockly.Msg.LISTS_INDEX_OF_FIRST, 'FIRST'],
[Blockly.Msg.LISTS_INDEX_OF_LAST, 'LAST']];
this.setHelpUrl(Blockly.Msg.LISTS_INDEX_OF_HELPURL);
this.setColour(Blockly.Blocks.lists.HUE);
this.setOutput(true, 'Number');
this.appendValueInput('VALUE')
.setCheck('Array')
.appendField(Blockly.Msg.LISTS_INDEX_OF_INPUT_IN_LIST);
this.appendValueInput('FIND')
.appendField(new Blockly.FieldDropdown(OPERATORS), 'END');
this.setInputsInline(true);
this.setTooltip(function() {
return Blockly.Msg.LISTS_INDEX_OF_TOOLTIP.replace('%1',
this.workspace.options.oneBasedIndex ? '0' : '-1');
});
}
};
Blockly.Blocks['lists_getIndex'] = {
/**
* Block for getting element at index.
* @this Blockly.Block
*/
init: function() {
var MODE =
[[Blockly.Msg.LISTS_GET_INDEX_GET, 'GET'],
[Blockly.Msg.LISTS_GET_INDEX_GET_REMOVE, 'GET_REMOVE'],
[Blockly.Msg.LISTS_GET_INDEX_REMOVE, 'REMOVE']];
this.WHERE_OPTIONS =
[[Blockly.Msg.LISTS_GET_INDEX_FROM_START, 'FROM_START'],
[Blockly.Msg.LISTS_GET_INDEX_FROM_END, 'FROM_END'],
[Blockly.Msg.LISTS_GET_INDEX_FIRST, 'FIRST'],
[Blockly.Msg.LISTS_GET_INDEX_LAST, 'LAST'],
[Blockly.Msg.LISTS_GET_INDEX_RANDOM, 'RANDOM']];
this.setHelpUrl(Blockly.Msg.LISTS_GET_INDEX_HELPURL);
this.setColour(Blockly.Blocks.lists.HUE);
var modeMenu = new Blockly.FieldDropdown(MODE, function(value) {
var isStatement = (value == 'REMOVE');
this.sourceBlock_.updateStatement_(isStatement);
});
this.appendValueInput('VALUE')
.setCheck('Array')
.appendField(Blockly.Msg.LISTS_GET_INDEX_INPUT_IN_LIST);
this.appendDummyInput()
.appendField(modeMenu, 'MODE')
.appendField('', 'SPACE');
this.appendDummyInput('AT');
if (Blockly.Msg.LISTS_GET_INDEX_TAIL) {
this.appendDummyInput('TAIL')
.appendField(Blockly.Msg.LISTS_GET_INDEX_TAIL);
}
this.setInputsInline(true);
this.setOutput(true);
this.updateAt_(true);
// Assign 'this' to a variable for use in the tooltip closure below.
var thisBlock = this;
this.setTooltip(function() {
var mode = thisBlock.getFieldValue('MODE');
var where = thisBlock.getFieldValue('WHERE');
var tooltip = '';
switch (mode + ' ' + where) {
case 'GET FROM_START':
case 'GET FROM_END':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FROM;
break;
case 'GET FIRST':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FIRST;
break;
case 'GET LAST':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_LAST;
break;
case 'GET RANDOM':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_RANDOM;
break;
case 'GET_REMOVE FROM_START':
case 'GET_REMOVE FROM_END':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM;
break;
case 'GET_REMOVE FIRST':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST;
break;
case 'GET_REMOVE LAST':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_LAST;
break;
case 'GET_REMOVE RANDOM':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_RANDOM;
break;
case 'REMOVE FROM_START':
case 'REMOVE FROM_END':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM;
break;
case 'REMOVE FIRST':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST;
break;
case 'REMOVE LAST':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST;
break;
case 'REMOVE RANDOM':
tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM;
break;
}
if (where == 'FROM_START' || where == 'FROM_END') {
var msg = (where == 'FROM_START') ?
Blockly.Msg.LISTS_INDEX_FROM_START_TOOLTIP :
Blockly.Msg.LISTS_INDEX_FROM_END_TOOLTIP;
tooltip += ' ' + msg.replace('%1',
thisBlock.workspace.options.oneBasedIndex ? '#1' : '#0');
}
return tooltip;
});
},
/**
* Create XML to represent whether the block is a statement or a value.
* Also represent whether there is an 'AT' input.
* @return {Element} XML storage element.
* @this Blockly.Block
*/
mutationToDom: function() {
var container = document.createElement('mutation');
var isStatement = !this.outputConnection;
container.setAttribute('statement', isStatement);
var isAt = this.getInput('AT').type == Blockly.INPUT_VALUE;
container.setAttribute('at', isAt);
return container;
},
/**
* Parse XML to restore the 'AT' input.
* @param {!Element} xmlElement XML storage element.
* @this Blockly.Block
*/
domToMutation: function(xmlElement) {
// Note: Until January 2013 this block did not have mutations,
// so 'statement' defaults to false and 'at' defaults to true.
var isStatement = (xmlElement.getAttribute('statement') == 'true');
this.updateStatement_(isStatement);
var isAt = (xmlElement.getAttribute('at') != 'false');
this.updateAt_(isAt);
},
/**
* Switch between a value block and a statement block.
* @param {boolean} newStatement True if the block should be a statement.
* False if the block should be a value.
* @private
* @this Blockly.Block
*/
updateStatement_: function(newStatement) {
var oldStatement = !this.outputConnection;
if (newStatement != oldStatement) {
this.unplug(true, true);
if (newStatement) {
this.setOutput(false);
this.setPreviousStatement(true);
this.setNextStatement(true);
} else {
this.setPreviousStatement(false);
this.setNextStatement(false);
this.setOutput(true);
}
}
},
/**
* Create or delete an input for the numeric index.
* @param {boolean} isAt True if the input should exist.
* @private
* @this Blockly.Block
*/
updateAt_: function(isAt) {
// Destroy old 'AT' and 'ORDINAL' inputs.
this.removeInput('AT');
this.removeInput('ORDINAL', true);
// Create either a value 'AT' input or a dummy input.
if (isAt) {
this.appendValueInput('AT').setCheck('Number');
if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
this.appendDummyInput('ORDINAL')
.appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX);
}
} else {
this.appendDummyInput('AT');
}
var menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, function(value) {
var newAt = (value == 'FROM_START') || (value == 'FROM_END');
// The 'isAt' variable is available due to this function being a closure.
if (newAt != isAt) {
var block = this.sourceBlock_;
block.updateAt_(newAt);
// This menu has been destroyed and replaced. Update the replacement.
block.setFieldValue(value, 'WHERE');
return null;
}
return undefined;
});
this.getInput('AT').appendField(menu, 'WHERE');
if (Blockly.Msg.LISTS_GET_INDEX_TAIL) {
this.moveInputBefore('TAIL', null);
}
}
};
Blockly.Blocks['lists_setIndex'] = {
/**
* Block for setting the element at index.
* @this Blockly.Block
*/
init: function() {
var MODE =
[[Blockly.Msg.LISTS_SET_INDEX_SET, 'SET'],
[Blockly.Msg.LISTS_SET_INDEX_INSERT, 'INSERT']];
this.WHERE_OPTIONS =
[[Blockly.Msg.LISTS_GET_INDEX_FROM_START, 'FROM_START'],
[Blockly.Msg.LISTS_GET_INDEX_FROM_END, 'FROM_END'],
[Blockly.Msg.LISTS_GET_INDEX_FIRST, 'FIRST'],
[Blockly.Msg.LISTS_GET_INDEX_LAST, 'LAST'],
[Blockly.Msg.LISTS_GET_INDEX_RANDOM, 'RANDOM']];
this.setHelpUrl(Blockly.Msg.LISTS_SET_INDEX_HELPURL);
this.setColour(Blockly.Blocks.lists.HUE);
this.appendValueInput('LIST')
.setCheck('Array')
.appendField(Blockly.Msg.LISTS_SET_INDEX_INPUT_IN_LIST);
this.appendDummyInput()
.appendField(new Blockly.FieldDropdown(MODE), 'MODE')
.appendField('', 'SPACE');
this.appendDummyInput('AT');
this.appendValueInput('TO')
.appendField(Blockly.Msg.LISTS_SET_INDEX_INPUT_TO);
this.setInputsInline(true);
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setTooltip(Blockly.Msg.LISTS_SET_INDEX_TOOLTIP);
this.updateAt_(true);
// Assign 'this' to a variable for use in the tooltip closure below.
var thisBlock = this;
this.setTooltip(function() {
var mode = thisBlock.getFieldValue('MODE');
var where = thisBlock.getFieldValue('WHERE');
var tooltip = '';
switch (mode + ' ' + where) {
case 'SET FROM_START':
case 'SET FROM_END':
tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FROM;
break;
case 'SET FIRST':
tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FIRST;
break;
case 'SET LAST':
tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_LAST;
break;
case 'SET RANDOM':
tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_RANDOM;
break;
case 'INSERT FROM_START':
case 'INSERT FROM_END':
tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FROM;
break;
case 'INSERT FIRST':
tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST;
break;
case 'INSERT LAST':
tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_LAST;
break;
case 'INSERT RANDOM':
tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM;
break;
}
if (where == 'FROM_START' || where == 'FROM_END') {
tooltip += ' ' + Blockly.Msg.LISTS_INDEX_FROM_START_TOOLTIP
.replace('%1',
thisBlock.workspace.options.oneBasedIndex ? '#1' : '#0');
}
return tooltip;
});
},
/**
* Create XML to represent whether there is an 'AT' input.
* @return {Element} XML storage element.
* @this Blockly.Block
*/
mutationToDom: function() {
var container = document.createElement('mutation');
var isAt = this.getInput('AT').type == Blockly.INPUT_VALUE;
container.setAttribute('at', isAt);
return container;
},
/**
* Parse XML to restore the 'AT' input.
* @param {!Element} xmlElement XML storage element.
* @this Blockly.Block
*/
domToMutation: function(xmlElement) {
// Note: Until January 2013 this block did not have mutations,
// so 'at' defaults to true.
var isAt = (xmlElement.getAttribute('at') != 'false');
this.updateAt_(isAt);
},
/**
* Create or delete an input for the numeric index.
* @param {boolean} isAt True if the input should exist.
* @private
* @this Blockly.Block
*/
updateAt_: function(isAt) {
// Destroy old 'AT' and 'ORDINAL' input.
this.removeInput('AT');
this.removeInput('ORDINAL', true);
// Create either a value 'AT' input or a dummy input.
if (isAt) {
this.appendValueInput('AT').setCheck('Number');
if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
this.appendDummyInput('ORDINAL')
.appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX);
}
} else {
this.appendDummyInput('AT');
}
var menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, function(value) {
var newAt = (value == 'FROM_START') || (value == 'FROM_END');
// The 'isAt' variable is available due to this function being a closure.
if (newAt != isAt) {
var block = this.sourceBlock_;
block.updateAt_(newAt);
// This menu has been destroyed and replaced. Update the replacement.
block.setFieldValue(value, 'WHERE');
return null;
}
return undefined;
});
this.moveInputBefore('AT', 'TO');
if (this.getInput('ORDINAL')) {
this.moveInputBefore('ORDINAL', 'TO');
}
this.getInput('AT').appendField(menu, 'WHERE');
}
};
Blockly.Blocks['lists_getSublist'] = {
/**
* Block for getting sublist.
* @this Blockly.Block
*/
init: function() {
this['WHERE_OPTIONS_1'] =
[[Blockly.Msg.LISTS_GET_SUBLIST_START_FROM_START, 'FROM_START'],
[Blockly.Msg.LISTS_GET_SUBLIST_START_FROM_END, 'FROM_END'],
[Blockly.Msg.LISTS_GET_SUBLIST_START_FIRST, 'FIRST']];
this['WHERE_OPTIONS_2'] =
[[Blockly.Msg.LISTS_GET_SUBLIST_END_FROM_START, 'FROM_START'],
[Blockly.Msg.LISTS_GET_SUBLIST_END_FROM_END, 'FROM_END'],
[Blockly.Msg.LISTS_GET_SUBLIST_END_LAST, 'LAST']];
this.setHelpUrl(Blockly.Msg.LISTS_GET_SUBLIST_HELPURL);
this.setColour(Blockly.Blocks.lists.HUE);
this.appendValueInput('LIST')
.setCheck('Array')
.appendField(Blockly.Msg.LISTS_GET_SUBLIST_INPUT_IN_LIST);
this.appendDummyInput('AT1');
this.appendDummyInput('AT2');
if (Blockly.Msg.LISTS_GET_SUBLIST_TAIL) {
this.appendDummyInput('TAIL')
.appendField(Blockly.Msg.LISTS_GET_SUBLIST_TAIL);
}
this.setInputsInline(true);
this.setOutput(true, 'Array');
this.updateAt_(1, true);
this.updateAt_(2, true);
this.setTooltip(Blockly.Msg.LISTS_GET_SUBLIST_TOOLTIP);
},
/**
* Create XML to represent whether there are 'AT' inputs.
* @return {Element} XML storage element.
* @this Blockly.Block
*/
mutationToDom: function() {
var container = document.createElement('mutation');
var isAt1 = this.getInput('AT1').type == Blockly.INPUT_VALUE;
container.setAttribute('at1', isAt1);
var isAt2 = this.getInput('AT2').type == Blockly.INPUT_VALUE;
container.setAttribute('at2', isAt2);
return container;
},
/**
* Parse XML to restore the 'AT' inputs.
* @param {!Element} xmlElement XML storage element.
* @this Blockly.Block
*/
domToMutation: function(xmlElement) {
var isAt1 = (xmlElement.getAttribute('at1') == 'true');
var isAt2 = (xmlElement.getAttribute('at2') == 'true');
this.updateAt_(1, isAt1);
this.updateAt_(2, isAt2);
},
/**
* Create or delete an input for a numeric index.
* This block has two such inputs, independant of each other.
* @param {number} n Specify first or second input (1 or 2).
* @param {boolean} isAt True if the input should exist.
* @private
* @this Blockly.Block
*/
updateAt_: function(n, isAt) {
// Create or delete an input for the numeric index.
// Destroy old 'AT' and 'ORDINAL' inputs.
this.removeInput('AT' + n);
this.removeInput('ORDINAL' + n, true);
// Create either a value 'AT' input or a dummy input.
if (isAt) {
this.appendValueInput('AT' + n).setCheck('Number');
if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
this.appendDummyInput('ORDINAL' + n)
.appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX);
}
} else {
this.appendDummyInput('AT' + n);
}
var menu = new Blockly.FieldDropdown(this['WHERE_OPTIONS_' + n],
function(value) {
var newAt = (value == 'FROM_START') || (value == 'FROM_END');
// The 'isAt' variable is available due to this function being a
// closure.
if (newAt != isAt) {
var block = this.sourceBlock_;
block.updateAt_(n, newAt);
// This menu has been destroyed and replaced.
// Update the replacement.
block.setFieldValue(value, 'WHERE' + n);
return null;
}
return undefined;
});
this.getInput('AT' + n)
.appendField(menu, 'WHERE' + n);
if (n == 1) {
this.moveInputBefore('AT1', 'AT2');
if (this.getInput('ORDINAL1')) {
this.moveInputBefore('ORDINAL1', 'AT2');
}
}
if (Blockly.Msg.LISTS_GET_SUBLIST_TAIL) {
this.moveInputBefore('TAIL', null);
}
}
};
Blockly.Blocks['lists_sort'] = {
/**
* Block for sorting a list.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": Blockly.Msg.LISTS_SORT_TITLE,
"args0": [
{
"type": "field_dropdown",
"name": "TYPE",
"options": [
[Blockly.Msg.LISTS_SORT_TYPE_NUMERIC, "NUMERIC"],
[Blockly.Msg.LISTS_SORT_TYPE_TEXT, "TEXT"],
[Blockly.Msg.LISTS_SORT_TYPE_IGNORECASE, "IGNORE_CASE"]
]
},
{
"type": "field_dropdown",
"name": "DIRECTION",
"options": [
[Blockly.Msg.LISTS_SORT_ORDER_ASCENDING, "1"],
[Blockly.Msg.LISTS_SORT_ORDER_DESCENDING, "-1"]
]
},
{
"type": "input_value",
"name": "LIST",
"check": "Array"
}
],
"output": "Array",
"colour": Blockly.Blocks.lists.HUE,
"tooltip": Blockly.Msg.LISTS_SORT_TOOLTIP,
"helpUrl": Blockly.Msg.LISTS_SORT_HELPURL
});
}
};
Blockly.Blocks['lists_split'] = {
/**
* Block for splitting text into a list, or joining a list into text.
* @this Blockly.Block
*/
init: function() {
// Assign 'this' to a variable for use in the closures below.
var thisBlock = this;
var dropdown = new Blockly.FieldDropdown(
[[Blockly.Msg.LISTS_SPLIT_LIST_FROM_TEXT, 'SPLIT'],
[Blockly.Msg.LISTS_SPLIT_TEXT_FROM_LIST, 'JOIN']],
function(newMode) {
thisBlock.updateType_(newMode);
});
this.setHelpUrl(Blockly.Msg.LISTS_SPLIT_HELPURL);
this.setColour(Blockly.Blocks.lists.HUE);
this.appendValueInput('INPUT')
.setCheck('String')
.appendField(dropdown, 'MODE');
this.appendValueInput('DELIM')
.setCheck('String')
.appendField(Blockly.Msg.LISTS_SPLIT_WITH_DELIMITER);
this.setInputsInline(true);
this.setOutput(true, 'Array');
this.setTooltip(function() {
var mode = thisBlock.getFieldValue('MODE');
if (mode == 'SPLIT') {
return Blockly.Msg.LISTS_SPLIT_TOOLTIP_SPLIT;
} else if (mode == 'JOIN') {
return Blockly.Msg.LISTS_SPLIT_TOOLTIP_JOIN;
}
throw 'Unknown mode: ' + mode;
});
},
/**
* Modify this block to have the correct input and output types.
* @param {string} newMode Either 'SPLIT' or 'JOIN'.
* @private
* @this Blockly.Block
*/
updateType_: function(newMode) {
if (newMode == 'SPLIT') {
this.outputConnection.setCheck('Array');
this.getInput('INPUT').setCheck('String');
} else {
this.outputConnection.setCheck('String');
this.getInput('INPUT').setCheck('Array');
}
},
/**
* Create XML to represent the input and output types.
* @return {!Element} XML storage element.
* @this Blockly.Block
*/
mutationToDom: function() {
var container = document.createElement('mutation');
container.setAttribute('mode', this.getFieldValue('MODE'));
return container;
},
/**
* Parse XML to restore the input and output types.
* @param {!Element} xmlElement XML storage element.
* @this Blockly.Block
*/
domToMutation: function(xmlElement) {
this.updateType_(xmlElement.getAttribute('mode'));
}
};

526
blocks/logic.js Normal file
View file

@ -0,0 +1,526 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Logic blocks for Blockly.
* @author q.neutron@gmail.com (Quynh Neutron)
*/
'use strict';
goog.provide('Blockly.Blocks.logic');
goog.require('Blockly.Blocks');
/**
* Common HSV hue for all blocks in this category.
*/
Blockly.Blocks.logic.HUE = 210;
Blockly.Blocks['controls_if'] = {
/**
* Block for if/elseif/else condition.
* @this Blockly.Block
*/
init: function() {
this.setHelpUrl(Blockly.Msg.CONTROLS_IF_HELPURL);
this.setColour(Blockly.Blocks.logic.HUE);
this.appendValueInput('IF0')
.setCheck('Boolean')
.appendField(Blockly.Msg.CONTROLS_IF_MSG_IF);
this.appendStatementInput('DO0')
.appendField(Blockly.Msg.CONTROLS_IF_MSG_THEN);
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setMutator(new Blockly.Mutator(['controls_if_elseif',
'controls_if_else']));
// Assign 'this' to a variable for use in the tooltip closure below.
var thisBlock = this;
this.setTooltip(function() {
if (!thisBlock.elseifCount_ && !thisBlock.elseCount_) {
return Blockly.Msg.CONTROLS_IF_TOOLTIP_1;
} else if (!thisBlock.elseifCount_ && thisBlock.elseCount_) {
return Blockly.Msg.CONTROLS_IF_TOOLTIP_2;
} else if (thisBlock.elseifCount_ && !thisBlock.elseCount_) {
return Blockly.Msg.CONTROLS_IF_TOOLTIP_3;
} else if (thisBlock.elseifCount_ && thisBlock.elseCount_) {
return Blockly.Msg.CONTROLS_IF_TOOLTIP_4;
}
return '';
});
this.elseifCount_ = 0;
this.elseCount_ = 0;
},
/**
* Create XML to represent the number of else-if and else inputs.
* @return {Element} XML storage element.
* @this Blockly.Block
*/
mutationToDom: function() {
if (!this.elseifCount_ && !this.elseCount_) {
return null;
}
var container = document.createElement('mutation');
if (this.elseifCount_) {
container.setAttribute('elseif', this.elseifCount_);
}
if (this.elseCount_) {
container.setAttribute('else', 1);
}
return container;
},
/**
* Parse XML to restore the else-if and else inputs.
* @param {!Element} xmlElement XML storage element.
* @this Blockly.Block
*/
domToMutation: function(xmlElement) {
this.elseifCount_ = parseInt(xmlElement.getAttribute('elseif'), 10) || 0;
this.elseCount_ = parseInt(xmlElement.getAttribute('else'), 10) || 0;
this.updateShape_();
},
/**
* Populate the mutator's dialog with this block's components.
* @param {!Blockly.Workspace} workspace Mutator's workspace.
* @return {!Blockly.Block} Root block in mutator.
* @this Blockly.Block
*/
decompose: function(workspace) {
var containerBlock = workspace.newBlock('controls_if_if');
containerBlock.initSvg();
var connection = containerBlock.nextConnection;
for (var i = 1; i <= this.elseifCount_; i++) {
var elseifBlock = workspace.newBlock('controls_if_elseif');
elseifBlock.initSvg();
connection.connect(elseifBlock.previousConnection);
connection = elseifBlock.nextConnection;
}
if (this.elseCount_) {
var elseBlock = workspace.newBlock('controls_if_else');
elseBlock.initSvg();
connection.connect(elseBlock.previousConnection);
}
return containerBlock;
},
/**
* Reconfigure this block based on the mutator dialog's components.
* @param {!Blockly.Block} containerBlock Root block in mutator.
* @this Blockly.Block
*/
compose: function(containerBlock) {
var clauseBlock = containerBlock.nextConnection.targetBlock();
// Count number of inputs.
this.elseifCount_ = 0;
this.elseCount_ = 0;
var valueConnections = [null];
var statementConnections = [null];
var elseStatementConnection = null;
while (clauseBlock) {
switch (clauseBlock.type) {
case 'controls_if_elseif':
this.elseifCount_++;
valueConnections.push(clauseBlock.valueConnection_);
statementConnections.push(clauseBlock.statementConnection_);
break;
case 'controls_if_else':
this.elseCount_++;
elseStatementConnection = clauseBlock.statementConnection_;
break;
default:
throw 'Unknown block type.';
}
clauseBlock = clauseBlock.nextConnection &&
clauseBlock.nextConnection.targetBlock();
}
this.updateShape_();
// Reconnect any child blocks.
for (var i = 1; i <= this.elseifCount_; i++) {
Blockly.Mutator.reconnect(valueConnections[i], this, 'IF' + i);
Blockly.Mutator.reconnect(statementConnections[i], this, 'DO' + i);
}
Blockly.Mutator.reconnect(elseStatementConnection, this, 'ELSE');
},
/**
* Store pointers to any connected child blocks.
* @param {!Blockly.Block} containerBlock Root block in mutator.
* @this Blockly.Block
*/
saveConnections: function(containerBlock) {
var clauseBlock = containerBlock.nextConnection.targetBlock();
var i = 1;
while (clauseBlock) {
switch (clauseBlock.type) {
case 'controls_if_elseif':
var inputIf = this.getInput('IF' + i);
var inputDo = this.getInput('DO' + i);
clauseBlock.valueConnection_ =
inputIf && inputIf.connection.targetConnection;
clauseBlock.statementConnection_ =
inputDo && inputDo.connection.targetConnection;
i++;
break;
case 'controls_if_else':
var inputDo = this.getInput('ELSE');
clauseBlock.statementConnection_ =
inputDo && inputDo.connection.targetConnection;
break;
default:
throw 'Unknown block type.';
}
clauseBlock = clauseBlock.nextConnection &&
clauseBlock.nextConnection.targetBlock();
}
},
/**
* Modify this block to have the correct number of inputs.
* @private
* @this Blockly.Block
*/
updateShape_: function() {
// Delete everything.
if (this.getInput('ELSE')) {
this.removeInput('ELSE');
}
var i = 1;
while (this.getInput('IF' + i)) {
this.removeInput('IF' + i);
this.removeInput('DO' + i);
i++;
}
// Rebuild block.
for (var i = 1; i <= this.elseifCount_; i++) {
this.appendValueInput('IF' + i)
.setCheck('Boolean')
.appendField(Blockly.Msg.CONTROLS_IF_MSG_ELSEIF);
this.appendStatementInput('DO' + i)
.appendField(Blockly.Msg.CONTROLS_IF_MSG_THEN);
}
if (this.elseCount_) {
this.appendStatementInput('ELSE')
.appendField(Blockly.Msg.CONTROLS_IF_MSG_ELSE);
}
}
};
Blockly.Blocks['controls_if_if'] = {
/**
* Mutator block for if container.
* @this Blockly.Block
*/
init: function() {
this.setColour(Blockly.Blocks.logic.HUE);
this.appendDummyInput()
.appendField(Blockly.Msg.CONTROLS_IF_IF_TITLE_IF);
this.setNextStatement(true);
this.setTooltip(Blockly.Msg.CONTROLS_IF_IF_TOOLTIP);
this.contextMenu = false;
}
};
Blockly.Blocks['controls_if_elseif'] = {
/**
* Mutator block for else-if condition.
* @this Blockly.Block
*/
init: function() {
this.setColour(Blockly.Blocks.logic.HUE);
this.appendDummyInput()
.appendField(Blockly.Msg.CONTROLS_IF_ELSEIF_TITLE_ELSEIF);
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setTooltip(Blockly.Msg.CONTROLS_IF_ELSEIF_TOOLTIP);
this.contextMenu = false;
}
};
Blockly.Blocks['controls_if_else'] = {
/**
* Mutator block for else condition.
* @this Blockly.Block
*/
init: function() {
this.setColour(Blockly.Blocks.logic.HUE);
this.appendDummyInput()
.appendField(Blockly.Msg.CONTROLS_IF_ELSE_TITLE_ELSE);
this.setPreviousStatement(true);
this.setTooltip(Blockly.Msg.CONTROLS_IF_ELSE_TOOLTIP);
this.contextMenu = false;
}
};
Blockly.Blocks['controls_ifelse'] = {
/**
* If/else block that does not use a mutator.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": "%{BKY_CONTROLS_IF_MSG_IF} %1",
"args0": [
{
"type": "input_value",
"name": "IF0",
"check": "Boolean"
}
],
"message1": "%{BKY_CONTROLS_IF_MSG_THEN} %1",
"args1": [
{
"type": "input_statement",
"name": "DO0"
}
],
"message2": "%{BKY_CONTROLS_IF_MSG_ELSE} %1",
"args2": [
{
"type": "input_statement",
"name": "ELSE"
}
],
"previousStatement": null,
"nextStatement": null,
"colour": Blockly.Blocks.logic.HUE,
"tooltip": Blockly.Msg.CONTROLS_IF_TOOLTIP_2,
"helpUrl": Blockly.Msg.CONTROLS_IF_HELPURL
});
}
};
Blockly.Blocks['logic_compare'] = {
/**
* Block for comparison operator.
* @this Blockly.Block
*/
init: function() {
var rtlOperators = [
['=', 'EQ'],
['\u2260', 'NEQ'],
['\u200F<\u200F', 'LT'],
['\u200F\u2264\u200F', 'LTE'],
['\u200F>\u200F', 'GT'],
['\u200F\u2265\u200F', 'GTE']
];
var ltrOperators = [
['=', 'EQ'],
['\u2260', 'NEQ'],
['<', 'LT'],
['\u2264', 'LTE'],
['>', 'GT'],
['\u2265', 'GTE']
];
var OPERATORS = this.RTL ? rtlOperators : ltrOperators;
this.setHelpUrl(Blockly.Msg.LOGIC_COMPARE_HELPURL);
this.setColour(Blockly.Blocks.logic.HUE);
this.setOutput(true, 'Boolean');
this.appendValueInput('A');
this.appendValueInput('B')
.appendField(new Blockly.FieldDropdown(OPERATORS), 'OP');
this.setInputsInline(true);
// Assign 'this' to a variable for use in the tooltip closure below.
var thisBlock = this;
this.setTooltip(function() {
var op = thisBlock.getFieldValue('OP');
var TOOLTIPS = {
'EQ': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_EQ,
'NEQ': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_NEQ,
'LT': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_LT,
'LTE': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_LTE,
'GT': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_GT,
'GTE': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_GTE
};
return TOOLTIPS[op];
});
this.prevBlocks_ = [null, null];
},
/**
* Called whenever anything on the workspace changes.
* Prevent mismatched types from being compared.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function(e) {
var blockA = this.getInputTargetBlock('A');
var blockB = this.getInputTargetBlock('B');
// Disconnect blocks that existed prior to this change if they don't match.
if (blockA && blockB &&
!blockA.outputConnection.checkType_(blockB.outputConnection)) {
// Mismatch between two inputs. Disconnect previous and bump it away.
// Ensure that any disconnections are grouped with the causing event.
Blockly.Events.setGroup(e.group);
for (var i = 0; i < this.prevBlocks_.length; i++) {
var block = this.prevBlocks_[i];
if (block === blockA || block === blockB) {
block.unplug();
block.bumpNeighbours_();
}
}
Blockly.Events.setGroup(false);
}
this.prevBlocks_[0] = blockA;
this.prevBlocks_[1] = blockB;
}
};
Blockly.Blocks['logic_operation'] = {
/**
* Block for logical operations: 'and', 'or'.
* @this Blockly.Block
*/
init: function() {
var OPERATORS =
[[Blockly.Msg.LOGIC_OPERATION_AND, 'AND'],
[Blockly.Msg.LOGIC_OPERATION_OR, 'OR']];
this.setHelpUrl(Blockly.Msg.LOGIC_OPERATION_HELPURL);
this.setColour(Blockly.Blocks.logic.HUE);
this.setOutput(true, 'Boolean');
this.appendValueInput('A')
.setCheck('Boolean');
this.appendValueInput('B')
.setCheck('Boolean')
.appendField(new Blockly.FieldDropdown(OPERATORS), 'OP');
this.setInputsInline(true);
// Assign 'this' to a variable for use in the tooltip closure below.
var thisBlock = this;
this.setTooltip(function() {
var op = thisBlock.getFieldValue('OP');
var TOOLTIPS = {
'AND': Blockly.Msg.LOGIC_OPERATION_TOOLTIP_AND,
'OR': Blockly.Msg.LOGIC_OPERATION_TOOLTIP_OR
};
return TOOLTIPS[op];
});
}
};
Blockly.Blocks['logic_negate'] = {
/**
* Block for negation.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": Blockly.Msg.LOGIC_NEGATE_TITLE,
"args0": [
{
"type": "input_value",
"name": "BOOL",
"check": "Boolean"
}
],
"output": "Boolean",
"colour": Blockly.Blocks.logic.HUE,
"tooltip": Blockly.Msg.LOGIC_NEGATE_TOOLTIP,
"helpUrl": Blockly.Msg.LOGIC_NEGATE_HELPURL
});
}
};
Blockly.Blocks['logic_boolean'] = {
/**
* Block for boolean data type: true and false.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": "%1",
"args0": [
{
"type": "field_dropdown",
"name": "BOOL",
"options": [
["%{bky_logic_boolean_true}", "TRUE"],
["%{bky_logic_boolean_false}", "FALSE"]
]
}
],
"output": "Boolean",
"colour": Blockly.Blocks.logic.HUE,
"tooltip": Blockly.Msg.LOGIC_BOOLEAN_TOOLTIP,
"helpUrl": Blockly.Msg.LOGIC_BOOLEAN_HELPURL
});
}
};
Blockly.Blocks['logic_null'] = {
/**
* Block for null data type.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": Blockly.Msg.LOGIC_NULL,
"output": null,
"colour": Blockly.Blocks.logic.HUE,
"tooltip": Blockly.Msg.LOGIC_NULL_TOOLTIP,
"helpUrl": Blockly.Msg.LOGIC_NULL_HELPURL
});
}
};
Blockly.Blocks['logic_ternary'] = {
/**
* Block for ternary operator.
* @this Blockly.Block
*/
init: function() {
this.setHelpUrl(Blockly.Msg.LOGIC_TERNARY_HELPURL);
this.setColour(Blockly.Blocks.logic.HUE);
this.appendValueInput('IF')
.setCheck('Boolean')
.appendField(Blockly.Msg.LOGIC_TERNARY_CONDITION);
this.appendValueInput('THEN')
.appendField(Blockly.Msg.LOGIC_TERNARY_IF_TRUE);
this.appendValueInput('ELSE')
.appendField(Blockly.Msg.LOGIC_TERNARY_IF_FALSE);
this.setOutput(true);
this.setTooltip(Blockly.Msg.LOGIC_TERNARY_TOOLTIP);
this.prevParentConnection_ = null;
},
/**
* Called whenever anything on the workspace changes.
* Prevent mismatched types.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function(e) {
var blockA = this.getInputTargetBlock('THEN');
var blockB = this.getInputTargetBlock('ELSE');
var parentConnection = this.outputConnection.targetConnection;
// Disconnect blocks that existed prior to this change if they don't match.
if ((blockA || blockB) && parentConnection) {
for (var i = 0; i < 2; i++) {
var block = (i == 1) ? blockA : blockB;
if (block && !block.outputConnection.checkType_(parentConnection)) {
// Ensure that any disconnections are grouped with the causing event.
Blockly.Events.setGroup(e.group);
if (parentConnection === this.prevParentConnection_) {
this.unplug();
parentConnection.getSourceBlock().bumpNeighbours_();
} else {
block.unplug();
block.bumpNeighbours_();
}
Blockly.Events.setGroup(false);
}
}
}
this.prevParentConnection_ = parentConnection;
}
};

292
blocks/loops.js Normal file
View file

@ -0,0 +1,292 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Loop blocks for Blockly.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Blocks.loops');
goog.require('Blockly.Blocks');
/**
* Common HSV hue for all blocks in this category.
*/
Blockly.Blocks.loops.HUE = 120;
Blockly.Blocks['controls_repeat_ext'] = {
/**
* Block for repeat n times (external number).
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": Blockly.Msg.CONTROLS_REPEAT_TITLE,
"args0": [
{
"type": "input_value",
"name": "TIMES",
"check": "Number"
}
],
"previousStatement": null,
"nextStatement": null,
"colour": Blockly.Blocks.loops.HUE,
"tooltip": Blockly.Msg.CONTROLS_REPEAT_TOOLTIP,
"helpUrl": Blockly.Msg.CONTROLS_REPEAT_HELPURL
});
this.appendStatementInput('DO')
.appendField(Blockly.Msg.CONTROLS_REPEAT_INPUT_DO);
}
};
Blockly.Blocks['controls_repeat'] = {
/**
* Block for repeat n times (internal number).
* The 'controls_repeat_ext' block is preferred as it is more flexible.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": Blockly.Msg.CONTROLS_REPEAT_TITLE,
"args0": [
{
"type": "field_number",
"name": "TIMES",
"value": 10,
"min": 0,
"precision": 1
}
],
"previousStatement": null,
"nextStatement": null,
"colour": Blockly.Blocks.loops.HUE,
"tooltip": Blockly.Msg.CONTROLS_REPEAT_TOOLTIP,
"helpUrl": Blockly.Msg.CONTROLS_REPEAT_HELPURL
});
this.appendStatementInput('DO')
.appendField(Blockly.Msg.CONTROLS_REPEAT_INPUT_DO);
}
};
Blockly.Blocks['controls_whileUntil'] = {
/**
* Block for 'do while/until' loop.
* @this Blockly.Block
*/
init: function() {
var OPERATORS =
[[Blockly.Msg.CONTROLS_WHILEUNTIL_OPERATOR_WHILE, 'WHILE'],
[Blockly.Msg.CONTROLS_WHILEUNTIL_OPERATOR_UNTIL, 'UNTIL']];
this.setHelpUrl(Blockly.Msg.CONTROLS_WHILEUNTIL_HELPURL);
this.setColour(Blockly.Blocks.loops.HUE);
this.appendValueInput('BOOL')
.setCheck('Boolean')
.appendField(new Blockly.FieldDropdown(OPERATORS), 'MODE');
this.appendStatementInput('DO')
.appendField(Blockly.Msg.CONTROLS_WHILEUNTIL_INPUT_DO);
this.setPreviousStatement(true);
this.setNextStatement(true);
// Assign 'this' to a variable for use in the tooltip closure below.
var thisBlock = this;
this.setTooltip(function() {
var op = thisBlock.getFieldValue('MODE');
var TOOLTIPS = {
'WHILE': Blockly.Msg.CONTROLS_WHILEUNTIL_TOOLTIP_WHILE,
'UNTIL': Blockly.Msg.CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL
};
return TOOLTIPS[op];
});
}
};
Blockly.Blocks['controls_for'] = {
/**
* Block for 'for' loop.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": Blockly.Msg.CONTROLS_FOR_TITLE,
"args0": [
{
"type": "field_variable",
"name": "VAR",
"variable": null
},
{
"type": "input_value",
"name": "FROM",
"check": "Number",
"align": "RIGHT"
},
{
"type": "input_value",
"name": "TO",
"check": "Number",
"align": "RIGHT"
},
{
"type": "input_value",
"name": "BY",
"check": "Number",
"align": "RIGHT"
}
],
"inputsInline": true,
"previousStatement": null,
"nextStatement": null,
"colour": Blockly.Blocks.loops.HUE,
"helpUrl": Blockly.Msg.CONTROLS_FOR_HELPURL
});
this.appendStatementInput('DO')
.appendField(Blockly.Msg.CONTROLS_FOR_INPUT_DO);
// Assign 'this' to a variable for use in the tooltip closure below.
var thisBlock = this;
this.setTooltip(function() {
return Blockly.Msg.CONTROLS_FOR_TOOLTIP.replace('%1',
thisBlock.getFieldValue('VAR'));
});
},
/**
* Add menu option to create getter block for loop variable.
* @param {!Array} options List of menu options to add to.
* @this Blockly.Block
*/
customContextMenu: function(options) {
if (!this.isCollapsed()) {
var option = {enabled: true};
var name = this.getFieldValue('VAR');
option.text = Blockly.Msg.VARIABLES_SET_CREATE_GET.replace('%1', name);
var xmlField = goog.dom.createDom('field', null, name);
xmlField.setAttribute('name', 'VAR');
var xmlBlock = goog.dom.createDom('block', null, xmlField);
xmlBlock.setAttribute('type', 'variables_get');
option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock);
options.push(option);
}
}
};
Blockly.Blocks['controls_forEach'] = {
/**
* Block for 'for each' loop.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": Blockly.Msg.CONTROLS_FOREACH_TITLE,
"args0": [
{
"type": "field_variable",
"name": "VAR",
"variable": null
},
{
"type": "input_value",
"name": "LIST",
"check": "Array"
}
],
"previousStatement": null,
"nextStatement": null,
"colour": Blockly.Blocks.loops.HUE,
"helpUrl": Blockly.Msg.CONTROLS_FOREACH_HELPURL
});
this.appendStatementInput('DO')
.appendField(Blockly.Msg.CONTROLS_FOREACH_INPUT_DO);
// Assign 'this' to a variable for use in the tooltip closure below.
var thisBlock = this;
this.setTooltip(function() {
return Blockly.Msg.CONTROLS_FOREACH_TOOLTIP.replace('%1',
thisBlock.getFieldValue('VAR'));
});
},
customContextMenu: Blockly.Blocks['controls_for'].customContextMenu
};
Blockly.Blocks['controls_flow_statements'] = {
/**
* Block for flow statements: continue, break.
* @this Blockly.Block
*/
init: function() {
var OPERATORS =
[[Blockly.Msg.CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK, 'BREAK'],
[Blockly.Msg.CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE, 'CONTINUE']];
this.setHelpUrl(Blockly.Msg.CONTROLS_FLOW_STATEMENTS_HELPURL);
this.setColour(Blockly.Blocks.loops.HUE);
this.appendDummyInput()
.appendField(new Blockly.FieldDropdown(OPERATORS), 'FLOW');
this.setPreviousStatement(true);
// Assign 'this' to a variable for use in the tooltip closure below.
var thisBlock = this;
this.setTooltip(function() {
var op = thisBlock.getFieldValue('FLOW');
var TOOLTIPS = {
'BREAK': Blockly.Msg.CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK,
'CONTINUE': Blockly.Msg.CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE
};
return TOOLTIPS[op];
});
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function() {
if (!this.workspace.isDragging || this.workspace.isDragging()) {
return; // Don't change state at the start of a drag.
}
var legal = false;
// Is the block nested in a loop?
var block = this;
do {
if (this.LOOP_TYPES.indexOf(block.type) != -1) {
legal = true;
break;
}
block = block.getSurroundParent();
} while (block);
if (legal) {
this.setWarningText(null);
if (!this.isInFlyout) {
this.setDisabled(false);
}
} else {
this.setWarningText(Blockly.Msg.CONTROLS_FLOW_STATEMENTS_WARNING);
if (!this.isInFlyout && !this.getInheritedDisabled()) {
this.setDisabled(true);
}
}
},
/**
* List of block types that are loops and thus do not need warnings.
* To add a new loop type add this to your code:
* Blockly.Blocks['controls_flow_statements'].LOOP_TYPES.push('custom_loop');
*/
LOOP_TYPES: ['controls_repeat', 'controls_repeat_ext', 'controls_forEach',
'controls_for', 'controls_whileUntil']
};

888
blocks/procedures.js Normal file
View file

@ -0,0 +1,888 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Procedure blocks for Blockly.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Blocks.procedures');
goog.require('Blockly.Blocks');
/**
* Common HSV hue for all blocks in this category.
*/
Blockly.Blocks.procedures.HUE = 290;
Blockly.Blocks['procedures_defnoreturn'] = {
/**
* Block for defining a procedure with no return value.
* @this Blockly.Block
*/
init: function() {
var nameField = new Blockly.FieldTextInput('',
Blockly.Procedures.rename);
nameField.setSpellcheck(false);
this.appendDummyInput()
.appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_TITLE)
.appendField(nameField, 'NAME')
.appendField('', 'PARAMS');
this.setMutator(new Blockly.Mutator(['procedures_mutatorarg']));
if ((this.workspace.options.comments ||
(this.workspace.options.parentWorkspace &&
this.workspace.options.parentWorkspace.options.comments)) &&
Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT) {
this.setCommentText(Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT);
}
this.setColour(Blockly.Blocks.procedures.HUE);
this.setTooltip(Blockly.Msg.PROCEDURES_DEFNORETURN_TOOLTIP);
this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFNORETURN_HELPURL);
this.arguments_ = [];
this.setStatements_(true);
this.statementConnection_ = null;
},
/**
* Add or remove the statement block from this function definition.
* @param {boolean} hasStatements True if a statement block is needed.
* @this Blockly.Block
*/
setStatements_: function(hasStatements) {
if (this.hasStatements_ === hasStatements) {
return;
}
if (hasStatements) {
this.appendStatementInput('STACK')
.appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_DO);
if (this.getInput('RETURN')) {
this.moveInputBefore('STACK', 'RETURN');
}
} else {
this.removeInput('STACK', true);
}
this.hasStatements_ = hasStatements;
},
/**
* Update the display of parameters for this procedure definition block.
* Display a warning if there are duplicately named parameters.
* @private
* @this Blockly.Block
*/
updateParams_: function() {
// Check for duplicated arguments.
var badArg = false;
var hash = {};
for (var i = 0; i < this.arguments_.length; i++) {
if (hash['arg_' + this.arguments_[i].toLowerCase()]) {
badArg = true;
break;
}
hash['arg_' + this.arguments_[i].toLowerCase()] = true;
}
if (badArg) {
this.setWarningText(Blockly.Msg.PROCEDURES_DEF_DUPLICATE_WARNING);
} else {
this.setWarningText(null);
}
// Merge the arguments into a human-readable list.
var paramString = '';
if (this.arguments_.length) {
paramString = Blockly.Msg.PROCEDURES_BEFORE_PARAMS +
' ' + this.arguments_.join(', ');
}
// The params field is deterministic based on the mutation,
// no need to fire a change event.
Blockly.Events.disable();
try {
this.setFieldValue(paramString, 'PARAMS');
} finally {
Blockly.Events.enable();
}
},
/**
* Create XML to represent the argument inputs.
* @param {boolean} opt_paramIds If true include the IDs of the parameter
* quarks. Used by Blockly.Procedures.mutateCallers for reconnection.
* @return {!Element} XML storage element.
* @this Blockly.Block
*/
mutationToDom: function(opt_paramIds) {
var container = document.createElement('mutation');
if (opt_paramIds) {
container.setAttribute('name', this.getFieldValue('NAME'));
}
for (var i = 0; i < this.arguments_.length; i++) {
var parameter = document.createElement('arg');
parameter.setAttribute('name', this.arguments_[i]);
if (opt_paramIds && this.paramIds_) {
parameter.setAttribute('paramId', this.paramIds_[i]);
}
container.appendChild(parameter);
}
// Save whether the statement input is visible.
if (!this.hasStatements_) {
container.setAttribute('statements', 'false');
}
return container;
},
/**
* Parse XML to restore the argument inputs.
* @param {!Element} xmlElement XML storage element.
* @this Blockly.Block
*/
domToMutation: function(xmlElement) {
this.arguments_ = [];
for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) {
if (childNode.nodeName.toLowerCase() == 'arg') {
this.arguments_.push(childNode.getAttribute('name'));
}
}
this.updateParams_();
Blockly.Procedures.mutateCallers(this);
// Show or hide the statement input.
this.setStatements_(xmlElement.getAttribute('statements') !== 'false');
},
/**
* Populate the mutator's dialog with this block's components.
* @param {!Blockly.Workspace} workspace Mutator's workspace.
* @return {!Blockly.Block} Root block in mutator.
* @this Blockly.Block
*/
decompose: function(workspace) {
var containerBlock = workspace.newBlock('procedures_mutatorcontainer');
containerBlock.initSvg();
// Check/uncheck the allow statement box.
if (this.getInput('RETURN')) {
containerBlock.setFieldValue(this.hasStatements_ ? 'TRUE' : 'FALSE',
'STATEMENTS');
} else {
containerBlock.getInput('STATEMENT_INPUT').setVisible(false);
}
// Parameter list.
var connection = containerBlock.getInput('STACK').connection;
for (var i = 0; i < this.arguments_.length; i++) {
var paramBlock = workspace.newBlock('procedures_mutatorarg');
paramBlock.initSvg();
paramBlock.setFieldValue(this.arguments_[i], 'NAME');
// Store the old location.
paramBlock.oldLocation = i;
connection.connect(paramBlock.previousConnection);
connection = paramBlock.nextConnection;
}
// Initialize procedure's callers with blank IDs.
Blockly.Procedures.mutateCallers(this);
return containerBlock;
},
/**
* Reconfigure this block based on the mutator dialog's components.
* @param {!Blockly.Block} containerBlock Root block in mutator.
* @this Blockly.Block
*/
compose: function(containerBlock) {
// Parameter list.
this.arguments_ = [];
this.paramIds_ = [];
var paramBlock = containerBlock.getInputTargetBlock('STACK');
while (paramBlock) {
this.arguments_.push(paramBlock.getFieldValue('NAME'));
this.paramIds_.push(paramBlock.id);
paramBlock = paramBlock.nextConnection &&
paramBlock.nextConnection.targetBlock();
}
this.updateParams_();
Blockly.Procedures.mutateCallers(this);
// Show/hide the statement input.
var hasStatements = containerBlock.getFieldValue('STATEMENTS');
if (hasStatements !== null) {
hasStatements = hasStatements == 'TRUE';
if (this.hasStatements_ != hasStatements) {
if (hasStatements) {
this.setStatements_(true);
// Restore the stack, if one was saved.
Blockly.Mutator.reconnect(this.statementConnection_, this, 'STACK');
this.statementConnection_ = null;
} else {
// Save the stack, then disconnect it.
var stackConnection = this.getInput('STACK').connection;
this.statementConnection_ = stackConnection.targetConnection;
if (this.statementConnection_) {
var stackBlock = stackConnection.targetBlock();
stackBlock.unplug();
stackBlock.bumpNeighbours_();
}
this.setStatements_(false);
}
}
}
},
/**
* Return the signature of this procedure definition.
* @return {!Array} Tuple containing three elements:
* - the name of the defined procedure,
* - a list of all its arguments,
* - that it DOES NOT have a return value.
* @this Blockly.Block
*/
getProcedureDef: function() {
return [this.getFieldValue('NAME'), this.arguments_, false];
},
/**
* Return all variables referenced by this block.
* @return {!Array.<string>} List of variable names.
* @this Blockly.Block
*/
getVars: function() {
return this.arguments_;
},
/**
* Notification that a variable is renaming.
* If the name matches one of this block's variables, rename it.
* @param {string} oldName Previous name of variable.
* @param {string} newName Renamed variable.
* @this Blockly.Block
*/
renameVar: function(oldName, newName) {
var change = false;
for (var i = 0; i < this.arguments_.length; i++) {
if (Blockly.Names.equals(oldName, this.arguments_[i])) {
this.arguments_[i] = newName;
change = true;
}
}
if (change) {
this.updateParams_();
// Update the mutator's variables if the mutator is open.
if (this.mutator.isVisible()) {
var blocks = this.mutator.workspace_.getAllBlocks();
for (var i = 0, block; block = blocks[i]; i++) {
if (block.type == 'procedures_mutatorarg' &&
Blockly.Names.equals(oldName, block.getFieldValue('NAME'))) {
block.setFieldValue(newName, 'NAME');
}
}
}
}
},
/**
* Add custom menu options to this block's context menu.
* @param {!Array} options List of menu options to add to.
* @this Blockly.Block
*/
customContextMenu: function(options) {
// Add option to create caller.
var option = {enabled: true};
var name = this.getFieldValue('NAME');
option.text = Blockly.Msg.PROCEDURES_CREATE_DO.replace('%1', name);
var xmlMutation = goog.dom.createDom('mutation');
xmlMutation.setAttribute('name', name);
for (var i = 0; i < this.arguments_.length; i++) {
var xmlArg = goog.dom.createDom('arg');
xmlArg.setAttribute('name', this.arguments_[i]);
xmlMutation.appendChild(xmlArg);
}
var xmlBlock = goog.dom.createDom('block', null, xmlMutation);
xmlBlock.setAttribute('type', this.callType_);
option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock);
options.push(option);
// Add options to create getters for each parameter.
if (!this.isCollapsed()) {
for (var i = 0; i < this.arguments_.length; i++) {
var option = {enabled: true};
var name = this.arguments_[i];
option.text = Blockly.Msg.VARIABLES_SET_CREATE_GET.replace('%1', name);
var xmlField = goog.dom.createDom('field', null, name);
xmlField.setAttribute('name', 'VAR');
var xmlBlock = goog.dom.createDom('block', null, xmlField);
xmlBlock.setAttribute('type', 'variables_get');
option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock);
options.push(option);
}
}
},
callType_: 'procedures_callnoreturn'
};
Blockly.Blocks['procedures_defreturn'] = {
/**
* Block for defining a procedure with a return value.
* @this Blockly.Block
*/
init: function() {
var nameField = new Blockly.FieldTextInput('',
Blockly.Procedures.rename);
nameField.setSpellcheck(false);
this.appendDummyInput()
.appendField(Blockly.Msg.PROCEDURES_DEFRETURN_TITLE)
.appendField(nameField, 'NAME')
.appendField('', 'PARAMS');
this.appendValueInput('RETURN')
.setAlign(Blockly.ALIGN_RIGHT)
.appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
this.setMutator(new Blockly.Mutator(['procedures_mutatorarg']));
if ((this.workspace.options.comments ||
(this.workspace.options.parentWorkspace &&
this.workspace.options.parentWorkspace.options.comments)) &&
Blockly.Msg.PROCEDURES_DEFRETURN_COMMENT) {
this.setCommentText(Blockly.Msg.PROCEDURES_DEFRETURN_COMMENT);
}
this.setColour(Blockly.Blocks.procedures.HUE);
this.setTooltip(Blockly.Msg.PROCEDURES_DEFRETURN_TOOLTIP);
this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFRETURN_HELPURL);
this.arguments_ = [];
this.setStatements_(true);
this.statementConnection_ = null;
},
setStatements_: Blockly.Blocks['procedures_defnoreturn'].setStatements_,
updateParams_: Blockly.Blocks['procedures_defnoreturn'].updateParams_,
mutationToDom: Blockly.Blocks['procedures_defnoreturn'].mutationToDom,
domToMutation: Blockly.Blocks['procedures_defnoreturn'].domToMutation,
decompose: Blockly.Blocks['procedures_defnoreturn'].decompose,
compose: Blockly.Blocks['procedures_defnoreturn'].compose,
/**
* Return the signature of this procedure definition.
* @return {!Array} Tuple containing three elements:
* - the name of the defined procedure,
* - a list of all its arguments,
* - that it DOES have a return value.
* @this Blockly.Block
*/
getProcedureDef: function() {
return [this.getFieldValue('NAME'), this.arguments_, true];
},
getVars: Blockly.Blocks['procedures_defnoreturn'].getVars,
renameVar: Blockly.Blocks['procedures_defnoreturn'].renameVar,
customContextMenu: Blockly.Blocks['procedures_defnoreturn'].customContextMenu,
callType_: 'procedures_callreturn'
};
Blockly.Blocks['procedures_mutatorcontainer'] = {
/**
* Mutator block for procedure container.
* @this Blockly.Block
*/
init: function() {
this.appendDummyInput()
.appendField(Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TITLE);
this.appendStatementInput('STACK');
this.appendDummyInput('STATEMENT_INPUT')
.appendField(Blockly.Msg.PROCEDURES_ALLOW_STATEMENTS)
.appendField(new Blockly.FieldCheckbox('TRUE'), 'STATEMENTS');
this.setColour(Blockly.Blocks.procedures.HUE);
this.setTooltip(Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TOOLTIP);
this.contextMenu = false;
}
};
Blockly.Blocks['procedures_mutatorarg'] = {
/**
* Mutator block for procedure argument.
* @this Blockly.Block
*/
init: function() {
var field = new Blockly.FieldTextInput('x', this.validator_);
this.appendDummyInput()
.appendField(Blockly.Msg.PROCEDURES_MUTATORARG_TITLE)
.appendField(field, 'NAME');
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setColour(Blockly.Blocks.procedures.HUE);
this.setTooltip(Blockly.Msg.PROCEDURES_MUTATORARG_TOOLTIP);
this.contextMenu = false;
// Create the default variable when we drag the block in from the flyout.
// Have to do this after installing the field on the block.
field.onFinishEditing_ = this.createNewVar_;
field.onFinishEditing_('x');
},
/**
* Obtain a valid name for the procedure.
* Merge runs of whitespace. Strip leading and trailing whitespace.
* Beyond this, all names are legal.
* @param {string} newVar User-supplied name.
* @return {?string} Valid name, or null if a name was not specified.
* @private
* @this Blockly.Block
*/
validator_: function(newVar) {
newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, '');
return newVar || null;
},
/**
* Called when focusing away from the text field.
* Creates a new variable with this name.
* @param {string} newText The new variable name.
* @private
* @this Blockly.FieldTextInput
*/
createNewVar_: function(newText) {
var source = this.sourceBlock_;
if (source && source.workspace && source.workspace.options &&
source.workspace.options.parentWorkspace) {
source.workspace.options.parentWorkspace.createVariable(newText);
}
}
};
Blockly.Blocks['procedures_callnoreturn'] = {
/**
* Block for calling a procedure with no return value.
* @this Blockly.Block
*/
init: function() {
this.appendDummyInput('TOPROW')
.appendField(this.id, 'NAME');
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setColour(Blockly.Blocks.procedures.HUE);
// Tooltip is set in renameProcedure.
this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLNORETURN_HELPURL);
this.arguments_ = [];
this.quarkConnections_ = {};
this.quarkIds_ = null;
},
/**
* Returns the name of the procedure this block calls.
* @return {string} Procedure name.
* @this Blockly.Block
*/
getProcedureCall: function() {
// The NAME field is guaranteed to exist, null will never be returned.
return /** @type {string} */ (this.getFieldValue('NAME'));
},
/**
* Notification that a procedure is renaming.
* If the name matches this block's procedure, rename it.
* @param {string} oldName Previous name of procedure.
* @param {string} newName Renamed procedure.
* @this Blockly.Block
*/
renameProcedure: function(oldName, newName) {
if (Blockly.Names.equals(oldName, this.getProcedureCall())) {
this.setFieldValue(newName, 'NAME');
this.setTooltip(
(this.outputConnection ? Blockly.Msg.PROCEDURES_CALLRETURN_TOOLTIP :
Blockly.Msg.PROCEDURES_CALLNORETURN_TOOLTIP)
.replace('%1', newName));
}
},
/**
* Notification that the procedure's parameters have changed.
* @param {!Array.<string>} paramNames New param names, e.g. ['x', 'y', 'z'].
* @param {!Array.<string>} paramIds IDs of params (consistent for each
* parameter through the life of a mutator, regardless of param renaming),
* e.g. ['piua', 'f8b_', 'oi.o'].
* @private
* @this Blockly.Block
*/
setProcedureParameters_: function(paramNames, paramIds) {
// Data structures:
// this.arguments = ['x', 'y']
// Existing param names.
// this.quarkConnections_ {piua: null, f8b_: Blockly.Connection}
// Look-up of paramIds to connections plugged into the call block.
// this.quarkIds_ = ['piua', 'f8b_']
// Existing param IDs.
// Note that quarkConnections_ may include IDs that no longer exist, but
// which might reappear if a param is reattached in the mutator.
var defBlock = Blockly.Procedures.getDefinition(this.getProcedureCall(),
this.workspace);
var mutatorOpen = defBlock && defBlock.mutator &&
defBlock.mutator.isVisible();
if (!mutatorOpen) {
this.quarkConnections_ = {};
this.quarkIds_ = null;
}
if (!paramIds) {
// Reset the quarks (a mutator is about to open).
return;
}
if (goog.array.equals(this.arguments_, paramNames)) {
// No change.
this.quarkIds_ = paramIds;
return;
}
if (paramIds.length != paramNames.length) {
throw 'Error: paramNames and paramIds must be the same length.';
}
this.setCollapsed(false);
if (!this.quarkIds_) {
// Initialize tracking for this block.
this.quarkConnections_ = {};
if (paramNames.join('\n') == this.arguments_.join('\n')) {
// No change to the parameters, allow quarkConnections_ to be
// populated with the existing connections.
this.quarkIds_ = paramIds;
} else {
this.quarkIds_ = [];
}
}
// Switch off rendering while the block is rebuilt.
var savedRendered = this.rendered;
this.rendered = false;
// Update the quarkConnections_ with existing connections.
for (var i = 0; i < this.arguments_.length; i++) {
var input = this.getInput('ARG' + i);
if (input) {
var connection = input.connection.targetConnection;
this.quarkConnections_[this.quarkIds_[i]] = connection;
if (mutatorOpen && connection &&
paramIds.indexOf(this.quarkIds_[i]) == -1) {
// This connection should no longer be attached to this block.
connection.disconnect();
connection.getSourceBlock().bumpNeighbours_();
}
}
}
// Rebuild the block's arguments.
this.arguments_ = [].concat(paramNames);
this.updateShape_();
this.quarkIds_ = paramIds;
// Reconnect any child blocks.
if (this.quarkIds_) {
for (var i = 0; i < this.arguments_.length; i++) {
var quarkId = this.quarkIds_[i];
if (quarkId in this.quarkConnections_) {
var connection = this.quarkConnections_[quarkId];
if (!Blockly.Mutator.reconnect(connection, this, 'ARG' + i)) {
// Block no longer exists or has been attached elsewhere.
delete this.quarkConnections_[quarkId];
}
}
}
}
// Restore rendering and show the changes.
this.rendered = savedRendered;
if (this.rendered) {
this.render();
}
},
/**
* Modify this block to have the correct number of arguments.
* @private
* @this Blockly.Block
*/
updateShape_: function() {
for (var i = 0; i < this.arguments_.length; i++) {
var field = this.getField('ARGNAME' + i);
if (field) {
// Ensure argument name is up to date.
// The argument name field is deterministic based on the mutation,
// no need to fire a change event.
Blockly.Events.disable();
try {
field.setValue(this.arguments_[i]);
} finally {
Blockly.Events.enable();
}
} else {
// Add new input.
field = new Blockly.FieldLabel(this.arguments_[i]);
var input = this.appendValueInput('ARG' + i)
.setAlign(Blockly.ALIGN_RIGHT)
.appendField(field, 'ARGNAME' + i);
input.init();
}
}
// Remove deleted inputs.
while (this.getInput('ARG' + i)) {
this.removeInput('ARG' + i);
i++;
}
// Add 'with:' if there are parameters, remove otherwise.
var topRow = this.getInput('TOPROW');
if (topRow) {
if (this.arguments_.length) {
if (!this.getField('WITH')) {
topRow.appendField(Blockly.Msg.PROCEDURES_CALL_BEFORE_PARAMS, 'WITH');
topRow.init();
}
} else {
if (this.getField('WITH')) {
topRow.removeField('WITH');
}
}
}
},
/**
* Create XML to represent the (non-editable) name and arguments.
* @return {!Element} XML storage element.
* @this Blockly.Block
*/
mutationToDom: function() {
var container = document.createElement('mutation');
container.setAttribute('name', this.getProcedureCall());
for (var i = 0; i < this.arguments_.length; i++) {
var parameter = document.createElement('arg');
parameter.setAttribute('name', this.arguments_[i]);
container.appendChild(parameter);
}
return container;
},
/**
* Parse XML to restore the (non-editable) name and parameters.
* @param {!Element} xmlElement XML storage element.
* @this Blockly.Block
*/
domToMutation: function(xmlElement) {
var name = xmlElement.getAttribute('name');
this.renameProcedure(this.getProcedureCall(), name);
var args = [];
var paramIds = [];
for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) {
if (childNode.nodeName.toLowerCase() == 'arg') {
args.push(childNode.getAttribute('name'));
paramIds.push(childNode.getAttribute('paramId'));
}
}
this.setProcedureParameters_(args, paramIds);
},
/**
* Notification that a variable is renaming.
* If the name matches one of this block's variables, rename it.
* @param {string} oldName Previous name of variable.
* @param {string} newName Renamed variable.
* @this Blockly.Block
*/
renameVar: function(oldName, newName) {
for (var i = 0; i < this.arguments_.length; i++) {
if (Blockly.Names.equals(oldName, this.arguments_[i])) {
this.arguments_[i] = newName;
this.getField('ARGNAME' + i).setValue(newName);
}
}
},
/**
* Procedure calls cannot exist without the corresponding procedure
* definition. Enforce this link whenever an event is fired.
* @param {!Blockly.Events.Abstract} event Procedure onchange event.
* @this Blockly.Block
*/
onchange: function(event) {
if (!this.workspace || this.workspace.isFlyout) {
// Block is deleted or is in a flyout.
return;
}
if (event.type == Blockly.Events.CREATE &&
event.ids.indexOf(this.id) != -1) {
// Look for the case where a procedure call was created (usually through
// paste) and there is no matching definition. In this case, create
// an empty definition block with the correct signature.
var name = this.getProcedureCall();
var def = Blockly.Procedures.getDefinition(name, this.workspace);
if (def && (def.type != this.defType_ ||
JSON.stringify(def.arguments_) != JSON.stringify(this.arguments_))) {
// The signatures don't match.
def = null;
}
if (!def) {
Blockly.Events.setGroup(event.group);
/**
* Create matching definition block.
* <xml>
* <block type="procedures_defreturn" x="10" y="20">
* <mutation name="test">
* <arg name="x"></arg>
* </mutation>
* <field name="NAME">test</field>
* </block>
* </xml>
*/
var xml = goog.dom.createDom('xml');
var block = goog.dom.createDom('block');
block.setAttribute('type', this.defType_);
var xy = this.getRelativeToSurfaceXY();
var x = xy.x + Blockly.SNAP_RADIUS * (this.RTL ? -1 : 1);
var y = xy.y + Blockly.SNAP_RADIUS * 2;
block.setAttribute('x', x);
block.setAttribute('y', y);
var mutation = this.mutationToDom();
block.appendChild(mutation);
var field = goog.dom.createDom('field');
field.setAttribute('name', 'NAME');
field.appendChild(document.createTextNode(this.getProcedureCall()));
block.appendChild(field);
xml.appendChild(block);
Blockly.Xml.domToWorkspace(xml, this.workspace);
Blockly.Events.setGroup(false);
}
} else if (event.type == Blockly.Events.DELETE) {
// Look for the case where a procedure definition has been deleted,
// leaving this block (a procedure call) orphaned. In this case, delete
// the orphan.
var name = this.getProcedureCall();
var def = Blockly.Procedures.getDefinition(name, this.workspace);
if (!def) {
Blockly.Events.setGroup(event.group);
this.dispose(true, false);
Blockly.Events.setGroup(false);
}
}
},
/**
* Add menu option to find the definition block for this call.
* @param {!Array} options List of menu options to add to.
* @this Blockly.Block
*/
customContextMenu: function(options) {
var option = {enabled: true};
option.text = Blockly.Msg.PROCEDURES_HIGHLIGHT_DEF;
var name = this.getProcedureCall();
var workspace = this.workspace;
option.callback = function() {
var def = Blockly.Procedures.getDefinition(name, workspace);
def && def.select();
};
options.push(option);
},
defType_: 'procedures_defnoreturn'
};
Blockly.Blocks['procedures_callreturn'] = {
/**
* Block for calling a procedure with a return value.
* @this Blockly.Block
*/
init: function() {
this.appendDummyInput('TOPROW')
.appendField('', 'NAME');
this.setOutput(true);
this.setColour(Blockly.Blocks.procedures.HUE);
// Tooltip is set in domToMutation.
this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLRETURN_HELPURL);
this.arguments_ = [];
this.quarkConnections_ = {};
this.quarkIds_ = null;
},
getProcedureCall: Blockly.Blocks['procedures_callnoreturn'].getProcedureCall,
renameProcedure: Blockly.Blocks['procedures_callnoreturn'].renameProcedure,
setProcedureParameters_:
Blockly.Blocks['procedures_callnoreturn'].setProcedureParameters_,
updateShape_: Blockly.Blocks['procedures_callnoreturn'].updateShape_,
mutationToDom: Blockly.Blocks['procedures_callnoreturn'].mutationToDom,
domToMutation: Blockly.Blocks['procedures_callnoreturn'].domToMutation,
renameVar: Blockly.Blocks['procedures_callnoreturn'].renameVar,
onchange: Blockly.Blocks['procedures_callnoreturn'].onchange,
customContextMenu:
Blockly.Blocks['procedures_callnoreturn'].customContextMenu,
defType_: 'procedures_defreturn'
};
Blockly.Blocks['procedures_ifreturn'] = {
/**
* Block for conditionally returning a value from a procedure.
* @this Blockly.Block
*/
init: function() {
this.appendValueInput('CONDITION')
.setCheck('Boolean')
.appendField(Blockly.Msg.CONTROLS_IF_MSG_IF);
this.appendValueInput('VALUE')
.appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
this.setInputsInline(true);
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setColour(Blockly.Blocks.procedures.HUE);
this.setTooltip(Blockly.Msg.PROCEDURES_IFRETURN_TOOLTIP);
this.setHelpUrl(Blockly.Msg.PROCEDURES_IFRETURN_HELPURL);
this.hasReturnValue_ = true;
},
/**
* Create XML to represent whether this block has a return value.
* @return {!Element} XML storage element.
* @this Blockly.Block
*/
mutationToDom: function() {
var container = document.createElement('mutation');
container.setAttribute('value', Number(this.hasReturnValue_));
return container;
},
/**
* Parse XML to restore whether this block has a return value.
* @param {!Element} xmlElement XML storage element.
* @this Blockly.Block
*/
domToMutation: function(xmlElement) {
var value = xmlElement.getAttribute('value');
this.hasReturnValue_ = (value == 1);
if (!this.hasReturnValue_) {
this.removeInput('VALUE');
this.appendDummyInput('VALUE')
.appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
}
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @this Blockly.Block
*/
onchange: function() {
if (!this.workspace.isDragging || this.workspace.isDragging()) {
return; // Don't change state at the start of a drag.
}
var legal = false;
// Is the block nested in a procedure?
var block = this;
do {
if (this.FUNCTION_TYPES.indexOf(block.type) != -1) {
legal = true;
break;
}
block = block.getSurroundParent();
} while (block);
if (legal) {
// If needed, toggle whether this block has a return value.
if (block.type == 'procedures_defnoreturn' && this.hasReturnValue_) {
this.removeInput('VALUE');
this.appendDummyInput('VALUE')
.appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
this.hasReturnValue_ = false;
} else if (block.type == 'procedures_defreturn' &&
!this.hasReturnValue_) {
this.removeInput('VALUE');
this.appendValueInput('VALUE')
.appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
this.hasReturnValue_ = true;
}
this.setWarningText(null);
if (!this.isInFlyout) {
this.setDisabled(false);
}
} else {
this.setWarningText(Blockly.Msg.PROCEDURES_IFRETURN_WARNING);
if (!this.isInFlyout && !this.getInheritedDisabled()) {
this.setDisabled(true);
}
}
},
/**
* List of block types that are functions and thus do not need warnings.
* To add a new function type add this to your code:
* Blockly.Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func');
*/
FUNCTION_TYPES: ['procedures_defnoreturn', 'procedures_defreturn']
};

33
blocks_compressed.js Normal file
View file

@ -0,0 +1,33 @@
// Do not edit this file; automatically generated by build.py.
'use strict';
// Copyright 2016 Google Inc. Apache License 2.0
Blockly.constants={};Blockly.DRAG_RADIUS=3;Blockly.SNAP_RADIUS=48;Blockly.CONNECTING_SNAP_RADIUS=96;Blockly.CURRENT_CONNECTION_PREFERENCE=20;Blockly.BUMP_DELAY=0;Blockly.COLLAPSE_CHARS=30;Blockly.LONGPRESS=750;Blockly.SOUND_LIMIT=100;Blockly.HSV_SATURATION=.45;Blockly.HSV_VALUE=.65;Blockly.SPRITE={width:96,height:124,url:"sprites.png"};Blockly.SVG_NS="http://www.w3.org/2000/svg";Blockly.HTML_NS="http://www.w3.org/1999/xhtml";Blockly.INPUT_VALUE=1;Blockly.OUTPUT_VALUE=2;Blockly.NEXT_STATEMENT=3;
Blockly.PREVIOUS_STATEMENT=4;Blockly.DUMMY_INPUT=5;Blockly.ALIGN_LEFT=-1;Blockly.ALIGN_CENTRE=0;Blockly.ALIGN_RIGHT=1;Blockly.DRAG_NONE=0;Blockly.DRAG_STICKY=1;Blockly.DRAG_BEGIN=1;Blockly.DRAG_FREE=2;Blockly.OPPOSITE_TYPE=[];Blockly.OPPOSITE_TYPE[Blockly.INPUT_VALUE]=Blockly.OUTPUT_VALUE;Blockly.OPPOSITE_TYPE[Blockly.OUTPUT_VALUE]=Blockly.INPUT_VALUE;Blockly.OPPOSITE_TYPE[Blockly.NEXT_STATEMENT]=Blockly.PREVIOUS_STATEMENT;Blockly.OPPOSITE_TYPE[Blockly.PREVIOUS_STATEMENT]=Blockly.NEXT_STATEMENT;
Blockly.TOOLBOX_AT_TOP=0;Blockly.TOOLBOX_AT_BOTTOM=1;Blockly.TOOLBOX_AT_LEFT=2;Blockly.TOOLBOX_AT_RIGHT=3;Blockly.OUTPUT_SHAPE_HEXAGONAL=1;Blockly.OUTPUT_SHAPE_ROUND=2;Blockly.OUTPUT_SHAPE_SQUARE=3;Blockly.STACK_GLOW_RADIUS=1.3;Blockly.REPLACEMENT_GLOW_RADIUS=2;Blockly.Categories={motion:"motion",looks:"looks",sound:"sounds",pen:"pen",data:"data",event:"events",control:"control",sensing:"sensing",operators:"operators",more:"more"};Blockly.DELETE_AREA_TRASH=1;Blockly.DELETE_AREA_TOOLBOX=2;
// Copyright 2012 Google Inc. Apache License 2.0
Blockly.Blocks.colour={};function randomColour(){return"#"+("00000"+Math.floor(Math.random()*Math.pow(2,24)).toString(16)).substr(-6)}Blockly.Blocks.colour_picker={init:function(){this.jsonInit({message0:"%1",args0:[{type:"field_colour",name:"COLOUR",colour:randomColour()}],outputShape:Blockly.OUTPUT_SHAPE_ROUND,output:"Colour"})}};/*
Visual Blocks Editor
Copyright 2016 Massachusetts Institute of Technology
All rights reserved.
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.
*/
Blockly.Colours={motion:{primary:"#4C97FF",secondary:"#4280D7",tertiary:"#3373CC"},looks:{primary:"#9966FF",secondary:"#855CD6",tertiary:"#774DCB"},sounds:{primary:"#CF63CF",secondary:"#C94FC9",tertiary:"#BD42BD"},control:{primary:"#FFAB19",secondary:"#EC9C13",tertiary:"#CF8B17"},event:{primary:"#FFBF00",secondary:"#E6AC00",tertiary:"#CC9900"},sensing:{primary:"#5CB1D6",secondary:"#47A8D1",tertiary:"#2E8EB8"},pen:{primary:"#0fBD8C",secondary:"#0DA57A",tertiary:"#0B8E69"},operators:{primary:"#59C059",
secondary:"#46B946",tertiary:"#389438"},data:{primary:"#FF8C1A",secondary:"#FF8000",tertiary:"#DB6E00"},more:{primary:"#FF6680",secondary:"#FF4D6A",tertiary:"#FF3355"},text:"#575E75",workspace:"#F5F8FF",toolboxHover:"#4C97FF",toolboxSelected:"#e9eef2",toolboxText:"#575E75",toolbox:"#FFFFFF",flyout:"#DDDDDD",scrollbar:"#CCCCCC",scrollbarHover:"#BBBBBB",textField:"#FFFFFF",insertionMarker:"#949494",insertionMarkerOpacity:.6,dragShadowOpacity:.3,stackGlow:"#FFF200",stackGlowOpacity:1,replacementGlow:"#FFFFFF",
replacementGlowOpacity:1,colourPickerStroke:"#FFFFFF",fieldShadow:"rgba(0,0,0,0.1)",dropDownShadow:"rgba(0, 0, 0, .3)",numPadBackground:"#547AB2",numPadBorder:"#435F91",numPadActiveBackground:"#435F91",numPadText:"#FFFFFF",valueReportBackground:"#FFFFFF",valueReportBorder:"#AAAAAA"};Blockly.Blocks.math={};Blockly.Blocks.math_number={init:function(){this.jsonInit({message0:"%1",args0:[{type:"field_number",name:"NUM",value:"0"}],output:"Number",outputShape:Blockly.OUTPUT_SHAPE_ROUND,colour:Blockly.Colours.textField})}};Blockly.Blocks.math_integer={init:function(){this.jsonInit({message0:"%1",args0:[{type:"field_number",name:"NUM",precision:1}],output:"Number",outputShape:Blockly.OUTPUT_SHAPE_ROUND,colour:Blockly.Colours.textField})}};
Blockly.Blocks.math_whole_number={init:function(){this.jsonInit({message0:"%1",args0:[{type:"field_number",name:"NUM",min:0,precision:1}],output:"Number",outputShape:Blockly.OUTPUT_SHAPE_ROUND,colour:Blockly.Colours.textField})}};Blockly.Blocks.math_positive_number={init:function(){this.jsonInit({message0:"%1",args0:[{type:"field_number",name:"NUM",min:0}],output:"Number",outputShape:Blockly.OUTPUT_SHAPE_ROUND,colour:Blockly.Colours.textField})}};
Blockly.Blocks.math_angle={init:function(){this.jsonInit({message0:"%1",args0:[{type:"field_angle",name:"NUM",value:90}],output:"Number",outputShape:Blockly.OUTPUT_SHAPE_ROUND,colour:Blockly.Colours.textField})}};Blockly.Blocks.texts={};Blockly.Blocks.text={init:function(){this.jsonInit({message0:"%1",args0:[{type:"field_input",name:"TEXT"}],output:"String",outputShape:Blockly.OUTPUT_SHAPE_ROUND,colour:Blockly.Colours.textField})}};

View file

@ -10,7 +10,8 @@ git rm -rf demos
git rm -rf generators
git rm -rf tests/generators
git rm -rf appengine
git rm blockly_compressed.js
git rm blockly_uncompressed.js
git rm -f python_compressed.js php_compressed.js tests/playground.html core/block_render_svg.js dart_compressed.js javascript_compressed.js lua_compressed.js
# Turn on more powerful globbing

View file

@ -57,7 +57,7 @@ Blockly.Block = function(workspace, prototypeName, opt_id) {
/** @type {string} */
this.id = (opt_id && !workspace.getBlockById(opt_id) &&
(!flyoutWorkspace || !flyoutWorkspace.getBlockById(opt_id))) ?
opt_id : Blockly.genUid();
opt_id : Blockly.utils.genUid();
workspace.blockDB_[this.id] = this;
/** @type {Blockly.Connection} */
this.outputConnection = null;
@ -370,7 +370,7 @@ Blockly.Block.prototype.bumpNeighbours_ = function() {
if (rootBlock.isInFlyout) {
return; // Don't move blocks around in a flyout.
}
// Loop though every connection on this block.
// Loop through every connection on this block.
var myConnections = this.getConnections_(false);
for (var i = 0, connection; connection = myConnections[i]; i++) {
// Spider down from this block bumping all sub-blocks.
@ -626,7 +626,7 @@ Blockly.Block.prototype.setInsertionMarker = function(insertionMarker) {
if (this.isInsertionMarker_) {
this.setColour(Blockly.Colours.insertionMarker);
this.setOpacity(Blockly.Colours.insertionMarkerOpacity);
Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyInsertionMarker');
}
};
@ -750,7 +750,6 @@ Blockly.Block.prototype.getColourTertiary = function() {
return this.colourTertiary_;
};
/**
* Create an #RRGGBB string colour from a colour HSV hue value or #RRGGBB string.
* @param {number|string} colour HSV hue value, or #RRGGBB string.

205
core/block_drag_surface.js Normal file
View file

@ -0,0 +1,205 @@
/**
* @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 A class that manages a surface for dragging blocks. When a
* block drag is started, we move the block (and children) to a separate dom
* element that we move around using translate3d. At the end of the drag, the
* blocks are put back in into the svg they came from. This helps performance by
* avoiding repainting the entire svg on every mouse move while dragging blocks.
* @author picklesrus
*/
'use strict';
goog.provide('Blockly.BlockDragSurfaceSvg');
goog.require('Blockly.utils');
goog.require('goog.asserts');
goog.require('goog.math.Coordinate');
/**
* Class for a drag surface for the currently dragged block. This is a separate
* SVG that contains only the currently moving block, or nothing.
* @param {!Element} container Containing element.
* @constructor
*/
Blockly.BlockDragSurfaceSvg = function(container) {
/**
* @type {!Element}
* @private
*/
this.container_ = container;
this.createDom();
};
/**
* The SVG drag surface. Set once by Blockly.BlockDragSurfaceSvg.createDom.
* @type {Element}
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.SVG_ = null;
/**
* This is where blocks live while they are being dragged if the drag surface
* is enabled.
* @type {Element}
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.dragGroup_ = null;
/**
* Containing HTML element; parent of the workspace and the drag surface.
* @type {Element}
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.container_ = null;
/**
* Cached value for the scale of the drag surface.
* Used to set/get the correct translation during and after a drag.
* @type {number}
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.scale_ = 1;
/**
* Create the drag surface and inject it into the container.
*/
Blockly.BlockDragSurfaceSvg.prototype.createDom = function() {
if (this.SVG_) {
return; // Already created.
}
this.SVG_ = Blockly.utils.createSvgElement('svg', {
'xmlns': Blockly.SVG_NS,
'xmlns:html': Blockly.HTML_NS,
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
'version': '1.1',
'class': 'blocklyBlockDragSurface'
}, this.container_);
this.dragGroup_ = Blockly.utils.createSvgElement('g', {}, this.SVG_);
};
/**
* Set the SVG blocks on the drag surface's group and show the surface.
* Only one block group should be on the drag surface at a time.
* @param {!Element} blocks Block or group of blocks to place on the drag
* surface.
*/
Blockly.BlockDragSurfaceSvg.prototype.setBlocksAndShow = function(blocks) {
goog.asserts.assert(this.dragGroup_.childNodes.length == 0,
'Already dragging a block.');
// appendChild removes the blocks from the previous parent
this.dragGroup_.appendChild(blocks);
this.SVG_.style.display = 'block';
// This allows blocks to be dragged outside of the blockly svg space.
// This should be reset to hidden at the end of the block drag.
// Note that this behavior is different from blockly where block disappear
// "under" the blockly area.
var injectionDiv = document.getElementsByClassName('injectionDiv')[0];
injectionDiv.style.overflow = 'visible';
};
/**
* Translate and scale the entire drag surface group to keep in sync with the
* workspace.
* @param {number} x X translation
* @param {number} y Y translation
* @param {number} scale Scale of the group.
*/
Blockly.BlockDragSurfaceSvg.prototype.translateAndScaleGroup = function(x, y, scale) {
this.scale_ = scale;
// This is a work-around to prevent a the blocks from rendering
// fuzzy while they are being dragged on the drag surface.
x = x.toFixed(0);
y = y.toFixed(0);
this.dragGroup_.setAttribute('transform', 'translate('+ x + ','+ y + ')' +
' scale(' + scale + ')');
};
/**
* Translate the entire drag surface during a drag.
* We translate the drag surface instead of the blocks inside the surface
* so that the browser avoids repainting the SVG.
* Because of this, the drag coordinates must be adjusted by scale.
* @param {number} x X translation for the entire surface.
* @param {number} y Y translation for the entire surface.
*/
Blockly.BlockDragSurfaceSvg.prototype.translateSurface = function(x, y) {
var transform;
x *= this.scale_;
y *= this.scale_;
// This is a work-around to prevent a the blocks from rendering
// fuzzy while they are being dragged on the drag surface.
x = x.toFixed(0);
y = y.toFixed(0);
transform =
'transform: translate3d(' + x + 'px, ' + y + 'px, 0px); display: block;';
this.SVG_.setAttribute('style', transform);
};
/**
* Reports the surface translation in scaled workspace coordinates.
* Use this when finishing a drag to return blocks to the correct position.
* @return {!goog.math.Coordinate} Current translation of the surface.
*/
Blockly.BlockDragSurfaceSvg.prototype.getSurfaceTranslation = function() {
var xy = Blockly.utils.getRelativeXY(this.SVG_);
return new goog.math.Coordinate(xy.x / this.scale_, xy.y / this.scale_);
};
/**
* Provide a reference to the drag group (primarily for
* BlockSvg.getRelativeToSurfaceXY).
* @return {Element} Drag surface group element.
*/
Blockly.BlockDragSurfaceSvg.prototype.getGroup = function() {
return this.dragGroup_;
};
/**
* Get the current blocks on the drag surface, if any (primarily
* for BlockSvg.getRelativeToSurfaceXY).
* @return {!Element|undefined} Drag surface block DOM element, or undefined
* if no blocks exist.
*/
Blockly.BlockDragSurfaceSvg.prototype.getCurrentBlock = function() {
return this.dragGroup_.firstChild;
};
/**
* Clear the group and hide the surface; move the blocks off onto the provided
* element.
* @param {!Element} newSurface Surface the dragging blocks should be moved to.
*/
Blockly.BlockDragSurfaceSvg.prototype.clearAndHide = function(newSurface) {
// appendChild removes the node from this.dragGroup_
newSurface.appendChild(this.getCurrentBlock());
this.SVG_.style.display = 'none';
// Reset the overflow property back to hidden so that nothing appears outside
// of the blockly area.
// Note that this behavior is different from blockly. See note in
// setBlocksAndShow.
var injectionDiv = document.getElementsByClassName('injectionDiv')[0];
injectionDiv.style.overflow = 'hidden';
goog.asserts.assert(this.dragGroup_.childNodes.length == 0,
'Drag group was not cleared.');
};

View file

@ -336,11 +336,11 @@ Blockly.BlockSvg.prototype.updateColour = function() {
Blockly.BlockSvg.prototype.highlightForReplacement = function(add) {
if (add) {
this.svgPath_.setAttribute('filter', 'url(#blocklyReplacementGlowFilter)');
Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.addClass_(/** @type {!Element} */ (this.svgGroup_),
'blocklyReplaceable');
} else {
this.svgPath_.removeAttribute('filter');
Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.removeClass_(/** @type {!Element} */ (this.svgGroup_),
'blocklyReplaceable');
}
};

View file

@ -510,11 +510,11 @@ Blockly.BlockSvg.prototype.updateColour = function() {
Blockly.BlockSvg.prototype.highlightForReplacement = function(add) {
if (add) {
this.svgPath_.setAttribute('filter', 'url(#blocklyReplacementGlowFilter)');
Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyReplaceable');
} else {
this.svgPath_.removeAttribute('filter');
Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyReplaceable');
}
};
@ -533,11 +533,11 @@ Blockly.BlockSvg.prototype.highlightShapeForInput = function(conn, add) {
var inputShape = this.inputShapes_[input.name];
if (add) {
inputShape.setAttribute('filter', 'url(#blocklyReplacementGlowFilter)');
Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyReplaceable');
} else {
inputShape.removeAttribute('filter');
Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyReplaceable');
}
};

View file

@ -53,9 +53,9 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) {
* @type {SVGElement}
* @private
*/
this.svgGroup_ = Blockly.createSvgElement('g', {}, null);
this.svgGroup_ = Blockly.utils.createSvgElement('g', {}, null);
/** @type {SVGElement} */
this.svgPath_ = Blockly.createSvgElement('path', {'class': 'blocklyPath blocklyBlockBackground'},
this.svgPath_ = Blockly.utils.createSvgElement('path', {'class': 'blocklyPath blocklyBlockBackground'},
this.svgGroup_);
this.svgPath_.tooltip = this;
@ -65,6 +65,14 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) {
/** @type {Object.<string,Element>} */
this.inputShapes_ = {};
/**
* Whether to move the block to the drag surface when it is dragged.
* True if it should move, false if it should be translated directly.
* @type {boolean}
* @private
*/
this.useDragSurface_ = Blockly.utils.is3dSupported() && workspace.blockDragSurface_;
Blockly.Tooltip.bindMouseEvents(this.svgPath_);
Blockly.BlockSvg.superClass_.constructor.call(this,
workspace, prototypeName, opt_id);
@ -170,7 +178,7 @@ Blockly.BlockSvg.prototype.initInputShape = function(input) {
// there's a shadow block that will always cover the input shape.
return;
}
this.inputShapes_[input.name] = Blockly.createSvgElement(
this.inputShapes_[input.name] = Blockly.utils.createSvgElement(
'path',
{
'class': 'blocklyPath',
@ -406,26 +414,28 @@ Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() {
// or to the drag surface group.
var x = 0;
var y = 0;
var dragSurfaceGroup = (this.workspace.dragSurface) ?
this.workspace.dragSurface.getGroup() : null;
var dragSurfaceGroup = this.useDragSurface_ ?
this.workspace.blockDragSurface_.getGroup() : null;
var element = this.getSvgRoot();
if (element) {
do {
// Loop through this block and every parent.
var xy = Blockly.getRelativeXY_(element);
var xy = Blockly.utils.getRelativeXY(element);
x += xy.x;
y += xy.y;
// If this element is the current element on the drag surface, include
// the translation of the drag surface itself.
if (this.workspace.dragSurface &&
this.workspace.dragSurface.getCurrentBlock() == element) {
var surfaceTranslation = this.workspace.dragSurface.getSurfaceTranslation();
if (this.useDragSurface_ &&
this.workspace.blockDragSurface_.getCurrentBlock() == element) {
var surfaceTranslation = this.workspace.blockDragSurface_.getSurfaceTranslation();
x += surfaceTranslation.x;
y += surfaceTranslation.y;
}
element = element.parentNode;
} while (element && element != this.workspace.getCanvas() &&
element != dragSurfaceGroup);
element != dragSurfaceGroup);
}
return new goog.math.Coordinate(x, y);
};
@ -467,6 +477,63 @@ Blockly.BlockSvg.prototype.translate = function(x, y, opt_use3d) {
}
};
/**
* Transforms a block by setting the translation on the transform attribute
* of the block's SVG.
* @param {number} x The x coordinate of the translation.
* @param {number} y The y coordinate of the translation.
*/
Blockly.BlockSvg.prototype.translate = function(x, y) {
this.getSvgRoot().setAttribute('transform',
'translate(' + x + ',' + y + ')');
};
/**
* Move this block to its workspace's drag surface, accounting for positioning.
* Generally should be called at the same time as setDragging_(true).
* Does nothing if useDragSurface_ is false.
* @private
*/
Blockly.BlockSvg.prototype.moveToDragSurface_ = function() {
if (!this.useDragSurface_) {
return;
}
// The translation for drag surface blocks,
// is equal to the current relative-to-surface position,
// to keep the position in sync as it move on/off the surface.
var xy = this.getRelativeToSurfaceXY();
this.clearTransformAttributes_();
this.workspace.blockDragSurface_.translateSurface(xy.x, xy.y);
// Execute the move on the top-level SVG component
this.workspace.blockDragSurface_.setBlocksAndShow(this.getSvgRoot());
};
/**
* Move this block back to the workspace block canvas.
* Generally should be called at the same time as setDragging_(false).
* Does nothing if useDragSurface_ is false.
* @private
*/
Blockly.BlockSvg.prototype.moveOffDragSurface_ = function() {
if (!this.useDragSurface_) {
return;
}
// Translate to current position, turning off 3d.
var xy = this.getRelativeToSurfaceXY();
this.clearTransformAttributes_();
this.translate(xy.x, xy.y);
this.workspace.blockDragSurface_.clearAndHide(this.workspace.getCanvas());
};
/**
* Clear the block of transform="..." attributes.
* Used when the block is switching from 3d to 2d transform or vice versa.
* @private
*/
Blockly.BlockSvg.prototype.clearTransformAttributes_ = function() {
this.getSvgRoot().removeAttribute('transform');
};
/**
* Snap this block to the nearest grid point.
*/
@ -645,7 +712,7 @@ Blockly.BlockSvg.prototype.onMouseDown_ = function(e) {
// that behaviour is wrong, because Blockly.Flyout.prototype.blockMouseDown
// should be called for a mousedown on a block in the flyout, which blocks
// execution of the block's onMouseDown_ function.
if (e.type == 'touchstart' && Blockly.isRightButton(e)) {
if (e.type == 'touchstart' && Blockly.utils.isRightButton(e)) {
Blockly.Flyout.blockRightClick_(e, this);
e.stopPropagation();
e.preventDefault();
@ -664,7 +731,7 @@ Blockly.BlockSvg.prototype.onMouseDown_ = function(e) {
this.select();
Blockly.hideChaff();
Blockly.DropDownDiv.hideWithoutAnimation();
if (Blockly.isRightButton(e)) {
if (Blockly.utils.isRightButton(e)) {
// Right-click.
this.showContextMenu_(e);
// Click, not drag, so stop waiting for other touches from this identifier.
@ -893,11 +960,11 @@ Blockly.BlockSvg.prototype.setDragging_ = function(adding) {
group.skew_ = '';
Blockly.draggingConnections_ =
Blockly.draggingConnections_.concat(this.getConnections_(true));
Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyDragging');
} else {
Blockly.draggingConnections_ = [];
Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyDragging');
}
// Recurse through all blocks attached under this one.
@ -906,21 +973,6 @@ Blockly.BlockSvg.prototype.setDragging_ = function(adding) {
}
};
/**
* Move this block to its workspace's drag surface, accounting for positioning.
* Generally should be called at the same time as setDragging_(true).
* @private
*/
Blockly.BlockSvg.prototype.moveToDragSurface_ = function() {
// The translation for drag surface blocks,
// is equal to the current relative-to-surface position,
// to keep the position in sync as it move on/off the surface.
var xy = this.getRelativeToSurfaceXY();
this.clearTransformAttributes_();
this.workspace.dragSurface.translateSurface(xy.x, xy.y);
// Execute the move on the top-level SVG component
this.workspace.dragSurface.setBlocksAndShow(this.getSvgRoot());
};
/**
* Move this block back to the workspace block canvas.
@ -932,7 +984,7 @@ Blockly.BlockSvg.prototype.moveOffDragSurface_ = function() {
var xy = this.getRelativeToSurfaceXY();
this.clearTransformAttributes_();
this.translate(xy.x, xy.y, false);
this.workspace.dragSurface.clearAndHide(this.workspace.getCanvas());
this.workspace.blockDragSurface_.clearAndHide(this.workspace.getCanvas());
};
/**
@ -977,12 +1029,7 @@ Blockly.BlockSvg.prototype.onMouseMove_ = function(e) {
// Switch to unrestricted dragging.
Blockly.dragMode_ = Blockly.DRAG_FREE;
Blockly.longStop_();
// Must move to drag surface before unplug(),
// or else connections will calculate the wrong relative to surface XY
// in tighten_(). Then blocks connected to this block move around on the
// drag surface. By moving to the drag surface before unplug, connection
// positions will be calculated correctly.
this.moveToDragSurface_();
// Disable workspace resizing as an optimization.
this.workspace.setResizesEnabled(false);
// Clear WidgetDiv/DropDownDiv without animating, in case blocks are moved
@ -994,6 +1041,7 @@ Blockly.BlockSvg.prototype.onMouseMove_ = function(e) {
this.unplug();
}
this.setDragging_(true);
this.moveToDragSurface_();
}
}
if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
@ -1015,7 +1063,13 @@ Blockly.BlockSvg.prototype.onMouseMove_ = function(e) {
*/
Blockly.BlockSvg.prototype.handleDragFree_ = function(oldXY, newXY, e) {
var dxy = goog.math.Coordinate.difference(oldXY, this.dragStartXY_);
this.workspace.dragSurface.translateSurface(newXY.x, newXY.y);
var group = this.getSvgRoot();
if (this.useDragSurface_) {
this.workspace.blockDragSurface_.translateSurface(newXY.x, newXY.y);
} else {
group.translate_ = 'translate(' + newXY.x + ',' + newXY.y + ')';
group.setAttribute('transform', group.translate_ + group.skew_);
}
// Drag all the nested bubbles.
for (var i = 0; i < this.draggedBubbles_.length; i++) {
var commentData = this.draggedBubbles_[i];
@ -1120,8 +1174,10 @@ Blockly.BlockSvg.prototype.updatePreviews = function(closestConnection,
Blockly.localConnection_ = null;
}
var wouldDeleteBlock = this.updateCursor_(e, closestConnection);
// Add an insertion marker or replacement marker if needed.
if (closestConnection &&
if (!wouldDeleteBlock && closestConnection &&
closestConnection != Blockly.highlightedConnection_ &&
!closestConnection.sourceBlock_.isInsertionMarker()) {
Blockly.highlightedConnection_ = closestConnection;
@ -1271,15 +1327,50 @@ Blockly.BlockSvg.disconnectInsertionMarker = function() {
Blockly.insertionMarker_.getSvgRoot().setAttribute('visibility', 'hidden');
};
/**
* Provide visual indication of whether the block will be deleted if
* dropped here.
* Prefer connecting over dropping into the trash can, but prefer dragging to
* the toolbox over connecting to other blocks.
* @param {!Event} e Mouse move event.
* @param {Blockly.Connection} closestConnection The connection this block would
* potentially connect to if dropped here, or null.
* @return {boolean} True if the block would be deleted if dropped here,
* otherwise false.
* @private
*/
Blockly.BlockSvg.prototype.updateCursor_ = function(e, closestConnection) {
var deleteArea = this.workspace.isDeleteArea(e);
var wouldConnect = Blockly.selected && closestConnection &&
deleteArea != Blockly.DELETE_AREA_TOOLBOX;
var wouldDelete = deleteArea && !this.getParent() &&
Blockly.selected.isDeletable();
var showDeleteCursor = wouldDelete && !wouldConnect;
if (showDeleteCursor) {
Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE);
if (deleteArea == Blockly.DELETE_AREA_TRASH && this.workspace.trashcan) {
this.workspace.trashcan.setOpen_(true);
}
return true;
} else {
Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
if (this.workspace.trashcan) {
this.workspace.trashcan.setOpen_(false);
}
return false;
}
};
/**
* Add or remove the UI indicating if this block is movable or not.
*/
Blockly.BlockSvg.prototype.updateMovable = function() {
if (this.isMovable()) {
Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyDraggable');
} else {
Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyDraggable');
}
};
@ -1392,8 +1483,7 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) {
Blockly.BlockSvg.prototype.disposeUiEffect = function() {
this.workspace.playAudio('delete');
var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_),
this.workspace);
var xy = this.workspace.getSvgXY(/** @type {!Element} */ (this.svgGroup_));
// Deeply clone the current block.
var clone = this.svgGroup_.cloneNode(true);
clone.translateX_ = xy.x;
@ -1416,7 +1506,7 @@ Blockly.BlockSvg.prototype.connectionUiEffect = function() {
/**
* Animate a cloned block and eventually dispose of it.
* This is a class method, not an instace method since the original block has
* This is a class method, not an instance method since the original block has
* been destroyed and is no longer accessible.
* @param {!Element} clone SVG element to animate and dispose of.
* @param {boolean} rtl True if RTL, false if LTR.
@ -1502,7 +1592,7 @@ Blockly.BlockSvg.prototype.setWarningText = function(text, opt_id) {
}
var id = opt_id || '';
if (!id) {
// Kill all previous pending processes, this edit supercedes them all.
// Kill all previous pending processes, this edit supersedes them all.
for (var n in this.setWarningText.pid_) {
clearTimeout(this.setWarningText.pid_[n]);
delete this.setWarningText.pid_[n];
@ -1576,7 +1666,7 @@ Blockly.BlockSvg.prototype.setMutator = function(mutator) {
* Select this block. Highlight it visually.
*/
Blockly.BlockSvg.prototype.addSelect = function() {
Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
'blocklySelected');
// Move the selected block to the top of the stack.
var block = this;
@ -1591,7 +1681,7 @@ Blockly.BlockSvg.prototype.addSelect = function() {
* Unselect this block. Remove its highlighting.
*/
Blockly.BlockSvg.prototype.removeSelect = function() {
Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_),
'blocklySelected');
};

View file

@ -156,26 +156,11 @@ Blockly.clipboardSource_ = null;
Blockly.dragMode_ = Blockly.DRAG_NONE;
/**
* Map from function names to callbacks, for deciding what to do when a button
* is clicked.
* @type {!Object<string, function(!Blockly.FlyoutButton)}
* Cached value for whether 3D is supported.
* @type {!boolean}
* @private
*/
Blockly.flyoutButtonCallbacks_ = {};
/**
* Register a callback function associated with a given key, for clicks on
* buttons and labels in the flyout.
* For instance, a button specified by the XML
* <button text="create variable" callbackKey="CREATE_VARIABLE"></button>
* should be matched by a call to
* registerButtonCallback("CREATE_VARIABLE", yourCallbackFunction).
* @param {string} key The name to use to look up this function.
* @param {function(!Blockly.FlyoutButton)} func The function to call when the
* given button is clicked.
*/
Blockly.registerButtonCallback = function(key, func) {
Blockly.flyoutButtonCallbacks_[key] = func;
};
Blockly.cache3dSupported_ = null;
/**
* Convert a hue (HSV model) into an RGB hex triplet.
@ -244,7 +229,7 @@ Blockly.svgResize = function(workspace) {
* @private
*/
Blockly.onKeyDown_ = function(e) {
if (Blockly.mainWorkspace.options.readOnly || Blockly.isTargetInput_(e)) {
if (Blockly.mainWorkspace.options.readOnly || Blockly.utils.isTargetInput(e)) {
// No key actions on readonly workspaces.
// When focused on an HTML text input widget, don't trap any keys.
return;
@ -343,7 +328,7 @@ Blockly.duplicate_ = function(block) {
* @private
*/
Blockly.onContextMenu_ = function(e) {
if (!Blockly.isTargetInput_(e)) {
if (!Blockly.utils.isTargetInput(e)) {
// When focused on an HTML text input widget, don't cancel the context menu.
e.preventDefault();
}
@ -406,12 +391,173 @@ Blockly.confirm = function(message, callback) {
* recommend testing mobile when overriding this.
* @param {string} message The message to display to the user.
* @param {string} defaultValue The value to initialize the prompt with.
* @param {!function(string)} callback The callback for handling user reponse.
* @param {!function(string)} callback The callback for handling user response.
*/
Blockly.prompt = function(message, defaultValue, callback) {
callback(window.prompt(message, defaultValue));
};
/**
* Helper function for defining a block from JSON. The resulting function has
* the correct value of jsonDef at the point in code where jsonInit is called.
* @param {!Object} jsonDef The JSON definition of a block.
* @return {function} A function that calls jsonInit with the correct value
* of jsonDef.
* @private
*/
Blockly.jsonInitFactory_ = function(jsonDef) {
return function() {
this.jsonInit(jsonDef);
};
};
/**
* Define blocks from an array of JSON block definitions, as might be generated
* by the Blockly Developer Tools.
* @param {!Array.<!Object>} jsonArray An array of JSON block definitions.
*/
Blockly.defineBlocksWithJsonArray = function(jsonArray) {
for (var i = 0, elem; elem = jsonArray[i]; i++) {
Blockly.Blocks[elem.type] = {
init: Blockly.jsonInitFactory_(elem)
};
}
};
/**
* Bind an event to a function call. When calling the function, verifies that
* it belongs to the touch stream that is currently being processed, and splits
* multitouch events into multiple events as needed.
* @param {!Node} node Node upon which to listen.
* @param {string} name Event name to listen to (e.g. 'mousedown').
* @param {Object} thisObject The value of 'this' in the function.
* @param {!Function} func Function to call when event is triggered.
* @param {boolean} opt_noCaptureIdentifier True if triggering on this event
* should not block execution of other event handlers on this touch or other
* simultaneous touches.
* @return {!Array.<!Array>} Opaque data that can be passed to unbindEvent_.
* @private
*/
Blockly.bindEventWithChecks_ = function(node, name, thisObject, func,
opt_noCaptureIdentifier) {
var handled = false;
var wrapFunc = function(e) {
var captureIdentifier = !opt_noCaptureIdentifier;
// Handle each touch point separately. If the event was a mouse event, this
// will hand back an array with one element, which we're fine handling.
var events = Blockly.Touch.splitEventByTouches(e);
for (var i = 0, event; event = events[i]; i++) {
if (captureIdentifier && !Blockly.Touch.shouldHandleEvent(event)) {
continue;
}
Blockly.Touch.setClientFromTouch(event);
if (thisObject) {
func.call(thisObject, event);
} else {
func(event);
}
handled = true;
}
};
node.addEventListener(name, wrapFunc, false);
var bindData = [[node, name, wrapFunc]];
// Add equivalent touch event.
if (name in Blockly.Touch.TOUCH_MAP) {
var touchWrapFunc = function(e) {
wrapFunc(e);
// Stop the browser from scrolling/zooming the page.
if (handled) {
e.preventDefault();
}
};
for (var i = 0, eventName;
eventName = Blockly.Touch.TOUCH_MAP[name][i]; i++) {
node.addEventListener(eventName, touchWrapFunc, false);
bindData.push([node, eventName, touchWrapFunc]);
}
}
return bindData;
};
/**
* Bind an event to a function call. Handles multitouch events by using the
* coordinates of the first changed touch, and doesn't do any safety checks for
* simultaneous event processing.
* @deprecated in favor of bindEventWithChecks_, but preserved for external
* users.
* @param {!Node} node Node upon which to listen.
* @param {string} name Event name to listen to (e.g. 'mousedown').
* @param {Object} thisObject The value of 'this' in the function.
* @param {!Function} func Function to call when event is triggered.
* @return {!Array.<!Array>} Opaque data that can be passed to unbindEvent_.
* @private
*/
Blockly.bindEvent_ = function(node, name, thisObject, func) {
var wrapFunc = function(e) {
if (thisObject) {
func.call(thisObject, e);
} else {
func(e);
}
};
node.addEventListener(name, wrapFunc, false);
var bindData = [[node, name, wrapFunc]];
// Add equivalent touch event.
if (name in Blockly.Touch.TOUCH_MAP) {
var touchWrapFunc = function(e) {
// Punt on multitouch events.
if (e.changedTouches.length == 1) {
// Map the touch event's properties to the event.
var touchPoint = e.changedTouches[0];
e.clientX = touchPoint.clientX;
e.clientY = touchPoint.clientY;
}
wrapFunc(e);
// Stop the browser from scrolling/zooming the page.
e.preventDefault();
};
for (var i = 0, eventName;
eventName = Blockly.Touch.TOUCH_MAP[name][i]; i++) {
node.addEventListener(eventName, touchWrapFunc, false);
bindData.push([node, eventName, touchWrapFunc]);
}
}
return bindData;
};
/**
* Unbind one or more events event from a function call.
* @param {!Array.<!Array>} bindData Opaque data from bindEvent_.
* This list is emptied during the course of calling this function.
* @return {!Function} The function call.
* @private
*/
Blockly.unbindEvent_ = function(bindData) {
while (bindData.length) {
var bindDatum = bindData.pop();
var node = bindDatum[0];
var name = bindDatum[1];
var func = bindDatum[2];
node.removeEventListener(name, func, false);
}
return func;
};
/**
* Is the given string a number (includes negative and decimals).
* @param {string} str Input string.
* @return {boolean} True if number, false otherwise.
*/
Blockly.isNumber = function(str) {
return !!str.match(/^\s*-?\d+(\.\d+)?\s*$/);
};
// IE9 does not have a console. Create a stub to stop errors.
if (!goog.global['console']) {
goog.global['console'] = {

View file

@ -222,7 +222,7 @@ Blockly.Bubble.prototype.createDom_ = function(content, hasResize) {
[...content goes here...]
</g>
*/
this.bubbleGroup_ = Blockly.createSvgElement('g', {}, null);
this.bubbleGroup_ = Blockly.utils.createSvgElement('g', {}, null);
var filter =
{'filter': 'url(#' + this.workspace_.options.embossFilterId + ')'};
if (goog.userAgent.getUserAgentString().indexOf('JavaFX') != -1) {
@ -232,27 +232,27 @@ Blockly.Bubble.prototype.createDom_ = function(content, hasResize) {
// https://github.com/google/blockly/issues/99
filter = {};
}
var bubbleEmboss = Blockly.createSvgElement('g',
var bubbleEmboss = Blockly.utils.createSvgElement('g',
filter, this.bubbleGroup_);
this.bubbleArrow_ = Blockly.createSvgElement('path', {}, bubbleEmboss);
this.bubbleBack_ = Blockly.createSvgElement('rect',
this.bubbleArrow_ = Blockly.utils.createSvgElement('path', {}, bubbleEmboss);
this.bubbleBack_ = Blockly.utils.createSvgElement('rect',
{'class': 'blocklyDraggable', 'x': 0, 'y': 0,
'rx': Blockly.Bubble.BORDER_WIDTH, 'ry': Blockly.Bubble.BORDER_WIDTH},
bubbleEmboss);
if (hasResize) {
this.resizeGroup_ = Blockly.createSvgElement('g',
this.resizeGroup_ = Blockly.utils.createSvgElement('g',
{'class': this.workspace_.RTL ?
'blocklyResizeSW' : 'blocklyResizeSE'},
this.bubbleGroup_);
var resizeSize = 2 * Blockly.Bubble.BORDER_WIDTH;
Blockly.createSvgElement('polygon',
Blockly.utils.createSvgElement('polygon',
{'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())},
this.resizeGroup_);
Blockly.createSvgElement('line',
Blockly.utils.createSvgElement('line',
{'class': 'blocklyResizeLine',
'x1': resizeSize / 3, 'y1': resizeSize - 1,
'x2': resizeSize - 1, 'y2': resizeSize / 3}, this.resizeGroup_);
Blockly.createSvgElement('line',
Blockly.utils.createSvgElement('line',
{'class': 'blocklyResizeLine',
'x1': resizeSize * 2 / 3, 'y1': resizeSize - 1,
'x2': resizeSize - 1, 'y2': resizeSize * 2 / 3}, this.resizeGroup_);
@ -271,11 +271,11 @@ Blockly.Bubble.prototype.createDom_ = function(content, hasResize) {
Blockly.Bubble.prototype.bubbleMouseDown_ = function(e) {
this.promote_();
Blockly.Bubble.unbindDragEvents_();
if (Blockly.isRightButton(e)) {
if (Blockly.utils.isRightButton(e)) {
// No right-click.
e.stopPropagation();
return;
} else if (Blockly.isTargetInput_(e)) {
} else if (Blockly.utils.isTargetInput(e)) {
// When focused on an HTML text input widget, don't trap any events.
return;
}
@ -317,7 +317,7 @@ Blockly.Bubble.prototype.bubbleMouseMove_ = function(e) {
Blockly.Bubble.prototype.resizeMouseDown_ = function(e) {
this.promote_();
Blockly.Bubble.unbindDragEvents_();
if (Blockly.isRightButton(e)) {
if (Blockly.utils.isRightButton(e)) {
// No right-click.
e.stopPropagation();
return;

View file

@ -68,20 +68,20 @@ Blockly.Comment.prototype.height_ = 80;
*/
Blockly.Comment.prototype.drawIcon_ = function(group) {
// Circle.
Blockly.createSvgElement('circle',
Blockly.utils.createSvgElement('circle',
{'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'},
group);
// Can't use a real '?' text character since different browsers and operating
// systems render it differently.
// Body of question mark.
Blockly.createSvgElement('path',
Blockly.utils.createSvgElement('path',
{'class': 'blocklyIconSymbol',
'd': 'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405' +
' 0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25' +
' -1.201,0.998 -1.201,1.528 -1.204,2.19z'},
group);
// Dot of question point.
Blockly.createSvgElement('rect',
// Dot of question mark.
Blockly.utils.createSvgElement('rect',
{'class': 'blocklyIconSymbol',
'x': '6.8', 'y': '10.78', 'height': '2', 'width': '2'},
group);
@ -102,7 +102,7 @@ Blockly.Comment.prototype.createEditor_ = function() {
</body>
</foreignObject>
*/
this.foreignObject_ = Blockly.createSvgElement('foreignObject',
this.foreignObject_ = Blockly.utils.createSvgElement('foreignObject',
{'x': Blockly.Bubble.BORDER_WIDTH, 'y': Blockly.Bubble.BORDER_WIDTH},
null);
var body = document.createElementNS(Blockly.HTML_NS, 'body');

View file

@ -261,3 +261,16 @@ Blockly.Categories = {
"operators": "operators",
"more": "more"
};
/**
* ENUM representing that an event is in the delete area of the trash can.
* @const
*/
Blockly.DELETE_AREA_TRASH = 1;
/**
* ENUM representing that an event is in the delete area of the toolbox or
* flyout.
* @const
*/
Blockly.DELETE_AREA_TOOLBOX = 2;

View file

@ -80,9 +80,10 @@ Blockly.ContextMenu.show = function(e, options, rtl) {
var div = Blockly.WidgetDiv.DIV;
menu.render(div);
var menuDom = menu.getElement();
Blockly.addClass_(menuDom, 'blocklyContextMenu');
Blockly.utils.addClass(menuDom, 'blocklyContextMenu');
// Prevent system context menu when right-clicking a Blockly context menu.
Blockly.bindEventWithChecks_(menuDom, 'contextmenu', null, Blockly.noEvent);
Blockly.bindEventWithChecks_(menuDom, 'contextmenu', null,
Blockly.utils.noEvent);
// Record menuSize after adding menu.
var menuSize = goog.style.getSize(menuDom);

View file

@ -98,9 +98,11 @@ Blockly.Css.inject = function(hasCss, pathToMedia) {
);
}
}
// Inject CSS tag.
// Inject CSS tag at start of head.
var cssNode = document.createElement('style');
document.head.appendChild(cssNode);
document.head.insertBefore(cssNode, document.head.firstChild);
var cssTextNode = document.createTextNode(text);
cssNode.appendChild(cssTextNode);
Blockly.Css.styleSheet_ = cssNode.sheet;
@ -160,6 +162,7 @@ Blockly.Css.CONTENT = [
'background-color: $colour_workspace;',
'outline: none;',
'overflow: hidden;', /* IE overflows by default. */
'position: absolute;',
'display: block;',
'}',
@ -179,6 +182,7 @@ Blockly.Css.CONTENT = [
'.injectionDiv {',
'height: 100%;',
'position: relative;',
'overflow: hidden;', /* So blocks in drag surface disappear at edges */
'}',
'.blocklyNonSelectable {',
@ -209,6 +213,25 @@ Blockly.Css.CONTENT = [
'-ms-user-select: none;',
'}',
'.blocklyWsDragSurface {',
'display: none;',
'position: absolute;',
'overflow: visible;',
'top: 0;',
'left: 0;',
'}',
'.blocklyBlockDragSurface {',
'display: none;',
'position: absolute;',
'top: 0;',
'left: 0;',
'right: 0;',
'bottom: 0;',
'overflow: visible !important;',
'z-index: 5000;',
'}',
'.blocklyTooltipDiv {',
'background-color: #ffffc7;',
'border: 1px solid #ddc;',
@ -223,21 +246,6 @@ Blockly.Css.CONTENT = [
'z-index: 100000;', /* big value for bootstrap3 compatibility */
'}',
'.blocklyDragSurface {',
'display: none;',
'position: absolute;',
'top: 0;',
'left: 0;',
'right: 0;',
'bottom: 0;',
'overflow: visible !important;',
'z-index: 5000;', /* Always display on top */
'-webkit-backface-visibility: hidden;',
'backface-visibility: hidden;',
'-webkit-perspective: 1000;',
'perspective: 1000;',
'}',
'.blocklyDropDownDiv {',
'position: fixed;',
'left: 0;',
@ -420,6 +428,10 @@ Blockly.Css.CONTENT = [
'fill: $colour_text;',
'}',
'.blocklyFlyout {',
'position: absolute;',
'z-index: 20;',
'}',
'.blocklyFlyoutButton {',
'fill: #888;',
'cursor: default;',
@ -445,15 +457,11 @@ Blockly.Css.CONTENT = [
'fill: #000;',
'}',
'.blocklyFlyoutLabelText:hover {',
'fill: #aaa;',
'}',
/*
Don't allow users to select text. It gets annoying when trying to
drag a block and selected text moves instead.
*/
'.blocklySvg text {',
'.blocklySvg text, .blocklyBlockDragSurface text {',
'user-select: none;',
'-moz-user-select: none;',
'-webkit-user-select: none;',
@ -530,6 +538,12 @@ Blockly.Css.CONTENT = [
'fill-opacity: .8;',
'}',
'.blocklyScrollbarHorizontal, .blocklyScrollbarVertical {',
'position: absolute;',
'outline: none;',
'z-index: 30;',
'}',
'.blocklyScrollbarBackground {',
'opacity: 0;',
'}',
@ -619,6 +633,7 @@ Blockly.Css.CONTENT = [
'overflow-y: auto;',
'position: absolute;',
'font-family: "Helvetica Neue", Helvetica, sans-serif;',
'z-index: 70;', /* so blocks go under toolbox when dragging */
'}',
'.blocklyTreeRoot {',

View file

@ -1,231 +0,0 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2016 Massachusetts Institute of Technology
* All rights reserved.
*
* 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 An SVG that floats on top of the workspace.
* Blocks are moved into this SVG during a drag, improving performance.
* The entire SVG is translated, so the blocks are never repainted during drag.
* @author tmickel@mit.edu (Tim Mickel)
*/
'use strict';
goog.provide('Blockly.DragSurfaceSvg');
goog.require('Blockly.utils');
goog.require('Blockly.constants');
goog.require('Blockly.Colours');
goog.require('goog.asserts');
goog.require('goog.math.Coordinate');
/**
* Class for a Drag Surface SVG.
* @param {Element} container Containing element.
* @constructor
*/
Blockly.DragSurfaceSvg = function(container) {
this.container_ = container;
};
/**
* The SVG drag surface. Set once by Blockly.DragSurfaceSvg.createDom.
* @type {Element}
* @private
*/
Blockly.DragSurfaceSvg.prototype.SVG_ = null;
/**
* SVG group inside the drag surface. This is where blocks are moved to.
* @type {Element}
* @private
*/
Blockly.DragSurfaceSvg.prototype.dragGroup_ = null;
/**
* Containing HTML element; parent of the workspace and the drag surface.
* @type {Element}
* @private
*/
Blockly.DragSurfaceSvg.prototype.container_ = null;
/**
* Cached value for the scale of the drag surface.
* Used to set/get the correct translation during and after a drag.
* @type {Number}
* @private
*/
Blockly.DragSurfaceSvg.prototype.scale_ = 1;
/**
* ID for the drag shadow filter, set in createDom.
* @type {string}
* @private
*/
Blockly.DragSurfaceSvg.prototype.dragShadowFilterId_ = '';
/**
* Standard deviation for gaussian blur on drag shadow, in px.
* @type {number}
* @const
*/
Blockly.DragSurfaceSvg.SHADOW_STD_DEVIATION = 6;
/**
* Create the drag surface and inject it into the container.
*/
Blockly.DragSurfaceSvg.prototype.createDom = function() {
if (this.SVG_) {
return; // Already created.
}
this.SVG_ = Blockly.createSvgElement('svg', {
'xmlns': Blockly.SVG_NS,
'xmlns:html': Blockly.HTML_NS,
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
'version': '1.1',
'class': 'blocklyDragSurface'
}, this.container_);
var defs = Blockly.createSvgElement('defs', {}, this.SVG_);
this.dragShadowFilterId_ = this.createDropShadowDom_(defs);
this.dragGroup_ = Blockly.createSvgElement('g', {}, this.SVG_);
this.dragGroup_.setAttribute('filter', 'url(#' + this.dragShadowFilterId_ + ')');
};
/**
* Create the SVG def for the drop shadow.
* @param {Element} defs Defs element to insert the shadow filter definition
* @return {string} ID for the filter element
*/
Blockly.DragSurfaceSvg.prototype.createDropShadowDom_ = function(defs) {
// Adjust these width/height, x/y properties to prevent the shadow from clipping
var dragShadowFilter = Blockly.createSvgElement('filter',
{'id': 'blocklyDragShadowFilter', 'height': '140%', 'width': '140%', y: '-20%', x: '-20%'}, defs);
Blockly.createSvgElement('feGaussianBlur',
{'in': 'SourceAlpha', 'stdDeviation': Blockly.DragSurfaceSvg.SHADOW_STD_DEVIATION}, dragShadowFilter);
var componentTransfer = Blockly.createSvgElement('feComponentTransfer', {'result': 'offsetBlur'}, dragShadowFilter);
// Shadow opacity is specified in the adjustable colour library,
// since the darkness of the shadow largely depends on the workspace colour.
Blockly.createSvgElement('feFuncA',
{'type': 'linear', 'slope': Blockly.Colours.dragShadowOpacity}, componentTransfer);
Blockly.createSvgElement('feComposite',
{'in': 'SourceGraphic', 'in2': 'offsetBlur', 'operator': 'over'}, dragShadowFilter);
return dragShadowFilter.id;
};
/**
* Set the SVG blocks on the drag surface's group and show the surface.
* Only one block should be on the drag surface at a time.
* @param {!Element} blocks Block or group of blocks to place on the drag surface
*/
Blockly.DragSurfaceSvg.prototype.setBlocksAndShow = function(blocks) {
goog.asserts.assert(this.dragGroup_.childNodes.length == 0, 'Already dragging a block.');
// appendChild removes the blocks from the previous parent
this.dragGroup_.appendChild(blocks);
this.SVG_.style.display = 'block';
};
/**
* Translate and scale the entire drag surface group to keep in sync with the workspace.
* @param {Number} x X translation
* @param {Number} y Y translation
* @param {Number} scale Scale of the group
*/
Blockly.DragSurfaceSvg.prototype.translateAndScaleGroup = function(x, y, scale) {
var transform;
this.scale_ = scale;
// Force values to have two decimal points.
// This is a work-around to prevent a bug in Safari, where numbers close to 0
// are sometimes reported as something like "2.9842794901924208e-12".
// That is incompatible with translate3d, causing bugs.
x = x.toFixed(2);
y = y.toFixed(2);
if (Blockly.is3dSupported()) {
transform = 'transform: translate3d(' + x + 'px, ' + y + 'px, 0px)' +
'scale3d(' + scale + ',' + scale + ',' + scale + ')';
this.dragGroup_.setAttribute('style', transform);
} else {
transform = 'translate(' + x + ', ' + y + ') scale(' + scale + ')';
this.dragGroup_.setAttribute('transform', transform);
}
};
/**
* Translate the entire drag surface during a drag.
* We translate the drag surface instead of the blocks inside the surface
* so that the browser avoids repainting the SVG.
* Because of this, the drag coordinates must be adjusted by scale.
* @param {Number} x X translation for the entire surface
* @param {Number} y Y translation for the entire surface
*/
Blockly.DragSurfaceSvg.prototype.translateSurface = function(x, y) {
var transform;
x *= this.scale_;
y *= this.scale_;
// Force values to have two decimal points.
// This is a work-around to prevent a bug in Safari, where numbers close to 0
// are sometimes reported as something like "2.9842794901924208e-12".
// That is incompatible with translate3d, causing bugs.
x = x.toFixed(2);
y = y.toFixed(2);
if (Blockly.is3dSupported()) {
transform = 'transform: translate3d(' + x + 'px, ' + y + 'px, 0px); display: block;';
this.SVG_.setAttribute('style', transform);
} else {
transform = 'translate(' + x + ', ' + y + ')';
this.SVG_.setAttribute('transform', transform);
}
};
/**
* Reports the surface translation in scaled workspace coordinates.
* Use this when finishing a drag to return blocks to the correct position.
* @return {goog.math.Coordinate} Current translation of the surface
*/
Blockly.DragSurfaceSvg.prototype.getSurfaceTranslation = function() {
var xy = Blockly.getRelativeXY_(this.SVG_);
return new goog.math.Coordinate(xy.x / this.scale_, xy.y / this.scale_);
};
/**
* Provide a reference to the drag group (primarily for BlockSvg.getRelativeToSurfaceXY).
* @return {Element} Drag surface group element
*/
Blockly.DragSurfaceSvg.prototype.getGroup = function() {
return this.dragGroup_;
};
/**
* Get the current blocks on the drag surface, if any (primarily for BlockSvg.getRelativeToSurfaceXY).
* @return {Element} Drag surface block DOM element
*/
Blockly.DragSurfaceSvg.prototype.getCurrentBlock = function() {
return this.dragGroup_.childNodes[0];
};
/**
* Clear the group and hide the surface; move the blocks off onto the provided element.
* @param {!Element} newSurface Surface the dragging blocks should be moved to
*/
Blockly.DragSurfaceSvg.prototype.clearAndHide = function(newSurface) {
// appendChild removes the node from this.dragGroup_
newSurface.appendChild(this.getCurrentBlock());
this.SVG_.style.display = 'none';
goog.asserts.assert(this.dragGroup_.childNodes.length == 0, 'Drag group was not cleared.');
};

View file

@ -234,7 +234,7 @@ Blockly.Events.getGroup = function() {
*/
Blockly.Events.setGroup = function(state) {
if (typeof state == 'boolean') {
Blockly.Events.group_ = state ? Blockly.genUid() : '';
Blockly.Events.group_ = state ? Blockly.utils.genUid() : '';
} else {
Blockly.Events.group_ = state;
}

View file

@ -145,7 +145,7 @@ Blockly.Field.prototype.init = function() {
return;
}
// Build the DOM.
this.fieldGroup_ = Blockly.createSvgElement('g', {}, null);
this.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null);
if (!this.visible_) {
this.fieldGroup_.style.display = 'none';
}
@ -163,7 +163,7 @@ Blockly.Field.prototype.init = function() {
var size = this.getSize();
var fieldX = (this.sourceBlock_.RTL) ? -size.width / 2 : size.width / 2;
/** @type {!Element} */
this.textElement_ = Blockly.createSvgElement('text',
this.textElement_ = Blockly.utils.createSvgElement('text',
{'class': 'blocklyText',
'x': fieldX,
'y': size.height / 2 + Blockly.BlockSvg.FIELD_TOP_PADDING,
@ -177,7 +177,8 @@ Blockly.Field.prototype.init = function() {
Blockly.bindEventWithChecks_(this.getClickTarget_(), 'mouseup', this,
this.onMouseUp_);
// Force a render.
this.updateTextNode_();
this.render_();
this.size_.width = 0;
};
/**
@ -204,13 +205,13 @@ Blockly.Field.prototype.updateEditable = function() {
return;
}
if (this.sourceBlock_.isEditable()) {
Blockly.addClass_(group, 'blocklyEditableText');
Blockly.removeClass_(group, 'blocklyNonEditableText');
this.getClickTarget_().style.cursor = this.CURSOR;
Blockly.utils.addClass(group, 'blocklyEditableText');
Blockly.utils.removeClass(group, 'blocklyNonEditableText');
this.fieldGroup_.style.cursor = this.CURSOR;
} else {
Blockly.addClass_(group, 'blocklyNonEditableText');
Blockly.removeClass_(group, 'blocklyEditableText');
this.getClickTarget_().style.cursor = '';
Blockly.utils.addClass(group, 'blocklyNonEditableText');
Blockly.utils.removeClass(group, 'blocklyEditableText');
this.fieldGroup_.style.cursor = '';
}
};
@ -328,42 +329,40 @@ Blockly.Field.prototype.getSvgRoot = function() {
* @private
*/
Blockly.Field.prototype.render_ = function() {
var width = 0;
if (this.visible_ && this.textElement_) {
var key = this.textElement_.textContent + '\n' +
this.textElement_.className.baseVal;
if (Blockly.Field.cacheWidths_ && Blockly.Field.cacheWidths_[key]) {
var width = Blockly.Field.cacheWidths_[key];
} else {
try {
var width = this.textElement_.getComputedTextLength();
} catch (e) {
// MSIE 11 is known to throw "Unexpected call to method or property
// access." if Blockly is hidden.
var width = this.textElement_.textContent.length * 8;
}
if (Blockly.Field.cacheWidths_) {
Blockly.Field.cacheWidths_[key] = width;
}
}
// Replace the text.
goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_));
var textNode = document.createTextNode(this.getDisplayText_());
this.textElement_.appendChild(textNode);
// Calculate width of field
width = Blockly.Field.getCachedWidth(this.textElement_);
// Add padding to left and right of text.
if (this.EDITABLE) {
// Add padding to left and right of text.
width += Blockly.BlockSvg.EDITABLE_FIELD_PADDING;
}
// Adjust width for drop-down arrows.
var arrowWidth = 0;
if (this.positionArrow) {
arrowWidth = this.positionArrow(width);
width += arrowWidth;
}
// Add padding to any drawn box.
if (this.box_) {
// Add padding to any drawn box.
width += 2 * Blockly.BlockSvg.BOX_FIELD_PADDING;
}
// Update text centering, based on newly calculated width.
var centerTextX = (width - arrowWidth) / 2;
if (this.sourceBlock_.RTL) {
centerTextX += arrowWidth;
}
// In a text-editing shadow block's field,
// if half the text length is not at least center of
// visible field (FIELD_WIDTH), center it there instead,
@ -382,12 +381,14 @@ Blockly.Field.prototype.render_ = function() {
centerTextX = Math.max(minOffset, centerTextX);
}
}
this.textElement_.setAttribute('x', centerTextX);
} else {
var width = 0;
// Apply new text element x position.
this.textElement_.setAttribute('x', centerTextX);
}
// Set width of the field.
this.size_.width = width;
// Update any drawn box to the correct width and height.
if (this.box_) {
this.box_.setAttribute('width', this.size_.width);
@ -395,6 +396,31 @@ Blockly.Field.prototype.render_ = function() {
}
};
/**
* Gets the width of a text element, caching it in the process.
* @param {!Element} textElement An SVG 'text' element.
* @return {number} Width of element.
*/
Blockly.Field.getCachedWidth = function(textElement) {
var key = textElement.textContent + '\n' + textElement.className.baseVal;
if (Blockly.Field.cacheWidths_ && Blockly.Field.cacheWidths_[key]) {
var width = Blockly.Field.cacheWidths_[key];
} else {
try {
var width = textElement.getComputedTextLength();
} catch (e) {
// MSIE 11 is known to throw "Unexpected call to method or property
// access." if Blockly is hidden.
var width = textElement.textContent.length * 8;
}
if (Blockly.Field.cacheWidths_) {
Blockly.Field.cacheWidths_[key] = width;
}
}
return width;
};
/**
* Start caching field widths. Every call to this function MUST also call
* stopCache. Caches must not survive between execution threads.
@ -441,6 +467,31 @@ Blockly.Field.prototype.getScaledBBox_ = function() {
size.height * this.sourceBlock_.workspace.scale);
};
/**
* Get the text from this field as displayed on screen. May differ from getText
* due to ellipsis, and other formatting.
* @return {string} Currently displayed text.
* @private
*/
Blockly.Field.prototype.getDisplayText_ = function() {
var text = this.text_;
if (!text) {
// Prevent the field from disappearing if empty.
return Blockly.Field.NBSP;
}
if (text.length > this.maxDisplayLength) {
// Truncate displayed string and add an ellipsis ('...').
text = text.substring(0, this.maxDisplayLength - 2) + '\u2026';
}
// Replace whitespace with non-breaking spaces so the text doesn't collapse.
text = text.replace(/\s/g, Blockly.Field.NBSP);
if (this.sourceBlock_.RTL) {
// The SVG is LTR, force text to be RTL.
text += '\u200F';
}
return text;
};
/**
* Get the text from this field.
* @return {string} Current text.
@ -451,21 +502,21 @@ Blockly.Field.prototype.getText = function() {
/**
* Set the text in this field. Trigger a rerender of the source block.
* @param {*} text New text.
* @param {*} newText New text.
*/
Blockly.Field.prototype.setText = function(text) {
if (text === null) {
Blockly.Field.prototype.setText = function(newText) {
if (newText === null) {
// No change if null.
return;
}
text = String(text);
if (text === this.text_) {
newText = String(newText);
if (newText === this.text_) {
// No change.
return;
}
this.text_ = text;
this.updateTextNode_();
this.text_ = newText;
// Set width to 0 to force a rerender of this field.
this.size_.width = 0;
if (this.sourceBlock_ && this.sourceBlock_.rendered) {
this.sourceBlock_.render();
this.sourceBlock_.bumpNeighbours_();
@ -512,7 +563,7 @@ Blockly.Field.prototype.updateTextNode_ = function() {
/**
* By default there is no difference between the human-readable text and
* the language-neutral values. Subclasses (such as dropdown) may define this.
* @return {string} Current text.
* @return {string} Current value.
*/
Blockly.Field.prototype.getValue = function() {
return this.getText();
@ -521,22 +572,22 @@ Blockly.Field.prototype.getValue = function() {
/**
* By default there is no difference between the human-readable text and
* the language-neutral values. Subclasses (such as dropdown) may define this.
* @param {string} newText New text.
* @param {string} newValue New value.
*/
Blockly.Field.prototype.setValue = function(newText) {
if (newText === null) {
Blockly.Field.prototype.setValue = function(newValue) {
if (newValue === null) {
// No change if null.
return;
}
var oldText = this.getValue();
if (oldText == newText) {
var oldValue = this.getValue();
if (oldValue == newValue) {
return;
}
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.Change(
this.sourceBlock_, 'field', this.name, oldText, newText));
this.sourceBlock_, 'field', this.name, oldValue, newValue));
}
this.setText(newText);
this.setText(newValue);
};
/**
@ -551,7 +602,7 @@ Blockly.Field.prototype.onMouseUp_ = function(e) {
// Old iOS spawns a bogus event on the next touch after a 'prompt()' edit.
// Unlike the real events, these have a layerX and layerY set.
return;
} else if (Blockly.isRightButton(e)) {
} else if (Blockly.utils.isRightButton(e)) {
// Right-click.
return;
} else if (this.sourceBlock_.workspace.isDragging()) {

View file

@ -44,7 +44,7 @@ goog.require('goog.userAgent');
*/
Blockly.FieldAngle = function(text, opt_validator) {
// Add degree symbol: "360°" (LTR) or "°360" (RTL)
this.symbol_ = Blockly.createSvgElement('tspan', {}, null);
this.symbol_ = Blockly.utils.createSvgElement('tspan', {}, null);
this.symbol_.appendChild(document.createTextNode('\u00B0'));
Blockly.FieldAngle.superClass_.constructor.call(this, text, opt_validator);
@ -135,7 +135,7 @@ Blockly.FieldAngle.prototype.showEditor_ = function() {
Blockly.DropDownDiv.clearContent();
var div = Blockly.DropDownDiv.getContentDiv();
// Build the SVG DOM.
var svg = Blockly.createSvgElement('svg', {
var svg = Blockly.utils.createSvgElement('svg', {
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:html': 'http://www.w3.org/1999/xhtml',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
@ -143,20 +143,20 @@ Blockly.FieldAngle.prototype.showEditor_ = function() {
'height': (Blockly.FieldAngle.HALF * 2) + 'px',
'width': (Blockly.FieldAngle.HALF * 2) + 'px'
}, div);
var circle = Blockly.createSvgElement('circle', {
var circle = Blockly.utils.createSvgElement('circle', {
'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF,
'r': Blockly.FieldAngle.RADIUS,
'class': 'blocklyAngleCircle'
}, svg);
this.gauge_ = Blockly.createSvgElement('path',
this.gauge_ = Blockly.utils.createSvgElement('path',
{'class': 'blocklyAngleGauge'}, svg);
this.line_ = Blockly.createSvgElement('line',
this.line_ = Blockly.utils.createSvgElement('line',
{'x1': Blockly.FieldAngle.HALF,
'y1': Blockly.FieldAngle.HALF,
'class': 'blocklyAngleLine'}, svg);
// Draw markers around the edge.
for (var angle = 0; angle < 360; angle += 15) {
Blockly.createSvgElement('line', {
Blockly.utils.createSvgElement('line', {
'x1': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS,
'y1': Blockly.FieldAngle.HALF,
'x2': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS -
@ -200,7 +200,7 @@ Blockly.FieldAngle.prototype.onMouseMove = function(e) {
var dy = e.clientY - bBox.top - Blockly.FieldAngle.HALF;
var angle = Math.atan(-dy / dx);
if (isNaN(angle)) {
// This shouldn't happen, but let's not let this error propogate further.
// This shouldn't happen, but let's not let this error propagate further.
return;
}
angle = goog.math.toDegrees(angle);

View file

@ -68,7 +68,7 @@ Blockly.FieldCheckbox.prototype.init = function() {
Blockly.FieldCheckbox.superClass_.init.call(this);
// The checkbox doesn't use the inherited text element.
// Instead it uses a custom checkmark element that is either visible or not.
this.checkElement_ = Blockly.createSvgElement('text',
this.checkElement_ = Blockly.utils.createSvgElement('text',
{'class': 'blocklyText blocklyCheckbox', 'x': -3, 'y': 14},
this.fieldGroup_);
var textNode = document.createTextNode(Blockly.FieldCheckbox.CHECK_CHAR);
@ -85,11 +85,13 @@ Blockly.FieldCheckbox.prototype.getValue = function() {
};
/**
* Set the checkbox to be checked if strBool is 'TRUE', unchecks otherwise.
* @param {string} strBool New state.
* Set the checkbox to be checked if newBool is 'TRUE' or true,
* unchecks otherwise.
* @param {string|boolean} newBool New state.
*/
Blockly.FieldCheckbox.prototype.setValue = function(strBool) {
var newState = (strBool.toUpperCase() == 'TRUE');
Blockly.FieldCheckbox.prototype.setValue = function(newBool) {
var newState = (typeof newBool == 'string') ?
(newBool.toUpperCase() == 'TRUE') : !!newBool;
if (this.state_ !== newState) {
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.Change(

View file

@ -243,4 +243,5 @@ Blockly.FieldColour.widgetDispose_ = function() {
if (Blockly.FieldColour.changeEventKey_) {
goog.events.unlistenByKey(Blockly.FieldColour.changeEventKey_);
}
Blockly.Events.setGroup(false);
};

View file

@ -166,6 +166,7 @@ Blockly.FieldDate.widgetDispose_ = function() {
if (Blockly.FieldDate.changeEventKey_) {
goog.events.unlistenByKey(Blockly.FieldDate.changeEventKey_);
}
Blockly.Events.setGroup(false);
};
/**

View file

@ -94,7 +94,7 @@ Blockly.FieldDropdown.prototype.init = function() {
this.arrowX_ = 0;
/** @type {Number} */
this.arrowY_ = 11;
this.arrow_ = Blockly.createSvgElement('image', {
this.arrow_ = Blockly.utils.createSvgElement('image', {
'height': this.arrowSize_ + 'px',
'width': this.arrowSize_ + 'px'
});
@ -104,7 +104,7 @@ Blockly.FieldDropdown.prototype.init = function() {
Blockly.FieldDropdown.superClass_.init.call(this);
// If not in a shadow block, draw a box.
if (!this.sourceBlock_.isShadow()) {
this.box_ = Blockly.createSvgElement('rect', {
this.box_ = Blockly.utils.createSvgElement('rect', {
'rx': Blockly.BlockSvg.CORNER_RADIUS,
'ry': Blockly.BlockSvg.CORNER_RADIUS,
'x': 0,
@ -185,7 +185,7 @@ Blockly.FieldDropdown.prototype.showEditor_ = function() {
// Record windowSize and scrollOffset before adding menu.
menu.render(contentDiv);
var menuDom = menu.getElement();
Blockly.addClass_(menuDom, 'blocklyDropdownMenu');
Blockly.utils.addClass(menuDom, 'blocklyDropdownMenu');
// Record menuSize after adding menu.
var menuSize = goog.style.getSize(menuDom);
// Recalculate height for the total content, not only box height.
@ -275,9 +275,9 @@ Blockly.FieldDropdown.prototype.trimOptions_ = function() {
return;
}
var strings = options.map(function(t) {return t[0];});
var shortest = Blockly.shortestStringLength(strings);
var prefixLength = Blockly.commonWordPrefix(strings, shortest);
var suffixLength = Blockly.commonWordSuffix(strings, shortest);
var shortest = Blockly.utils.shortestStringLength(strings);
var prefixLength = Blockly.utils.commonWordPrefix(strings, shortest);
var suffixLength = Blockly.utils.commonWordSuffix(strings, shortest);
if (!prefixLength && !suffixLength) {
return;
}
@ -388,6 +388,10 @@ Blockly.FieldDropdown.prototype.setText = function(text) {
* @return {number} Amount of space the arrow is taking up, in px.
*/
Blockly.FieldDropdown.prototype.positionArrow = function(x) {
if (!this.arrow_) {
return 0;
}
var addedWidth = 0;
if (this.sourceBlock_.RTL) {
this.arrowX_ = this.arrowSize_ - Blockly.BlockSvg.DROPDOWN_ARROW_PADDING;

View file

@ -81,7 +81,7 @@ Blockly.FieldIconMenu.prototype.init = function(block) {
this.arrowX_ = -this.arrowX_ - arrowSize;
}
/** @type {Element} */
this.arrowIcon_ = Blockly.createSvgElement('image', {
this.arrowIcon_ = Blockly.utils.createSvgElement('image', {
'height': arrowSize + 'px',
'width': arrowSize + 'px',
'transform': 'translate(' + this.arrowX_ + ',' + this.arrowY_ + ')'

View file

@ -19,7 +19,7 @@
*/
/**
* @fileoverview Image field. Used for titles, labels, etc.
* @fileoverview Image field. Used for pictures, icons, etc.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
@ -54,13 +54,6 @@ Blockly.FieldImage = function(src, width, height, opt_alt, flip_rtl) {
};
goog.inherits(Blockly.FieldImage, Blockly.Field);
/**
* Rectangular mask used by Firefox.
* @type {Element}
* @private
*/
Blockly.FieldImage.prototype.rectElement_ = null;
/**
* Editable fields are saved by the XML renderer, non-editable fields are not.
*/
@ -76,32 +69,20 @@ Blockly.FieldImage.prototype.init = function() {
}
// Build the DOM.
/** @type {SVGElement} */
this.fieldGroup_ = Blockly.createSvgElement('g', {}, null);
this.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null);
if (!this.visible_) {
this.fieldGroup_.style.display = 'none';
}
/** @type {SVGElement} */
this.imageElement_ = Blockly.createSvgElement('image',
this.imageElement_ = Blockly.utils.createSvgElement('image',
{'height': this.height_ + 'px',
'width': this.width_ + 'px'}, this.fieldGroup_);
this.setValue(this.src_);
if (goog.userAgent.GECKO) {
/**
* Due to a Firefox bug which eats mouse events on image elements,
* a transparent rectangle needs to be placed on top of the image.
* @type {SVGElement}
*/
this.rectElement_ = Blockly.createSvgElement('rect',
{'height': this.height_ + 'px',
'width': this.width_ + 'px',
'fill-opacity': 0}, this.fieldGroup_);
}
this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
// Configure the field to be transparent with respect to tooltips.
var topElement = this.rectElement_ || this.imageElement_;
topElement.tooltip = this.sourceBlock_;
Blockly.Tooltip.bindMouseEvents(topElement);
this.setTooltip(this.sourceBlock_);
Blockly.Tooltip.bindMouseEvents(this.imageElement_);
};
/**
@ -111,7 +92,6 @@ Blockly.FieldImage.prototype.dispose = function() {
goog.dom.removeNode(this.fieldGroup_);
this.fieldGroup_ = null;
this.imageElement_ = null;
this.rectElement_ = null;
};
/**
@ -120,8 +100,7 @@ Blockly.FieldImage.prototype.dispose = function() {
* link to for its tooltip.
*/
Blockly.FieldImage.prototype.setTooltip = function(newTip) {
var topElement = this.rectElement_ || this.imageElement_;
topElement.tooltip = newTip;
this.imageElement_.tooltip = newTip;
};
/**
@ -146,7 +125,7 @@ Blockly.FieldImage.prototype.setValue = function(src) {
this.src_ = src;
if (this.imageElement_) {
this.imageElement_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', goog.isString(src) ? src : '');
'xlink:href', src || '');
}
};

View file

@ -60,14 +60,14 @@ Blockly.FieldLabel.prototype.init = function() {
return;
}
// Build the DOM.
this.textElement_ = Blockly.createSvgElement('text',
this.textElement_ = Blockly.utils.createSvgElement('text',
{'class': 'blocklyText',
'y': Blockly.BlockSvg.FIELD_TOP_PADDING,
'text-anchor': 'middle',
'dominant-baseline': 'middle'
}, null);
if (this.class_) {
Blockly.addClass_(this.textElement_, this.class_);
Blockly.utils.addClass(this.textElement_, this.class_);
}
if (!this.visible_) {
this.textElement_.style.display = 'none';
@ -78,7 +78,7 @@ Blockly.FieldLabel.prototype.init = function() {
this.textElement_.tooltip = this.sourceBlock_;
Blockly.Tooltip.bindMouseEvents(this.textElement_);
// Force a render.
this.updateTextNode_();
this.render_();
};
/**

View file

@ -69,7 +69,7 @@ Blockly.FieldTextDropdown.prototype.init = function() {
this.arrowX_ = 0;
/** @type {Number} */
this.arrowY_ = 11;
this.arrow_ = Blockly.createSvgElement('image', {
this.arrow_ = Blockly.utils.createSvgElement('image', {
'height': this.arrowSize_ + 'px',
'width': this.arrowSize_ + 'px'
});

View file

@ -87,7 +87,7 @@ Blockly.FieldTextInput.prototype.init = function() {
Blockly.FieldTextInput.superClass_.init.call(this);
// If not in a shadow block, draw a box.
if (!this.sourceBlock_.isShadow()) {
this.box_ = Blockly.createSvgElement('rect', {
this.box_ = Blockly.utils.createSvgElement('rect', {
'rx': Blockly.BlockSvg.CORNER_RADIUS,
'ry': Blockly.BlockSvg.CORNER_RADIUS,
'x': 0,
@ -110,23 +110,44 @@ Blockly.FieldTextInput.prototype.dispose = function() {
};
/**
* Set the text in this field.
* @param {?string} text New text.
* Set the value of this field.
* @param {?string} newValue New value.
* @override
*/
Blockly.FieldTextInput.prototype.setValue = function(text) {
if (text === null) {
Blockly.FieldTextInput.prototype.setValue = function(newValue) {
if (newValue === null) {
return; // No change if null.
}
if (this.sourceBlock_) {
var validated = this.callValidator(text);
// If the new text is invalid, validation returns null.
var validated = this.callValidator(newValue);
// If the new value is invalid, validation returns null.
// In this case we still want to display the illegal result.
if (validated !== null) {
text = validated;
newValue = validated;
}
}
Blockly.Field.prototype.setValue.call(this, text);
Blockly.Field.prototype.setValue.call(this, newValue);
};
/**
* Set the text in this field and fire a change event.
* @param {*} newText New text.
*/
Blockly.FieldTextInput.prototype.setText = function(newText) {
if (newText === null) {
// No change if null.
return;
}
newText = String(newText);
if (newText === this.text_) {
// No change.
return;
}
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.Change(
this.sourceBlock_, 'field', this.name, this.text_, newText));
}
Blockly.Field.prototype.setText.call(this, newText);
};
/**
@ -326,7 +347,7 @@ Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(e) {
var text = htmlInput.value;
if (text !== htmlInput.oldValue_) {
htmlInput.oldValue_ = text;
this.setValue(text);
this.setText(text);
this.validate_();
} else if (goog.userAgent.WEBKIT) {
// Cursor key. Render the source block to show the caret moving.
@ -349,9 +370,9 @@ Blockly.FieldTextInput.prototype.validate_ = function() {
valid = this.callValidator(htmlInput.value);
}
if (valid === null) {
Blockly.addClass_(htmlInput, 'blocklyInvalidInput');
Blockly.utils.addClass(htmlInput, 'blocklyInvalidInput');
} else {
Blockly.removeClass_(htmlInput, 'blocklyInvalidInput');
Blockly.utils.removeClass(htmlInput, 'blocklyInvalidInput');
}
};
@ -478,6 +499,7 @@ Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
}
thisField.workspace_.removeChangeListener(
htmlInput.onWorkspaceChangeWrapper_);
Blockly.Events.setGroup(false);
// Animation of disposal
htmlInput.style.fontSize = Blockly.BlockSvg.FIELD_TEXTINPUT_FONTSIZE_INITIAL + 'pt';

View file

@ -148,8 +148,8 @@ Blockly.FieldVariable.dropdownCreate = function() {
* Handle the selection of an item in the variable dropdown menu.
* Special case the 'Rename variable...' and 'Delete variable...' options.
* In the rename case, prompt the user for a new name.
* @param {goog.ui.Menu} menu The Menu component clicked.
* @param {goog.ui.MenuItem} menuItem The MenuItem selected within menu.
* @param {!goog.ui.Menu} menu The Menu component clicked.
* @param {!goog.ui.MenuItem} menuItem The MenuItem selected within menu.
*/
Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) {
var itemText = menuItem.getValue();
@ -157,7 +157,7 @@ Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) {
var workspace = this.sourceBlock_.workspace;
if (this.renameVarItemIndex_ >= 0 &&
menu.getChildAt(this.renameVarItemIndex_) === menuItem) {
// Rename variable
// Rename variable.
var oldName = this.getText();
Blockly.hideChaff();
Blockly.Variables.promptName(
@ -170,7 +170,7 @@ Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) {
return;
} else if (this.deleteVarItemIndex_ >= 0 &&
menu.getChildAt(this.deleteVarItemIndex_) === menuItem) {
// Delete variable
// Delete variable.
workspace.deleteVariable(this.getText());
return;
}

View file

@ -186,6 +186,20 @@ Blockly.Flyout.onMouseMoveBlockWrapper_ = null;
*/
Blockly.Flyout.prototype.autoClose = true;
/**
* Whether the flyout is visible.
* @type {boolean}
* @private
*/
Blockly.Flyout.prototype.isVisible_ = false;
/**
* Whether the workspace containing this flyout is visible.
* @type {boolean}
* @private
*/
Blockly.Flyout.prototype.containerVisible_ = true;
/**
* Corner radius of the flyout background.
* @type {number}
@ -292,19 +306,25 @@ Blockly.Flyout.prototype.dragMode_ = Blockly.DRAG_NONE;
/**
* Creates the flyout's DOM. Only needs to be called once.
* Creates the flyout's DOM. Only needs to be called once. The flyout can
* either exist as its own <svg> element or be a <g> nested inside a separate
* <svg> element.
* @param {string} tagName The type of tag to put the flyout in. This
* should be <svg> or <g>.
* @return {!Element} The flyout's SVG group.
*/
Blockly.Flyout.prototype.createDom = function() {
Blockly.Flyout.prototype.createDom = function(tagName) {
/*
<g>
<svg | g>
<path class="blocklyFlyoutBackground"/>
<g class="blocklyFlyout"></g>
</g>
</ svg | g>
*/
this.svgGroup_ = Blockly.createSvgElement('g',
{'class': 'blocklyFlyout'}, null);
this.svgBackground_ = Blockly.createSvgElement('path',
// Setting style to display:none to start. The toolbox and flyout
// hide/show code will set up proper visibility and size later.
this.svgGroup_ = Blockly.utils.createSvgElement(tagName,
{'class': 'blocklyFlyout', 'style' : 'display: none'}, null);
this.svgBackground_ = Blockly.utils.createSvgElement('path',
{'class': 'blocklyFlyoutBackground'}, this.svgGroup_);
this.svgGroup_.appendChild(this.workspace_.createDom());
return this.svgGroup_;
@ -394,7 +414,51 @@ Blockly.Flyout.prototype.getWorkspace = function() {
* @return {boolean} True if visible.
*/
Blockly.Flyout.prototype.isVisible = function() {
return this.svgGroup_ && this.svgGroup_.style.display == 'block';
return this.isVisible_;
};
/**
* Set whether the flyout is visible. A value of true does not necessarily mean
* that the flyout is shown. It could be hidden because its container is hidden.
* @param {boolean} visible True if visible.
*/
Blockly.Flyout.prototype.setVisible = function(visible) {
var visibilityChanged = (visible != this.isVisible());
this.isVisible_ = visible;
if (visibilityChanged) {
this.updateDisplay_();
}
};
/**
* Set whether this flyout's container is visible.
* @param {boolean} visible Whether the container is visible.
*/
Blockly.Flyout.prototype.setContainerVisible = function(visible) {
var visibilityChanged = (visible != this.containerVisible_);
this.containerVisible_ = visible;
if (visibilityChanged) {
this.updateDisplay_();
}
};
/**
* Update the display property of the flyout based whether it thinks it should
* be visible and whether its containing workspace is visible.
* @private
*/
Blockly.Flyout.prototype.updateDisplay_ = function() {
var show = true;
if (!this.containerVisible_) {
show = false;
} else {
show = this.isVisible();
}
this.svgGroup_.style.display = show ? 'block' : 'none';
// Update the scrollbar's visiblity too since it should mimic the
// flyout's visibility.
this.scrollbar_.setContainerVisible(show);
};
/**
@ -404,7 +468,7 @@ Blockly.Flyout.prototype.hide = function() {
if (!this.isVisible()) {
return;
}
this.svgGroup_.style.display = 'none';
this.setVisible(false);
// Delete all the event listeners.
for (var x = 0, listen; listen = this.listeners_[x]; x++) {
Blockly.unbindEvent_(listen);
@ -424,6 +488,7 @@ Blockly.Flyout.prototype.hide = function() {
* Variables and procedures have a custom set of blocks.
*/
Blockly.Flyout.prototype.show = function(xmlList) {
this.workspace_.setResizesEnabled(false);
this.hide();
this.clearOldBlocks_();
@ -437,7 +502,7 @@ Blockly.Flyout.prototype.show = function(xmlList) {
Blockly.Procedures.flyoutCategory(this.workspace_.targetWorkspace);
}
this.svgGroup_.style.display = 'block';
this.setVisible(true);
// Create the blocks to be shown in this flyout.
var contents = [];
var gaps = [];
@ -473,10 +538,8 @@ Blockly.Flyout.prototype.show = function(xmlList) {
} else if (tagName == 'BUTTON' || tagName == 'LABEL') {
// Labels behave the same as buttons, but are styled differently.
var isLabel = tagName == 'LABEL';
var text = xml.getAttribute('text');
var callbackKey = xml.getAttribute('callbackKey');
var curButton = new Blockly.FlyoutButton(this.workspace_,
this.targetWorkspace_, text, callbackKey, isLabel);
this.targetWorkspace_, xml, isLabel);
contents.push({type: 'button', button: curButton});
gaps.push(default_gap);
}
@ -585,7 +648,7 @@ Blockly.Flyout.blockRightClick_ = function(e, block) {
Blockly.Flyout.prototype.blockMouseDown_ = function(block) {
var flyout = this;
return function(e) {
if (Blockly.isRightButton(e)) {
if (Blockly.utils.isRightButton(e)) {
Blockly.Flyout.blockRightClick_(e, block);
} else {
flyout.dragMode_ = Blockly.DRAG_NONE;
@ -618,7 +681,7 @@ Blockly.Flyout.prototype.blockMouseDown_ = function(block) {
*/
Blockly.Flyout.prototype.onMouseDown_ = function(e) {
this.dragMode_ = Blockly.DRAG_FREE;
if (Blockly.isRightButton(e)) {
if (Blockly.utils.isRightButton(e)) {
// Don't start drags with right clicks.
Blockly.Touch.clearTouchIdentifier();
return;
@ -772,7 +835,7 @@ Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
// Hide drop-downs and animating WidgetDiv immediately
Blockly.WidgetDiv.hide(true);
Blockly.DropDownDiv.hideWithoutAnimation();
if (Blockly.isRightButton(e)) {
if (Blockly.utils.isRightButton(e)) {
// Right-click. Don't create a block, let the context menu show.
return;
}
@ -781,6 +844,10 @@ Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
return;
}
Blockly.Events.disable();
// Disable workspace resizing. Reenable at the end of the drag. This avoids
// a spurious resize between creating the new block and placing it in the
// workspace.
flyout.targetWorkspace_.setResizesEnabled(false);
try {
var block = flyout.placeNewBlock_(originBlock);
} finally {
@ -797,8 +864,6 @@ Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
block.onMouseDown_(e);
Blockly.dragMode_ = Blockly.DRAG_FREE;
block.setDragging_(true);
// Disable workspace resizing. Reenable at the end of the drag.
flyout.targetWorkspace_.setResizesEnabled(false);
block.moveToDragSurface_();
};
};

View file

@ -35,14 +35,13 @@ goog.require('goog.math.Coordinate');
* @param {!Blockly.WorkspaceSvg} workspace The workspace in which to place this
* button.
* @param {!Blockly.WorkspaceSvg} targetWorkspace The flyout's target workspace.
* @param {string} text The text to display on the button.
* @param {string} callbackKey The key to use when looking up the callback for a
* click on this button.
* @param {!Element} xml The XML specifying the label/button.
* @param {boolean} isLabel Whether this button should be styled as a label.
* @constructor
*/
Blockly.FlyoutButton = function(workspace, targetWorkspace, text, callbackKey,
isLabel) {
Blockly.FlyoutButton = function(workspace, targetWorkspace, xml, isLabel) {
// Labels behave the same as buttons, but are styled differently.
/**
* @type {!Blockly.WorkspaceSvg}
* @private
@ -59,7 +58,7 @@ Blockly.FlyoutButton = function(workspace, targetWorkspace, text, callbackKey,
* @type {string}
* @private
*/
this.text_ = text;
this.text_ = xml.getAttribute('text');
/**
* @type {!goog.math.Coordinate}
@ -67,19 +66,36 @@ Blockly.FlyoutButton = function(workspace, targetWorkspace, text, callbackKey,
*/
this.position_ = new goog.math.Coordinate(0, 0);
/**
* Function to call when this button is clicked.
* @type {function(!Blockly.FlyoutButton)}
* @private
*/
this.callback_ = Blockly.flyoutButtonCallbacks_[callbackKey];
/**
* Whether this button should be styled as a label.
* @type {boolean}
* @private
*/
this.isLabel_ = isLabel;
/**
* Function to call when this button is clicked.
* @type {function(!Blockly.FlyoutButton)}
* @private
*/
this.callback_ = null;
var callbackKey = xml.getAttribute('callbackKey');
if (this.isLabel_ && callbackKey) {
console.warn('Labels should not have callbacks. Label text: ' + this.text_);
} else if (!this.isLabel_ &&
!(callbackKey && targetWorkspace.getButtonCallback(callbackKey))) {
console.warn('Buttons should have callbacks. Button text: ' + this.text_);
} else {
this.callback_ = targetWorkspace.getButtonCallback(callbackKey);
}
/**
* If specified, a CSS class to add to this button.
* @type {?string}
* @private
*/
this.cssClass_ = xml.getAttribute('web-class') || null;
};
/**
@ -104,28 +120,32 @@ Blockly.FlyoutButton.prototype.height = 0;
* @return {!Element} The button's SVG group.
*/
Blockly.FlyoutButton.prototype.createDom = function() {
this.svgGroup_ = Blockly.createSvgElement('g',
{'class': this.isLabel_ ? 'blocklyFlyoutLabel' : 'blocklyFlyoutButton'},
var cssClass = this.isLabel_ ? 'blocklyFlyoutLabel' : 'blocklyFlyoutButton';
if (this.cssClass_) {
cssClass += ' ' + this.cssClass_;
}
this.svgGroup_ = Blockly.utils.createSvgElement('g', {'class': cssClass},
this.workspace_.getCanvas());
if (!this.isLabel_) {
// Shadow rectangle (light source does not mirror in RTL).
var shadow = Blockly.createSvgElement('rect',
var shadow = Blockly.utils.createSvgElement('rect',
{'class': 'blocklyFlyoutButtonShadow',
'rx': 4, 'ry': 4, 'x': 1, 'y': 1},
this.svgGroup_);
}
// Background rectangle.
var rect = Blockly.createSvgElement('rect',
var rect = Blockly.utils.createSvgElement('rect',
{'class': this.isLabel_ ?
'blocklyFlyoutLabelBackground' : 'blocklyFlyoutButtonBackground',
'rx': 4, 'ry': 4},
this.svgGroup_);
var svgText = Blockly.createSvgElement('text',
{'class': this.isLabel_ ? 'blocklyFlyoutLabelText' : 'blocklyText',
'x': 0, 'y': 0,
'text-anchor': 'middle'}, this.svgGroup_);
var svgText = Blockly.utils.createSvgElement('text',
{'class': this.isLabel_ ? 'blocklyFlyoutLabelText' : 'blocklyText',
'x': 0, 'y': 0, 'text-anchor': 'middle'},
this.svgGroup_);
svgText.textContent = this.text_;
this.width = svgText.getComputedTextLength() +

View file

@ -94,7 +94,7 @@ Blockly.HorizontalFlyout.prototype.getMetrics_ = function() {
}
var viewHeight = this.height_;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
viewHeight += this.MARGIN - this.SCROLLBAR_PADDING;
viewHeight += this.MARGIN;
}
var viewWidth = this.width_ - 2 * this.SCROLLBAR_PADDING;
@ -148,15 +148,14 @@ Blockly.HorizontalFlyout.prototype.position = function() {
return;
}
var edgeWidth = this.horizontalLayout_ ?
targetWorkspaceMetrics.viewWidth : this.width_;
edgeWidth -= this.CORNER_RADIUS;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
edgeWidth *= -1;
}
targetWorkspaceMetrics.viewWidth - 2 * this.CORNER_RADIUS :
this.width_ - this.CORNER_RADIUS;
this.setBackgroundPath_(edgeWidth,
this.horizontalLayout_ ? this.height_ :
targetWorkspaceMetrics.viewHeight);
var edgeHeight = this.horizontalLayout_ ?
this.height_ - this.CORNER_RADIUS :
targetWorkspaceMetrics.viewHeight - 2 * this.CORNER_RADIUS;
this.setBackgroundPath_(edgeWidth, edgeHeight);
var x = targetWorkspaceMetrics.absoluteLeft;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
@ -170,8 +169,6 @@ Blockly.HorizontalFlyout.prototype.position = function() {
y -= this.height_;
}
this.svgGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')');
// Record the height for Blockly.Flyout.getMetrics_, or width if the layout is
// horizontal.
if (this.horizontalLayout_) {
@ -180,8 +177,15 @@ Blockly.HorizontalFlyout.prototype.position = function() {
this.height_ = targetWorkspaceMetrics.viewHeight;
}
this.svgGroup_.setAttribute("width", this.width_);
this.svgGroup_.setAttribute("height", this.height_);
var transform = 'translate(' + x + 'px,' + y + 'px)';
this.svgGroup_.style.transform = transform;
// Update the scrollbar (if one exists).
if (this.scrollbar_) {
// Set the scrollbars origin to be the top left of the flyout.
this.scrollbar_.setOrigin(x, y);
this.scrollbar_.resize();
}
// The blocks need to be visible in order to be laid out and measured correctly, but we don't
@ -205,13 +209,13 @@ Blockly.HorizontalFlyout.prototype.setBackgroundPath_ = function(width, height)
if (atTop) {
// Top.
path.push('h', width + this.CORNER_RADIUS);
path.push('h', width + 2 * this.CORNER_RADIUS);
// Right.
path.push('v', height);
// Bottom.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('h', -1 * (width - this.CORNER_RADIUS));
path.push('h', -1 * width);
// Left.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, -this.CORNER_RADIUS);
@ -220,13 +224,13 @@ Blockly.HorizontalFlyout.prototype.setBackgroundPath_ = function(width, height)
// Top.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('h', width - this.CORNER_RADIUS);
path.push('h', width);
// Right.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('v', height - this.CORNER_RADIUS);
path.push('v', height);
// Bottom.
path.push('h', -width - this.CORNER_RADIUS);
path.push('h', -width - 2 * this.CORNER_RADIUS);
// Left.
path.push('z');
}
@ -308,7 +312,7 @@ Blockly.HorizontalFlyout.prototype.layout_ = function(contents, gaps) {
// Create an invisible rectangle under the block to act as a button. Just
// using the block as a button is poor, since blocks have holes in them.
var rect = Blockly.createSvgElement('rect', {'fill-opacity': 0}, null);
var rect = Blockly.utils.createSvgElement('rect', {'fill-opacity': 0}, null);
rect.tooltip = block;
Blockly.Tooltip.bindMouseEvents(rect);
// Add the rectangles under the blocks, so that the blocks' tooltips work.
@ -390,7 +394,7 @@ Blockly.HorizontalFlyout.prototype.placeNewBlock_ = function(originBlock) {
}
// Figure out where the original block is on the screen, relative to the upper
// left corner of the main workspace.
var xyOld = Blockly.getSvgXY_(svgRootOld, targetWorkspace);
var xyOld = Blockly.utils.getInjectionDivXY_(svgRootOld);
// Take into account that the flyout might have been scrolled horizontally
// (separately from the main workspace).
// Generally a no-op in vertical mode but likely to happen in horizontal
@ -434,7 +438,8 @@ Blockly.HorizontalFlyout.prototype.placeNewBlock_ = function(originBlock) {
// upper left corner of the workspace. This may not be the same as the
// original block because the flyout's origin may not be the same as the
// main workspace's origin.
var xyNew = Blockly.getSvgXY_(svgRootNew, targetWorkspace);
var xyNew = Blockly.utils.getInjectionDivXY_(svgRootNew);
// Scale the scroll (getSvgXY_ did not do this).
xyNew.x +=
targetWorkspace.scrollX / targetWorkspace.scale - targetWorkspace.scrollX;

View file

@ -114,10 +114,11 @@ Blockly.VerticalFlyout.prototype.init = function(targetWorkspace) {
/**
* Creates the flyout's DOM. Only needs to be called once.
* @param {string} tagName HTML element
* @return {!Element} The flyout's SVG group.
*/
Blockly.VerticalFlyout.prototype.createDom = function() {
Blockly.VerticalFlyout.superClass_.createDom.call(this);
Blockly.VerticalFlyout.prototype.createDom = function(tagName) {
Blockly.VerticalFlyout.superClass_.createDom.call(this, tagName);
/*
<defs>
@ -128,10 +129,10 @@ Blockly.VerticalFlyout.prototype.createDom = function() {
</clipPath>
</defs>
*/
this.defs_ = Blockly.createSvgElement('defs', {}, this.svgGroup_);
var clipPath = Blockly.createSvgElement('clipPath',
this.defs_ = Blockly.utils.createSvgElement('defs', {}, this.svgGroup_);
var clipPath = Blockly.utils.createSvgElement('clipPath',
{'id':'blocklyBlockMenuClipPath'}, this.defs_);
this.clipRect_ = Blockly.createSvgElement('rect',
this.clipRect_ = Blockly.utils.createSvgElement('rect',
{'id': 'blocklyBlockMenuClipRect',
'height': '0',
'width': '0',
@ -246,14 +247,20 @@ Blockly.VerticalFlyout.prototype.position = function() {
var y = 0;
}
this.svgGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')');
// Record the height for Blockly.Flyout.getMetrics_
this.height_ = targetWorkspaceMetrics.viewHeight - y;
this.setBackgroundPath_(this.width_, this.height_);
this.svgGroup_.setAttribute("width", this.width_);
this.svgGroup_.setAttribute("height", this.height_);
var transform = 'translate(' + x + 'px,' + y + 'px)';
this.svgGroup_.style.transform = transform;
// Update the scrollbar (if one exists).
if (this.scrollbar_) {
// Set the scrollbars origin to be the top left of the flyout.
this.scrollbar_.setOrigin(x, y);
this.scrollbar_.resize();
}
// The blocks need to be visible in order to be laid out and measured
@ -444,7 +451,7 @@ Blockly.VerticalFlyout.prototype.createRect_ = function(block, x, y,
blockHW, index) {
// Create an invisible rectangle under the block to act as a button. Just
// using the block as a button is poor, since blocks have holes in them.
var rect = Blockly.createSvgElement('rect',
var rect = Blockly.utils.createSvgElement('rect',
{
'fill-opacity': 0,
'x': x,
@ -475,7 +482,7 @@ Blockly.VerticalFlyout.prototype.createCheckbox_ = function(block, cursorX,
cursorY, blockHW) {
var svgRoot = block.getSvgRoot();
var extraSpace = this.CHECKBOX_SIZE + this.CHECKBOX_MARGIN;
var checkboxRect = Blockly.createSvgElement('rect',
var checkboxRect = Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyFlyoutCheckbox',
'height': this.CHECKBOX_SIZE,
@ -593,7 +600,7 @@ Blockly.VerticalFlyout.prototype.placeNewBlock_ = function(originBlock) {
// Figure out where the original block is on the screen, relative to the upper
// left corner of the main workspace.
// In what coordinates? Pixels?
var xyOld = Blockly.getSvgXY_(svgRootOld, targetWorkspace);
var xyOld = Blockly.utils.getInjectionDivXY_(svgRootOld);
// Take into account that the flyout might have been scrolled horizontally
// (separately from the main workspace).
@ -651,7 +658,7 @@ Blockly.VerticalFlyout.prototype.placeNewBlock_ = function(originBlock) {
// upper left corner of the workspace. This may not be the same as the
// original block because the flyout's origin may not be the same as the
// main workspace's origin.
var xyNew = Blockly.getSvgXY_(svgRootNew, targetWorkspace);
var xyNew = Blockly.utils.getInjectionDivXY_(svgRootNew);
// Scale the scroll (getSvgXY_ did not do this).
xyNew.x +=

View file

@ -249,7 +249,7 @@ Blockly.Generator.prototype.valueToCode = function(block, name, outerOrder) {
// In all known languages multiple such code blocks are not order
// sensitive. In fact in Python ('a' 'b') 'c' would fail.
} else {
// The operators outside this code are stonger than the operators
// The operators outside this code are stronger than the operators
// inside this code. To prevent the code from being pulled apart,
// wrap the code in parentheses.
parensNeeded = true;
@ -373,3 +373,43 @@ Blockly.Generator.prototype.provideFunction_ = function(desiredName, code) {
}
return this.functionNames_[desiredName];
};
/**
* Hook for code to run before code generation starts.
* Subclasses may override this, e.g. to initialise the database of variable
* names.
* @param {!Blockly.Workspace} workspace Workspace to generate code from.
*/
Blockly.Generator.prototype.init = undefined;
/**
* Common tasks for generating code from blocks. This is called from
* blockToCode and is called on every block, not just top level blocks.
* Subclasses may override this, e.g. to generate code for statements following
* the block, or to handle comments for the specified block and any connected
* value blocks.
* @param {!Blockly.Block} block The current block.
* @param {string} code The JavaScript code created for this block.
* @return {string} JavaScript code with comments and subsequent blocks added.
* @private
*/
Blockly.Generator.prototype.scrub_ = undefined;
/**
* Hook for code to run at end of code generation.
* Subclasses may override this, e.g. to prepend the generated code with the
* variable definitions.
* @param {string} code Generated code.
* @return {string} Completed code.
*/
Blockly.Generator.prototype.finish = undefined;
/**
* Naked values are top-level blocks with outputs that aren't plugged into
* anything.
* Subclasses may override this, e.g. if their language does not allow
* naked values.
* @param {string} line Line of generated code.
* @return {string} Legal line of code.
*/
Blockly.Generator.prototype.scrubNakedValue = undefined;

View file

@ -76,10 +76,10 @@ Blockly.Icon.prototype.createIcon = function() {
...
</g>
*/
this.iconGroup_ = Blockly.createSvgElement('g',
this.iconGroup_ = Blockly.utils.createSvgElement('g',
{'class': 'blocklyIconGroup'}, null);
if (this.block_.isInFlyout) {
Blockly.addClass_(/** @type {!Element} */ (this.iconGroup_),
Blockly.utils.addClass(/** @type {!Element} */ (this.iconGroup_),
'blocklyIconGroupReadonly');
}
this.drawIcon_(this.iconGroup_);
@ -126,7 +126,7 @@ Blockly.Icon.prototype.iconClick_ = function(e) {
// Drag operation is concluding. Don't open the editor.
return;
}
if (!this.block_.isInFlyout && !Blockly.isRightButton(e)) {
if (!this.block_.isInFlyout && !Blockly.utils.isRightButton(e)) {
this.setVisible(!this.isVisible());
}
};
@ -186,7 +186,7 @@ Blockly.Icon.prototype.setIconLocation = function(xy) {
Blockly.Icon.prototype.computeIconLocation = function() {
// Find coordinates for the centre of the icon and update the arrow.
var blockXY = this.block_.getRelativeToSurfaceXY();
var iconXY = Blockly.getRelativeXY_(this.iconGroup_);
var iconXY = Blockly.utils.getRelativeXY(this.iconGroup_);
var newXY = new goog.math.Coordinate(
blockXY.x + iconXY.x + this.SIZE / 2,
blockXY.y + iconXY.y + this.SIZE / 2);

View file

@ -26,12 +26,13 @@
goog.provide('Blockly.inject');
goog.require('Blockly.BlockDragSurfaceSvg');
goog.require('Blockly.Css');
goog.require('Blockly.constants');
goog.require('Blockly.Options');
goog.require('Blockly.WorkspaceSvg');
goog.require('Blockly.DragSurfaceSvg');
goog.require('Blockly.DropDownDiv');
goog.require('Blockly.WorkspaceDragSurfaceSvg');
goog.require('goog.dom');
goog.require('goog.ui.Component');
goog.require('goog.userAgent');
@ -57,10 +58,13 @@ Blockly.inject = function(container, opt_options) {
container.appendChild(subContainer);
var svg = Blockly.createDom_(subContainer, options);
var dragSurface = new Blockly.DragSurfaceSvg(subContainer);
dragSurface.createDom();
var workspace = Blockly.createMainWorkspace_(svg, options, dragSurface);
// Create surfaces for dragging things. These are optimizations
// so that the broowser does not repaint during the drag.
var blockDragSurface = new Blockly.BlockDragSurfaceSvg(subContainer);
var workspaceDragSurface = new Blockly.workspaceDragSurfaceSvg(subContainer);
var workspace = Blockly.createMainWorkspace_(svg, options, blockDragSurface,
workspaceDragSurface);
Blockly.init_(workspace);
workspace.markFocused();
Blockly.bindEventWithChecks_(svg, 'focus', workspace, workspace.markFocused);
@ -87,14 +91,29 @@ Blockly.createDom_ = function(container, options) {
Blockly.Css.inject(options.hasCss, options.pathToMedia);
// Build the SVG DOM.
var svg = Blockly.createSvgElement('svg', {
/*
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
class="blocklySvg">
...
</svg>
*/
var svg = Blockly.utils.createSvgElement('svg', {
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:html': 'http://www.w3.org/1999/xhtml',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
'version': '1.1',
'class': 'blocklySvg'
}, container);
var defs = Blockly.createSvgElement('defs', {}, svg);
/*
<defs>
... filters go here ...
</defs>
*/
var defs = Blockly.utils.createSvgElement('defs', {}, svg);
// Each filter/pattern needs a unique ID for the case of multiple Blockly
// instances on a page. Browser behaviour becomes undefined otherwise.
// https://neil.fraser.name/news/2015/11/01/
@ -105,66 +124,80 @@ Blockly.createDom_ = function(container, options) {
// Using a dilate distorts the block shape.
// Instead use a gaussian blur, and then set all alpha to 1 with a transfer.
var stackGlowFilter = Blockly.createSvgElement('filter',
var stackGlowFilter = Blockly.utils.createSvgElement('filter',
{'id': 'blocklyStackGlowFilter',
'height': '160%', 'width': '180%', y: '-30%', x: '-40%'}, defs);
options.stackGlowBlur = Blockly.createSvgElement('feGaussianBlur',
options.stackGlowBlur = Blockly.utils.createSvgElement('feGaussianBlur',
{'in': 'SourceGraphic',
'stdDeviation': Blockly.STACK_GLOW_RADIUS}, stackGlowFilter);
// Set all gaussian blur pixels to 1 opacity before applying flood
var componentTransfer = Blockly.createSvgElement('feComponentTransfer', {'result': 'outBlur'}, stackGlowFilter);
Blockly.createSvgElement('feFuncA',
var componentTransfer = Blockly.utils.createSvgElement('feComponentTransfer', {'result': 'outBlur'}, stackGlowFilter);
Blockly.utils.createSvgElement('feFuncA',
{'type': 'table', 'tableValues': '0' + goog.string.repeat(' 1', 16)}, componentTransfer);
// Color the highlight
Blockly.createSvgElement('feFlood',
Blockly.utils.createSvgElement('feFlood',
{'flood-color': Blockly.Colours.stackGlow,
'flood-opacity': Blockly.Colours.stackGlowOpacity, 'result': 'outColor'}, stackGlowFilter);
Blockly.createSvgElement('feComposite',
Blockly.utils.createSvgElement('feComposite',
{'in': 'outColor', 'in2': 'outBlur',
'operator': 'in', 'result': 'outGlow'}, stackGlowFilter);
Blockly.createSvgElement('feComposite',
Blockly.utils.createSvgElement('feComposite',
{'in': 'SourceGraphic', 'in2': 'outGlow', 'operator': 'over'}, stackGlowFilter);
// Filter for replacement marker
var replacementGlowFilter = Blockly.createSvgElement('filter',
var replacementGlowFilter = Blockly.utils.createSvgElement('filter',
{'id': 'blocklyReplacementGlowFilter',
'height': '160%', 'width': '180%', y: '-30%', x: '-40%'}, defs);
Blockly.createSvgElement('feGaussianBlur',
Blockly.utils.createSvgElement('feGaussianBlur',
{'in': 'SourceGraphic',
'stdDeviation': Blockly.REPLACEMENT_GLOW_RADIUS}, replacementGlowFilter);
// Set all gaussian blur pixels to 1 opacity before applying flood
var componentTransfer = Blockly.createSvgElement('feComponentTransfer', {'result': 'outBlur'}, replacementGlowFilter);
Blockly.createSvgElement('feFuncA',
var componentTransfer = Blockly.utils.createSvgElement('feComponentTransfer',
{'result': 'outBlur'}, replacementGlowFilter);
Blockly.utils.createSvgElement('feFuncA',
{'type': 'table', 'tableValues': '0' + goog.string.repeat(' 1', 16)}, componentTransfer);
// Color the highlight
Blockly.createSvgElement('feFlood',
Blockly.utils.createSvgElement('feFlood',
{'flood-color': Blockly.Colours.replacementGlow,
'flood-opacity': Blockly.Colours.replacementGlowOpacity, 'result': 'outColor'}, replacementGlowFilter);
Blockly.createSvgElement('feComposite',
Blockly.utils.createSvgElement('feComposite',
{'in': 'outColor', 'in2': 'outBlur',
'operator': 'in', 'result': 'outGlow'}, replacementGlowFilter);
Blockly.createSvgElement('feComposite',
Blockly.utils.createSvgElement('feComposite',
{'in': 'SourceGraphic', 'in2': 'outGlow', 'operator': 'over'}, replacementGlowFilter);
var disabledPattern = Blockly.createSvgElement('pattern',
/*
<pattern id="blocklyDisabledPattern837493" patternUnits="userSpaceOnUse"
width="10" height="10">
<rect width="10" height="10" fill="#aaa" />
<path d="M 0 0 L 10 10 M 10 0 L 0 10" stroke="#cc0" />
</pattern>
*/
var disabledPattern = Blockly.utils.createSvgElement('pattern',
{'id': 'blocklyDisabledPattern' + rnd,
'patternUnits': 'userSpaceOnUse',
'width': 10, 'height': 10}, defs);
Blockly.createSvgElement('rect',
Blockly.utils.createSvgElement('rect',
{'width': 10, 'height': 10, 'fill': '#aaa'}, disabledPattern);
Blockly.createSvgElement('path',
Blockly.utils.createSvgElement('path',
{'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, disabledPattern);
options.disabledPatternId = disabledPattern.id;
var gridPattern = Blockly.createSvgElement('pattern',
/*
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
<rect stroke="#888" />
<rect stroke="#888" />
</pattern>
*/
var gridPattern = Blockly.utils.createSvgElement('pattern',
{'id': 'blocklyGridPattern' + rnd,
'patternUnits': 'userSpaceOnUse'}, defs);
if (options.gridOptions['length'] > 0 && options.gridOptions['spacing'] > 0) {
Blockly.createSvgElement('line',
Blockly.utils.createSvgElement('line',
{'stroke': options.gridOptions['colour']},
gridPattern);
if (options.gridOptions['length'] > 1) {
Blockly.createSvgElement('line',
Blockly.utils.createSvgElement('line',
{'stroke': options.gridOptions['colour']},
gridPattern);
}
@ -178,15 +211,25 @@ Blockly.createDom_ = function(container, options) {
* Create a main workspace and add it to the SVG.
* @param {!Element} svg SVG element with pattern defined.
* @param {!Blockly.Options} options Dictionary of options.
* @param {!Blockly.DragSurfaceSvg} dragSurface Drag surface SVG for the workspace.
* @param {!Blockly.BlockDragSurfaceSvg} blockDragSurface Drag surface SVG
* for the blocks.
* @param {!Blockly.WorkspaceDragSurfaceSvg} workspaceDragSurface Drag surface
* SVG for the workspace.
* @return {!Blockly.Workspace} Newly created main workspace.
* @private
*/
Blockly.createMainWorkspace_ = function(svg, options, dragSurface) {
Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, workspaceDragSurface) {
options.parentWorkspace = null;
var mainWorkspace = new Blockly.WorkspaceSvg(options, dragSurface);
var mainWorkspace = new Blockly.WorkspaceSvg(options, blockDragSurface, workspaceDragSurface);
mainWorkspace.scale = options.zoomOptions.startScale;
svg.appendChild(mainWorkspace.createDom('blocklyMainBackground'));
if (!options.hasCategories && options.languageTree) {
// Add flyout as an <svg> that is a sibling of the workspace svg.
var flyout = mainWorkspace.addFlyout_('svg');
Blockly.utils.insertAfter_(flyout, svg);
}
// A null translation will also apply the correct initial scale.
mainWorkspace.translate(0, 0);
mainWorkspace.markFocused();
@ -256,10 +299,10 @@ Blockly.init_ = function(mainWorkspace) {
var options = mainWorkspace.options;
var svg = mainWorkspace.getParentSvg();
// Supress the browser's context menu.
// Suppress the browser's context menu.
Blockly.bindEventWithChecks_(svg, 'contextmenu', null,
function(e) {
if (!Blockly.isTargetInput_(e)) {
if (!Blockly.utils.isTargetInput(e)) {
e.preventDefault();
}
});
@ -327,7 +370,7 @@ Blockly.inject.bindDocumentEvents_ = function() {
Blockly.bindEvent_(document, 'touchcancel', null,
Blockly.longStop_);
// Don't use bindEvent_ for document's mouseup since that would create a
// corresponding touch handler that would squeltch the ability to interact
// corresponding touch handler that would squelch the ability to interact
// with non-Blockly elements.
document.addEventListener('mouseup', Blockly.onMouseUp_, false);
// Some iPad versions don't fire resize after portrait to landscape change.
@ -377,6 +420,7 @@ Blockly.inject.loadSounds_ = function(pathToMedia, workspace) {
/**
* Modify the block tree on the existing toolbox.
* @param {Node|string} tree DOM tree of blocks, or text representation of same.
* @deprecated April 2015
*/
Blockly.updateToolbox = function(tree) {
console.warn('Deprecated call to Blockly.updateToolbox, ' +

View file

@ -64,13 +64,13 @@ Blockly.Mutator.prototype.workspaceHeight_ = 0;
*/
Blockly.Mutator.prototype.drawIcon_ = function(group) {
// Square with rounded corners.
Blockly.createSvgElement('rect',
Blockly.utils.createSvgElement('rect',
{'class': 'blocklyIconShape',
'rx': '4', 'ry': '4',
'height': '16', 'width': '16'},
group);
// Gear teeth.
Blockly.createSvgElement('path',
Blockly.utils.createSvgElement('path',
{'class': 'blocklyIconSymbol',
'd': 'm4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 ' +
'0.41,0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 ' +
@ -81,7 +81,7 @@ Blockly.Mutator.prototype.drawIcon_ = function(group) {
'-0.41,0.11 -0.899,1.559 0.108,0.409z'},
group);
// Axle hole.
Blockly.createSvgElement('circle',
Blockly.utils.createSvgElement('circle',
{'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'},
group);
};
@ -110,7 +110,7 @@ Blockly.Mutator.prototype.createEditor_ = function() {
[Workspace]
</svg>
*/
this.svgDialog_ = Blockly.createSvgElement('svg',
this.svgDialog_ = Blockly.utils.createSvgElement('svg',
{'x': Blockly.Bubble.BORDER_WIDTH, 'y': Blockly.Bubble.BORDER_WIDTH},
null);
// Convert the list of names into a list of XML objects for the flyout.
@ -135,8 +135,16 @@ Blockly.Mutator.prototype.createEditor_ = function() {
};
this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions, this.block_.workspace.dragSurface);
this.workspace_.isMutator = true;
this.svgDialog_.appendChild(
this.workspace_.createDom('blocklyMutatorBackground'));
// Mutator flyouts go inside the mutator workspace's <g> rather than in
// a top level svg. Instead of handling scale themselves, mutators
// inherit scale from the parent workspace.
// To fix this, scale needs to be applied at a different level in the dom.
var flyoutSvg = this.workspace_.addFlyout_('g');
var background = this.workspace_.createDom('blocklyMutatorBackground');
background.appendChild(flyoutSvg);
this.svgDialog_.appendChild(background);
return this.svgDialog_;
};
@ -147,14 +155,14 @@ Blockly.Mutator.prototype.updateEditable = function() {
if (!this.block_.isInFlyout) {
if (this.block_.isEditable()) {
if (this.iconGroup_) {
Blockly.removeClass_(/** @type {!Element} */ (this.iconGroup_),
Blockly.utils.removeClass(/** @type {!Element} */ (this.iconGroup_),
'blocklyIconGroupReadonly');
}
} else {
// Close any mutator bubble. Icon is not clickable.
this.setVisible(false);
if (this.iconGroup_) {
Blockly.addClass_(/** @type {!Element} */ (this.iconGroup_),
Blockly.utils.addClass(/** @type {!Element} */ (this.iconGroup_),
'blocklyIconGroupReadonly');
}
}
@ -308,7 +316,7 @@ Blockly.Mutator.prototype.workspaceChanged_ = function() {
block.compose(this.rootBlock_);
// Restore rendering and show the changes.
block.rendered = savedRendered;
// Mutation may have added some elements that need initalizing.
// Mutation may have added some elements that need initializing.
block.initSvg();
var newMutationDom = block.mutationToDom();
var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);

View file

@ -161,17 +161,29 @@ Blockly.Procedures.rename = function(name) {
Blockly.Procedures.flyoutCategory = function(workspace) {
var xmlList = [];
if (Blockly.Blocks['procedures_defnoreturn']) {
// <block type="procedures_defnoreturn" gap="16"></block>
// <block type="procedures_defnoreturn" gap="16">
// <field name="NAME">do something</field>
// </block>
var block = goog.dom.createDom('block');
block.setAttribute('type', 'procedures_defnoreturn');
block.setAttribute('gap', 16);
var nameField = goog.dom.createDom('field', null,
Blockly.Msg.PROCEDURES_DEFNORETURN_PROCEDURE);
nameField.setAttribute('name', 'NAME');
block.appendChild(nameField);
xmlList.push(block);
}
if (Blockly.Blocks['procedures_defreturn']) {
// <block type="procedures_defreturn" gap="16"></block>
// <block type="procedures_defreturn" gap="16">
// <field name="NAME">do something</field>
// </block>
var block = goog.dom.createDom('block');
block.setAttribute('type', 'procedures_defreturn');
block.setAttribute('gap', 16);
var nameField = goog.dom.createDom('field', null,
Blockly.Msg.PROCEDURES_DEFRETURN_PROCEDURE);
nameField.setAttribute('name', 'NAME');
block.appendChild(nameField);
xmlList.push(block);
}
if (Blockly.Blocks['procedures_report']) {

View file

@ -161,7 +161,7 @@ Blockly.RenderedConnection.prototype.tighten_ = function() {
if (!svgRoot) {
throw 'block is not rendered.';
}
var xy = Blockly.getRelativeXY_(svgRoot);
var xy = Blockly.utils.getRelativeXY(svgRoot);
block.getSvgRoot().setAttribute('transform',
'translate(' + (xy.x - dx) + ',' + (xy.y - dy) + ')');
block.moveConnections_(-dx, -dy);
@ -190,7 +190,7 @@ Blockly.RenderedConnection.prototype.highlight = function() {
var xy = this.sourceBlock_.getRelativeToSurfaceXY();
var x = this.x_ - xy.x;
var y = this.y_ - xy.y;
Blockly.Connection.highlightedPath_ = Blockly.createSvgElement('path',
Blockly.Connection.highlightedPath_ = Blockly.utils.createSvgElement('path',
{'class': 'blocklyHighlightedConnectionPath',
'd': steps,
transform: 'translate(' + x + ',' + y + ')' +

View file

@ -40,11 +40,11 @@ Blockly.ScrollbarPair = function(workspace) {
this.workspace_ = workspace;
this.hScroll = new Blockly.Scrollbar(workspace, true, true);
this.vScroll = new Blockly.Scrollbar(workspace, false, true);
this.corner_ = Blockly.createSvgElement('rect',
this.corner_ = Blockly.utils.createSvgElement('rect',
{'height': Blockly.Scrollbar.scrollbarThickness,
'width': Blockly.Scrollbar.scrollbarThickness,
'class': 'blocklyScrollbarBackground'}, null);
Blockly.Scrollbar.insertAfter_(this.corner_, workspace.getBubbleCanvas());
Blockly.utils.insertAfter_(this.corner_, workspace.getBubbleCanvas());
};
/**
@ -202,6 +202,8 @@ Blockly.Scrollbar = function(workspace, horizontal, opt_pair) {
if (horizontal) {
this.svgBackground_.setAttribute('height',
Blockly.Scrollbar.scrollbarThickness);
this.outerSvg_.setAttribute('height',
Blockly.Scrollbar.scrollbarThickness);
this.svgHandle_.setAttribute('height',
Blockly.Scrollbar.scrollbarThickness - 5);
this.svgHandle_.setAttribute('y', 2.5);
@ -211,6 +213,8 @@ Blockly.Scrollbar = function(workspace, horizontal, opt_pair) {
} else {
this.svgBackground_.setAttribute('width',
Blockly.Scrollbar.scrollbarThickness);
this.outerSvg_.setAttribute('width',
Blockly.Scrollbar.scrollbarThickness);
this.svgHandle_.setAttribute('width',
Blockly.Scrollbar.scrollbarThickness - 5);
this.svgHandle_.setAttribute('x', 2.5);
@ -224,6 +228,20 @@ Blockly.Scrollbar = function(workspace, horizontal, opt_pair) {
this.onMouseDownHandleWrapper_ = Blockly.bindEventWithChecks_(this.svgHandle_,
'mousedown', scrollbar, scrollbar.onMouseDownHandle_);
};
/**
* The coordinate of the upper left corner of the scrollbar SVG.
* @type {goog.math.Coordinate}
* @private
*/
Blockly.Scrollbar.prototype.origin_ = new goog.math.Coordinate(0, 0);
/**
* Whether or not the origin of the scrollbar has changed. Used
* to help decide whether or not the reflow/resize calls need to happen.
* @type {boolean}
* @private
*/
Blockly.Scrollbar.prototype.originHasChanged_ = true;
/**
* The size of the area within which the scrollbar handle can move.
@ -253,6 +271,13 @@ Blockly.Scrollbar.prototype.handlePosition_ = 0;
*/
Blockly.Scrollbar.prototype.isVisible_ = true;
/**
* Whether the workspace containing this scrollbar is visible.
* @type {boolean}
* @private
*/
Blockly.Scrollbar.prototype.containerVisible_ = true;
/**
* Width of vertical scrollbar or height of horizontal scrollbar.
* Increase the size of scrollbars on touch devices.
@ -303,7 +328,8 @@ Blockly.Scrollbar.prototype.dispose = function() {
Blockly.unbindEvent_(this.onMouseDownHandleWrapper_);
this.onMouseDownHandleWrapper_ = null;
goog.dom.removeNode(this.svgGroup_);
goog.dom.removeNode(this.outerSvg_);
this.outerSvg_ = null;
this.svgGroup_ = null;
this.svgBackground_ = null;
this.svgHandle_ = null;
@ -338,9 +364,19 @@ Blockly.Scrollbar.prototype.setHandlePosition = function(newPosition) {
*/
Blockly.Scrollbar.prototype.setScrollViewSize_ = function(newSize) {
this.scrollViewSize_ = newSize;
this.outerSvg_.setAttribute(this.lengthAttribute_, this.scrollViewSize_);
this.svgBackground_.setAttribute(this.lengthAttribute_, this.scrollViewSize_);
};
/**
* Set whether this scrollbar's container is visible.
* @param {boolean} visible Whether the container is visible.
*/
Blockly.ScrollbarPair.prototype.setContainerVisible = function(visible) {
this.hScroll.setContainerVisible(visible);
this.vScroll.setContainerVisible(visible);
};
/**
* Set the position of the scrollbar's svg group.
* @param {number} x The new x coordinate.
@ -350,8 +386,10 @@ Blockly.Scrollbar.prototype.setPosition = function(x, y) {
this.position_.x = x;
this.position_.y = y;
this.svgGroup_.setAttribute('transform',
'translate(' + this.position_.x + ',' + this.position_.y + ')');
var tempX = this.position_.x + this.origin_.x;
var tempY = this.position_.y + this.origin_.y;
var transform = 'translate(' + tempX + 'px,' + tempY + 'px)';
this.outerSvg_.style.transform = transform;
};
/**
@ -371,7 +409,11 @@ Blockly.Scrollbar.prototype.resize = function(opt_metrics) {
}
}
if (Blockly.Scrollbar.metricsAreEquivalent_(hostMetrics,
// If the origin has changed (e.g. the toolbox is moving from start to end)
// we want to continue with the resize even if workspace metrics haven't.
if (this.originHasChanged_) {
this.originHasChanged_ = false;
} else if (Blockly.Scrollbar.metricsAreEquivalent_(hostMetrics,
this.oldHostMetrics_)) {
return;
}
@ -539,22 +581,26 @@ Blockly.Scrollbar.prototype.resizeContentVertical = function(hostMetrics) {
*/
Blockly.Scrollbar.prototype.createDom_ = function() {
/* Create the following DOM:
<g class="blocklyScrollbarHorizontal">
<rect class="blocklyScrollbarBackground" />
<rect class="blocklyScrollbarHandle" rx="8" ry="8" />
</g>
<svg class="blocklyScrollbarHorizontal">
<g>
<rect class="blocklyScrollbarBackground" />
<rect class="blocklyScrollbarHandle" rx="8" ry="8" />
</g>
</svg>
*/
var className = 'blocklyScrollbar' +
(this.horizontal_ ? 'Horizontal' : 'Vertical');
this.svgGroup_ = Blockly.createSvgElement('g', {'class': className}, null);
this.svgBackground_ = Blockly.createSvgElement('rect',
this.outerSvg_ = Blockly.utils.createSvgElement('svg', {'class': className},
null);
this.svgGroup_ = Blockly.utils.createSvgElement('g', {}, this.outerSvg_);
this.svgBackground_ = Blockly.utils.createSvgElement('rect',
{'class': 'blocklyScrollbarBackground'}, this.svgGroup_);
var radius = Math.floor((Blockly.Scrollbar.scrollbarThickness - 5) / 2);
this.svgHandle_ = Blockly.createSvgElement('rect',
this.svgHandle_ = Blockly.utils.createSvgElement('rect',
{'class': 'blocklyScrollbarHandle', 'rx': radius, 'ry': radius},
this.svgGroup_);
Blockly.Scrollbar.insertAfter_(this.svgGroup_,
this.workspace_.getBubbleCanvas());
Blockly.utils.insertAfter_(this.outerSvg_,
this.workspace_.getParentSvg());
};
/**
@ -566,29 +612,57 @@ Blockly.Scrollbar.prototype.isVisible = function() {
return this.isVisible_;
};
/**
* Set whether the scrollbar's container is visible and update
* display accordingly if visibility has changed.
* @param {boolean} visible Whether the container is visible
*/
Blockly.Scrollbar.prototype.setContainerVisible = function(visible) {
var visibilityChanged = (visible != this.containerVisible_);
this.containerVisible_ = visible;
if (visibilityChanged) {
this.updateDisplay_();
}
};
/**
* Set whether the scrollbar is visible.
* Only applies to non-paired scrollbars.
* @param {boolean} visible True if visible.
*/
Blockly.Scrollbar.prototype.setVisible = function(visible) {
if (visible == this.isVisible()) {
return;
}
var visibilityChanged = (visible != this.isVisible());
// Ideally this would also apply to scrollbar pairs, but that's a bigger
// headache (due to interactions with the corner square).
if (this.pair_) {
throw 'Unable to toggle visibility of paired scrollbars.';
}
this.isVisible_ = visible;
if (visibilityChanged) {
this.updateDisplay_();
}
};
if (visible) {
this.svgGroup_.setAttribute('display', 'block');
/**
* Update visibility of scrollbar based on whether it thinks it should
* be visible and whether its containing workspace is visible.
* We cannot rely on the containing workspace being hidden to hide us
* because it is not necessarily our parent in the dom.
*/
Blockly.Scrollbar.prototype.updateDisplay_ = function() {
var show = true;
// Check whether our parent/container is visible.
if (!this.containerVisible_) {
show = false;
} else {
// Hide the scrollbar.
this.workspace_.setMetrics({x: 0, y: 0});
this.svgGroup_.setAttribute('display', 'none');
show = this.isVisible();
}
if (show) {
this.outerSvg_.setAttribute('display', 'block');
} else {
this.outerSvg_.setAttribute('display', 'none');
}
};
@ -602,17 +676,17 @@ Blockly.Scrollbar.prototype.onMouseDownBar_ = function(e) {
this.workspace_.markFocused();
Blockly.Touch.clearTouchIdentifier(); // This is really a click.
this.cleanUp_();
if (Blockly.isRightButton(e)) {
if (Blockly.utils.isRightButton(e)) {
// Right-click.
// Scrollbars have no context menu.
e.stopPropagation();
return;
}
var mouseXY = Blockly.mouseToSvg(e, this.workspace_.getParentSvg(),
var mouseXY = Blockly.utils.mouseToSvg(e, this.workspace_.getParentSvg(),
this.workspace_.getInverseScreenCTM());
var mouseLocation = this.horizontal_ ? mouseXY.x : mouseXY.y;
var handleXY = Blockly.getSvgXY_(this.svgHandle_, this.workspace_);
var handleXY = Blockly.utils.getInjectionDivXY_(this.svgHandle_);
var handleStart = this.horizontal_ ? handleXY.x : handleXY.y;
var handlePosition = this.handlePosition_;
@ -644,7 +718,7 @@ Blockly.Scrollbar.prototype.onMouseDownBar_ = function(e) {
Blockly.Scrollbar.prototype.onMouseDownHandle_ = function(e) {
this.workspace_.markFocused();
this.cleanUp_();
if (Blockly.isRightButton(e)) {
if (Blockly.utils.isRightButton(e)) {
// Right-click.
// Scrollbars have no context menu.
e.stopPropagation();
@ -652,6 +726,12 @@ Blockly.Scrollbar.prototype.onMouseDownHandle_ = function(e) {
}
// Look up the current translation and record it.
this.startDragHandle = this.handlePosition_;
// Tell the workspace to setup its drag surface since it is about to move.
// onMouseMoveHandle will call onScroll which actually tells the workspace
// to move.
this.workspace_.setupDragSurface();
// Record the current mouse position.
this.startDragMouse = this.horizontal_ ? e.clientX : e.clientY;
Blockly.Scrollbar.onMouseUpWrapper_ = Blockly.bindEventWithChecks_(document,
@ -686,6 +766,8 @@ Blockly.Scrollbar.prototype.onMouseMoveHandle_ = function(e) {
* @private
*/
Blockly.Scrollbar.prototype.onMouseUpHandle_ = function() {
// Tell the workspace to clean up now that the workspace is done moving.
this.workspace_.resetDragSurface();
Blockly.Touch.clearTouchIdentifier();
this.cleanUp_();
};
@ -751,21 +833,15 @@ Blockly.Scrollbar.prototype.set = function(value) {
};
/**
* Insert a node after a reference node.
* Contrast with node.insertBefore function.
* @param {!Element} newNode New element to insert.
* @param {!Element} refNode Existing element to precede new node.
* @private
* Set the origin of the upper left of the scrollbar. This if for times
* when the scrollbar is used in an object whose origin isn't the same
* as the main workspace (e.g. in a flyout.)
* @param {number} x The x coordinate of the scrollbar's origin.
* @param {number} y The y coordinate of the scrollbar's origin.
*/
Blockly.Scrollbar.insertAfter_ = function(newNode, refNode) {
var siblingNode = refNode.nextSibling;
var parentNode = refNode.parentNode;
if (!parentNode) {
throw 'Reference node has no parent.';
}
if (siblingNode) {
parentNode.insertBefore(newNode, siblingNode);
} else {
parentNode.appendChild(newNode);
Blockly.Scrollbar.prototype.setOrigin = function(x, y) {
if (x != this.origin_.x || y != this.origin_.y) {
this.origin_ = new goog.math.Coordinate(x, y);
this.originHasChanged_ = true;
}
};

View file

@ -117,7 +117,7 @@ Blockly.Toolbox.prototype.init = function() {
Blockly.bindEventWithChecks_(this.HtmlDiv, 'mousedown', this,
function(e) {
Blockly.DropDownDiv.hide();
if (Blockly.isRightButton(e) || e.target == this.HtmlDiv) {
if (Blockly.utils.isRightButton(e) || e.target == this.HtmlDiv) {
// Close flyout.
Blockly.hideChaff(false);
} else {
@ -168,7 +168,8 @@ Blockly.Toolbox.prototype.createFlyout_ = function() {
}
this.flyout_.setParentToolbox(this);
goog.dom.insertSiblingAfter(this.flyout_.createDom(), workspace.svgGroup_);
goog.dom.insertSiblingAfter(this.flyout_.createDom('svg'),
this.workspace_.getParentSvg());
this.flyout_.init(workspace);
};

View file

@ -160,7 +160,7 @@ Blockly.Tooltip.onMouseOver_ = function(e) {
Blockly.Tooltip.poisonedElement_ = null;
Blockly.Tooltip.element_ = element;
}
// Forget about any immediately preceeding mouseOut event.
// Forget about any immediately preceding mouseOut event.
clearTimeout(Blockly.Tooltip.mouseOutPid_);
};

View file

@ -107,6 +107,9 @@ Blockly.onMouseUp_ = function(/* e */) {
return;
}
Blockly.Touch.clearTouchIdentifier();
// TODO(#781): Check whether this needs to be called for all drag modes.
workspace.resetDragSurface();
Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
workspace.dragMode_ = Blockly.DRAG_NONE;
// Unbind the touch event if it exists.

View file

@ -163,17 +163,17 @@ Blockly.Trashcan.prototype.createDom = function() {
clip-path="url(#blocklyTrashLidClipPath837493)"></image>
</g>
*/
this.svgGroup_ = Blockly.createSvgElement('g',
this.svgGroup_ = Blockly.utils.createSvgElement('g',
{'class': 'blocklyTrash'}, null);
var rnd = String(Math.random()).substring(2);
var clip = Blockly.createSvgElement('clipPath',
var clip = Blockly.utils.createSvgElement('clipPath',
{'id': 'blocklyTrashBodyClipPath' + rnd},
this.svgGroup_);
Blockly.createSvgElement('rect',
Blockly.utils.createSvgElement('rect',
{'width': this.WIDTH_, 'height': this.BODY_HEIGHT_,
'y': this.LID_HEIGHT_},
clip);
var body = Blockly.createSvgElement('image',
var body = Blockly.utils.createSvgElement('image',
{'width': Blockly.SPRITE.width, 'x': -this.SPRITE_LEFT_,
'height': Blockly.SPRITE.height, 'y': -this.SPRITE_TOP_,
'clip-path': 'url(#blocklyTrashBodyClipPath' + rnd + ')'},
@ -181,12 +181,12 @@ Blockly.Trashcan.prototype.createDom = function() {
body.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
this.workspace_.options.pathToMedia + Blockly.SPRITE.url);
var clip = Blockly.createSvgElement('clipPath',
var clip = Blockly.utils.createSvgElement('clipPath',
{'id': 'blocklyTrashLidClipPath' + rnd},
this.svgGroup_);
Blockly.createSvgElement('rect',
Blockly.utils.createSvgElement('rect',
{'width': this.WIDTH_, 'height': this.LID_HEIGHT_}, clip);
this.svgLid_ = Blockly.createSvgElement('image',
this.svgLid_ = Blockly.utils.createSvgElement('image',
{'width': Blockly.SPRITE.width, 'x': -this.SPRITE_LEFT_,
'height': Blockly.SPRITE.height, 'y': -this.SPRITE_TOP_,
'clip-path': 'url(#blocklyTrashLidClipPath' + rnd + ')'},

View file

@ -47,16 +47,18 @@ Blockly.cache3dSupported_ = null;
* Similar to Closure's goog.dom.classes.add, except it handles SVG elements.
* @param {!Element} element DOM element to add class to.
* @param {string} className Name of class to add.
* @private
* @return {boolean} True if class was added, false if already present.
*/
Blockly.addClass_ = function(element, className) {
Blockly.utils.addClass = function(element, className) {
var classes = element.getAttribute('class') || '';
if ((' ' + classes + ' ').indexOf(' ' + className + ' ') == -1) {
if (classes) {
classes += ' ';
}
element.setAttribute('class', classes + className);
if ((' ' + classes + ' ').indexOf(' ' + className + ' ') != -1) {
return false;
}
if (classes) {
classes += ' ';
}
element.setAttribute('class', classes + className);
return true;
};
/**
@ -64,24 +66,26 @@ Blockly.addClass_ = function(element, className) {
* Similar to Closure's goog.dom.classes.remove, except it handles SVG elements.
* @param {!Element} element DOM element to remove class from.
* @param {string} className Name of class to remove.
* @private
* @return {boolean} True if class was removed, false if never present.
*/
Blockly.removeClass_ = function(element, className) {
Blockly.utils.removeClass = function(element, className) {
var classes = element.getAttribute('class');
if ((' ' + classes + ' ').indexOf(' ' + className + ' ') != -1) {
var classList = classes.split(/\s+/);
for (var i = 0; i < classList.length; i++) {
if (!classList[i] || classList[i] == className) {
classList.splice(i, 1);
i--;
}
}
if (classList.length) {
element.setAttribute('class', classList.join(' '));
} else {
element.removeAttribute('class');
if ((' ' + classes + ' ').indexOf(' ' + className + ' ') == -1) {
return false;
}
var classList = classes.split(/\s+/);
for (var i = 0; i < classList.length; i++) {
if (!classList[i] || classList[i] == className) {
classList.splice(i, 1);
i--;
}
}
if (classList.length) {
element.setAttribute('class', classList.join(' '));
} else {
element.removeAttribute('class');
}
return true;
};
/**
@ -92,141 +96,16 @@ Blockly.removeClass_ = function(element, className) {
* @return {boolean} True if class exists, false otherwise.
* @private
*/
Blockly.hasClass_ = function(element, className) {
Blockly.utils.hasClass = function(element, className) {
var classes = element.getAttribute('class');
return (' ' + classes + ' ').indexOf(' ' + className + ' ') != -1;
};
/**
* Bind an event to a function call. When calling the function, verifies that
* it belongs to the touch stream that is currently being processsed, and splits
* multitouch events into multiple events as needed.
* @param {!Node} node Node upon which to listen.
* @param {string} name Event name to listen to (e.g. 'mousedown').
* @param {Object} thisObject The value of 'this' in the function.
* @param {!Function} func Function to call when event is triggered.
* @param {boolean} opt_noCaptureIdentifier True if triggering on this event
* should not block execution of other event handlers on this touch or other
* simultaneous touches.
* @return {!Array.<!Array>} Opaque data that can be passed to unbindEvent_.
* @private
*/
Blockly.bindEventWithChecks_ = function(node, name, thisObject, func,
opt_noCaptureIdentifier) {
var handled = false;
var wrapFunc = function(e) {
var captureIdentifier = !opt_noCaptureIdentifier;
// Handle each touch point separately. If the event was a mouse event, this
// will hand back an array with one element, which we're fine handling.
var events = Blockly.Touch.splitEventByTouches(e);
for (var i = 0, event; event = events[i]; i++) {
if (captureIdentifier && !Blockly.Touch.shouldHandleEvent(event)) {
continue;
}
Blockly.Touch.setClientFromTouch(event);
if (thisObject) {
func.call(thisObject, event);
} else {
func(event);
}
handled = true;
}
};
node.addEventListener(name, wrapFunc, false);
var bindData = [[node, name, wrapFunc]];
// Add equivalent touch event.
if (name in Blockly.Touch.TOUCH_MAP) {
var touchWrapFunc = function(e) {
wrapFunc(e);
// Stop the browser from scrolling/zooming the page.
if (handled) {
e.preventDefault();
}
};
for (var i = 0, eventName;
eventName = Blockly.Touch.TOUCH_MAP[name][i]; i++) {
node.addEventListener(eventName, touchWrapFunc, false);
bindData.push([node, eventName, touchWrapFunc]);
}
}
return bindData;
};
/**
* Bind an event to a function call. Handles multitouch events by using the
* coordinates of the first changed touch, and doesn't do any safety checks for
* simultaneous event processing.
* @deprecated in favor of bindEventWithChecks_, but preserved for external
* users.
* @param {!Node} node Node upon which to listen.
* @param {string} name Event name to listen to (e.g. 'mousedown').
* @param {Object} thisObject The value of 'this' in the function.
* @param {!Function} func Function to call when event is triggered.
* @return {!Array.<!Array>} Opaque data that can be passed to unbindEvent_.
* @private
*/
Blockly.bindEvent_ = function(node, name, thisObject, func) {
var wrapFunc = function(e) {
if (thisObject) {
func.call(thisObject, e);
} else {
func(e);
}
};
node.addEventListener(name, wrapFunc, false);
var bindData = [[node, name, wrapFunc]];
// Add equivalent touch event.
if (name in Blockly.Touch.TOUCH_MAP) {
var touchWrapFunc = function(e) {
// Punt on multitouch events.
if (e.changedTouches.length == 1) {
// Map the touch event's properties to the event.
var touchPoint = e.changedTouches[0];
e.clientX = touchPoint.clientX;
e.clientY = touchPoint.clientY;
}
wrapFunc(e);
// Stop the browser from scrolling/zooming the page.
e.preventDefault();
};
for (var i = 0, eventName;
eventName = Blockly.Touch.TOUCH_MAP[name][i]; i++) {
node.addEventListener(eventName, touchWrapFunc, false);
bindData.push([node, eventName, touchWrapFunc]);
}
}
return bindData;
};
/**
* Unbind one or more events event from a function call.
* @param {!Array.<!Array>} bindData Opaque data from bindEvent_. This list is
* emptied during the course of calling this function.
* @return {!Function} The function call.
* @private
*/
Blockly.unbindEvent_ = function(bindData) {
while (bindData.length) {
var bindDatum = bindData.pop();
var node = bindDatum[0];
var name = bindDatum[1];
var func = bindDatum[2];
node.removeEventListener(name, func, false);
}
return func;
};
/**
* Don't do anything for this event, just halt propagation.
* @param {!Event} e An event.
*/
Blockly.noEvent = function(e) {
Blockly.utils.noEvent = function(e) {
// This event has been handled. No need to bubble up to the document.
e.preventDefault();
e.stopPropagation();
@ -236,9 +115,8 @@ Blockly.noEvent = function(e) {
* Is this event targeting a text input widget?
* @param {!Event} e An event.
* @return {boolean} True if text input.
* @private
*/
Blockly.isTargetInput_ = function(e) {
Blockly.utils.isTargetInput = function(e) {
return e.target.type == 'textarea' || e.target.type == 'text' ||
e.target.type == 'number' || e.target.type == 'email' ||
e.target.type == 'password' || e.target.type == 'search' ||
@ -251,9 +129,8 @@ Blockly.isTargetInput_ = function(e) {
* its parent. Only for SVG elements and children (e.g. rect, g, path).
* @param {!Element} element SVG element to find the coordinates of.
* @return {!goog.math.Coordinate} Object with .x and .y properties.
* @private
*/
Blockly.getRelativeXY_ = function(element) {
Blockly.utils.getRelativeXY = function(element) {
var xy = new goog.math.Coordinate(0, 0);
// First, check for x and y attributes.
var x = element.getAttribute('x');
@ -266,21 +143,22 @@ Blockly.getRelativeXY_ = function(element) {
}
// Second, check for transform="translate(...)" attribute.
var transform = element.getAttribute('transform');
if (transform) {
var transformComponents =
transform.match(Blockly.getRelativeXY_.XY_REGEXP_);
if (transformComponents) {
xy.x += parseFloat(transformComponents[1]);
if (transformComponents[3]) {
xy.y += parseFloat(transformComponents[3]);
}
var r = transform && transform.match(Blockly.utils.getRelativeXY.XY_REGEX_);
if (r) {
xy.x += parseFloat(r[1]);
if (r[3]) {
xy.y += parseFloat(r[3]);
}
}
// Third, check for style="transform: translate3d(...)".
// Then check for style = transform: translate(...) or translate3d(...)
var style = element.getAttribute('style');
if (style && style.indexOf('translate3d') > -1) {
var styleComponents = style.match(Blockly.getRelativeXY_.XY_3D_REGEXP_);
if (style && style.indexOf('translate') > -1) {
var styleComponents = style.match(Blockly.utils.getRelativeXY.XY_2D_REGEX_);
// Try transform3d if 2d transform wasn't there.
if (!styleComponents) {
styleComponents = style.match(Blockly.utils.getRelativeXY.XY_3D_REGEX_);
}
if (styleComponents) {
xy.x += parseFloat(styleComponents[1]);
if (styleComponents[3]) {
@ -288,10 +166,54 @@ Blockly.getRelativeXY_ = function(element) {
}
}
}
return xy;
};
/**
* Return the coordinates of the top-left corner of this element relative to
* the div blockly was injected into.
* @param {!Element} element SVG element to find the coordinates of. If this is
* not a child of the div blockly was injected into, the behaviour is
* undefined.
* @return {!goog.math.Coordinate} Object with .x and .y properties.
*/
Blockly.utils.getInjectionDivXY_ = function(element) {
var x = 0;
var y = 0;
var scale = 1;
while (element) {
var xy = Blockly.utils.getRelativeXY(element);
var scale = Blockly.utils.getScale_(element);
x = (x * scale) + xy.x;
y = (y * scale) + xy.y;
var classes = element.getAttribute('class') || '';
if ((' ' + classes + ' ').indexOf(' injectionDiv ') != -1) {
break;
}
element = element.parentNode;
}
return new goog.math.Coordinate(x, y);
};
/**
* Return the scale of this element.
* @param {!Element} element The element to find the coordinates of.
* @return {!number} number represending the scale applied to the element.
* @private
*/
Blockly.utils.getScale_ = function(element) {
var scale = 1;
var transform = element.getAttribute('transform');
if (transform) {
var transformComponents =
transform.match(Blockly.utils.getScale_.REGEXP_);
if (transformComponents && transformComponents[0]) {
scale = parseFloat(transformComponents[0]);
}
}
return scale;
};
/**
* Static regex to pull the x,y values out of an SVG translate() directive.
* Note that Firefox and IE (9,10) return 'translate(12)' instead of
@ -301,90 +223,35 @@ Blockly.getRelativeXY_ = function(element) {
* @type {!RegExp}
* @private
*/
Blockly.getRelativeXY_.XY_REGEXP_ =
Blockly.utils.getRelativeXY.XY_REGEX_ =
/translate\(\s*([-+\d.e]+)([ ,]\s*([-+\d.e]+)\s*\))?/;
/**
* Static regex to pull the scale values out of a transform style property.
* Accounts for same exceptions as XY_REGEXP_.
* @type {!RegExp}
* @private
*/
Blockly.utils.getScale_REGEXP_ = /scale\(\s*([-+\d.e]+)\s*\)/;
/**
* Static regex to pull the x,y,z values out of a translate3d() style property.
* Accounts for same exceptions as XY_REGEXP_.
* @type {!RegExp}
* @private
*/
Blockly.getRelativeXY_.XY_3D_REGEXP_ =
Blockly.utils.getRelativeXY.XY_3D_REGEX_ =
/transform:\s*translate3d\(\s*([-+\d.e]+)px([ ,]\s*([-+\d.e]+)\s*)px([ ,]\s*([-+\d.e]+)\s*)px\)?/;
/**
* Return the absolute coordinates of the top-left corner of this element,
* scales that after canvas SVG element, if it's a descendant.
* The origin (0,0) is the top-left corner of the Blockly SVG.
* @param {!Element} element Element to find the coordinates of.
* @param {!Blockly.Workspace} workspace Element must be in this workspace.
* @return {!goog.math.Coordinate} Object with .x and .y properties.
* Static regex to pull the x,y,z values out of a translate3d() style property.
* Accounts for same exceptions as XY_REGEXP_.
* @type {!RegExp}
* @private
*/
Blockly.getSvgXY_ = function(element, workspace) {
var x = 0;
var y = 0;
var scale = 1;
if (goog.dom.contains(workspace.getCanvas(), element) ||
goog.dom.contains(workspace.getBubbleCanvas(), element)) {
// Before the SVG canvas, scale the coordinates.
scale = workspace.scale;
}
do {
// Loop through this block and every parent.
var xy = Blockly.getRelativeXY_(element);
if (element == workspace.getCanvas() ||
element == workspace.getBubbleCanvas()) {
// After the SVG canvas, don't scale the coordinates.
scale = 1;
}
x += xy.x * scale;
y += xy.y * scale;
element = element.parentNode;
} while (element && element != workspace.getParentSvg());
return new goog.math.Coordinate(x, y);
};
/**
* Check if 3D transforms are supported by adding an element
* and attempting to set the property.
* @return {boolean} true if 3D transforms are supported
*/
Blockly.is3dSupported = function() {
if (Blockly.cache3dSupported_ !== null) {
return Blockly.cache3dSupported_;
}
// CC-BY-SA Lorenzo Polidori
// https://stackoverflow.com/questions/5661671/detecting-transform-translate3d-support
if (!window.getComputedStyle) {
return false;
}
var el = document.createElement('p'),
has3d,
transforms = {
'webkitTransform': '-webkit-transform',
'OTransform': '-o-transform',
'msTransform': '-ms-transform',
'MozTransform': '-moz-transform',
'transform': 'transform'
};
// Add it to the body to get the computed style.
document.body.insertBefore(el, null);
for (var t in transforms) {
if (el.style[t] !== undefined) {
el.style[t] = 'translate3d(1px,1px,1px)';
has3d = window.getComputedStyle(el).getPropertyValue(transforms[t]);
}
}
document.body.removeChild(el);
Blockly.cache3dSupported_ = (has3d !== undefined && has3d.length > 0 && has3d !== "none");
return Blockly.cache3dSupported_;
};
Blockly.utils.getRelativeXY.XY_2D_REGEX_ =
/transform:\s*translate\(\s*([-+\d.e]+)px([ ,]\s*([-+\d.e]+)\s*)px\)?/;
/**
* Helper method for creating SVG elements.
@ -393,7 +260,7 @@ Blockly.is3dSupported = function() {
* @param {Element} parent Optional parent on which to append the element.
* @return {!SVGElement} Newly created SVG element.
*/
Blockly.createSvgElement = function(name, attrs, parent) {
Blockly.utils.createSvgElement = function(name, attrs, parent) {
var e = /** @type {!SVGElement} */ (
document.createElementNS(Blockly.SVG_NS, name));
for (var key in attrs) {
@ -416,7 +283,7 @@ Blockly.createSvgElement = function(name, attrs, parent) {
* @param {!Event} e Mouse event.
* @return {boolean} True if right-click.
*/
Blockly.isRightButton = function(e) {
Blockly.utils.isRightButton = function(e) {
if (e.ctrlKey && goog.userAgent.MAC) {
// Control-clicking on Mac OS X is treated as a right-click.
// WebKit on Mac OS X fails to change button to 2 (but Gecko does).
@ -427,13 +294,13 @@ Blockly.isRightButton = function(e) {
/**
* Return the converted coordinates of the given mouse event.
* The origin (0,0) is the top-left corner of the Blockly svg.
* The origin (0,0) is the top-left corner of the Blockly SVG.
* @param {!Event} e Mouse event.
* @param {!Element} svg SVG element.
* @param {SVGMatrix} matrix Inverted screen CTM to use.
* @return {!Object} Object with .x and .y properties.
*/
Blockly.mouseToSvg = function(e, svg, matrix) {
Blockly.utils.mouseToSvg = function(e, svg, matrix) {
var svgPoint = svg.createSVGPoint();
svgPoint.x = e.clientX;
svgPoint.y = e.clientY;
@ -449,15 +316,13 @@ Blockly.mouseToSvg = function(e, svg, matrix) {
* @param {!Array.<string>} array Array of strings.
* @return {number} Length of shortest string.
*/
Blockly.shortestStringLength = function(array) {
Blockly.utils.shortestStringLength = function(array) {
if (!array.length) {
return 0;
}
var len = array[0].length;
for (var i = 1; i < array.length; i++) {
len = Math.min(len, array[i].length);
}
return len;
return array.reduce(function(a, b) {
return a.length < b.length ? a : b;
}).length;
};
/**
@ -467,14 +332,14 @@ Blockly.shortestStringLength = function(array) {
* @param {number=} opt_shortest Length of shortest string.
* @return {number} Length of common prefix.
*/
Blockly.commonWordPrefix = function(array, opt_shortest) {
Blockly.utils.commonWordPrefix = function(array, opt_shortest) {
if (!array.length) {
return 0;
} else if (array.length == 1) {
return array[0].length;
}
var wordPrefix = 0;
var max = opt_shortest || Blockly.shortestStringLength(array);
var max = opt_shortest || Blockly.utils.shortestStringLength(array);
for (var len = 0; len < max; len++) {
var letter = array[0][len];
for (var i = 1; i < array.length; i++) {
@ -502,14 +367,14 @@ Blockly.commonWordPrefix = function(array, opt_shortest) {
* @param {number=} opt_shortest Length of shortest string.
* @return {number} Length of common suffix.
*/
Blockly.commonWordSuffix = function(array, opt_shortest) {
Blockly.utils.commonWordSuffix = function(array, opt_shortest) {
if (!array.length) {
return 0;
} else if (array.length == 1) {
return array[0].length;
}
var wordPrefix = 0;
var max = opt_shortest || Blockly.shortestStringLength(array);
var max = opt_shortest || Blockly.utils.shortestStringLength(array);
for (var len = 0; len < max; len++) {
var letter = array[0].substr(-len - 1, 1);
for (var i = 1; i < array.length; i++) {
@ -530,22 +395,45 @@ Blockly.commonWordSuffix = function(array, opt_shortest) {
return max;
};
/**
* Is the given string a number (includes negative and decimals).
* @param {string} str Input string.
* @return {boolean} True if number, false otherwise.
*/
Blockly.isNumber = function(str) {
return !!str.match(/^\s*-?\d+(\.\d+)?\s*$/);
};
/**
* Parse a string with any number of interpolation tokens (%1, %2, ...).
* '%' characters may be self-escaped (%%).
* @param {string} message Text containing interpolation tokens.
* It will also replace string table references (e.g., %{bky_my_msg} and
* %{BKY_MY_MSG} will both be replaced with the value in
* Blockly.Msg['MY_MSG']). Percentage sign characters '%' may be self-escaped
* (e.g., '%%').
* @param {string} message Text which might contain string table references and
* interpolation tokens.
* @return {!Array.<string|number>} Array of strings and numbers.
*/
Blockly.utils.tokenizeInterpolation = function(message) {
return Blockly.utils.tokenizeInterpolation_(message, true);
};
/**
* Replaces string table references in a message string. For example,
* %{bky_my_msg} and %{BKY_MY_MSG} will both be replaced with the value in
* Blockly.Msg['MY_MSG'].
* @param {string} message Text which might contain string table references.
* @return {!string} String with message references replaced.
*/
Blockly.utils.replaceMessageReferences = function(message) {
var interpolatedResult = Blockly.utils.tokenizeInterpolation_(message, false);
// When parseInterpolationTokens == false, interpolatedResult should be at
// most length 1.
return interpolatedResult.length ? interpolatedResult[0] : "";
};
/**
* Internal implemention of the message reference and interpolation token
* parsing used by tokenizeInterpolation() and replaceMessageReferences().
* @param {string} message Text which might contain string table references and
* interpolation tokens.
* @param {boolean} parseInterpolationTokens Option to parse numeric
* interpolation tokens (%1, %2, ...) when true.
* @return {!Array.<string|number>} Array of strings and numbers.
* @private
*/
Blockly.utils.tokenizeInterpolation_ = function(message, parseInterpolationTokens) {
var tokens = [];
var chars = message.split('');
chars.push(''); // End marker.
@ -553,6 +441,7 @@ Blockly.utils.tokenizeInterpolation = function(message) {
// 0 - Base case.
// 1 - % found.
// 2 - Digit found.
// 3 - Message ref found
var state = 0;
var buffer = [];
var number = null;
@ -560,6 +449,11 @@ Blockly.utils.tokenizeInterpolation = function(message) {
var c = chars[i];
if (state == 0) {
if (c == '%') {
var text = buffer.join('');
if (text) {
tokens.push(text);
}
buffer.length = 0;
state = 1; // Start escape.
} else {
buffer.push(c); // Regular char.
@ -568,7 +462,7 @@ Blockly.utils.tokenizeInterpolation = function(message) {
if (c == '%') {
buffer.push(c); // Escaped %: %%
state = 0;
} else if ('0' <= c && c <= '9') {
} else if (parseInterpolationTokens && '0' <= c && c <= '9') {
state = 2;
number = c;
var text = buffer.join('');
@ -576,8 +470,10 @@ Blockly.utils.tokenizeInterpolation = function(message) {
tokens.push(text);
}
buffer.length = 0;
} else if (c == '{') {
state = 3;
} else {
buffer.push('%', c); // Not an escape: %a
buffer.push('%', c); // Not recognized. Return as literal.
state = 0;
}
} else if (state == 2) {
@ -588,13 +484,70 @@ Blockly.utils.tokenizeInterpolation = function(message) {
i--; // Parse this char again.
state = 0;
}
} else if (state == 3) { // String table reference
if (c == '') {
// Premature end before closing '}'
buffer.splice(0, 0, '%{'); // Re-insert leading delimiter
i--; // Parse this char again.
state = 0; // and parse as string literal.
} else if (c != '}') {
buffer.push(c);
} else {
var rawKey = buffer.join('');
if (/[a-zA-Z][a-zA-Z0-9_]*/.test(rawKey)) { // Strict matching
// Found a valid string key. Attempt case insensitive match.
var keyUpper = rawKey.toUpperCase();
// BKY_ is the prefix used to namespace the strings used in Blockly
// core files and the predefined blocks in ../blocks/. These strings
// are defined in ../msgs/ files.
var bklyKey = goog.string.startsWith(keyUpper, 'BKY_') ?
keyUpper.substring(4) : null;
if (bklyKey && bklyKey in Blockly.Msg) {
var rawValue = Blockly.Msg[bklyKey];
var subTokens = Blockly.utils.tokenizeInterpolation(rawValue);
tokens = tokens.concat(subTokens);
} else {
// No entry found in the string table. Pass reference as string.
tokens.push('%{' + rawKey + '}');
}
buffer.length = 0; // Clear the array
state = 0;
} else {
tokens.push('%{' + rawKey + '}');
buffer.length = 0;
state = 0; // and parse as string literal.
}
}
}
}
var text = buffer.join('');
if (text) {
tokens.push(text);
}
return tokens;
// Merge adjacent text tokens into a single string.
var mergedTokens = [];
buffer.length = 0;
for (var i = 0; i < tokens.length; ++i) {
if (typeof tokens[i] == 'string') {
buffer.push(tokens[i]);
} else {
text = buffer.join('');
if (text) {
mergedTokens.push(text);
}
buffer.length = 0;
mergedTokens.push(tokens[i]);
}
}
text = buffer.join('');
if (text) {
mergedTokens.push(text);
}
buffer.length = 0;
return mergedTokens;
};
/**
@ -602,12 +555,12 @@ Blockly.utils.tokenizeInterpolation = function(message) {
* 87 characters ^ 20 length > 128 bits (better than a UUID).
* @return {string} A globally unique ID string.
*/
Blockly.genUid = function() {
Blockly.utils.genUid = function() {
var length = 20;
var soupLength = Blockly.genUid.soup_.length;
var soupLength = Blockly.utils.genUid.soup_.length;
var id = [];
for (var i = 0; i < length; i++) {
id[i] = Blockly.genUid.soup_.charAt(Math.random() * soupLength);
id[i] = Blockly.utils.genUid.soup_.charAt(Math.random() * soupLength);
}
return id.join('');
};
@ -619,7 +572,7 @@ Blockly.genUid = function() {
* to properly escape in your own environment. Issues #251, #625, #682.
* @private
*/
Blockly.genUid.soup_ = '!#$%()*+,-./:;=?@[]^_`{|}~' +
Blockly.utils.genUid.soup_ = '!#$%()*+,-./:;=?@[]^_`{|}~' +
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
/**
@ -631,7 +584,7 @@ Blockly.genUid.soup_ = '!#$%()*+,-./:;=?@[]^_`{|}~' +
Blockly.utils.wrap = function(text, limit) {
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
lines[i] = Blockly.utils.wrap_line_(lines[i], limit);
lines[i] = Blockly.utils.wrapLine_(lines[i], limit);
}
return lines.join('\n');
};
@ -643,7 +596,7 @@ Blockly.utils.wrap = function(text, limit) {
* @return {string} Wrapped text.
* @private
*/
Blockly.utils.wrap_line_ = function(text, limit) {
Blockly.utils.wrapLine_ = function(text, limit) {
if (text.length <= limit) {
// Short text, no need to wrap.
return text;
@ -817,3 +770,73 @@ Blockly.encodeEntities = function(rawStr) {
return '&#' + i.charCodeAt(0) + ';';
});
};
/**
* Check if 3D transforms are supported by adding an element
* and attempting to set the property.
* @return {boolean} true if 3D transforms are supported.
*/
Blockly.utils.is3dSupported = function() {
if (Blockly.utils.is3dSupported.cached_ !== undefined) {
return Blockly.utils.is3dSupported.cached_;
}
// CC-BY-SA Lorenzo Polidori
// stackoverflow.com/questions/5661671/detecting-transform-translate3d-support
if (!goog.global.getComputedStyle) {
return false;
}
var el = document.createElement('p');
var has3d = 'none';
var transforms = {
'webkitTransform': '-webkit-transform',
'OTransform': '-o-transform',
'msTransform': '-ms-transform',
'MozTransform': '-moz-transform',
'transform': 'transform'
};
// Add it to the body to get the computed style.
document.body.insertBefore(el, null);
for (var t in transforms) {
if (el.style[t] !== undefined) {
el.style[t] = 'translate3d(1px,1px,1px)';
var computedStyle = goog.global.getComputedStyle(el);
if (!computedStyle) {
// getComputedStyle in Firefox returns null when blockly is loaded
// inside an iframe with display: none. Returning false and not
// caching is3dSupported means we try again later. This is most likely
// when users are interacting with blocks which should mean blockly is
// visible again.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=548397
document.body.removeChild(el);
return false;
}
has3d = computedStyle.getPropertyValue(transforms[t]);
}
}
document.body.removeChild(el);
Blockly.utils.is3dSupported.cached_ = has3d !== 'none';
return Blockly.utils.is3dSupported.cached_;
};
/**
* Insert a node after a reference node.
* Contrast with node.insertBefore function.
* @param {!Element} newNode New element to insert.
* @param {!Element} refNode Existing element to precede new node.
* @private
*/
Blockly.utils.insertAfter_ = function(newNode, refNode) {
var siblingNode = refNode.nextSibling;
var parentNode = refNode.parentNode;
if (!parentNode) {
throw 'Reference node has no parent.';
}
if (siblingNode) {
parentNode.insertBefore(newNode, siblingNode);
} else {
parentNode.appendChild(newNode);
}
};

View file

@ -106,7 +106,7 @@ Blockly.Variables.flyoutCategory = function(workspace) {
button.setAttribute('text', Blockly.Msg.NEW_VARIABLE);
button.setAttribute('callbackKey', 'CREATE_VARIABLE');
Blockly.registerButtonCallback('CREATE_VARIABLE', function(button) {
workspace.registerButtonCallback('CREATE_VARIABLE', function(button) {
Blockly.Variables.createVariable(button.getTargetWorkspace());
});

View file

@ -56,19 +56,19 @@ Blockly.Warning.prototype.collapseHidden = false;
*/
Blockly.Warning.prototype.drawIcon_ = function(group) {
// Triangle with rounded corners.
Blockly.createSvgElement('path',
Blockly.utils.createSvgElement('path',
{'class': 'blocklyIconShape',
'd': 'M2,15Q-1,15 0.5,12L6.5,1.7Q8,-1 9.5,1.7L15.5,12Q17,15 14,15z'},
group);
// Can't use a real '!' text character since different browsers and operating
// systems render it differently.
// Body of exclamation point.
Blockly.createSvgElement('path',
Blockly.utils.createSvgElement('path',
{'class': 'blocklyIconSymbol',
'd': 'm7,4.8v3.16l0.27,2.27h1.46l0.27,-2.27v-3.16z'},
group);
// Dot of exclamation point.
Blockly.createSvgElement('rect',
Blockly.utils.createSvgElement('rect',
{'class': 'blocklyIconSymbol',
'x': '7', 'y': '11', 'height': '2', 'width': '2'},
group);
@ -82,13 +82,13 @@ Blockly.Warning.prototype.drawIcon_ = function(group) {
*/
Blockly.Warning.textToDom_ = function(text) {
var paragraph = /** @type {!SVGTextElement} */ (
Blockly.createSvgElement('text',
Blockly.utils.createSvgElement('text',
{'class': 'blocklyText blocklyBubbleText',
'y': Blockly.Bubble.BORDER_WIDTH},
null));
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var tspanElement = Blockly.createSvgElement('tspan',
var tspanElement = Blockly.utils.createSvgElement('tspan',
{'dy': '1em', 'x': Blockly.Bubble.BORDER_WIDTH}, paragraph);
var textNode = document.createTextNode(lines[i]);
tspanElement.appendChild(textNode);

View file

@ -38,7 +38,7 @@ goog.require('goog.math');
*/
Blockly.Workspace = function(opt_options) {
/** @type {string} */
this.id = Blockly.genUid();
this.id = Blockly.utils.genUid();
Blockly.Workspace.WorkspaceDB_[this.id] = this;
/** @type {!Blockly.Options} */
this.options = opt_options || {};
@ -313,45 +313,46 @@ Blockly.Workspace.prototype.getVariableUses = function(name) {
* @param {string} name Name of variable to delete.
*/
Blockly.Workspace.prototype.deleteVariable = function(name) {
var variableIndex = this.variableIndexOf(name);
if (variableIndex == -1) {
return;
}
// Check whether this variable is a function parameter before deleting.
var uses = this.getVariableUses(name);
for (var i = 0, block; block = uses[i]; i++) {
if (block.type == 'procedures_defnoreturn' ||
block.type == 'procedures_defreturn') {
var procedureName = block.getFieldValue('NAME');
Blockly.alert(
Blockly.Msg.CANNOT_DELETE_VARIABLE_PROCEDURE.
replace('%1', name).
replace('%2', procedureName));
return;
}
}
var workspace = this;
function doDeletion(variableUses, index) {
function doDeletion() {
Blockly.Events.setGroup(true);
for (var i = 0; i < variableUses.length; i++) {
variableUses[i].dispose(true, false);
for (var i = 0; i < uses.length; i++) {
uses[i].dispose(true, false);
}
Blockly.Events.setGroup(false);
workspace.variableList.splice(index, 1);
workspace.variableList.splice(variableIndex, 1);
}
var variableIndex = this.variableIndexOf(name);
if (variableIndex != -1) {
// Check whether this variable is a function parameter before deleting.
var uses = this.getVariableUses(name);
for (var i = 0, block; block = uses[i]; i++) {
if (block.type == 'procedures_defnoreturn' ||
block.type == 'procedures_defreturn') {
var procedureName = block.getFieldValue('NAME');
Blockly.alert(
Blockly.Msg.CANNOT_DELETE_VARIABLE_PROCEDURE.
replace('%1', name).
replace('%2', procedureName));
return;
}
}
if (uses.length > 1) {
// Confirm before deleting multiple blocks.
Blockly.confirm(
Blockly.Msg.DELETE_VARIABLE_CONFIRMATION.replace('%1', uses.length).
replace('%2', name),
function(ok) {
if (ok) {
doDeletion(uses, variableIndex);
}
});
} else {
// No confirmation necessary for a single block.
doDeletion();
}
if (uses.length > 1) {
// Confirm before deleting multiple blocks.
Blockly.confirm(
Blockly.Msg.DELETE_VARIABLE_CONFIRMATION.replace('%1', uses.length).
replace('%2', name),
function(ok) {
if (ok) {
doDeletion();
}
});
} else {
// No confirmation necessary for a single block.
doDeletion();
}
};

View file

@ -0,0 +1,191 @@
/**
* @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 An SVG that floats on top of the workspace.
* Blocks are moved into this SVG during a drag, improving performance.
* The entire SVG is translated using css translation instead of SVG so the
* blocks are never repainted during drag improving performance.
* @author katelyn@google.com (Katelyn Mann)
*/
'use strict';
goog.provide('Blockly.WorkspaceDragSurfaceSvg');
goog.require('Blockly.utils');
goog.require('goog.asserts');
goog.require('goog.math.Coordinate');
/**
* Blocks are moved into this SVG during a drag, improving performance.
* The entire SVG is translated using css transforms instead of SVG so the
* blocks are never repainted during drag improving performance.
* @param {!Element} container Containing element.
* @constructor
*/
Blockly.workspaceDragSurfaceSvg = function(container) {
this.container_ = container;
this.createDom();
};
/**
* The SVG drag surface. Set once by Blockly.workspaceDragSurfaceSvg.createDom.
* @type {Element}
* @private
*/
Blockly.workspaceDragSurfaceSvg.prototype.SVG_ = null;
/**
* SVG group inside the drag surface that holds blocks while a drag is in
* progress. Blocks are moved here by the workspace at start of a drag and moved
* back into the main SVG at the end of a drag.
*
* @type {Element}
* @private
*/
Blockly.workspaceDragSurfaceSvg.prototype.dragGroup_ = null;
/**
* Containing HTML element; parent of the workspace and the drag surface.
* @type {Element}
* @private
*/
Blockly.workspaceDragSurfaceSvg.prototype.container_ = null;
/**
* Create the drag surface and inject it into the container.
*/
Blockly.workspaceDragSurfaceSvg.prototype.createDom = function() {
if (this.SVG_) {
return; // Already created.
}
/**
* Dom structure when the workspace is being dragged. If there is no drag in
* progress, the SVG is empty and display: none.
* <svg class="blocklyWsDragSurface" style=transform:translate3d(...)>
* <g class="blocklyBlockCanvas"></g>
* <g class="blocklyBubbleCanvas">/g>
* </svg>
*/
this.SVG_ = Blockly.utils.createSvgElement('svg', {
'xmlns': Blockly.SVG_NS,
'xmlns:html': Blockly.HTML_NS,
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
'version': '1.1',
'class': 'blocklyWsDragSurface'
}, null);
this.container_.appendChild(this.SVG_);
};
/**
* Translate the entire drag surface during a drag.
* We translate the drag surface instead of the blocks inside the surface
* so that the browser avoids repainting the SVG.
* Because of this, the drag coordinates must be adjusted by scale.
* @param {number} x X translation for the entire surface
* @param {number} y Y translation for the entire surface
* @package
*/
Blockly.workspaceDragSurfaceSvg.prototype.translateSurface = function(x, y) {
// This is a work-around to prevent a the blocks from rendering
// fuzzy while they are being moved on the drag surface.
x = x.toFixed(0);
y = y.toFixed(0);
var transform =
'transform: translate3d(' + x + 'px, ' + y + 'px, 0px); display: block;';
this.SVG_.setAttribute('style', transform);
};
/**
* Reports the surface translation in scaled workspace coordinates.
* Use this when finishing a drag to return blocks to the correct position.
* @return {!goog.math.Coordinate} Current translation of the surface
* @package
*/
Blockly.workspaceDragSurfaceSvg.prototype.getSurfaceTranslation = function() {
return Blockly.utils.getRelativeXY(this.SVG_);
};
/**
* Move the blockCanvas and bubbleCanvas out of the surface SVG and on to
* newSurface.
* @param {!SVGElement} newSurface The element to put the drag surface contents
* into.
* @package
*/
Blockly.workspaceDragSurfaceSvg.prototype.clearAndHide = function(newSurface) {
var blockCanvas = this.SVG_.childNodes[0];
var bubbleCanvas = this.SVG_.childNodes[1];
if (!blockCanvas || !bubbleCanvas ||
!Blockly.utils.hasClass(blockCanvas, 'blocklyBlockCanvas') ||
!Blockly.utils.hasClass(bubbleCanvas, 'blocklyBubbleCanvas')) {
throw 'Couldn\'t clear and hide the drag surface. A node was missing.';
}
// If there is a previous sibling, put the blockCanvas back right afterwards,
// otherwise insert it as the first child node in newSurface.
if (this.previousSibling_ != null) {
Blockly.utils.insertAfter_(blockCanvas, this.previousSibling_);
} else {
newSurface.insertBefore(blockCanvas, newSurface.firstChild);
}
// Reattach the bubble canvas after the blockCanvas.
Blockly.utils.insertAfter_(bubbleCanvas, blockCanvas);
// Hide the drag surface.
this.SVG_.style.display = 'none';
goog.asserts.assert(this.SVG_.childNodes.length == 0,
'Drag surface was not cleared.');
this.SVG_.style.transform = '';
this.previousSibling_ = null;
};
/**
* Set the SVG to have the block canvas and bubble canvas in it and then
* show the surface.
* @param {!Element} blockCanvas The block canvas <g> element from the workspace.
* @param {!Element} bubbleCanvas The <g> element that contains the bubbles.
* @param {?Element} previousSibling The element to insert the block canvas &
bubble canvas after when it goes back in the dom at the end of a drag.
* @param {number} width The width of the workspace svg element.
* @param {number} height The height of the workspace svg element.
* @param {number} scale The scale of the workspace being dragged.
* @package
*/
Blockly.workspaceDragSurfaceSvg.prototype.setContentsAndShow = function(
blockCanvas, bubbleCanvas, previousSibling, width, height, scale) {
goog.asserts.assert(this.SVG_.childNodes.length == 0,
'Already dragging a block.');
this.previousSibling_ = previousSibling;
// Make sure the blocks and bubble canvas are scaled appropriately.
blockCanvas.setAttribute('transform', 'translate(0, 0) scale(' + scale + ')');
bubbleCanvas.setAttribute('transform',
'translate(0, 0) scale(' + scale + ')');
this.SVG_.setAttribute('width', width);
this.SVG_.setAttribute('height', height);
this.SVG_.appendChild(blockCanvas);
this.SVG_.appendChild(bubbleCanvas);
this.SVG_.style.display = 'block';
};

View file

@ -40,6 +40,7 @@ goog.require('Blockly.Touch');
goog.require('Blockly.Trashcan');
//goog.require('Blockly.VerticalFlyout');
goog.require('Blockly.Workspace');
goog.require('Blockly.WorkspaceDragSurfaceSvg');
goog.require('Blockly.Xml');
goog.require('Blockly.ZoomControls');
@ -53,11 +54,14 @@ goog.require('goog.userAgent');
* Class for a workspace. This is an onscreen area with optional trashcan,
* scrollbars, bubbles, and dragging.
* @param {!Blockly.Options} options Dictionary of options.
* @param {Blockly.DragSurfaceSvg=} opt_dragSurface Drag surface for the workspace.
* @param {Blockly.BlockDragSurfaceSvg=} opt_blockDragSurface Drag surface for
* blocks.
* @param {Blockly.workspaceDragSurfaceSvg=} opt_wsDragSurface Drag surface for
* the workspace.
* @extends {Blockly.Workspace}
* @constructor
*/
Blockly.WorkspaceSvg = function(options, opt_dragSurface) {
Blockly.WorkspaceSvg = function(options, opt_blockDragSurface, opt_wsDragSurface) {
Blockly.WorkspaceSvg.superClass_.constructor.call(this, options);
this.getMetrics =
options.getMetrics || Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_;
@ -65,12 +69,17 @@ Blockly.WorkspaceSvg = function(options, opt_dragSurface) {
options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_;
Blockly.ConnectionDB.init(this);
if (opt_dragSurface) {
this.dragSurface = opt_dragSurface;
if (opt_blockDragSurface) {
this.blockDragSurface_ = opt_blockDragSurface;
}
Blockly.ConnectionDB.init(this);
if (opt_wsDragSurface) {
this.workspaceDragSurface_ = opt_wsDragSurface;
}
this.useWorkspaceDragSurface_ =
this.workspaceDragSurface_ && Blockly.utils.is3dSupported();
/**
* Database of pre-loaded sounds.
* @private
@ -78,7 +87,8 @@ Blockly.WorkspaceSvg = function(options, opt_dragSurface) {
*/
this.SOUNDS_ = Object.create(null);
/**
* List of currently highlighted blocks.
* List of currently highlighted blocks. Block highlighting is often used to
* visually mark blocks currently being executed.
* @type !Array.<!Blockly.BlockSvg>
* @private
*/
@ -87,14 +97,16 @@ Blockly.WorkspaceSvg = function(options, opt_dragSurface) {
goog.inherits(Blockly.WorkspaceSvg, Blockly.Workspace);
/**
* A wrapper function called when a resize event occurs. You can pass the result to `unbindEvent_`.
* A wrapper function called when a resize event occurs.
* You can pass the result to `unbindEvent_`.
* @type {Array.<!Array>}
*/
Blockly.WorkspaceSvg.prototype.resizeHandlerWrapper_ = null;
/**
* The render status of an SVG workspace.
* Returns `true` for visible workspaces and `false` for non-visible, or headless, workspaces.
* Returns `true` for visible workspaces and `false` for non-visible,
* or headless, workspaces.
* @type {boolean}
*/
Blockly.WorkspaceSvg.prototype.rendered = true;
@ -179,15 +191,40 @@ Blockly.WorkspaceSvg.prototype.trashcan = null;
Blockly.WorkspaceSvg.prototype.scrollbar = null;
/**
* This workspace's drag surface, if it exists.
* @type {Blockly.DragSurfaceSvg}
* This workspace's surface for dragging blocks, if it exists.
* @type {Blockly.BlockDragSurfaceSvg}
* @private
*/
Blockly.WorkspaceSvg.prototype.dragSurface = null;
Blockly.WorkspaceSvg.prototype.blockDragSurface_ = null;
/**
* Inverted screen CTM, for use in mouseToSvg.
* @type {SVGMatrix}
* This workspace's drag surface, if it exists.
* @type {Blockly.WorkspaceDragSurfaceSvg}
* @private
*/
Blockly.WorkspaceSvg.prototype.workspaceDragSurface_ = null;
/**
* Whether to move workspace to the drag surface when it is dragged.
* True if it should move, false if it should be translated directly.
* @type {boolean}
* @private
*/
Blockly.WorkspaceSvg.prototype.useWorkspaceDragSurface_ = false;
/**
* Whether the drag surface is actively in use. When true, calls to
* translate will translate the drag surface instead of the translating the
* workspace directly.
* This is set to true in setupDragSurface and to false in resetDragSurface.
* @type {boolean}
* @private
*/
Blockly.WorkspaceSvg.prototype.isDragSurfaceActive_ = false;
/**
* Time that the last sound was played.
* @type {Date}
* @private
*/
Blockly.WorkspaceSvg.prototype.inverseScreenCTM_ = null;
@ -224,6 +261,14 @@ Blockly.WorkspaceSvg.prototype.setResizeHandlerWrapper = function(handler) {
*/
Blockly.WorkspaceSvg.prototype.lastRecordedPageScroll_ = null;
/**
* Map from function names to callbacks, for deciding what to do when a button
* is clicked.
* @type {!Object<string, function(!Blockly.FlyoutButton)>}
* @private
*/
Blockly.WorkspaceSvg.prototype.flyoutButtonCallbacks_ = {};
/**
* Inverted screen CTM, for use in mouseToSvg.
* @type {SVGMatrix}
@ -249,6 +294,38 @@ Blockly.WorkspaceSvg.prototype.updateInverseScreenCTM = function() {
}
};
/**
* Return the absolute coordinates of the top-left corner of this element,
* scales that after canvas SVG element, if it's a descendant.
* The origin (0,0) is the top-left corner of the Blockly SVG.
* @param {!Element} element Element to find the coordinates of.
* @return {!goog.math.Coordinate} Object with .x and .y properties.
* @private
*/
Blockly.WorkspaceSvg.prototype.getSvgXY = function(element) {
var x = 0;
var y = 0;
var scale = 1;
if (goog.dom.contains(this.getCanvas(), element) ||
goog.dom.contains(this.getBubbleCanvas(), element)) {
// Before the SVG canvas, scale the coordinates.
scale = this.scale;
}
do {
// Loop through this block and every parent.
var xy = Blockly.utils.getRelativeXY(element);
if (element == this.getCanvas() ||
element == this.getBubbleCanvas()) {
// After the SVG canvas, don't scale the coordinates.
scale = 1;
}
x += xy.x * scale;
y += xy.y * scale;
element = element.parentNode;
} while (element && element != this.getParentSvg());
return new goog.math.Coordinate(x, y);
};
/**
* Save resize handler data so we can delete it later in dispose.
* @param {!Array.<!Array>} handler Data that can be passed to unbindEvent_.
@ -274,11 +351,11 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
* </g>
* @type {SVGElement}
*/
this.svgGroup_ = Blockly.createSvgElement('g',
this.svgGroup_ = Blockly.utils.createSvgElement('g',
{'class': 'blocklyWorkspace'}, null);
if (opt_backgroundClass) {
/** @type {SVGElement} */
this.svgBackground_ = Blockly.createSvgElement('rect',
this.svgBackground_ = Blockly.utils.createSvgElement('rect',
{'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
this.svgGroup_);
if (opt_backgroundClass == 'blocklyMainBackground') {
@ -287,10 +364,10 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
}
}
/** @type {SVGElement} */
this.svgBlockCanvas_ = Blockly.createSvgElement('g',
this.svgBlockCanvas_ = Blockly.utils.createSvgElement('g',
{'class': 'blocklyBlockCanvas'}, this.svgGroup_, this);
/** @type {SVGElement} */
this.svgBubbleCanvas_ = Blockly.createSvgElement('g',
this.svgBubbleCanvas_ = Blockly.utils.createSvgElement('g',
{'class': 'blocklyBubbleCanvas'}, this.svgGroup_, this);
var bottom = Blockly.Scrollbar.scrollbarThickness;
if (this.options.hasTrashcan) {
@ -321,8 +398,6 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
* @private
*/
this.toolbox_ = new Blockly.Toolbox(this);
} else if (this.options.languageTree) {
this.addFlyout_();
}
this.updateGridPattern_();
this.updateStackGlowScale_();
@ -416,10 +491,12 @@ Blockly.WorkspaceSvg.prototype.addZoomControls_ = function(bottom) {
};
/**
* Add a flyout.
* Add a flyout element in an element with the given tag name.
* @param {string} tagName What type of tag the flyout belongs in.
* @return {!Element} The element containing the flyout dom.
* @private
*/
Blockly.WorkspaceSvg.prototype.addFlyout_ = function() {
Blockly.WorkspaceSvg.prototype.addFlyout_ = function(tagName) {
var workspaceOptions = {
disabledPatternId: this.options.disabledPatternId,
parentWorkspace: this,
@ -434,8 +511,28 @@ Blockly.WorkspaceSvg.prototype.addFlyout_ = function() {
this.flyout_ = new Blockly.VerticalFlyout(workspaceOptions);
}
this.flyout_.autoClose = false;
var svgFlyout = this.flyout_.createDom();
this.svgGroup_.insertBefore(svgFlyout, this.svgBlockCanvas_);
// Return the element so that callers can place it in their desired
// spot in the dom. For exmaple, mutator flyouts do not go in the same place
// as main workspace flyouts.
return this.flyout_.createDom(tagName);
};
/**
* Getter for the flyout associated with this workspace. This flyout may be
* owned by either the toolbox or the workspace, depending on toolbox
* configuration. It will be null if there is no flyout.
* @return {Blockly.Flyout} The flyout on this workspace.
* @package
*/
Blockly.WorkspaceSvg.prototype.getFlyout_ = function() {
if (this.flyout_) {
return this.flyout_;
}
if (this.toolbox_) {
return this.toolbox_.flyout_;
}
return null;
};
/**
@ -455,7 +552,7 @@ Blockly.WorkspaceSvg.prototype.updateScreenCalculations_ = function() {
* @package
*/
Blockly.WorkspaceSvg.prototype.resizeContents = function() {
if (!this.resizesEnabled_) {
if (!this.resizesEnabled_ || !this.rendered) {
return;
}
if (this.scrollbar) {
@ -564,13 +661,65 @@ Blockly.WorkspaceSvg.prototype.getFlyout = function() {
* @param {number} y Vertical translation.
*/
Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
var translation = 'translate(' + x + ',' + y + ') ' +
'scale(' + this.scale + ')';
if (this.useWorkspaceDragSurface_ && this.isDragSurfaceActive_) {
this.workspaceDragSurface_.translateSurface(x,y);
} else {
var translation = 'translate(' + x + ',' + y + ') ' +
'scale(' + this.scale + ')';
this.svgBlockCanvas_.setAttribute('transform', translation);
this.svgBubbleCanvas_.setAttribute('transform', translation);
}
// Now update the block drag surface if we're using one.
if (this.blockDragSurface_) {
this.blockDragSurface_.translateAndScaleGroup(x, y, this.scale);
}
};
/**
* Called at the end of a workspace drag to take the contents
* out of the drag surface and put them back into the workspace svg.
* Does nothing if the workspace drag surface is not enabled.
* @package
*/
Blockly.WorkspaceSvg.prototype.resetDragSurface = function() {
// Don't do anything if we aren't using a drag surface.
if (!this.useWorkspaceDragSurface_) {
return;
}
this.isDragSurfaceActive_ = false;
var trans = this.workspaceDragSurface_.getSurfaceTranslation();
this.workspaceDragSurface_.clearAndHide(this.svgGroup_);
var translation = 'translate(' + trans.x + ',' + trans.y + ') ' +
'scale(' + this.scale + ')';
this.svgBlockCanvas_.setAttribute('transform', translation);
this.svgBubbleCanvas_.setAttribute('transform', translation);
if (this.dragSurface) {
this.dragSurface.translateAndScaleGroup(x, y, this.scale);
};
/**
* Called at the beginning of a workspace drag to move contents of
* the workspace to the drag surface.
* Does nothing if the drag surface is not enabled.
* @package
*/
Blockly.WorkspaceSvg.prototype.setupDragSurface = function() {
// Don't do anything if we aren't using a drag surface.
if (!this.useWorkspaceDragSurface_) {
return;
}
this.isDragSurfaceActive_ = true;
// Figure out where we want to put the canvas back. The order
// in the is important because things are layered.
var previousElement = this.svgBlockCanvas_.previousSibling;
var width = this.getParentSvg().getAttribute("width");
var height = this.getParentSvg().getAttribute("height");
var coord = Blockly.utils.getRelativeXY(this.svgBlockCanvas_);
this.workspaceDragSurface_.setContentsAndShow(this.svgBlockCanvas_,
this.svgBubbleCanvas_, previousElement, width, height, this.scale);
this.workspaceDragSurface_.translateSurface(coord.x, coord.y);
};
/**
@ -589,6 +738,19 @@ Blockly.WorkspaceSvg.prototype.getWidth = function() {
* @param {boolean} isVisible True if workspace should be visible.
*/
Blockly.WorkspaceSvg.prototype.setVisible = function(isVisible) {
// Tell the scrollbar whether its container is visible so it can
// tell when to hide itself.
if (this.scrollbar) {
this.scrollbar.setContainerVisible(isVisible);
}
// Tell the flyout whether its container is visible so it can
// tell when to hide itself.
if (this.getFlyout_()) {
this.getFlyout_().setContainerVisible(isVisible);
}
this.getParentSvg().style.display = isVisible ? 'block' : 'none';
if (this.toolbox_) {
// Currently does not support toolboxes in mutators.
@ -628,7 +790,8 @@ Blockly.WorkspaceSvg.prototype.traceOn = function() {
};
/**
* Highlight or unhighlight a block in the workspace.
* Highlight or unhighlight a block in the workspace. Block highlighting is
* often used to visually mark blocks currently being executed.
* @param {?string} id ID of block to highlight/unhighlight,
* or null for no block (used to unhighlight all blocks).
* @param {boolean=} opt_state If undefined, highlight specified block and
@ -814,26 +977,18 @@ Blockly.WorkspaceSvg.prototype.recordDeleteAreas = function() {
* Is the mouse event over a delete area (toolbox or non-closing flyout)?
* Opens or closes the trashcan and sets the cursor as a side effect.
* @param {!Event} e Mouse move event.
* @return {boolean} True if event is in a delete area.
* @return {?number} Null if not over a delete area, or an enum representing
* which delete area the event is over.
*/
Blockly.WorkspaceSvg.prototype.isDeleteArea = function(e) {
var xy = new goog.math.Coordinate(e.clientX, e.clientY);
if (this.deleteAreaTrash_) {
if (this.deleteAreaTrash_.contains(xy)) {
this.trashcan.setOpen_(true);
Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE);
return true;
}
this.trashcan.setOpen_(false);
if (this.deleteAreaTrash_ && this.deleteAreaTrash_.contains(xy)) {
return Blockly.DELETE_AREA_TRASH;
}
if (this.deleteAreaToolbox_) {
if (this.deleteAreaToolbox_.contains(xy)) {
Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE);
return true;
}
if (this.deleteAreaToolbox_ && this.deleteAreaToolbox_.contains(xy)) {
return Blockly.DELETE_AREA_TOOLBOX;
}
Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
return false;
return null;
};
/**
@ -843,7 +998,7 @@ Blockly.WorkspaceSvg.prototype.isDeleteArea = function(e) {
*/
Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
this.markFocused();
if (Blockly.isTargetInput_(e)) {
if (Blockly.utils.isTargetInput(e)) {
Blockly.Touch.clearTouchIdentifier();
return;
}
@ -857,9 +1012,13 @@ Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
// Clicking on the document clears the selection.
Blockly.selected.unselect();
}
if (Blockly.isRightButton(e)) {
if (Blockly.utils.isRightButton(e)) {
// Right-click.
this.showContextMenu_(e);
// This is to handle the case where the event is pretending to be a right
// click event but it was really a long press. In that case, we want to make
// sure any in progress drags are stopped.
Blockly.onMouseUp_(e);
// Since this was a click, not a drag, end the gesture immediately.
Blockly.Touch.clearTouchIdentifier();
} else if (this.scrollbar) {
@ -871,6 +1030,7 @@ Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
this.startScrollX = this.scrollX;
this.startScrollY = this.scrollY;
this.setupDragSurface();
// If this is a touch event then bind to the mouseup so workspace drag mode
// is turned off and double move events are not performed on a block.
// See comment in inject.js Blockly.init_ as to why mouseup events are
@ -901,7 +1061,7 @@ Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
*/
Blockly.WorkspaceSvg.prototype.startDrag = function(e, xy) {
// Record the starting offset between the bubble's location and the mouse.
var point = Blockly.mouseToSvg(e, this.getParentSvg(),
var point = Blockly.utils.mouseToSvg(e, this.getParentSvg(),
this.getInverseScreenCTM());
// Fix scale of mouse event.
point.x /= this.scale;
@ -915,7 +1075,7 @@ Blockly.WorkspaceSvg.prototype.startDrag = function(e, xy) {
* @return {!goog.math.Coordinate} New location of object.
*/
Blockly.WorkspaceSvg.prototype.moveDrag = function(e) {
var point = Blockly.mouseToSvg(e, this.getParentSvg(),
var point = Blockly.utils.mouseToSvg(e, this.getParentSvg(),
this.getInverseScreenCTM());
// Fix scale of mouse event.
point.x /= this.scale;
@ -942,10 +1102,11 @@ Blockly.WorkspaceSvg.prototype.isDragging = function() {
Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
// TODO: Remove terminateDrag and compensate for coordinate skew during zoom.
if (e.ctrlKey) {
// Pinch-to-zoom in Chrome only
Blockly.terminateDrag_();
var delta = e.deltaY > 0 ? -1 : 1;
var position = Blockly.mouseToSvg(e, this.getParentSvg(),
// The vertical scroll distance that corresponds to a click of a zoom button.
var PIXELS_PER_ZOOM_STEP = 50;
var delta = -e.deltaY / PIXELS_PER_ZOOM_STEP;
var position = Blockly.utils.mouseToSvg(e, this.getParentSvg(),
this.getInverseScreenCTM());
this.zoom(position.x, position.y, delta);
} else {
@ -1032,7 +1193,7 @@ Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
}
var menuOptions = [];
var topBlocks = this.getTopBlocks(true);
var eventGroup = Blockly.genUid();
var eventGroup = Blockly.utils.genUid();
// Options to undo/redo previous action.
var undoOption = {};
@ -1302,9 +1463,10 @@ Blockly.WorkspaceSvg.prototype.markFocused = function() {
* Zooming the blocks centered in (x, y) coordinate with zooming in or out.
* @param {number} x X coordinate of center.
* @param {number} y Y coordinate of center.
* @param {number} type Type of zooming (-1 zooming out and 1 zooming in).
* @param {number} amount Amount of zooming
* (negative zooms out and positive zooms in).
*/
Blockly.WorkspaceSvg.prototype.zoom = function(x, y, type) {
Blockly.WorkspaceSvg.prototype.zoom = function(x, y, amount) {
var speed = this.options.zoomOptions.scaleSpeed;
var metrics = this.getMetrics();
var center = this.getParentSvg().createSVGPoint();
@ -1315,7 +1477,7 @@ Blockly.WorkspaceSvg.prototype.zoom = function(x, y, type) {
y = center.y;
var canvas = this.getCanvas();
// Scale factor.
var scaleChange = (type == 1) ? speed : 1 / speed;
var scaleChange = Math.pow(speed, amount);
// Clamp scale within valid range.
var newScale = this.scale * scaleChange;
if (newScale > this.options.zoomOptions.maxScale) {
@ -1368,7 +1530,7 @@ Blockly.WorkspaceSvg.prototype.zoomToFit = function() {
workspaceWidth -= this.flyout_.width_;
}
if (!this.scrollbar) {
// Orgin point of 0,0 is fixed, blocks will not scroll to center.
// Origin point of 0,0 is fixed, blocks will not scroll to center.
blocksWidth += metrics.contentLeft;
blocksHeight += metrics.contentTop;
}
@ -1617,8 +1779,9 @@ Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) {
if (this.options.gridPattern) {
this.options.gridPattern.setAttribute('x', x);
this.options.gridPattern.setAttribute('y', y);
if (goog.userAgent.IE) {
// IE doesn't notice that the x/y offsets have changed. Force an update.
if (goog.userAgent.IE || goog.userAgent.EDGE) {
// IE/Edge doesn't notice that the x/y offsets have changed.
// Force an update.
this.updateGridPattern_();
}
}
@ -1649,6 +1812,32 @@ Blockly.WorkspaceSvg.prototype.clear = function() {
this.setResizesEnabled(true);
};
/**
* Register a callback function associated with a given key, for clicks on
* buttons and labels in the flyout.
* For instance, a button specified by the XML
* <button text="create variable" callbackKey="CREATE_VARIABLE"></button>
* should be matched by a call to
* registerButtonCallback("CREATE_VARIABLE", yourCallbackFunction).
* @param {string} key The name to use to look up this function.
* @param {function(!Blockly.FlyoutButton)} func The function to call when the
* given button is clicked.
*/
Blockly.WorkspaceSvg.prototype.registerButtonCallback = function(key, func) {
this.flyoutButtonCallbacks_[key] = func;
};
/**
* Get the callback function associated with a given key, for clicks on buttons
* and labels in the flyout.
* @param {string} key The name to use to look up the function.
* @return {function(!Blockly.FlyoutButton)} The function corresponding to the
* given key for this workspace.
*/
Blockly.WorkspaceSvg.prototype.getButtonCallback = function(key) {
return this.flyoutButtonCallbacks_[key];
};
// Export symbols that would otherwise be renamed by Closure compiler.
Blockly.WorkspaceSvg.prototype['setVisible'] =
Blockly.WorkspaceSvg.prototype.setVisible;

View file

@ -362,7 +362,7 @@ Blockly.Xml.domToBlock = function(xmlBlock, workspace) {
for (var i = blocks.length - 1; i >= 0; i--) {
blocks[i].render(false);
}
// Populating the connection database may be defered until after the
// Populating the connection database may be deferred until after the
// blocks have rendered.
if (!workspace.isFlyout) {
setTimeout(function() {
@ -434,7 +434,7 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) {
if (block.domToMutation) {
block.domToMutation(xmlChild);
if (block.initSvg) {
// Mutation may have added some elements that need initalizing.
// Mutation may have added some elements that need initializing.
block.initSvg();
}
}

View file

@ -113,17 +113,17 @@ Blockly.ZoomControls.prototype.createDom = function() {
clip-path="url(#blocklyZoomresetClipPath837493)"></image>
</g>
*/
this.svgGroup_ = Blockly.createSvgElement('g',
this.svgGroup_ = Blockly.utils.createSvgElement('g',
{'class': 'blocklyZoom'}, null);
var rnd = String(Math.random()).substring(2);
var clip = Blockly.createSvgElement('clipPath',
var clip = Blockly.utils.createSvgElement('clipPath',
{'id': 'blocklyZoomoutClipPath' + rnd},
this.svgGroup_);
Blockly.createSvgElement('rect',
Blockly.utils.createSvgElement('rect',
{'width': 32, 'height': 32, 'y': 77},
clip);
var zoomoutSvg = Blockly.createSvgElement('image',
var zoomoutSvg = Blockly.utils.createSvgElement('image',
{'width': Blockly.SPRITE.width,
'height': Blockly.SPRITE.height, 'x': -64,
'y': -15,
@ -132,13 +132,13 @@ Blockly.ZoomControls.prototype.createDom = function() {
zoomoutSvg.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
workspace.options.pathToMedia + Blockly.SPRITE.url);
var clip = Blockly.createSvgElement('clipPath',
var clip = Blockly.utils.createSvgElement('clipPath',
{'id': 'blocklyZoominClipPath' + rnd},
this.svgGroup_);
Blockly.createSvgElement('rect',
Blockly.utils.createSvgElement('rect',
{'width': 32, 'height': 32, 'y': 43},
clip);
var zoominSvg = Blockly.createSvgElement('image',
var zoominSvg = Blockly.utils.createSvgElement('image',
{'width': Blockly.SPRITE.width,
'height': Blockly.SPRITE.height,
'x': -32,
@ -148,13 +148,13 @@ Blockly.ZoomControls.prototype.createDom = function() {
zoominSvg.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
workspace.options.pathToMedia + Blockly.SPRITE.url);
var clip = Blockly.createSvgElement('clipPath',
var clip = Blockly.utils.createSvgElement('clipPath',
{'id': 'blocklyZoomresetClipPath' + rnd},
this.svgGroup_);
Blockly.createSvgElement('rect',
Blockly.utils.createSvgElement('rect',
{'width': 32, 'height': 32},
clip);
var zoomresetSvg = Blockly.createSvgElement('image',
var zoomresetSvg = Blockly.utils.createSvgElement('image',
{'width': Blockly.SPRITE.width,
'height': Blockly.SPRITE.height, 'y': -92,
'clip-path': 'url(#blocklyZoomresetClipPath' + rnd + ')'},
@ -167,21 +167,21 @@ Blockly.ZoomControls.prototype.createDom = function() {
workspace.markFocused();
workspace.setScale(workspace.options.zoomOptions.startScale);
workspace.scrollCenter();
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
e.stopPropagation(); // Don't start a workspace scroll.
e.preventDefault(); // Stop double-clicking from selecting text.
});
Blockly.bindEventWithChecks_(zoominSvg, 'mousedown', null, function(e) {
workspace.markFocused();
workspace.zoomCenter(1);
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
e.stopPropagation(); // Don't start a workspace scroll.
e.preventDefault(); // Stop double-clicking from selecting text.
});
Blockly.bindEventWithChecks_(zoomoutSvg, 'mousedown', null, function(e) {
workspace.markFocused();
workspace.zoomCenter(-1);
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
e.stopPropagation(); // Don't start a workspace scroll.
e.preventDefault(); // Stop double-clicking from selecting text.
});

View file

@ -40,7 +40,6 @@ Blockly.Msg.CONTROLS_FOREACH_TOOLTIP = "For each item in a list, set the variabl
Blockly.Msg.CONTROLS_FOR_HELPURL = "https://github.com/google/blockly/wiki/Loops#count-with";
Blockly.Msg.CONTROLS_FOR_TITLE = "count with %1 from %2 to %3 by %4";
Blockly.Msg.CONTROLS_FOR_TOOLTIP = "Have the variable '%1' take on the values from the start number to the end number, counting by the specified interval, and do the specified blocks.";
Blockly.Msg.CONTROLS_IFELSE_TITLE = "if %1 do %2 else %3";
Blockly.Msg.CONTROLS_IF_ELSEIF_TOOLTIP = "Add a condition to the if block.";
Blockly.Msg.CONTROLS_IF_ELSE_TOOLTIP = "Add a final, catch-all condition to the if block.";
Blockly.Msg.CONTROLS_IF_HELPURL = "https://github.com/google/blockly/wiki/IfElse";

View file

@ -1,7 +1,7 @@
{
"@metadata": {
"author": "Ellen Spertus <ellen.spertus@gmail.com>",
"lastupdated": "2017-01-02 12:17:41.695529",
"lastupdated": "2017-02-01 21:01:32.200551",
"locale": "en",
"messagedocumentation" : "qqq"
},
@ -82,7 +82,6 @@
"CONTROLS_IF_IF_TOOLTIP": "Add, remove, or reorder sections to reconfigure this if block.",
"CONTROLS_IF_ELSEIF_TOOLTIP": "Add a condition to the if block.",
"CONTROLS_IF_ELSE_TOOLTIP": "Add a final, catch-all condition to the if block.",
"CONTROLS_IFELSE_TITLE": "if %1 do %2 else %3",
"LOGIC_COMPARE_HELPURL": "https://en.wikipedia.org/wiki/Inequality_(mathematics)",
"LOGIC_COMPARE_TOOLTIP_EQ": "Return true if both inputs equal each other.",
"LOGIC_COMPARE_TOOLTIP_NEQ": "Return true if both inputs are not equal to each other.",

View file

@ -239,9 +239,6 @@ Blockly.Msg.CONTROLS_IF_ELSE_TITLE_ELSE = Blockly.Msg.CONTROLS_IF_MSG_ELSE;
/// tooltip - Describes the 'else' subblock during [https://github.com/google/blockly/wiki/IfElse#block-modification if block modification].
Blockly.Msg.CONTROLS_IF_ELSE_TOOLTIP = 'Add a final, catch-all condition to the if block.';
/// block text - See [https://github.com/google/blockly/wiki/IfElse https://github.com/google/blockly/wiki/IfElse]. The English word "otherwise" would probably be superior to "else", but the latter is used because it is traditional and shorter.
Blockly.Msg.CONTROLS_IFELSE_TITLE = 'if %1 do %2 else %3';
/// url - Information about comparisons.
Blockly.Msg.LOGIC_COMPARE_HELPURL = 'https://en.wikipedia.org/wiki/Inequality_(mathematics)';
/// tooltip - Describes the equals (=) block.

View file

@ -1,140 +0,0 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2011 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.
*/
'use strict';
function test_genUid() {
var uuids = {};
for (var i = 0; i < 1000; i++) {
var uuid = Blockly.genUid();
assertFalse('UUID different: ' + uuid, uuid in uuids);
uuids[uuid] = true;
}
}
function test_addClass() {
var p = document.createElement('p');
Blockly.addClass_(p, 'one');
assertEquals('Adding "one"', 'one', p.className);
Blockly.addClass_(p, 'one');
assertEquals('Adding duplicate "one"', 'one', p.className);
Blockly.addClass_(p, 'two');
assertEquals('Adding "two"', 'one two', p.className);
Blockly.addClass_(p, 'two');
assertEquals('Adding duplicate "two"', 'one two', p.className);
Blockly.addClass_(p, 'three');
assertEquals('Adding "three"', 'one two three', p.className);
}
function test_removeClass() {
var p = document.createElement('p');
p.className = ' one three two three ';
Blockly.removeClass_(p, 'two');
assertEquals('Removing "two"', 'one three three', p.className);
Blockly.removeClass_(p, 'four');
assertEquals('Removing "four"', 'one three three', p.className);
Blockly.removeClass_(p, 'three');
assertEquals('Removing "three"', 'one', p.className);
Blockly.removeClass_(p, 'ne');
assertEquals('Removing "ne"', 'one', p.className);
Blockly.removeClass_(p, 'one');
assertEquals('Removing "one"', '', p.className);
Blockly.removeClass_(p, 'zero');
assertEquals('Removing "zero"', '', p.className);
}
function test_hasClass() {
var p = document.createElement('p');
p.className = ' one three two three ';
assertTrue('Has "one"', Blockly.hasClass_(p, 'one'));
assertTrue('Has "two"', Blockly.hasClass_(p, 'two'));
assertTrue('Has "three"', Blockly.hasClass_(p, 'three'));
assertFalse('Has no "four"', Blockly.hasClass_(p, 'four'));
assertFalse('Has no "t"', Blockly.hasClass_(p, 't'));
}
function test_shortestStringLength() {
var len = Blockly.shortestStringLength('one,two,three,four,five'.split(','));
assertEquals('Length of "one"', 3, len);
len = Blockly.shortestStringLength('one,two,three,four,five,'.split(','));
assertEquals('Length of ""', 0, len);
len = Blockly.shortestStringLength(['Hello World']);
assertEquals('List of one', 11, len);
len = Blockly.shortestStringLength([]);
assertEquals('Empty list', 0, len);
}
function test_commonWordPrefix() {
var len = Blockly.commonWordPrefix('one,two,three,four,five'.split(','));
assertEquals('No prefix', 0, len);
len = Blockly.commonWordPrefix('Xone,Xtwo,Xthree,Xfour,Xfive'.split(','));
assertEquals('No word prefix', 0, len);
len = Blockly.commonWordPrefix('abc de,abc de,abc de,abc de'.split(','));
assertEquals('Full equality', 6, len);
len = Blockly.commonWordPrefix('abc deX,abc deY'.split(','));
assertEquals('One word prefix', 4, len);
len = Blockly.commonWordPrefix('abc de,abc deY'.split(','));
assertEquals('Overflow no', 4, len);
len = Blockly.commonWordPrefix('abc de,abc de Y'.split(','));
assertEquals('Overflow yes', 6, len);
len = Blockly.commonWordPrefix(['Hello World']);
assertEquals('List of one', 11, len);
len = Blockly.commonWordPrefix([]);
assertEquals('Empty list', 0, len);
len = Blockly.commonWordPrefix('turn&nbsp;left,turn&nbsp;right'.split(','));
assertEquals('No prefix due to &amp;nbsp;', 0, len);
len = Blockly.commonWordPrefix('turn\u00A0left,turn\u00A0right'.split(','));
assertEquals('No prefix due to \\u00A0', 0, len);
}
function test_commonWordSuffix() {
var len = Blockly.commonWordSuffix('one,two,three,four,five'.split(','));
assertEquals('No prefix', 0, len);
len = Blockly.commonWordSuffix('oneX,twoX,threeX,fourX,fiveX'.split(','));
assertEquals('No word prefix', 0, len);
len = Blockly.commonWordSuffix('abc de,abc de,abc de,abc de'.split(','));
assertEquals('Full equality', 6, len);
len = Blockly.commonWordSuffix('Xabc de,Yabc de'.split(','));
assertEquals('One word prefix', 3, len);
len = Blockly.commonWordSuffix('abc de,Yabc de'.split(','));
assertEquals('Overflow no', 3, len);
len = Blockly.commonWordSuffix('abc de,Y abc de'.split(','));
assertEquals('Overflow yes', 6, len);
len = Blockly.commonWordSuffix(['Hello World']);
assertEquals('List of one', 11, len);
len = Blockly.commonWordSuffix([]);
assertEquals('Empty list', 0, len);
}
function test_tokenizeInterpolation() {
var tokens = Blockly.utils.tokenizeInterpolation('');
assertArrayEquals('Null interpolation', [], tokens);
tokens = Blockly.utils.tokenizeInterpolation('Hello');
assertArrayEquals('No interpolation', ['Hello'], tokens);
tokens = Blockly.utils.tokenizeInterpolation('Hello%World');
assertArrayEquals('Unescaped %.', ['Hello%World'], tokens);
tokens = Blockly.utils.tokenizeInterpolation('Hello%%World');
assertArrayEquals('Escaped %.', ['Hello%World'], tokens);
tokens = Blockly.utils.tokenizeInterpolation('Hello %1 World');
assertArrayEquals('Interpolation.', ['Hello ', 1, ' World'], tokens);
tokens = Blockly.utils.tokenizeInterpolation('%123Hello%456World%789');
assertArrayEquals('Interpolations.', [123, 'Hello', 456, 'World', 789], tokens);
tokens = Blockly.utils.tokenizeInterpolation('%%%x%%0%00%01%');
assertArrayEquals('Torture interpolations.', ['%%x%0', 0, 1, '%'], tokens);
}

218
tests/jsunit/utils_test.js Normal file
View file

@ -0,0 +1,218 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2011 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.
*/
'use strict';
function test_genUid() {
var uuids = {};
for (var i = 0; i < 1000; i++) {
var uuid = Blockly.utils.genUid();
assertFalse('UUID different: ' + uuid, uuid in uuids);
uuids[uuid] = true;
}
}
function test_addClass() {
var p = document.createElement('p');
Blockly.utils.addClass(p, 'one');
assertEquals('Adding "one"', 'one', p.className);
Blockly.utils.addClass(p, 'one');
assertEquals('Adding duplicate "one"', 'one', p.className);
Blockly.utils.addClass(p, 'two');
assertEquals('Adding "two"', 'one two', p.className);
Blockly.utils.addClass(p, 'two');
assertEquals('Adding duplicate "two"', 'one two', p.className);
Blockly.utils.addClass(p, 'three');
assertEquals('Adding "three"', 'one two three', p.className);
}
function test_hasClass() {
var p = document.createElement('p');
p.className = ' one three two three ';
assertTrue('Has "one"', Blockly.utils.hasClass(p, 'one'));
assertTrue('Has "two"', Blockly.utils.hasClass(p, 'two'));
assertTrue('Has "three"', Blockly.utils.hasClass(p, 'three'));
assertFalse('Has no "four"', Blockly.utils.hasClass(p, 'four'));
assertFalse('Has no "t"', Blockly.utils.hasClass(p, 't'));
}
function test_removeClass() {
var p = document.createElement('p');
p.className = ' one three two three ';
Blockly.utils.removeClass(p, 'two');
assertEquals('Removing "two"', 'one three three', p.className);
Blockly.utils.removeClass(p, 'four');
assertEquals('Removing "four"', 'one three three', p.className);
Blockly.utils.removeClass(p, 'three');
assertEquals('Removing "three"', 'one', p.className);
Blockly.utils.removeClass(p, 'ne');
assertEquals('Removing "ne"', 'one', p.className);
Blockly.utils.removeClass(p, 'one');
assertEquals('Removing "one"', '', p.className);
Blockly.utils.removeClass(p, 'zero');
assertEquals('Removing "zero"', '', p.className);
}
function test_shortestStringLength() {
var len = Blockly.utils.shortestStringLength('one,two,three,four,five'.split(','));
assertEquals('Length of "one"', 3, len);
len = Blockly.utils.shortestStringLength('one,two,three,four,five,'.split(','));
assertEquals('Length of ""', 0, len);
len = Blockly.utils.shortestStringLength(['Hello World']);
assertEquals('List of one', 11, len);
len = Blockly.utils.shortestStringLength([]);
assertEquals('Empty list', 0, len);
}
function test_commonWordPrefix() {
var len = Blockly.utils.commonWordPrefix('one,two,three,four,five'.split(','));
assertEquals('No prefix', 0, len);
len = Blockly.utils.commonWordPrefix('Xone,Xtwo,Xthree,Xfour,Xfive'.split(','));
assertEquals('No word prefix', 0, len);
len = Blockly.utils.commonWordPrefix('abc de,abc de,abc de,abc de'.split(','));
assertEquals('Full equality', 6, len);
len = Blockly.utils.commonWordPrefix('abc deX,abc deY'.split(','));
assertEquals('One word prefix', 4, len);
len = Blockly.utils.commonWordPrefix('abc de,abc deY'.split(','));
assertEquals('Overflow no', 4, len);
len = Blockly.utils.commonWordPrefix('abc de,abc de Y'.split(','));
assertEquals('Overflow yes', 6, len);
len = Blockly.utils.commonWordPrefix(['Hello World']);
assertEquals('List of one', 11, len);
len = Blockly.utils.commonWordPrefix([]);
assertEquals('Empty list', 0, len);
len = Blockly.utils.commonWordPrefix('turn&nbsp;left,turn&nbsp;right'.split(','));
assertEquals('No prefix due to &amp;nbsp;', 0, len);
len = Blockly.utils.commonWordPrefix('turn\u00A0left,turn\u00A0right'.split(','));
assertEquals('No prefix due to \\u00A0', 0, len);
}
function test_commonWordSuffix() {
var len = Blockly.utils.commonWordSuffix('one,two,three,four,five'.split(','));
assertEquals('No prefix', 0, len);
len = Blockly.utils.commonWordSuffix('oneX,twoX,threeX,fourX,fiveX'.split(','));
assertEquals('No word prefix', 0, len);
len = Blockly.utils.commonWordSuffix('abc de,abc de,abc de,abc de'.split(','));
assertEquals('Full equality', 6, len);
len = Blockly.utils.commonWordSuffix('Xabc de,Yabc de'.split(','));
assertEquals('One word prefix', 3, len);
len = Blockly.utils.commonWordSuffix('abc de,Yabc de'.split(','));
assertEquals('Overflow no', 3, len);
len = Blockly.utils.commonWordSuffix('abc de,Y abc de'.split(','));
assertEquals('Overflow yes', 6, len);
len = Blockly.utils.commonWordSuffix(['Hello World']);
assertEquals('List of one', 11, len);
len = Blockly.utils.commonWordSuffix([]);
assertEquals('Empty list', 0, len);
}
function test_tokenizeInterpolation() {
var tokens = Blockly.utils.tokenizeInterpolation('');
assertArrayEquals('Null interpolation', [], tokens);
tokens = Blockly.utils.tokenizeInterpolation('Hello');
assertArrayEquals('No interpolation', ['Hello'], tokens);
tokens = Blockly.utils.tokenizeInterpolation('Hello%World');
assertArrayEquals('Unescaped %.', ['Hello%World'], tokens);
tokens = Blockly.utils.tokenizeInterpolation('Hello%%World');
assertArrayEquals('Escaped %.', ['Hello%World'], tokens);
tokens = Blockly.utils.tokenizeInterpolation('Hello %1 World');
assertArrayEquals('Interpolation.', ['Hello ', 1, ' World'], tokens);
tokens = Blockly.utils.tokenizeInterpolation('%123Hello%456World%789');
assertArrayEquals('Interpolations.', [123, 'Hello', 456, 'World', 789], tokens);
tokens = Blockly.utils.tokenizeInterpolation('%%%x%%0%00%01%');
assertArrayEquals('Torture interpolations.', ['%%x%0', 0, 1, '%'], tokens);
Blockly.Msg = Blockly.Msg || {};
Blockly.Msg.STRING_REF = 'test string';
tokens = Blockly.utils.tokenizeInterpolation('%{bky_string_ref}');
assertArrayEquals('String table reference, lowercase', ['test string'], tokens);
tokens = Blockly.utils.tokenizeInterpolation('%{BKY_STRING_REF}');
assertArrayEquals('String table reference, uppercase', ['test string'], tokens);
Blockly.Msg.WITH_PARAM = 'before %1 after';
tokens = Blockly.utils.tokenizeInterpolation('%{bky_with_param}');
assertArrayEquals('String table reference, with parameter', ['before ', 1, ' after'], tokens);
Blockly.Msg.RECURSE = 'before %{bky_string_ref} after';
tokens = Blockly.utils.tokenizeInterpolation('%{bky_recurse}');
assertArrayEquals('String table reference, with subreference', ['before test string after'], tokens);
// Error cases...
tokens = Blockly.utils.tokenizeInterpolation('%{bky_undefined}');
assertArrayEquals('Undefined string table reference', ['%{bky_undefined}'], tokens);
Blockly.Msg['1'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{1} after');
assertArrayEquals('Invalid initial digit in string table reference', ['before %{1} after'], tokens);
Blockly.Msg['TWO WORDS'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{two words} after');
assertArrayEquals('Invalid character in string table reference: space', ['before %{two words} after'], tokens);
Blockly.Msg['TWO-WORDS'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{two-words} after');
assertArrayEquals('Invalid character in string table reference: dash', ['before %{two-words} after'], tokens);
Blockly.Msg['TWO.WORDS'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{two.words} after');
assertArrayEquals('Invalid character in string table reference: period', ['before %{two.words} after'], tokens);
Blockly.Msg['AB&C'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{ab&c} after');
assertArrayEquals('Invalid character in string table reference: &', ['before %{ab&c} after'], tokens);
Blockly.Msg['UNCLOSED'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{unclosed');
assertArrayEquals('String table reference, with parameter', ['before %{unclosed'], tokens);
}
function test_replaceMessageReferences() {
Blockly.Msg = Blockly.Msg || {};
Blockly.Msg.STRING_REF = 'test string';
var resultString = Blockly.utils.replaceMessageReferences('');
assertEquals('Empty string produces empty string', '', resultString);
resultString = Blockly.utils.replaceMessageReferences('%{bky_string_ref}');
assertEquals('Message ref dereferenced.', 'test string', resultString);
resultString = Blockly.utils.replaceMessageReferences('before %{bky_string_ref} after');
assertEquals('Message ref dereferenced.', 'before test string after', resultString);
resultString = Blockly.utils.replaceMessageReferences('%1');
assertEquals('Interpolation tokens ignored.', '%1', resultString);
resultString = Blockly.utils.replaceMessageReferences('%1 %2');
assertEquals('Interpolation tokens ignored.', '%1 %2', resultString);
resultString = Blockly.utils.replaceMessageReferences('before %1 after');
assertEquals('Interpolation tokens ignored.', 'before %1 after', resultString);
resultString = Blockly.utils.replaceMessageReferences('%%');
assertEquals('Escaped %', '%', resultString);
resultString = Blockly.utils.replaceMessageReferences('%%{bky_string_ref}');
assertEquals('Escaped %', '%{bky_string_ref}', resultString);
resultString = Blockly.utils.replaceMessageReferences('%a');
assertEquals('Unrecognized % escape code treated as literal', '%a', resultString);
}