Merge branch 'develop' into master_into_develop

This commit is contained in:
alschmiedt
2022-03-29 15:25:39 -07:00
committed by GitHub
339 changed files with 51419 additions and 58707 deletions

View File

@@ -1,10 +1,7 @@
*_compressed*.js
blockly_uncompressed.js
gulpfile.js
/msg/*
/build/*
/dist/*
/core/utils/global.js
/tests/blocks/*
/tests/themes/*
/tests/compile/*

View File

@@ -10,13 +10,15 @@
'use strict';
/* eslint-disable no-var */
/**
* Blockly uncompiled-mode startup code. If running in a browser
* loads closure/goog/base.js and tests/deps.js, then (in any case)
* requires Blockly.requires.
*/
(function(globalThis) {
/* eslint-disable no-undef */
/* eslint-disable-next-line no-undef */
var IS_NODE_JS = !!(typeof module !== 'undefined' && module.exports);
if (IS_NODE_JS) {
@@ -52,4 +54,5 @@
// Load the rest of Blockly.
document.write('<script>goog.require(\'Blockly\');</script>');
}
/* eslint-disable-next-line no-invalid-this */
})(this);

View File

@@ -1,23 +0,0 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview All the blocks. (Entry point for blocks_compressed.js.)
* @suppress {extraRequire}
*/
'use strict';
goog.module('Blockly.blocks.all');
goog.require('Blockly.blocks.colour');
goog.require('Blockly.blocks.lists');
goog.require('Blockly.blocks.logic');
goog.require('Blockly.blocks.loops');
goog.require('Blockly.blocks.math');
goog.require('Blockly.blocks.procedures');
goog.require('Blockly.blocks.texts');
goog.require('Blockly.blocks.variables');
goog.require('Blockly.blocks.variablesDynamic');

47
blocks/blocks.js Normal file
View File

@@ -0,0 +1,47 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview All the blocks. (Entry point for blocks_compressed.js.)
* @suppress {extraRequire}
*/
'use strict';
goog.module('Blockly.libraryBlocks');
goog.module.declareLegacyNamespace();
const colour = goog.require('Blockly.libraryBlocks.colour');
const lists = goog.require('Blockly.libraryBlocks.lists');
const logic = goog.require('Blockly.libraryBlocks.logic');
const loops = goog.require('Blockly.libraryBlocks.loops');
const math = goog.require('Blockly.libraryBlocks.math');
const procedures = goog.require('Blockly.libraryBlocks.procedures');
const texts = goog.require('Blockly.libraryBlocks.texts');
const variables = goog.require('Blockly.libraryBlocks.variables');
const variablesDynamic = goog.require('Blockly.libraryBlocks.variablesDynamic');
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
exports.colour = colour;
exports.lists = lists;
exports.logic = logic;
exports.loops = loops;
exports.math = math;
exports.procedures = procedures;
exports.texts = texts;
exports.variables = variables;
exports.variablesDynamic = variablesDynamic;
/**
* A dictionary of the block definitions provided by all the
* Blockly.libraryBlocks.* modules.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = Object.assign(
{}, colour.blocks, lists.blocks, logic.blocks, loops.blocks, math.blocks,
procedures.blocks, variables.blocks, variablesDynamic.blocks);
exports.blocks = blocks;

View File

@@ -9,14 +9,20 @@
*/
'use strict';
goog.module('Blockly.blocks.colour');
goog.module('Blockly.libraryBlocks.colour');
const {defineBlocksWithJsonArray} = goog.require('Blockly.common');
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldColour');
defineBlocksWithJsonArray([
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = createBlockDefinitionsFromJsonArray([
// Block for colour picker.
{
'type': 'colour_picker',
@@ -107,3 +113,7 @@ defineBlocksWithJsonArray([
'tooltip': '%{BKY_COLOUR_BLEND_TOOLTIP}',
},
]);
exports.blocks = blocks;
// Register provided blocks.
defineBlocks(blocks);

View File

@@ -10,25 +10,30 @@
*/
'use strict';
goog.module('Blockly.blocks.lists');
goog.module('Blockly.libraryBlocks.lists');
const xmlUtils = goog.require('Blockly.utils.xml');
const {Align} = goog.require('Blockly.Input');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {Blocks} = goog.require('Blockly.blocks');
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const {ConnectionType} = goog.require('Blockly.ConnectionType');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
const {Msg} = goog.require('Blockly.Msg');
const {Mutator} = goog.require('Blockly.Mutator');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
const {defineBlocksWithJsonArray} = goog.require('Blockly.common');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldDropdown');
defineBlocksWithJsonArray([
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = createBlockDefinitionsFromJsonArray([
// Block for creating an empty list
// The 'list_create_with' block is preferred as it is more flexible.
// <block type="lists_create_with">
@@ -112,8 +117,9 @@ defineBlocksWithJsonArray([
'helpUrl': '%{BKY_LISTS_LENGTH_HELPURL}',
},
]);
exports.blocks = blocks;
Blocks['lists_create_with'] = {
blocks['lists_create_with'] = {
/**
* Block for creating a list with any number of elements of any type.
* @this {Block}
@@ -255,7 +261,7 @@ Blocks['lists_create_with'] = {
},
};
Blocks['lists_create_with_container'] = {
blocks['lists_create_with_container'] = {
/**
* Mutator block for list container.
* @this {Block}
@@ -270,7 +276,7 @@ Blocks['lists_create_with_container'] = {
},
};
Blocks['lists_create_with_item'] = {
blocks['lists_create_with_item'] = {
/**
* Mutator block for adding items.
* @this {Block}
@@ -285,7 +291,7 @@ Blocks['lists_create_with_item'] = {
},
};
Blocks['lists_indexOf'] = {
blocks['lists_indexOf'] = {
/**
* Block for finding an item in the list.
* @this {Block}
@@ -312,7 +318,7 @@ Blocks['lists_indexOf'] = {
},
};
Blocks['lists_getIndex'] = {
blocks['lists_getIndex'] = {
/**
* Block for getting element at index.
* @this {Block}
@@ -516,7 +522,7 @@ Blocks['lists_getIndex'] = {
},
};
Blocks['lists_setIndex'] = {
blocks['lists_setIndex'] = {
/**
* Block for setting the element at index.
* @this {Block}
@@ -668,7 +674,7 @@ Blocks['lists_setIndex'] = {
},
};
Blocks['lists_getSublist'] = {
blocks['lists_getSublist'] = {
/**
* Block for getting sublist.
* @this {Block}
@@ -786,7 +792,7 @@ Blocks['lists_getSublist'] = {
},
};
Blocks['lists_sort'] = {
blocks['lists_sort'] = {
/**
* Block for sorting a list.
* @this {Block}
@@ -826,7 +832,7 @@ Blocks['lists_sort'] = {
},
};
Blocks['lists_split'] = {
blocks['lists_split'] = {
/**
* Block for splitting text into a list, or joining a list into text.
* @this {Block}
@@ -913,3 +919,6 @@ Blocks['lists_split'] = {
// dropdown values.
// XML hooks are kept for backwards compatibility.
};
// Register provided blocks.
defineBlocks(blocks);

View File

@@ -10,7 +10,7 @@
*/
'use strict';
goog.module('Blockly.blocks.logic');
goog.module('Blockly.libraryBlocks.logic');
/* eslint-disable-next-line no-unused-vars */
const AbstractEvent = goog.requireType('Blockly.Events.Abstract');
@@ -19,20 +19,26 @@ const Extensions = goog.require('Blockly.Extensions');
const xmlUtils = goog.require('Blockly.utils.xml');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const {Msg} = goog.require('Blockly.Msg');
const {Mutator} = goog.require('Blockly.Mutator');
/* eslint-disable-next-line no-unused-vars */
const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
const {defineBlocksWithJsonArray} = goog.require('Blockly.common');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldDropdown');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldLabel');
defineBlocksWithJsonArray([
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = createBlockDefinitionsFromJsonArray([
// Block for boolean data type: true and false.
{
'type': 'logic_boolean',
@@ -258,6 +264,7 @@ defineBlocksWithJsonArray([
'tooltip': '%{BKY_CONTROLS_IF_ELSE_TOOLTIP}',
},
]);
exports.blocks = blocks;
/**
* Tooltip text, keyed by block OP value. Used by logic_compare and
@@ -645,3 +652,6 @@ const LOGIC_TERNARY_ONCHANGE_MIXIN = {
};
Extensions.registerMixin('logic_ternary', LOGIC_TERNARY_ONCHANGE_MIXIN);
// Register provided blocks.
defineBlocks(blocks);

View File

@@ -10,7 +10,7 @@
*/
'use strict';
goog.module('Blockly.blocks.loops');
goog.module('Blockly.libraryBlocks.loops');
/* eslint-disable-next-line no-unused-vars */
const AbstractEvent = goog.requireType('Blockly.Events.Abstract');
@@ -18,11 +18,13 @@ const ContextMenu = goog.require('Blockly.ContextMenu');
const Events = goog.require('Blockly.Events');
const Extensions = goog.require('Blockly.Extensions');
const Variables = goog.require('Blockly.Variables');
const common = goog.require('Blockly.common');
const xmlUtils = goog.require('Blockly.utils.xml');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const {Msg} = goog.require('Blockly.Msg');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldDropdown');
/** @suppress {extraRequire} */
@@ -35,7 +37,11 @@ goog.require('Blockly.FieldVariable');
goog.require('Blockly.Warning');
common.defineBlocksWithJsonArray([
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = createBlockDefinitionsFromJsonArray([
// Block for repeat n times (external number).
{
'type': 'controls_repeat_ext',
@@ -205,6 +211,7 @@ common.defineBlocksWithJsonArray([
],
},
]);
exports.blocks = blocks;
/**
* Tooltips for the 'controls_whileUntil' block, keyed by MODE value.
@@ -287,21 +294,24 @@ Extensions.register(
*
* // If using the Blockly npm package and es6 import syntax:
* import {loopTypes} from 'blockly/blocks';
* loopTypes.push('custom_loop');
* loopTypes.add('custom_loop');
*
* // Else if using Closure Compiler and goog.modules:
* const {loopTypes} = goog.require('Blockly.blocks.loops');
* loopTypes.push('custom_loop');
* const {loopTypes} = goog.require('Blockly.libraryBlocks.loops');
* loopTypes.add('custom_loop');
*
* @type {!Array<string>}
* // Else if using blockly_compressed + blockss_compressed.js in browser:
* Blockly.libraryBlocks.loopTypes.add('custom_loop');
*
* @type {!Set<string>}
*/
const loopTypes = [
const loopTypes = new Set([
'controls_repeat',
'controls_repeat_ext',
'controls_forEach',
'controls_for',
'controls_whileUntil',
];
]);
exports.loopTypes = loopTypes;
/**
@@ -321,7 +331,7 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
getSurroundLoop: function() {
let block = this;
do {
if (loopTypes.includes(block.type)) {
if (loopTypes.has(block.type)) {
return block;
}
block = block.getSurroundParent();
@@ -332,7 +342,7 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!AbstractEvent} e Change event.
* @param {!AbstractEvent} e Move event.
* @this {Block}
*/
onchange: function(e) {
@@ -358,3 +368,6 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
Extensions.registerMixin(
'controls_flow_in_loop_check', CONTROL_FLOW_IN_LOOP_CHECK_MIXIN);
// Register provided blocks.
defineBlocks(blocks);

View File

@@ -10,7 +10,7 @@
*/
'use strict';
goog.module('Blockly.blocks.math');
goog.module('Blockly.libraryBlocks.math');
const Extensions = goog.require('Blockly.Extensions');
// N.B.: Blockly.FieldDropdown needed for type AND side-effects.
@@ -20,7 +20,8 @@ const xmlUtils = goog.require('Blockly.utils.xml');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {defineBlocksWithJsonArray} = goog.require('Blockly.common');
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldLabel');
/** @suppress {extraRequire} */
@@ -29,7 +30,11 @@ goog.require('Blockly.FieldNumber');
goog.require('Blockly.FieldVariable');
defineBlocksWithJsonArray([
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = createBlockDefinitionsFromJsonArray([
// Block for numeric value.
{
'type': 'math_number',
@@ -384,6 +389,7 @@ defineBlocksWithJsonArray([
'helpUrl': '%{BKY_MATH_ATAN2_HELPURL}',
},
]);
exports.blocks = blocks;
/**
* Mapping of math block OP value to tooltip message for blocks
@@ -581,3 +587,6 @@ const LIST_MODES_MUTATOR_EXTENSION = function() {
Extensions.registerMutator(
'math_modes_of_list_mutator', LIST_MODES_MUTATOR_MIXIN,
LIST_MODES_MUTATOR_EXTENSION);
// Register provided blocks.
defineBlocks(blocks);

View File

@@ -10,7 +10,7 @@
*/
'use strict';
goog.module('Blockly.blocks.procedures');
goog.module('Blockly.libraryBlocks.procedures');
/* eslint-disable-next-line no-unused-vars */
const AbstractEvent = goog.requireType('Blockly.Events.Abstract');
@@ -19,12 +19,13 @@ const Events = goog.require('Blockly.Events');
const Procedures = goog.require('Blockly.Procedures');
const Variables = goog.require('Blockly.Variables');
const Xml = goog.require('Blockly.Xml');
const internalConstants = goog.require('Blockly.internalConstants');
const xmlUtils = goog.require('Blockly.utils.xml');
const {Align} = goog.require('Blockly.Input');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {Blocks} = goog.require('Blockly.blocks');
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const {config} = goog.require('Blockly.config');
/* eslint-disable-next-line no-unused-vars */
const {FieldCheckbox} = goog.require('Blockly.FieldCheckbox');
const {FieldLabel} = goog.require('Blockly.FieldLabel');
@@ -36,12 +37,20 @@ const {Names} = goog.require('Blockly.Names');
const {VariableModel} = goog.requireType('Blockly.VariableModel');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
const {defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.Comment');
/** @suppress {extraRequire} */
goog.require('Blockly.Warning');
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = {};
exports.blocks = blocks;
/**
* Common properties for the procedure_defnoreturn and
* procedure_defreturn blocks.
@@ -434,10 +443,9 @@ const PROCEDURE_DEF_COMMON = {
}
}
},
callType_: 'procedures_callnoreturn',
};
Blocks['procedures_defnoreturn'] = {
blocks['procedures_defnoreturn'] = {
...PROCEDURE_DEF_COMMON,
/**
* Block for defining a procedure with no return value.
@@ -477,9 +485,10 @@ Blocks['procedures_defnoreturn'] = {
getProcedureDef: function() {
return [this.getFieldValue('NAME'), this.arguments_, false];
},
callType_: 'procedures_callnoreturn',
};
Blocks['procedures_defreturn'] = {
blocks['procedures_defreturn'] = {
...PROCEDURE_DEF_COMMON,
/**
* Block for defining a procedure with a return value.
@@ -522,9 +531,10 @@ Blocks['procedures_defreturn'] = {
getProcedureDef: function() {
return [this.getFieldValue('NAME'), this.arguments_, true];
},
callType_: 'procedures_callreturn',
};
Blocks['procedures_mutatorcontainer'] = {
blocks['procedures_mutatorcontainer'] = {
/**
* Mutator block for procedure container.
* @this {Block}
@@ -542,7 +552,7 @@ Blocks['procedures_mutatorcontainer'] = {
},
};
Blocks['procedures_mutatorarg'] = {
blocks['procedures_mutatorarg'] = {
/**
* Mutator block for procedure argument.
* @this {Block}
@@ -950,19 +960,18 @@ const PROCEDURE_CALL_COMMON = {
const block = xmlUtils.createElement('block');
block.setAttribute('type', this.defType_);
const xy = this.getRelativeToSurfaceXY();
const x = xy.x + internalConstants.SNAP_RADIUS * (this.RTL ? -1 : 1);
const y = xy.y + internalConstants.SNAP_RADIUS * 2;
const x = xy.x + config.snapRadius * (this.RTL ? -1 : 1);
const y = xy.y + config.snapRadius * 2;
block.setAttribute('x', x);
block.setAttribute('y', y);
const mutation = this.mutationToDom();
block.appendChild(mutation);
const field = xmlUtils.createElement('field');
field.setAttribute('name', 'NAME');
let callName = this.getProcedureCall();
if (!callName) {
// Rename if name is empty string.
callName = Procedures.findLegalName('', this);
this.renameProcedure('', callName);
const callName = this.getProcedureCall();
const newName = Procedures.findLegalName(callName, this);
if (callName !== newName) {
this.renameProcedure(callName, newName);
}
field.appendChild(xmlUtils.createTextNode(callName));
block.appendChild(field);
@@ -1033,7 +1042,7 @@ const PROCEDURE_CALL_COMMON = {
},
};
Blocks['procedures_callnoreturn'] = {
blocks['procedures_callnoreturn'] = {
...PROCEDURE_CALL_COMMON,
/**
* Block for calling a procedure with no return value.
@@ -1056,7 +1065,7 @@ Blocks['procedures_callnoreturn'] = {
defType_: 'procedures_defnoreturn',
};
Blocks['procedures_callreturn'] = {
blocks['procedures_callreturn'] = {
...PROCEDURE_CALL_COMMON,
/**
* Block for calling a procedure with a return value.
@@ -1078,7 +1087,7 @@ Blocks['procedures_callreturn'] = {
defType_: 'procedures_defreturn',
};
Blocks['procedures_ifreturn'] = {
blocks['procedures_ifreturn'] = {
/**
* Block for conditionally returning a value from a procedure.
* @this {Block}
@@ -1130,11 +1139,12 @@ Blocks['procedures_ifreturn'] = {
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!AbstractEvent} _e Change event.
* @param {!AbstractEvent} e Move event.
* @this {Block}
*/
onchange: function(_e) {
if (this.workspace.isDragging && this.workspace.isDragging()) {
onchange: function(e) {
if (this.workspace.isDragging && this.workspace.isDragging() ||
e.type !== Events.BLOCK_MOVE) {
return; // Don't change state at the start of a drag.
}
let legal = false;
@@ -1162,14 +1172,15 @@ Blocks['procedures_ifreturn'] = {
this.hasReturnValue_ = true;
}
this.setWarningText(null);
if (!this.isInFlyout) {
this.setEnabled(true);
}
} else {
this.setWarningText(Msg['PROCEDURES_IFRETURN_WARNING']);
if (!this.isInFlyout && !this.getInheritedDisabled()) {
this.setEnabled(false);
}
}
if (!this.isInFlyout) {
const group = Events.getGroup();
// Makes it so the move and the disable event get undone together.
Events.setGroup(e.group);
this.setEnabled(legal);
Events.setGroup(group);
}
},
/**
@@ -1179,3 +1190,6 @@ Blocks['procedures_ifreturn'] = {
*/
FUNCTION_TYPES: ['procedures_defnoreturn', 'procedures_defreturn'],
};
// Register provided blocks.
defineBlocks(blocks);

View File

@@ -10,7 +10,7 @@
*/
'use strict';
goog.module('Blockly.blocks.texts');
goog.module('Blockly.libraryBlocks.texts');
const Extensions = goog.require('Blockly.Extensions');
const {Msg} = goog.require('Blockly.Msg');
@@ -19,7 +19,8 @@ const xmlUtils = goog.require('Blockly.utils.xml');
const {Align} = goog.require('Blockly.Input');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {Blocks} = goog.require('Blockly.blocks');
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const {ConnectionType} = goog.require('Blockly.ConnectionType');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
const {FieldImage} = goog.require('Blockly.FieldImage');
@@ -27,14 +28,18 @@ const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
const {Mutator} = goog.require('Blockly.Mutator');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
const {defineBlocksWithJsonArray} = goog.require('Blockly.common');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldMultilineInput');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldVariable');
defineBlocksWithJsonArray([
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = createBlockDefinitionsFromJsonArray([
// Block for text value
{
'type': 'text',
@@ -238,8 +243,9 @@ defineBlocksWithJsonArray([
'mutator': 'text_charAt_mutator',
},
]);
exports.blocks = blocks;
Blocks['text_getSubstring'] = {
blocks['text_getSubstring'] = {
/**
* Block for getting substring.
* @this {Block}
@@ -363,7 +369,7 @@ Blocks['text_getSubstring'] = {
},
};
Blocks['text_changeCase'] = {
blocks['text_changeCase'] = {
/**
* Block for changing capitalization.
* @this {Block}
@@ -383,7 +389,7 @@ Blocks['text_changeCase'] = {
},
};
Blocks['text_trim'] = {
blocks['text_trim'] = {
/**
* Block for trimming spaces.
* @this {Block}
@@ -403,7 +409,7 @@ Blocks['text_trim'] = {
},
};
Blocks['text_print'] = {
blocks['text_print'] = {
/**
* Block for print statement.
* @this {Block}
@@ -463,7 +469,7 @@ const TEXT_PROMPT_COMMON = {
},
};
Blocks['text_prompt_ext'] = {
blocks['text_prompt_ext'] = {
...TEXT_PROMPT_COMMON,
/**
* Block for prompt function (external message).
@@ -496,7 +502,7 @@ Blocks['text_prompt_ext'] = {
// XML hooks are kept for backwards compatibility.
};
Blocks['text_prompt'] = {
blocks['text_prompt'] = {
...TEXT_PROMPT_COMMON,
/**
* Block for prompt function (internal message).
@@ -531,7 +537,7 @@ Blocks['text_prompt'] = {
},
};
Blocks['text_count'] = {
blocks['text_count'] = {
/**
* Block for counting how many times one string appears within another string.
* @this {Block}
@@ -560,7 +566,7 @@ Blocks['text_count'] = {
},
};
Blocks['text_replace'] = {
blocks['text_replace'] = {
/**
* Block for replacing one string with another in the text.
* @this {Block}
@@ -594,7 +600,7 @@ Blocks['text_replace'] = {
},
};
Blocks['text_reverse'] = {
blocks['text_reverse'] = {
/**
* Block for reversing a string.
* @this {Block}
@@ -981,3 +987,6 @@ Extensions.registerMutator(
Extensions.registerMutator(
'text_charAt_mutator', TEXT_CHARAT_MUTATOR_MIXIN, TEXT_CHARAT_EXTENSION);
// Register provided blocks.
defineBlocks(blocks);

View File

@@ -10,7 +10,7 @@
*/
'use strict';
goog.module('Blockly.blocks.variables');
goog.module('Blockly.libraryBlocks.variables');
const ContextMenu = goog.require('Blockly.ContextMenu');
const Extensions = goog.require('Blockly.Extensions');
@@ -18,15 +18,21 @@ const Variables = goog.require('Blockly.Variables');
const xmlUtils = goog.require('Blockly.utils.xml');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const {Msg} = goog.require('Blockly.Msg');
const {defineBlocksWithJsonArray} = goog.require('Blockly.common');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldLabel');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldVariable');
defineBlocksWithJsonArray([
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = createBlockDefinitionsFromJsonArray([
// Block for variable getter.
{
'type': 'variables_get',
@@ -67,6 +73,8 @@ defineBlocksWithJsonArray([
'extensions': ['contextMenu_variableSetterGetter'],
},
]);
exports.blocks = blocks;
/**
* Mixin to add context menu items to create getter/setter blocks for this
@@ -161,3 +169,6 @@ const deleteOptionCallbackFactory = function(block) {
Extensions.registerMixin(
'contextMenu_variableSetterGetter',
CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN);
// Register provided blocks.
defineBlocks(blocks);

View File

@@ -10,7 +10,7 @@
*/
'use strict';
goog.module('Blockly.blocks.variablesDynamic');
goog.module('Blockly.libraryBlocks.variablesDynamic');
/* eslint-disable-next-line no-unused-vars */
const AbstractEvent = goog.requireType('Blockly.Events.Abstract');
@@ -20,15 +20,21 @@ const Variables = goog.require('Blockly.Variables');
const xml = goog.require('Blockly.utils.xml');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const {Msg} = goog.require('Blockly.Msg');
const {defineBlocksWithJsonArray} = goog.require('Blockly.common');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldLabel');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldVariable');
defineBlocksWithJsonArray([
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = createBlockDefinitionsFromJsonArray([
// Block for variable getter.
{
'type': 'variables_get_dynamic',
@@ -67,6 +73,7 @@ defineBlocksWithJsonArray([
'extensions': ['contextMenu_variableDynamicSetterGetter'],
},
]);
exports.blocks = blocks;
/**
* Mixin to add context menu items to create getter/setter blocks for this
@@ -178,3 +185,6 @@ const deleteOptionCallbackFactory = function(block) {
Extensions.registerMixin(
'contextMenu_variableDynamicSetterGetter',
CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN);
// Register provided blocks.
defineBlocks(blocks);

View File

@@ -91,7 +91,14 @@ goog.global.CLOSURE_UNCOMPILED_DEFINES;
* var CLOSURE_DEFINES = {'goog.DEBUG': false} ;
* </pre>
*
* @type {Object<string, (string|number|boolean)>|undefined}
* Currently the Closure Compiler will only recognize very simple definitions of
* this value when looking for values to apply to compiled code and ignore all
* other references. Specifically, it looks the value defined at the variable
* declaration, as with the example above.
*
* TODO(user): Improve the recognized definitions.
*
* @type {!Object<string, (string|number|boolean)>|null|undefined}
*/
goog.global.CLOSURE_DEFINES;
@@ -3175,23 +3182,10 @@ if (!COMPILED && goog.DEPENDENCIES_ENABLED) {
scriptEl.nonce = nonce;
}
if (goog.DebugLoader_.IS_OLD_IE_) {
// Execution order is not guaranteed on old IE, halt loading and write
// these scripts one at a time, after each loads.
controller.pause();
scriptEl.onreadystatechange = function() {
if (scriptEl.readyState == 'loaded' ||
scriptEl.readyState == 'complete') {
controller.loaded();
controller.resume();
}
};
} else {
scriptEl.onload = function() {
scriptEl.onload = null;
controller.loaded();
};
}
scriptEl.onload = function() {
scriptEl.onload = null;
controller.loaded();
};
scriptEl.src = goog.TRUSTED_TYPES_POLICY_ ?
goog.TRUSTED_TYPES_POLICY_.createScriptURL(this.path) :
@@ -3502,13 +3496,6 @@ if (!COMPILED && goog.DEPENDENCIES_ENABLED) {
// If one thing is pending it is this.
var anythingElsePending = controller.pending().length > 1;
// If anything else is loading we need to lazy load due to bugs in old IE.
// Specifically script tags with src and script tags with contents could
// execute out of order if document.write is used, so we cannot use
// document.write. Do not pause here; it breaks old IE as well.
var useOldIeWorkAround =
anythingElsePending && goog.DebugLoader_.IS_OLD_IE_;
// Additionally if we are meant to defer scripts but the page is still
// loading (e.g. an ES6 module is loading) then also defer. Or if we are
// meant to defer and anything else is pending then defer (those may be
@@ -3517,7 +3504,7 @@ if (!COMPILED && goog.DEPENDENCIES_ENABLED) {
var needsAsyncLoading = goog.Dependency.defer_ &&
(anythingElsePending || goog.isDocumentLoading_());
if (useOldIeWorkAround || needsAsyncLoading) {
if (needsAsyncLoading) {
// Note that we only defer when we have to rather than 100% of the time.
// Always defering would work, but then in theory the order of
// goog.require calls would then matter. We want to enforce that most of
@@ -3561,8 +3548,7 @@ if (!COMPILED && goog.DEPENDENCIES_ENABLED) {
};
} else {
// Always eval on old IE.
if (goog.DebugLoader_.IS_OLD_IE_ || !goog.inHtmlDocument_() ||
!goog.isDocumentLoading_()) {
if (!goog.inHtmlDocument_() || !goog.isDocumentLoading_()) {
load();
} else {
fetchInOwnScriptThenLoad();
@@ -3706,15 +3692,6 @@ if (!COMPILED && goog.DEPENDENCIES_ENABLED) {
};
/**
* Whether the browser is IE9 or earlier, which needs special handling
* for deferred modules.
* @const @private {boolean}
*/
goog.DebugLoader_.IS_OLD_IE_ = !!(
!goog.global.atob && goog.global.document && goog.global.document['all']);
/**
* @param {string} relPath
* @param {!Array<string>|undefined} provides

99
closure/goog/goog.js Normal file
View File

@@ -0,0 +1,99 @@
// Copyright 2018 The Closure Library Authors. 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 ES6 module that exports symbols from base.js so that ES6
* modules do not need to use globals and so that is clear if a project is using
* Closure's base.js file. It is also a subset of properties in base.js, meaning
* it should be clearer what should not be used in ES6 modules
* (goog.module/provide are not exported here, for example). Though that is not
* to say that everything in this file should be used in an ES6 module; some
* depreciated functions are exported to make migration easier (e.g.
* goog.scope).
*
* Note that this does not load Closure's base.js file, it is still up to the
* programmer to include it. Nor does the fact that this is an ES6 module mean
* that projects no longer require deps.js files for debug loading - they do.
* Closure will need to load your ES6 modules for you if you have any Closure
* file (goog.provide/goog.module) dependencies, as they need to be available
* before the ES6 module evaluates.
*
* Also note that this file has special compiler handling! It is okay to export
* anything from this file, but the name also needs to exist on the global goog.
* This special compiler pass enforces that you always import this file as
* `import * as goog`, as many tools use regex based parsing to find
* goog.require calls.
*/
export const global = goog.global;
export const require = goog.require;
export const define = goog.define;
export const DEBUG = goog.DEBUG;
export const LOCALE = goog.LOCALE;
export const TRUSTED_SITE = goog.TRUSTED_SITE;
export const DISALLOW_TEST_ONLY_CODE = goog.DISALLOW_TEST_ONLY_CODE;
export const getGoogModule = goog.module.get;
export const setTestOnly = goog.setTestOnly;
export const forwardDeclare = goog.forwardDeclare;
export const getObjectByName = goog.getObjectByName;
export const basePath = goog.basePath;
export const addSingletonGetter = goog.addSingletonGetter;
export const typeOf = goog.typeOf;
export const isArrayLike = goog.isArrayLike;
export const isDateLike = goog.isDateLike;
export const isObject = goog.isObject;
export const getUid = goog.getUid;
export const hasUid = goog.hasUid;
export const removeUid = goog.removeUid;
export const mixin = goog.mixin;
export const now = Date.now;
export const globalEval = goog.globalEval;
export const getCssName = goog.getCssName;
export const setCssNameMapping = goog.setCssNameMapping;
export const getMsg = goog.getMsg;
export const getMsgWithFallback = goog.getMsgWithFallback;
export const exportSymbol = goog.exportSymbol;
export const exportProperty = goog.exportProperty;
export const nullFunction = goog.nullFunction;
export const abstractMethod = goog.abstractMethod;
export const cloneObject = goog.cloneObject;
export const bind = goog.bind;
export const partial = goog.partial;
export const inherits = goog.inherits;
export const scope = goog.scope;
export const defineClass = goog.defineClass;
export const declareModuleId = goog.declareModuleId;
// Export select properties of module. Do not export the function itself or
// goog.module.declareLegacyNamespace.
export const module = {
get: goog.module.get,
};
// Omissions include:
// goog.ENABLE_DEBUG_LOADER - define only used in base.
// goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING - define only used in base.
// goog.provide - ES6 modules do not provide anything.
// goog.module - ES6 modules cannot be goog.modules.
// goog.module.declareLegacyNamespace - ES6 modules cannot declare namespaces.
// goog.addDependency - meant to only be used by dependency files.
// goog.DEPENDENCIES_ENABLED - constant only used in base.
// goog.TRANSPILE - define only used in base.
// goog.TRANSPILER - define only used in base.
// goog.loadModule - should not be called by any ES6 module; exists for
// generated bundles.
// goog.LOAD_MODULE_USING_EVAL - define only used in base.
// goog.SEAL_MODULE_EXPORTS - define only used in base.
// goog.DebugLoader - used rarely, only outside of compiled code.
// goog.Transpiler - used rarely, only outside of compiled code.

File diff suppressed because it is too large Load Diff

View File

@@ -35,234 +35,235 @@ const {Svg} = goog.require('Blockly.utils.Svg');
/**
* 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
* @alias Blockly.BlockDragSurfaceSvg
*/
const BlockDragSurfaceSvg = function(container) {
const BlockDragSurfaceSvg = class {
/**
* @type {!Element}
* @param {!Element} container Containing element.
*/
constructor(container) {
/**
* The SVG drag surface. Set once by BlockDragSurfaceSvg.createDom.
* @type {?SVGElement}
* @private
*/
this.SVG_ = null;
/**
* This is where blocks live while they are being dragged if the drag
* surface is enabled.
* @type {?SVGElement}
* @private
*/
this.dragGroup_ = null;
/**
* Containing HTML element; parent of the workspace and the drag surface.
* @type {!Element}
* @private
*/
this.container_ = container;
/**
* Cached value for the scale of the drag surface.
* Used to set/get the correct translation during and after a drag.
* @type {number}
* @private
*/
this.scale_ = 1;
/**
* Cached value for the translation of the drag surface.
* This translation is in pixel units, because the scale is applied to the
* drag group rather than the top-level SVG.
* @type {?Coordinate}
* @private
*/
this.surfaceXY_ = null;
/**
* Cached value for the translation of the child drag surface in pixel
* units. Since the child drag surface tracks the translation of the
* workspace this is ultimately the translation of the workspace.
* @type {!Coordinate}
* @private
*/
this.childSurfaceXY_ = new Coordinate(0, 0);
this.createDom();
}
/**
* Create the drag surface and inject it into the container.
*/
createDom() {
if (this.SVG_) {
return; // Already created.
}
this.SVG_ = dom.createSvgElement(
Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
'xmlns:xlink': dom.XLINK_NS,
'version': '1.1',
'class': 'blocklyBlockDragSurface',
},
this.container_);
this.dragGroup_ = dom.createSvgElement(Svg.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 {!SVGElement} blocks Block or group of blocks to place on the drag
* surface.
*/
setBlocksAndShow(blocks) {
if (this.dragGroup_.childNodes.length) {
throw Error('Already dragging a block.');
}
// appendChild removes the blocks from the previous parent
this.dragGroup_.appendChild(blocks);
this.SVG_.style.display = 'block';
this.surfaceXY_ = new Coordinate(0, 0);
}
/**
* Translate and scale the entire drag surface group to the given position, to
* keep in sync with the workspace.
* @param {number} x X translation in pixel coordinates.
* @param {number} y Y translation in pixel coordinates.
* @param {number} scale Scale of the group.
*/
translateAndScaleGroup(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.
const fixedX = x.toFixed(0);
const fixedY = y.toFixed(0);
this.childSurfaceXY_.x = parseInt(fixedX, 10);
this.childSurfaceXY_.y = parseInt(fixedY, 10);
this.dragGroup_.setAttribute(
'transform',
'translate(' + fixedX + ',' + fixedY + ') scale(' + scale + ')');
}
/**
* Translate the drag surface's SVG based on its internal state.
* @private
*/
this.container_ = container;
this.createDom();
};
translateSurfaceInternal_() {
let x = this.surfaceXY_.x;
let y = this.surfaceXY_.y;
// 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.SVG_.style.display = 'block';
/**
* The SVG drag surface. Set once by BlockDragSurfaceSvg.createDom.
* @type {?SVGElement}
* @private
*/
BlockDragSurfaceSvg.prototype.SVG_ = null;
/**
* This is where blocks live while they are being dragged if the drag surface
* is enabled.
* @type {?SVGElement}
* @private
*/
BlockDragSurfaceSvg.prototype.dragGroup_ = null;
/**
* Containing HTML element; parent of the workspace and the drag surface.
* @type {?Element}
* @private
*/
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
*/
BlockDragSurfaceSvg.prototype.scale_ = 1;
/**
* Cached value for the translation of the drag surface.
* This translation is in pixel units, because the scale is applied to the
* drag group rather than the top-level SVG.
* @type {?Coordinate}
* @private
*/
BlockDragSurfaceSvg.prototype.surfaceXY_ = null;
/**
* Cached value for the translation of the child drag surface in pixel units.
* Since the child drag surface tracks the translation of the workspace this is
* ultimately the translation of the workspace.
* @type {!Coordinate}
* @private
*/
BlockDragSurfaceSvg.prototype.childSurfaceXY_ = new Coordinate(0, 0);
/**
* Create the drag surface and inject it into the container.
*/
BlockDragSurfaceSvg.prototype.createDom = function() {
if (this.SVG_) {
return; // Already created.
dom.setCssTransform(this.SVG_, 'translate3d(' + x + 'px, ' + y + 'px, 0)');
}
this.SVG_ = dom.createSvgElement(
Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
'xmlns:xlink': dom.XLINK_NS,
'version': '1.1',
'class': 'blocklyBlockDragSurface',
},
this.container_);
this.dragGroup_ = dom.createSvgElement(Svg.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 {!SVGElement} blocks Block or group of blocks to place on the drag
* surface.
*/
BlockDragSurfaceSvg.prototype.setBlocksAndShow = function(blocks) {
if (this.dragGroup_.childNodes.length) {
throw Error('Already dragging a block.');
/**
* Translates the entire surface by a relative offset.
* @param {number} deltaX Horizontal offset in pixel units.
* @param {number} deltaY Vertical offset in pixel units.
*/
translateBy(deltaX, deltaY) {
const x = this.surfaceXY_.x + deltaX;
const y = this.surfaceXY_.y + deltaY;
this.surfaceXY_ = new Coordinate(x, y);
this.translateSurfaceInternal_();
}
// appendChild removes the blocks from the previous parent
this.dragGroup_.appendChild(blocks);
this.SVG_.style.display = 'block';
this.surfaceXY_ = new Coordinate(0, 0);
};
/**
* Translate and scale the entire drag surface group to the given position, to
* keep in sync with the workspace.
* @param {number} x X translation in pixel coordinates.
* @param {number} y Y translation in pixel coordinates.
* @param {number} scale Scale of the group.
*/
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.
const fixedX = x.toFixed(0);
const fixedY = y.toFixed(0);
this.childSurfaceXY_.x = parseInt(fixedX, 10);
this.childSurfaceXY_.y = parseInt(fixedY, 10);
this.dragGroup_.setAttribute(
'transform',
'translate(' + fixedX + ',' + fixedY + ') scale(' + scale + ')');
};
/**
* Translate the drag surface's SVG based on its internal state.
* @private
*/
BlockDragSurfaceSvg.prototype.translateSurfaceInternal_ = function() {
let x = this.surfaceXY_.x;
let y = this.surfaceXY_.y;
// 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.SVG_.style.display = 'block';
dom.setCssTransform(this.SVG_, 'translate3d(' + x + 'px, ' + y + 'px, 0)');
};
/**
* Translates the entire surface by a relative offset.
* @param {number} deltaX Horizontal offset in pixel units.
* @param {number} deltaY Vertical offset in pixel units.
*/
BlockDragSurfaceSvg.prototype.translateBy = function(deltaX, deltaY) {
const x = this.surfaceXY_.x + deltaX;
const y = this.surfaceXY_.y + deltaY;
this.surfaceXY_ = new Coordinate(x, y);
this.translateSurfaceInternal_();
};
/**
* 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.
*/
BlockDragSurfaceSvg.prototype.translateSurface = function(x, y) {
this.surfaceXY_ = new Coordinate(x * this.scale_, y * this.scale_);
this.translateSurfaceInternal_();
};
/**
* Reports the surface translation in scaled workspace coordinates.
* Use this when finishing a drag to return blocks to the correct position.
* @return {!Coordinate} Current translation of the surface.
*/
BlockDragSurfaceSvg.prototype.getSurfaceTranslation = function() {
const xy = svgMath.getRelativeXY(/** @type {!SVGElement} */ (this.SVG_));
return new Coordinate(xy.x / this.scale_, xy.y / this.scale_);
};
/**
* Provide a reference to the drag group (primarily for
* BlockSvg.getRelativeToSurfaceXY).
* @return {?SVGElement} Drag surface group element.
*/
BlockDragSurfaceSvg.prototype.getGroup = function() {
return this.dragGroup_;
};
/**
* Returns the SVG drag surface.
* @returns {?SVGElement} The SVG drag surface.
*/
BlockDragSurfaceSvg.prototype.getSvgRoot = function() {
return this.SVG_;
};
/**
* Get the current blocks on the drag surface, if any (primarily
* for BlockSvg.getRelativeToSurfaceXY).
* @return {?Element} Drag surface block DOM element, or null if no blocks
* exist.
*/
BlockDragSurfaceSvg.prototype.getCurrentBlock = function() {
return /** @type {Element} */ (this.dragGroup_.firstChild);
};
/**
* Gets the translation of the child block surface
* This surface is in charge of keeping track of how much the workspace has
* moved.
* @return {!Coordinate} The amount the workspace has been moved.
*/
BlockDragSurfaceSvg.prototype.getWsTranslation = function() {
// Returning a copy so the coordinate can not be changed outside this class.
return this.childSurfaceXY_.clone();
};
/**
* Clear the group and hide the surface; move the blocks off onto the provided
* element.
* If the block is being deleted it doesn't need to go back to the original
* surface, since it would be removed immediately during dispose.
* @param {Element=} opt_newSurface Surface the dragging blocks should be moved
* to, or null if the blocks should be removed from this surface without
* being moved to a different surface.
*/
BlockDragSurfaceSvg.prototype.clearAndHide = function(opt_newSurface) {
if (opt_newSurface) {
// appendChild removes the node from this.dragGroup_
opt_newSurface.appendChild(this.getCurrentBlock());
} else {
this.dragGroup_.removeChild(this.getCurrentBlock());
/**
* 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.
*/
translateSurface(x, y) {
this.surfaceXY_ = new Coordinate(x * this.scale_, y * this.scale_);
this.translateSurfaceInternal_();
}
this.SVG_.style.display = 'none';
if (this.dragGroup_.childNodes.length) {
throw Error('Drag group was not cleared.');
/**
* Reports the surface translation in scaled workspace coordinates.
* Use this when finishing a drag to return blocks to the correct position.
* @return {!Coordinate} Current translation of the surface.
*/
getSurfaceTranslation() {
const xy = svgMath.getRelativeXY(/** @type {!SVGElement} */ (this.SVG_));
return new Coordinate(xy.x / this.scale_, xy.y / this.scale_);
}
/**
* Provide a reference to the drag group (primarily for
* BlockSvg.getRelativeToSurfaceXY).
* @return {?SVGElement} Drag surface group element.
*/
getGroup() {
return this.dragGroup_;
}
/**
* Returns the SVG drag surface.
* @returns {?SVGElement} The SVG drag surface.
*/
getSvgRoot() {
return this.SVG_;
}
/**
* Get the current blocks on the drag surface, if any (primarily
* for BlockSvg.getRelativeToSurfaceXY).
* @return {?Element} Drag surface block DOM element, or null if no blocks
* exist.
*/
getCurrentBlock() {
return /** @type {Element} */ (this.dragGroup_.firstChild);
}
/**
* Gets the translation of the child block surface
* This surface is in charge of keeping track of how much the workspace has
* moved.
* @return {!Coordinate} The amount the workspace has been moved.
*/
getWsTranslation() {
// Returning a copy so the coordinate can not be changed outside this class.
return this.childSurfaceXY_.clone();
}
/**
* Clear the group and hide the surface; move the blocks off onto the provided
* element.
* If the block is being deleted it doesn't need to go back to the original
* surface, since it would be removed immediately during dispose.
* @param {Element=} opt_newSurface Surface the dragging blocks should be
* moved to, or null if the blocks should be removed from this surface
* without being moved to a different surface.
*/
clearAndHide(opt_newSurface) {
const currentBlockElement = this.getCurrentBlock();
if (currentBlockElement) {
if (opt_newSurface) {
// appendChild removes the node from this.dragGroup_
opt_newSurface.appendChild(currentBlockElement);
} else {
this.dragGroup_.removeChild(currentBlockElement);
}
}
this.SVG_.style.display = 'none';
if (this.dragGroup_.childNodes.length) {
throw Error('Drag group was not cleared.');
}
this.surfaceXY_ = null;
}
this.surfaceXY_ = null;
};
exports.BlockDragSurfaceSvg = BlockDragSurfaceSvg;

View File

@@ -22,6 +22,8 @@ const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {BlockMove} = goog.requireType('Blockly.Events.BlockMove');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
/* eslint-disable-next-line no-unused-vars */
@@ -40,76 +42,411 @@ goog.require('Blockly.Events.BlockMove');
/**
* Class for a block dragger. It moves blocks around the workspace when they
* are being dragged by a mouse or touch.
* @param {!BlockSvg} block The block to drag.
* @param {!WorkspaceSvg} workspace The workspace to drag on.
* @constructor
* @implements {IBlockDragger}
* @alias Blockly.BlockDragger
*/
const BlockDragger = function(block, workspace) {
const BlockDragger = class {
/**
* The top block in the stack that is being dragged.
* @type {!BlockSvg}
* @param {!BlockSvg} block The block to drag.
* @param {!WorkspaceSvg} workspace The workspace to drag on.
*/
constructor(block, workspace) {
/**
* The top block in the stack that is being dragged.
* @type {!BlockSvg}
* @protected
*/
this.draggingBlock_ = block;
/**
* The workspace on which the block is being dragged.
* @type {!WorkspaceSvg}
* @protected
*/
this.workspace_ = workspace;
/**
* Object that keeps track of connections on dragged blocks.
* @type {!InsertionMarkerManager}
* @protected
*/
this.draggedConnectionManager_ =
new InsertionMarkerManager(this.draggingBlock_);
/**
* Which drag area the mouse pointer is over, if any.
* @type {?IDragTarget}
* @private
*/
this.dragTarget_ = null;
/**
* Whether the block would be deleted if dropped immediately.
* @type {boolean}
* @protected
*/
this.wouldDeleteBlock_ = false;
/**
* The location of the top left corner of the dragging block at the
* beginning of the drag in workspace coordinates.
* @type {!Coordinate}
* @protected
*/
this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY();
/**
* A list of all of the icons (comment, warning, and mutator) that are
* on this block and its descendants. Moving an icon moves the bubble that
* extends from it if that bubble is open.
* @type {Array<!Object>}
* @protected
*/
this.dragIconData_ = initIconData(block);
}
/**
* Sever all links from this object.
* @package
*/
dispose() {
this.dragIconData_.length = 0;
if (this.draggedConnectionManager_) {
this.draggedConnectionManager_.dispose();
}
}
/**
* Start dragging a block. This includes moving it to the drag surface.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @param {boolean} healStack Whether or not to heal the stack after
* disconnecting.
* @public
*/
startDrag(currentDragDeltaXY, healStack) {
if (!eventUtils.getGroup()) {
eventUtils.setGroup(true);
}
this.fireDragStartEvent_();
// Mutators don't have the same type of z-ordering as the normal workspace
// during a drag. They have to rely on the order of the blocks in the SVG.
// For performance reasons that usually happens at the end of a drag,
// but do it at the beginning for mutators.
if (this.workspace_.isMutator) {
this.draggingBlock_.bringToFront();
}
// During a drag there may be a lot of rerenders, but not field changes.
// Turn the cache on so we don't do spurious remeasures during the drag.
dom.startTextWidthCache();
this.workspace_.setResizesEnabled(false);
blockAnimation.disconnectUiStop();
if (this.shouldDisconnect_(healStack)) {
this.disconnectBlock_(healStack, currentDragDeltaXY);
}
this.draggingBlock_.setDragging(true);
// For future consideration: we may be able to put moveToDragSurface inside
// the block dragger, which would also let the block not track the block
// drag surface.
this.draggingBlock_.moveToDragSurface();
}
/**
* Whether or not we should disconnect the block when a drag is started.
* @param {boolean} healStack Whether or not to heal the stack after
* disconnecting.
* @return {boolean} True to disconnect the block, false otherwise.
* @protected
*/
this.draggingBlock_ = block;
shouldDisconnect_(healStack) {
return !!(
this.draggingBlock_.getParent() ||
(healStack && this.draggingBlock_.nextConnection &&
this.draggingBlock_.nextConnection.targetBlock()));
}
/**
* The workspace on which the block is being dragged.
* @type {!WorkspaceSvg}
* Disconnects the block and moves it to a new location.
* @param {boolean} healStack Whether or not to heal the stack after
* disconnecting.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @protected
*/
this.workspace_ = workspace;
disconnectBlock_(healStack, currentDragDeltaXY) {
this.draggingBlock_.unplug(healStack);
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.translate(newLoc.x, newLoc.y);
blockAnimation.disconnectUiEffect(this.draggingBlock_);
this.draggedConnectionManager_.updateAvailableConnections();
}
/**
* Object that keeps track of connections on dragged blocks.
* @type {!InsertionMarkerManager}
* Fire a UI event at the start of a block drag.
* @protected
*/
this.draggedConnectionManager_ =
new InsertionMarkerManager(this.draggingBlock_);
fireDragStartEvent_() {
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
this.draggingBlock_, true, this.draggingBlock_.getDescendants(false));
eventUtils.fire(event);
}
/**
* Which drag area the mouse pointer is over, if any.
* @type {?IDragTarget}
* @private
* Execute a step of block dragging, based on the given event. Update the
* display accordingly.
* @param {!Event} e The most recent move event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @public
*/
this.dragTarget_ = null;
drag(e, currentDragDeltaXY) {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.moveDuringDrag(newLoc);
this.dragIcons_(delta);
const oldDragTarget = this.dragTarget_;
this.dragTarget_ = this.workspace_.getDragTarget(e);
this.draggedConnectionManager_.update(delta, this.dragTarget_);
const oldWouldDeleteBlock = this.wouldDeleteBlock_;
this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock();
if (oldWouldDeleteBlock !== this.wouldDeleteBlock_) {
// Prevent unnecessary add/remove class calls.
this.updateCursorDuringBlockDrag_();
}
// Call drag enter/exit/over after wouldDeleteBlock is called in
// InsertionMarkerManager.update.
if (this.dragTarget_ !== oldDragTarget) {
oldDragTarget && oldDragTarget.onDragExit(this.draggingBlock_);
this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBlock_);
}
this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBlock_);
}
/**
* Whether the block would be deleted if dropped immediately.
* @type {boolean}
* Finish a block drag and put the block back on the workspace.
* @param {!Event} e The mouseup/touchend event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @public
*/
endDrag(e, currentDragDeltaXY) {
// Make sure internal state is fresh.
this.drag(e, currentDragDeltaXY);
this.dragIconData_ = [];
this.fireDragEndEvent_();
dom.stopTextWidthCache();
blockAnimation.disconnectUiStop();
const preventMove = !!this.dragTarget_ &&
this.dragTarget_.shouldPreventMove(this.draggingBlock_);
/** @type {Coordinate} */
let newLoc;
/** @type {Coordinate} */
let delta;
if (preventMove) {
newLoc = this.startXY_;
} else {
const newValues = this.getNewLocationAfterDrag_(currentDragDeltaXY);
delta = newValues.delta;
newLoc = newValues.newLocation;
}
this.draggingBlock_.moveOffDragSurface(newLoc);
if (this.dragTarget_) {
this.dragTarget_.onDrop(this.draggingBlock_);
}
const deleted = this.maybeDeleteBlock_();
if (!deleted) {
// These are expensive and don't need to be done if we're deleting.
this.draggingBlock_.setDragging(false);
if (delta) { // !preventMove
this.updateBlockAfterMove_(delta);
} else {
// Blocks dragged directly from a flyout may need to be bumped into
// bounds.
bumpObjects.bumpIntoBounds(
this.draggingBlock_.workspace,
this.workspace_.getMetricsManager().getScrollMetrics(true),
this.draggingBlock_);
}
}
this.workspace_.setResizesEnabled(true);
eventUtils.setGroup(false);
}
/**
* Calculates the drag delta and new location values after a block is dragged.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the start of the drag, in pixel units.
* @return {{delta: !Coordinate, newLocation:
* !Coordinate}} New location after drag. delta is in
* workspace units. newLocation is the new coordinate where the block
* should end up.
* @protected
*/
this.wouldDeleteBlock_ = false;
getNewLocationAfterDrag_(currentDragDeltaXY) {
const newValues = {};
newValues.delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
newValues.newLocation = Coordinate.sum(this.startXY_, newValues.delta);
return newValues;
}
/**
* The location of the top left corner of the dragging block at the beginning
* of the drag in workspace coordinates.
* @type {!Coordinate}
* May delete the dragging block, if allowed. If `this.wouldDeleteBlock_` is
* not true, the block will not be deleted. This should be called at the end
* of a block drag.
* @return {boolean} True if the block was deleted.
* @protected
*/
this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY();
maybeDeleteBlock_() {
if (this.wouldDeleteBlock_) {
// Fire a move event, so we know where to go back to for an undo.
this.fireMoveEvent_();
this.draggingBlock_.dispose(false, true);
common.draggingConnections.length = 0;
return true;
}
return false;
}
/**
* A list of all of the icons (comment, warning, and mutator) that are
* on this block and its descendants. Moving an icon moves the bubble that
* extends from it if that bubble is open.
* @type {Array<!Object>}
* Updates the necessary information to place a block at a certain location.
* @param {!Coordinate} delta The change in location from where
* the block started the drag to where it ended the drag.
* @protected
*/
this.dragIconData_ = initIconData(block);
};
updateBlockAfterMove_(delta) {
this.draggingBlock_.moveConnections(delta.x, delta.y);
this.fireMoveEvent_();
if (this.draggedConnectionManager_.wouldConnectBlock()) {
// Applying connections also rerenders the relevant blocks.
this.draggedConnectionManager_.applyConnections();
} else {
this.draggingBlock_.render();
}
this.draggingBlock_.scheduleSnapAndBump();
}
/**
* Sever all links from this object.
* @package
*/
BlockDragger.prototype.dispose = function() {
this.dragIconData_.length = 0;
/**
* Fire a UI event at the end of a block drag.
* @protected
*/
fireDragEndEvent_() {
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
this.draggingBlock_, false, this.draggingBlock_.getDescendants(false));
eventUtils.fire(event);
}
if (this.draggedConnectionManager_) {
this.draggedConnectionManager_.dispose();
/**
* Adds or removes the style of the cursor for the toolbox.
* This is what changes the cursor to display an x when a deletable block is
* held over the toolbox.
* @param {boolean} isEnd True if we are at the end of a drag, false
* otherwise.
* @protected
*/
updateToolboxStyle_(isEnd) {
const toolbox = this.workspace_.getToolbox();
if (toolbox) {
const style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' :
'blocklyToolboxGrab';
if (isEnd && typeof toolbox.removeStyle === 'function') {
toolbox.removeStyle(style);
} else if (!isEnd && typeof toolbox.addStyle === 'function') {
toolbox.addStyle(style);
}
}
}
/**
* Fire a move event at the end of a block drag.
* @protected
*/
fireMoveEvent_() {
const event = /** @type {!BlockMove} */
(new (eventUtils.get(eventUtils.BLOCK_MOVE))(this.draggingBlock_));
event.oldCoordinate = this.startXY_;
event.recordNew();
eventUtils.fire(event);
}
/**
* Update the cursor (and possibly the trash can lid) to reflect whether the
* dragging block would be deleted if released immediately.
* @protected
*/
updateCursorDuringBlockDrag_() {
this.draggingBlock_.setDeleteStyle(this.wouldDeleteBlock_);
}
/**
* Convert a coordinate object from pixels to workspace units, including a
* correction for mutator workspaces.
* This function does not consider differing origins. It simply scales the
* input's x and y values.
* @param {!Coordinate} pixelCoord A coordinate with x and y
* values in CSS pixel units.
* @return {!Coordinate} The input coordinate divided by the
* workspace scale.
* @protected
*/
pixelsToWorkspaceUnits_(pixelCoord) {
const result = new Coordinate(
pixelCoord.x / this.workspace_.scale,
pixelCoord.y / this.workspace_.scale);
if (this.workspace_.isMutator) {
// If we're in a mutator, its scale is always 1, purely because of some
// oddities in our rendering optimizations. The actual scale is the same
// as the scale on the parent workspace. Fix that for dragging.
const mainScale = this.workspace_.options.parentWorkspace.scale;
result.scale(1 / mainScale);
}
return result;
}
/**
* Move all of the icons connected to this drag.
* @param {!Coordinate} dxy How far to move the icons from their
* original positions, in workspace units.
* @protected
*/
dragIcons_(dxy) {
// Moving icons moves their associated bubbles.
for (let i = 0; i < this.dragIconData_.length; i++) {
const data = this.dragIconData_[i];
data.icon.setIconLocation(Coordinate.sum(data.location, dxy));
}
}
/**
* Get a list of the insertion markers that currently exist. Drags have 0, 1,
* or 2 insertion markers.
* @return {!Array<!BlockSvg>} A possibly empty list of insertion
* marker blocks.
* @public
*/
getInsertionMarkers() {
// No insertion markers with the old style of dragged connection managers.
if (this.draggedConnectionManager_ &&
this.draggedConnectionManager_.getInsertionMarkers) {
return this.draggedConnectionManager_.getInsertionMarkers();
}
return [];
}
};
@@ -123,7 +460,8 @@ BlockDragger.prototype.dispose = function() {
const initIconData = function(block) {
// Build a list of icons that need to be moved and where they started.
const dragIconData = [];
const descendants = block.getDescendants(false);
const descendants =
/** @type {!Array<!BlockSvg>} */ (block.getDescendants(false));
for (let i = 0, descendant; (descendant = descendants[i]); i++) {
const icons = descendant.getIcons();
@@ -141,340 +479,6 @@ const initIconData = function(block) {
return dragIconData;
};
/**
* Start dragging a block. This includes moving it to the drag surface.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @param {boolean} healStack Whether or not to heal the stack after
* disconnecting.
* @public
*/
BlockDragger.prototype.startDrag = function(currentDragDeltaXY, healStack) {
if (!eventUtils.getGroup()) {
eventUtils.setGroup(true);
}
this.fireDragStartEvent_();
// Mutators don't have the same type of z-ordering as the normal workspace
// during a drag. They have to rely on the order of the blocks in the SVG.
// For performance reasons that usually happens at the end of a drag,
// but do it at the beginning for mutators.
if (this.workspace_.isMutator) {
this.draggingBlock_.bringToFront();
}
// During a drag there may be a lot of rerenders, but not field changes.
// Turn the cache on so we don't do spurious remeasures during the drag.
dom.startTextWidthCache();
this.workspace_.setResizesEnabled(false);
blockAnimation.disconnectUiStop();
if (this.shouldDisconnect_(healStack)) {
this.disconnectBlock_(healStack, currentDragDeltaXY);
}
this.draggingBlock_.setDragging(true);
// For future consideration: we may be able to put moveToDragSurface inside
// the block dragger, which would also let the block not track the block drag
// surface.
this.draggingBlock_.moveToDragSurface();
};
/**
* Whether or not we should disconnect the block when a drag is started.
* @param {boolean} healStack Whether or not to heal the stack after
* disconnecting.
* @return {boolean} True to disconnect the block, false otherwise.
* @protected
*/
BlockDragger.prototype.shouldDisconnect_ = function(healStack) {
return !!(
this.draggingBlock_.getParent() ||
(healStack && this.draggingBlock_.nextConnection &&
this.draggingBlock_.nextConnection.targetBlock()));
};
/**
* Disconnects the block and moves it to a new location.
* @param {boolean} healStack Whether or not to heal the stack after
* disconnecting.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @protected
*/
BlockDragger.prototype.disconnectBlock_ = function(
healStack, currentDragDeltaXY) {
this.draggingBlock_.unplug(healStack);
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.translate(newLoc.x, newLoc.y);
blockAnimation.disconnectUiEffect(this.draggingBlock_);
this.draggedConnectionManager_.updateAvailableConnections();
};
/**
* Fire a UI event at the start of a block drag.
* @protected
*/
BlockDragger.prototype.fireDragStartEvent_ = function() {
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
this.draggingBlock_, true, this.draggingBlock_.getDescendants(false));
eventUtils.fire(event);
};
/**
* Execute a step of block dragging, based on the given event. Update the
* display accordingly.
* @param {!Event} e The most recent move event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @public
*/
BlockDragger.prototype.drag = function(e, currentDragDeltaXY) {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.moveDuringDrag(newLoc);
this.dragIcons_(delta);
const oldDragTarget = this.dragTarget_;
this.dragTarget_ = this.workspace_.getDragTarget(e);
this.draggedConnectionManager_.update(delta, this.dragTarget_);
const oldWouldDeleteBlock = this.wouldDeleteBlock_;
this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock();
if (oldWouldDeleteBlock !== this.wouldDeleteBlock_) {
// Prevent unnecessary add/remove class calls.
this.updateCursorDuringBlockDrag_();
}
// Call drag enter/exit/over after wouldDeleteBlock is called in
// InsertionMarkerManager.update.
if (this.dragTarget_ !== oldDragTarget) {
oldDragTarget && oldDragTarget.onDragExit(this.draggingBlock_);
this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBlock_);
}
this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBlock_);
};
/**
* Finish a block drag and put the block back on the workspace.
* @param {!Event} e The mouseup/touchend event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @public
*/
BlockDragger.prototype.endDrag = function(e, currentDragDeltaXY) {
// Make sure internal state is fresh.
this.drag(e, currentDragDeltaXY);
this.dragIconData_ = [];
this.fireDragEndEvent_();
dom.stopTextWidthCache();
blockAnimation.disconnectUiStop();
const preventMove = !!this.dragTarget_ &&
this.dragTarget_.shouldPreventMove(this.draggingBlock_);
/** @type {Coordinate} */
let newLoc;
/** @type {Coordinate} */
let delta;
if (preventMove) {
newLoc = this.startXY_;
} else {
const newValues = this.getNewLocationAfterDrag_(currentDragDeltaXY);
delta = newValues.delta;
newLoc = newValues.newLocation;
}
this.draggingBlock_.moveOffDragSurface(newLoc);
if (this.dragTarget_) {
this.dragTarget_.onDrop(this.draggingBlock_);
}
const deleted = this.maybeDeleteBlock_();
if (!deleted) {
// These are expensive and don't need to be done if we're deleting.
this.draggingBlock_.setDragging(false);
if (delta) { // !preventMove
this.updateBlockAfterMove_(delta);
} else {
// Blocks dragged directly from a flyout may need to be bumped into
// bounds.
bumpObjects.bumpIntoBounds(
this.draggingBlock_.workspace,
this.workspace_.getMetricsManager().getScrollMetrics(true),
this.draggingBlock_);
}
}
this.workspace_.setResizesEnabled(true);
eventUtils.setGroup(false);
};
/**
* Calculates the drag delta and new location values after a block is dragged.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the start of the drag, in pixel units.
* @return {{delta: !Coordinate, newLocation:
* !Coordinate}} New location after drag. delta is in
* workspace units. newLocation is the new coordinate where the block should
* end up.
* @protected
*/
BlockDragger.prototype.getNewLocationAfterDrag_ = function(currentDragDeltaXY) {
const newValues = {};
newValues.delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
newValues.newLocation = Coordinate.sum(this.startXY_, newValues.delta);
return newValues;
};
/**
* May delete the dragging block, if allowed. If `this.wouldDeleteBlock_` is not
* true, the block will not be deleted. This should be called at the end of a
* block drag.
* @return {boolean} True if the block was deleted.
* @protected
*/
BlockDragger.prototype.maybeDeleteBlock_ = function() {
if (this.wouldDeleteBlock_) {
// Fire a move event, so we know where to go back to for an undo.
this.fireMoveEvent_();
this.draggingBlock_.dispose(false, true);
common.draggingConnections.length = 0;
return true;
}
return false;
};
/**
* Updates the necessary information to place a block at a certain location.
* @param {!Coordinate} delta The change in location from where
* the block started the drag to where it ended the drag.
* @protected
*/
BlockDragger.prototype.updateBlockAfterMove_ = function(delta) {
this.draggingBlock_.moveConnections(delta.x, delta.y);
this.fireMoveEvent_();
if (this.draggedConnectionManager_.wouldConnectBlock()) {
// Applying connections also rerenders the relevant blocks.
this.draggedConnectionManager_.applyConnections();
} else {
this.draggingBlock_.render();
}
this.draggingBlock_.scheduleSnapAndBump();
};
/**
* Fire a UI event at the end of a block drag.
* @protected
*/
BlockDragger.prototype.fireDragEndEvent_ = function() {
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
this.draggingBlock_, false, this.draggingBlock_.getDescendants(false));
eventUtils.fire(event);
};
/**
* Adds or removes the style of the cursor for the toolbox.
* This is what changes the cursor to display an x when a deletable block is
* held over the toolbox.
* @param {boolean} isEnd True if we are at the end of a drag, false otherwise.
* @protected
*/
BlockDragger.prototype.updateToolboxStyle_ = function(isEnd) {
const toolbox = this.workspace_.getToolbox();
if (toolbox) {
const style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' :
'blocklyToolboxGrab';
if (isEnd && typeof toolbox.removeStyle === 'function') {
toolbox.removeStyle(style);
} else if (!isEnd && typeof toolbox.addStyle === 'function') {
toolbox.addStyle(style);
}
}
};
/**
* Fire a move event at the end of a block drag.
* @protected
*/
BlockDragger.prototype.fireMoveEvent_ = function() {
const event =
new (eventUtils.get(eventUtils.BLOCK_MOVE))(this.draggingBlock_);
event.oldCoordinate = this.startXY_;
event.recordNew();
eventUtils.fire(event);
};
/**
* Update the cursor (and possibly the trash can lid) to reflect whether the
* dragging block would be deleted if released immediately.
* @protected
*/
BlockDragger.prototype.updateCursorDuringBlockDrag_ = function() {
this.draggingBlock_.setDeleteStyle(this.wouldDeleteBlock_);
};
/**
* Convert a coordinate object from pixels to workspace units, including a
* correction for mutator workspaces.
* This function does not consider differing origins. It simply scales the
* input's x and y values.
* @param {!Coordinate} pixelCoord A coordinate with x and y
* values in CSS pixel units.
* @return {!Coordinate} The input coordinate divided by the
* workspace scale.
* @protected
*/
BlockDragger.prototype.pixelsToWorkspaceUnits_ = function(pixelCoord) {
const result = new Coordinate(
pixelCoord.x / this.workspace_.scale,
pixelCoord.y / this.workspace_.scale);
if (this.workspace_.isMutator) {
// If we're in a mutator, its scale is always 1, purely because of some
// oddities in our rendering optimizations. The actual scale is the same as
// the scale on the parent workspace.
// Fix that for dragging.
const mainScale = this.workspace_.options.parentWorkspace.scale;
result.scale(1 / mainScale);
}
return result;
};
/**
* Move all of the icons connected to this drag.
* @param {!Coordinate} dxy How far to move the icons from their
* original positions, in workspace units.
* @protected
*/
BlockDragger.prototype.dragIcons_ = function(dxy) {
// Moving icons moves their associated bubbles.
for (let i = 0; i < this.dragIconData_.length; i++) {
const data = this.dragIconData_[i];
data.icon.setIconLocation(Coordinate.sum(data.location, dxy));
}
};
/**
* Get a list of the insertion markers that currently exist. Drags have 0, 1,
* or 2 insertion markers.
* @return {!Array<!BlockSvg>} A possibly empty list of insertion
* marker blocks.
* @public
*/
BlockDragger.prototype.getInsertionMarkers = function() {
// No insertion markers with the old style of dragged connection managers.
if (this.draggedConnectionManager_ &&
this.draggedConnectionManager_.getInsertionMarkers) {
return this.draggedConnectionManager_.getInsertionMarkers();
}
return [];
};
registry.register(registry.Type.BLOCK_DRAGGER, registry.DEFAULT, BlockDragger);
exports.BlockDragger = BlockDragger;

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,7 @@ const common = goog.require('Blockly.common');
const constants = goog.require('Blockly.constants');
const deprecation = goog.require('Blockly.utils.deprecation');
const dialog = goog.require('Blockly.dialog');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const geras = goog.require('Blockly.geras');
const internalConstants = goog.require('Blockly.internalConstants');
@@ -71,6 +72,7 @@ const {Bubble} = goog.require('Blockly.Bubble');
const {CollapsibleToolboxCategory} = goog.require('Blockly.CollapsibleToolboxCategory');
const {Comment} = goog.require('Blockly.Comment');
const {ComponentManager} = goog.require('Blockly.ComponentManager');
const {config} = goog.require('Blockly.config');
const {ConnectionChecker} = goog.require('Blockly.ConnectionChecker');
const {ConnectionDB} = goog.require('Blockly.ConnectionDB');
const {ConnectionType} = goog.require('Blockly.ConnectionType');
@@ -79,7 +81,6 @@ const {ContextMenuRegistry} = goog.require('Blockly.ContextMenuRegistry');
const {Cursor} = goog.require('Blockly.Cursor');
const {DeleteArea} = goog.require('Blockly.DeleteArea');
const {DragTarget} = goog.require('Blockly.DragTarget');
const {DropDownDiv} = goog.require('Blockly.DropDownDiv');
const {FieldAngle} = goog.require('Blockly.FieldAngle');
const {FieldCheckbox} = goog.require('Blockly.FieldCheckbox');
const {FieldColour} = goog.require('Blockly.FieldColour');
@@ -306,7 +307,8 @@ exports.svgResize = common.svgResize;
* @alias Blockly.hideChaff
*/
const hideChaff = function(opt_onlyClosePopups) {
common.getMainWorkspace().hideChaff(opt_onlyClosePopups);
/** @type {!WorkspaceSvg} */ (common.getMainWorkspace())
.hideChaff(opt_onlyClosePopups);
};
exports.hideChaff = hideChaff;
@@ -331,7 +333,7 @@ exports.defineBlocksWithJsonArray = common.defineBlocksWithJsonArray;
/**
* Set the parent container. This is the container element that the WidgetDiv,
* DropDownDiv, and Tooltip are rendered into the first time `Blockly.inject`
* dropDownDiv, and Tooltip are rendered into the first time `Blockly.inject`
* is called.
* This method is a NOP if called after the first ``Blockly.inject``.
* @param {!Element} container The container element.
@@ -528,7 +530,7 @@ const paste = function() {
deprecation.warn(
'Blockly.paste', 'December 2021', 'December 2022',
'Blockly.clipboard.paste');
return clipboard.paste();
return !!clipboard.paste();
};
exports.paste = paste;
@@ -655,25 +657,8 @@ const bindEventWithChecks_ = function(
exports.bindEventWithChecks_ = bindEventWithChecks_;
// Aliases to allow external code to access these values for legacy reasons.
exports.LINE_MODE_MULTIPLIER = internalConstants.LINE_MODE_MULTIPLIER;
exports.PAGE_MODE_MULTIPLIER = internalConstants.PAGE_MODE_MULTIPLIER;
exports.DRAG_RADIUS = internalConstants.DRAG_RADIUS;
exports.FLYOUT_DRAG_RADIUS = internalConstants.FLYOUT_DRAG_RADIUS;
exports.SNAP_RADIUS = internalConstants.SNAP_RADIUS;
exports.CONNECTING_SNAP_RADIUS = internalConstants.CONNECTING_SNAP_RADIUS;
exports.CURRENT_CONNECTION_PREFERENCE =
internalConstants.CURRENT_CONNECTION_PREFERENCE;
exports.BUMP_DELAY = internalConstants.BUMP_DELAY;
exports.BUMP_RANDOMNESS = internalConstants.BUMP_RANDOMNESS;
exports.COLLAPSE_CHARS = internalConstants.COLLAPSE_CHARS;
exports.LONGPRESS = internalConstants.LONGPRESS;
exports.SOUND_LIMIT = internalConstants.SOUND_LIMIT;
exports.DRAG_STACK = internalConstants.DRAG_STACK;
exports.SPRITE = internalConstants.SPRITE;
exports.DRAG_NONE = internalConstants.DRAG_NONE;
exports.DRAG_STICKY = internalConstants.DRAG_STICKY;
exports.DRAG_BEGIN = internalConstants.DRAG_BEGIN;
exports.DRAG_FREE = internalConstants.DRAG_FREE;
exports.OPPOSITE_TYPE = internalConstants.OPPOSITE_TYPE;
exports.RENAME_VARIABLE_ID = internalConstants.RENAME_VARIABLE_ID;
exports.DELETE_VARIABLE_ID = internalConstants.DELETE_VARIABLE_ID;
@@ -731,7 +716,7 @@ exports.Css = Css;
exports.Cursor = Cursor;
exports.DeleteArea = DeleteArea;
exports.DragTarget = DragTarget;
exports.DropDownDiv = DropDownDiv;
exports.DropDownDiv = dropDownDiv;
exports.Events = Events;
exports.Extensions = Extensions;
exports.Field = Field;
@@ -833,6 +818,7 @@ exports.browserEvents = browserEvents;
exports.bumpObjects = bumpObjects;
exports.clipboard = clipboard;
exports.common = common;
exports.config = config;
/** @deprecated Use Blockly.ConnectionType instead. */
exports.connectionTypes = ConnectionType;
exports.constants = constants;

View File

@@ -16,11 +16,18 @@
goog.module('Blockly.blocks');
/**
* A block definition. For now this very lose, but it can potentially
* be refined e.g. by replacing this typedef with a class definition.
* @typedef {!Object}
*/
let BlockDefinition;
exports.BlockDefinition = BlockDefinition;
/**
* A mapping of block type names to block prototype objects.
* @type {!Object<string,!Object>}
* @type {!Object<string,!BlockDefinition>}
* @alias Blockly.blocks.Blocks
*/
const Blocks = Object.create(null);
exports.Blocks = Blocks;

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.browserEvents');
const Touch = goog.require('Blockly.Touch');
const internalConstants = goog.require('Blockly.internalConstants');
const userAgent = goog.require('Blockly.utils.userAgent');
const {globalThis} = goog.require('Blockly.utils.global');
@@ -30,6 +29,24 @@ const {globalThis} = goog.require('Blockly.utils.global');
let Data;
exports.Data = Data;
/**
* The multiplier for scroll wheel deltas using the line delta mode.
* See https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode
* for more information on deltaMode.
* @type {number}
* @const
*/
const LINE_MODE_MULTIPLIER = 40;
/**
* The multiplier for scroll wheel deltas using the page delta mode.
* See https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode
* for more information on deltaMode.
* @type {number}
* @const
*/
const PAGE_MODE_MULTIPLIER = 125;
/**
* Bind an event handler that can be ignored if it is not part of the active
* touch stream.
@@ -254,13 +271,13 @@ const getScrollDeltaPixels = function(e) {
return {x: e.deltaX, y: e.deltaY};
case 0x01: // Line mode.
return {
x: e.deltaX * internalConstants.LINE_MODE_MULTIPLIER,
y: e.deltaY * internalConstants.LINE_MODE_MULTIPLIER,
x: e.deltaX * LINE_MODE_MULTIPLIER,
y: e.deltaY * LINE_MODE_MULTIPLIER,
};
case 0x02: // Page mode.
return {
x: e.deltaX * internalConstants.PAGE_MODE_MULTIPLIER,
y: e.deltaY * internalConstants.PAGE_MODE_MULTIPLIER,
x: e.deltaX * PAGE_MODE_MULTIPLIER,
y: e.deltaY * PAGE_MODE_MULTIPLIER,
};
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,8 @@ const eventUtils = goog.require('Blockly.Events.utils');
const svgMath = goog.require('Blockly.utils.svgMath');
/* eslint-disable-next-line no-unused-vars */
const {BlockDragSurfaceSvg} = goog.requireType('Blockly.BlockDragSurfaceSvg');
/* eslint-disable-next-line no-unused-vars */
const {CommentMove} = goog.requireType('Blockly.Events.CommentMove');
const {ComponentManager} = goog.require('Blockly.ComponentManager');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
/* eslint-disable-next-line no-unused-vars */
@@ -43,249 +45,254 @@ goog.require('Blockly.constants');
* Class for a bubble dragger. It moves things on the bubble canvas around the
* workspace when they are being dragged by a mouse or touch. These can be
* block comments, mutators, warnings, or workspace comments.
* @param {!IBubble} bubble The item on the bubble canvas to drag.
* @param {!WorkspaceSvg} workspace The workspace to drag on.
* @constructor
* @alias Blockly.BubbleDragger
*/
const BubbleDragger = function(bubble, workspace) {
const BubbleDragger = class {
/**
* The item on the bubble canvas that is being dragged.
* @type {!IBubble}
* @private
* @param {!IBubble} bubble The item on the bubble canvas to drag.
* @param {!WorkspaceSvg} workspace The workspace to drag on.
*/
this.draggingBubble_ = bubble;
constructor(bubble, workspace) {
/**
* The item on the bubble canvas that is being dragged.
* @type {!IBubble}
* @private
*/
this.draggingBubble_ = bubble;
/**
* The workspace on which the bubble is being dragged.
* @type {!WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* The workspace on which the bubble is being dragged.
* @type {!WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* Which drag target the mouse pointer is over, if any.
* @type {?IDragTarget}
* @private
*/
this.dragTarget_ = null;
/**
* Which drag target the mouse pointer is over, if any.
* @type {?IDragTarget}
* @private
*/
this.dragTarget_ = null;
/**
* Whether the bubble would be deleted if dropped immediately.
* @type {boolean}
* @private
*/
this.wouldDeleteBubble_ = false;
/**
* Whether the bubble would be deleted if dropped immediately.
* @type {boolean}
* @private
*/
this.wouldDeleteBubble_ = false;
/**
* The location of the top left corner of the dragging bubble's body at the
* beginning of the drag, in workspace coordinates.
* @type {!Coordinate}
* @private
*/
this.startXY_ = this.draggingBubble_.getRelativeToSurfaceXY();
/**
* The location of the top left corner of the dragging bubble's body at the
* beginning of the drag, in workspace coordinates.
* @type {!Coordinate}
* @private
*/
this.startXY_ = this.draggingBubble_.getRelativeToSurfaceXY();
/**
* The drag surface to move bubbles to during a drag, or null if none should
* be used. Block dragging and bubble dragging use the same surface.
* @type {BlockDragSurfaceSvg}
* @private
*/
this.dragSurface_ =
svgMath.is3dSupported() && !!workspace.getBlockDragSurface() ?
workspace.getBlockDragSurface() :
null;
};
/**
* Sever all links from this object.
* @package
* @suppress {checkTypes}
*/
BubbleDragger.prototype.dispose = function() {
this.draggingBubble_ = null;
this.workspace_ = null;
this.dragSurface_ = null;
};
/**
* Start dragging a bubble. This includes moving it to the drag surface.
* @package
*/
BubbleDragger.prototype.startBubbleDrag = function() {
if (!eventUtils.getGroup()) {
eventUtils.setGroup(true);
/**
* The drag surface to move bubbles to during a drag, or null if none should
* be used. Block dragging and bubble dragging use the same surface.
* @type {BlockDragSurfaceSvg}
* @private
*/
this.dragSurface_ =
svgMath.is3dSupported() && !!workspace.getBlockDragSurface() ?
workspace.getBlockDragSurface() :
null;
}
this.workspace_.setResizesEnabled(false);
this.draggingBubble_.setAutoLayout(false);
if (this.dragSurface_) {
this.moveToDragSurface_();
/**
* Sever all links from this object.
* @package
* @suppress {checkTypes}
*/
dispose() {
this.draggingBubble_ = null;
this.workspace_ = null;
this.dragSurface_ = null;
}
this.draggingBubble_.setDragging && this.draggingBubble_.setDragging(true);
};
/**
* Execute a step of bubble dragging, based on the given event. Update the
* display accordingly.
* @param {!Event} e The most recent move event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
*/
BubbleDragger.prototype.dragBubble = function(e, currentDragDeltaXY) {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBubble_.moveDuringDrag(this.dragSurface_, newLoc);
const oldDragTarget = this.dragTarget_;
this.dragTarget_ = this.workspace_.getDragTarget(e);
const oldWouldDeleteBubble = this.wouldDeleteBubble_;
this.wouldDeleteBubble_ = this.shouldDelete_(this.dragTarget_);
if (oldWouldDeleteBubble !== this.wouldDeleteBubble_) {
// Prevent unnecessary add/remove class calls.
this.updateCursorDuringBubbleDrag_();
}
// Call drag enter/exit/over after wouldDeleteBlock is called in shouldDelete_
if (this.dragTarget_ !== oldDragTarget) {
oldDragTarget && oldDragTarget.onDragExit(this.draggingBubble_);
this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBubble_);
}
this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBubble_);
};
/**
* Whether ending the drag would delete the bubble.
* @param {?IDragTarget} dragTarget The drag target that the bubblee is
* currently over.
* @return {boolean} Whether dropping the bubble immediately would delete the
* block.
* @private
*/
BubbleDragger.prototype.shouldDelete_ = function(dragTarget) {
if (dragTarget) {
const componentManager = this.workspace_.getComponentManager();
const isDeleteArea = componentManager.hasCapability(
dragTarget.id, ComponentManager.Capability.DELETE_AREA);
if (isDeleteArea) {
return (/** @type {!IDeleteArea} */ (dragTarget))
.wouldDelete(this.draggingBubble_, false);
/**
* Start dragging a bubble. This includes moving it to the drag surface.
* @package
*/
startBubbleDrag() {
if (!eventUtils.getGroup()) {
eventUtils.setGroup(true);
}
}
return false;
};
/**
* Update the cursor (and possibly the trash can lid) to reflect whether the
* dragging bubble would be deleted if released immediately.
* @private
*/
BubbleDragger.prototype.updateCursorDuringBubbleDrag_ = function() {
this.draggingBubble_.setDeleteStyle(this.wouldDeleteBubble_);
};
/**
* Finish a bubble drag and put the bubble back on the workspace.
* @param {!Event} e The mouseup/touchend event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
*/
BubbleDragger.prototype.endBubbleDrag = function(e, currentDragDeltaXY) {
// Make sure internal state is fresh.
this.dragBubble(e, currentDragDeltaXY);
const preventMove = this.dragTarget_ &&
this.dragTarget_.shouldPreventMove(this.draggingBubble_);
let newLoc;
if (preventMove) {
newLoc = this.startXY_;
} else {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
newLoc = Coordinate.sum(this.startXY_, delta);
}
// Move the bubble to its final location.
this.draggingBubble_.moveTo(newLoc.x, newLoc.y);
if (this.dragTarget_) {
this.dragTarget_.onDrop(this.draggingBubble_);
}
if (this.wouldDeleteBubble_) {
// Fire a move event, so we know where to go back to for an undo.
this.fireMoveEvent_();
this.draggingBubble_.dispose(false, true);
} else {
// Put everything back onto the bubble canvas.
this.workspace_.setResizesEnabled(false);
this.draggingBubble_.setAutoLayout(false);
if (this.dragSurface_) {
this.dragSurface_.clearAndHide(this.workspace_.getBubbleCanvas());
this.moveToDragSurface_();
}
if (this.draggingBubble_.setDragging) {
this.draggingBubble_.setDragging(false);
this.draggingBubble_.setDragging && this.draggingBubble_.setDragging(true);
}
/**
* Execute a step of bubble dragging, based on the given event. Update the
* display accordingly.
* @param {!Event} e The most recent move event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
*/
dragBubble(e, currentDragDeltaXY) {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBubble_.moveDuringDrag(this.dragSurface_, newLoc);
const oldDragTarget = this.dragTarget_;
this.dragTarget_ = this.workspace_.getDragTarget(e);
const oldWouldDeleteBubble = this.wouldDeleteBubble_;
this.wouldDeleteBubble_ = this.shouldDelete_(this.dragTarget_);
if (oldWouldDeleteBubble !== this.wouldDeleteBubble_) {
// Prevent unnecessary add/remove class calls.
this.updateCursorDuringBubbleDrag_();
}
this.fireMoveEvent_();
// Call drag enter/exit/over after wouldDeleteBlock is called in
// shouldDelete_
if (this.dragTarget_ !== oldDragTarget) {
oldDragTarget && oldDragTarget.onDragExit(this.draggingBubble_);
this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBubble_);
}
this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBubble_);
}
this.workspace_.setResizesEnabled(true);
eventUtils.setGroup(false);
};
/**
* Fire a move event at the end of a bubble drag.
* @private
*/
BubbleDragger.prototype.fireMoveEvent_ = function() {
if (this.draggingBubble_.isComment) {
// TODO (adodson): Resolve build errors when requiring WorkspaceCommentSvg.
const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(
/** @type {!WorkspaceCommentSvg} */ (this.draggingBubble_));
event.setOldCoordinate(this.startXY_);
event.recordNew();
eventUtils.fire(event);
/**
* Whether ending the drag would delete the bubble.
* @param {?IDragTarget} dragTarget The drag target that the bubblee is
* currently over.
* @return {boolean} Whether dropping the bubble immediately would delete the
* block.
* @private
*/
shouldDelete_(dragTarget) {
if (dragTarget) {
const componentManager = this.workspace_.getComponentManager();
const isDeleteArea = componentManager.hasCapability(
dragTarget.id, ComponentManager.Capability.DELETE_AREA);
if (isDeleteArea) {
return (/** @type {!IDeleteArea} */ (dragTarget))
.wouldDelete(this.draggingBubble_, false);
}
}
return false;
}
// TODO (fenichel): move events for comments.
return;
};
/**
* Convert a coordinate object from pixels to workspace units, including a
* correction for mutator workspaces.
* This function does not consider differing origins. It simply scales the
* input's x and y values.
* @param {!Coordinate} pixelCoord A coordinate with x and y
* values in CSS pixel units.
* @return {!Coordinate} The input coordinate divided by the
* workspace scale.
* @private
*/
BubbleDragger.prototype.pixelsToWorkspaceUnits_ = function(pixelCoord) {
const result = new Coordinate(
pixelCoord.x / this.workspace_.scale,
pixelCoord.y / this.workspace_.scale);
if (this.workspace_.isMutator) {
// If we're in a mutator, its scale is always 1, purely because of some
// oddities in our rendering optimizations. The actual scale is the same as
// the scale on the parent workspace.
// Fix that for dragging.
const mainScale = this.workspace_.options.parentWorkspace.scale;
result.scale(1 / mainScale);
/**
* Update the cursor (and possibly the trash can lid) to reflect whether the
* dragging bubble would be deleted if released immediately.
* @private
*/
updateCursorDuringBubbleDrag_() {
this.draggingBubble_.setDeleteStyle(this.wouldDeleteBubble_);
}
return result;
};
/**
* Move the bubble onto the drag surface at the beginning of a drag. Move the
* drag surface to preserve the apparent location of the bubble.
* @private
*/
BubbleDragger.prototype.moveToDragSurface_ = function() {
this.draggingBubble_.moveTo(0, 0);
this.dragSurface_.translateSurface(this.startXY_.x, this.startXY_.y);
// Execute the move on the top-level SVG component.
this.dragSurface_.setBlocksAndShow(this.draggingBubble_.getSvgRoot());
/**
* Finish a bubble drag and put the bubble back on the workspace.
* @param {!Event} e The mouseup/touchend event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
*/
endBubbleDrag(e, currentDragDeltaXY) {
// Make sure internal state is fresh.
this.dragBubble(e, currentDragDeltaXY);
const preventMove = this.dragTarget_ &&
this.dragTarget_.shouldPreventMove(this.draggingBubble_);
let newLoc;
if (preventMove) {
newLoc = this.startXY_;
} else {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
newLoc = Coordinate.sum(this.startXY_, delta);
}
// Move the bubble to its final location.
this.draggingBubble_.moveTo(newLoc.x, newLoc.y);
if (this.dragTarget_) {
this.dragTarget_.onDrop(this.draggingBubble_);
}
if (this.wouldDeleteBubble_) {
// Fire a move event, so we know where to go back to for an undo.
this.fireMoveEvent_();
this.draggingBubble_.dispose(false, true);
} else {
// Put everything back onto the bubble canvas.
if (this.dragSurface_) {
this.dragSurface_.clearAndHide(this.workspace_.getBubbleCanvas());
}
if (this.draggingBubble_.setDragging) {
this.draggingBubble_.setDragging(false);
}
this.fireMoveEvent_();
}
this.workspace_.setResizesEnabled(true);
eventUtils.setGroup(false);
}
/**
* Fire a move event at the end of a bubble drag.
* @private
*/
fireMoveEvent_() {
if (this.draggingBubble_.isComment) {
// TODO (adodson): Resolve build errors when requiring
// WorkspaceCommentSvg.
const event = /** @type {!CommentMove} */
(new (eventUtils.get(eventUtils.COMMENT_MOVE))(
/** @type {!WorkspaceCommentSvg} */ (this.draggingBubble_)));
event.setOldCoordinate(this.startXY_);
event.recordNew();
eventUtils.fire(event);
}
// TODO (fenichel): move events for comments.
return;
}
/**
* Convert a coordinate object from pixels to workspace units, including a
* correction for mutator workspaces.
* This function does not consider differing origins. It simply scales the
* input's x and y values.
* @param {!Coordinate} pixelCoord A coordinate with x and y
* values in CSS pixel units.
* @return {!Coordinate} The input coordinate divided by the
* workspace scale.
* @private
*/
pixelsToWorkspaceUnits_(pixelCoord) {
const result = new Coordinate(
pixelCoord.x / this.workspace_.scale,
pixelCoord.y / this.workspace_.scale);
if (this.workspace_.isMutator) {
// If we're in a mutator, its scale is always 1, purely because of some
// oddities in our rendering optimizations. The actual scale is the same
// as the scale on the parent workspace. Fix that for dragging.
const mainScale = this.workspace_.options.parentWorkspace.scale;
result.scale(1 / mainScale);
}
return result;
}
/**
* Move the bubble onto the drag surface at the beginning of a drag. Move the
* drag surface to preserve the apparent location of the bubble.
* @private
*/
moveToDragSurface_() {
this.draggingBubble_.moveTo(0, 0);
this.dragSurface_.translateSurface(this.startXY_.x, this.startXY_.y);
// Execute the move on the top-level SVG component.
this.dragSurface_.setBlocksAndShow(this.draggingBubble_.getSvgRoot());
}
};
exports.BubbleDragger = BubbleDragger;

View File

@@ -15,11 +15,11 @@
*/
goog.module('Blockly.bumpObjects');
/* eslint-disable-next-line no-unused-vars */
const Abstract = goog.requireType('Blockly.Events.Abstract');
const eventUtils = goog.require('Blockly.Events.utils');
const mathUtils = goog.require('Blockly.utils.math');
/* eslint-disable-next-line no-unused-vars */
const {Abstract} = goog.requireType('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {IBoundedElement} = goog.requireType('Blockly.IBoundedElement');

View File

@@ -15,7 +15,6 @@
*/
goog.module('Blockly.clipboard');
const eventUtils = goog.require('Blockly.Events.utils');
/* eslint-disable-next-line no-unused-vars */
const {ICopyable} = goog.requireType('Blockly.ICopyable');
@@ -39,13 +38,14 @@ exports.copy = copy;
/**
* Paste a block or workspace comment on to the main workspace.
* @return {boolean} True if the paste was successful, false otherwise.
* @return {!ICopyable|null} The pasted thing if the paste
* was successful, null otherwise.
* @alias Blockly.clipboard.paste
* @package
*/
const paste = function() {
if (!copyData) {
return false;
return null;
}
// Pasting always pastes to the main workspace, even if the copy
// started in a flyout workspace.
@@ -55,12 +55,9 @@ const paste = function() {
}
if (copyData.typeCounts &&
workspace.isCapacityAvailable(copyData.typeCounts)) {
eventUtils.setGroup(true);
workspace.paste(copyData.saveInfo);
eventUtils.setGroup(false);
return true;
return workspace.paste(copyData.saveInfo);
}
return false;
return null;
};
exports.paste = paste;
@@ -68,13 +65,16 @@ exports.paste = paste;
* Duplicate this block and its children, or a workspace comment.
* @param {!ICopyable} toDuplicate Block or Workspace Comment to be
* duplicated.
* @return {!ICopyable|null} The block or workspace comment that was duplicated,
* or null if the duplication failed.
* @alias Blockly.clipboard.duplicate
* @package
*/
const duplicate = function(toDuplicate) {
const oldCopyData = copyData;
copy(toDuplicate);
toDuplicate.workspace.paste(copyData.saveInfo);
const pastedThing = toDuplicate.workspace.paste(copyData.saveInfo);
copyData = oldCopyData;
return pastedThing;
};
exports.duplicate = duplicate;

View File

@@ -19,7 +19,6 @@ const Css = goog.require('Blockly.Css');
const browserEvents = goog.require('Blockly.browserEvents');
const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const userAgent = goog.require('Blockly.utils.userAgent');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
@@ -44,384 +43,406 @@ goog.require('Blockly.Warning');
/**
* Class for a comment.
* @param {!Block} block The block associated with this comment.
* @extends {Icon}
* @constructor
* @alias Blockly.Comment
*/
const Comment = function(block) {
Comment.superClass_.constructor.call(this, block);
class Comment extends Icon {
/**
* The model for this comment.
* @type {!Block.CommentModel}
* @private
* @param {!BlockSvg} block The block associated with this comment.
*/
this.model_ = block.commentModel;
// If someone creates the comment directly instead of calling
// block.setCommentText we want to make sure the text is non-null;
this.model_.text = this.model_.text || '';
constructor(block) {
super(block);
/**
* The model's text value at the start of an edit.
* Used to tell if an event should be fired at the end of an edit.
* @type {?string}
* @private
*/
this.cachedText_ = '';
/**
* The model for this comment.
* @type {!Block.CommentModel}
* @private
*/
this.model_ = block.commentModel;
// If someone creates the comment directly instead of calling
// block.setCommentText we want to make sure the text is non-null;
this.model_.text = this.model_.text || '';
/**
* Mouse up event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseUpWrapper_ = null;
/**
* The model's text value at the start of an edit.
* Used to tell if an event should be fired at the end of an edit.
* @type {?string}
* @private
*/
this.cachedText_ = '';
/**
* Wheel event data.
* @type {?browserEvents.Data}
* @private
*/
this.onWheelWrapper_ = null;
/**
* Change event data.
* @type {?browserEvents.Data}
* @private
*/
this.onChangeWrapper_ = null;
/**
* Input event data.
* @type {?browserEvents.Data}
* @private
*/
this.onInputWrapper_ = null;
this.createIcon();
};
object.inherits(Comment, Icon);
/**
* Draw the comment icon.
* @param {!Element} group The icon group.
* @protected
*/
Comment.prototype.drawIcon_ = function(group) {
// Circle.
dom.createSvgElement(
Svg.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.
dom.createSvgElement(
Svg.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 mark.
dom.createSvgElement(
Svg.RECT, {
'class': 'blocklyIconSymbol',
'x': '6.8',
'y': '10.78',
'height': '2',
'width': '2',
},
group);
};
/**
* Create the editor for the comment's bubble.
* @return {!SVGElement} The top-level node of the editor.
* @private
*/
Comment.prototype.createEditor_ = function() {
/* Create the editor. Here's the markup that will be generated in
* editable mode:
<foreignObject x="8" y="8" width="164" height="164">
<body xmlns="http://www.w3.org/1999/xhtml" class="blocklyMinimalBody">
<textarea xmlns="http://www.w3.org/1999/xhtml"
class="blocklyCommentTextarea"
style="height: 164px; width: 164px;"></textarea>
</body>
</foreignObject>
* For non-editable mode see Warning.textToDom_.
*/
this.foreignObject_ = dom.createSvgElement(
Svg.FOREIGNOBJECT, {'x': Bubble.BORDER_WIDTH, 'y': Bubble.BORDER_WIDTH},
null);
const body = document.createElementNS(dom.HTML_NS, 'body');
body.setAttribute('xmlns', dom.HTML_NS);
body.className = 'blocklyMinimalBody';
this.textarea_ = document.createElementNS(dom.HTML_NS, 'textarea');
const textarea = this.textarea_;
textarea.className = 'blocklyCommentTextarea';
textarea.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR');
textarea.value = this.model_.text;
this.resizeTextarea_();
body.appendChild(textarea);
this.foreignObject_.appendChild(body);
// Ideally this would be hooked to the focus event for the comment.
// However doing so in Firefox swallows the cursor for unknown reasons.
// So this is hooked to mouseup instead. No big deal.
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
textarea, 'mouseup', this, this.startEdit_, true, true);
// Don't zoom with mousewheel.
this.onWheelWrapper_ =
browserEvents.conditionalBind(textarea, 'wheel', this, function(e) {
e.stopPropagation();
});
this.onChangeWrapper_ = browserEvents.conditionalBind(
textarea, 'change', this,
/**
* @this {Comment}
* @param {Event} _e Unused event parameter.
*/
function(_e) {
if (this.cachedText_ !== this.model_.text) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this.block_, 'comment', null, this.cachedText_,
this.model_.text));
}
});
this.onInputWrapper_ = browserEvents.conditionalBind(
textarea, 'input', this,
/**
* @this {Comment}
* @param {Event} _e Unused event parameter.
*/
function(_e) {
this.model_.text = textarea.value;
});
setTimeout(textarea.focus.bind(textarea), 0);
return this.foreignObject_;
};
/**
* Add or remove editability of the comment.
* @override
*/
Comment.prototype.updateEditable = function() {
Comment.superClass_.updateEditable.call(this);
if (this.isVisible()) {
// Recreate the bubble with the correct UI.
this.disposeBubble_();
this.createBubble_();
}
};
/**
* Callback function triggered when the bubble has resized.
* Resize the text area accordingly.
* @private
*/
Comment.prototype.onBubbleResize_ = function() {
if (!this.isVisible()) {
return;
}
this.model_.size = this.bubble_.getBubbleSize();
this.resizeTextarea_();
};
/**
* Resizes the text area to match the size defined on the model (which is
* the size of the bubble).
* @private
*/
Comment.prototype.resizeTextarea_ = function() {
const size = this.model_.size;
const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
const widthMinusBorder = size.width - doubleBorderWidth;
const heightMinusBorder = size.height - doubleBorderWidth;
this.foreignObject_.setAttribute('width', widthMinusBorder);
this.foreignObject_.setAttribute('height', heightMinusBorder);
this.textarea_.style.width = (widthMinusBorder - 4) + 'px';
this.textarea_.style.height = (heightMinusBorder - 4) + 'px';
};
/**
* Show or hide the comment bubble.
* @param {boolean} visible True if the bubble should be visible.
*/
Comment.prototype.setVisible = function(visible) {
if (visible === this.isVisible()) {
return;
}
eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
this.block_, visible, 'comment'));
this.model_.pinned = visible;
if (visible) {
this.createBubble_();
} else {
this.disposeBubble_();
}
};
/**
* Show the bubble. Handles deciding if it should be editable or not.
* @private
*/
Comment.prototype.createBubble_ = function() {
if (!this.block_.isEditable() || userAgent.IE) {
// MSIE does not support foreignobject; textareas are impossible.
// https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-svg/56e6e04c-7c8c-44dd-8100-bd745ee42034
// Always treat comments in IE as uneditable.
this.createNonEditableBubble_();
} else {
this.createEditableBubble_();
}
};
/**
* Show an editable bubble.
* @private
*/
Comment.prototype.createEditableBubble_ = function() {
this.bubble_ = new Bubble(
/** @type {!WorkspaceSvg} */ (this.block_.workspace),
this.createEditor_(), this.block_.pathObject.svgPath,
/** @type {!Coordinate} */ (this.iconXY_), this.model_.size.width,
this.model_.size.height);
// Expose this comment's block's ID on its top-level SVG group.
this.bubble_.setSvgId(this.block_.id);
this.bubble_.registerResizeEvent(this.onBubbleResize_.bind(this));
this.applyColour();
};
/**
* Show a non-editable bubble.
* @private
* @suppress {checkTypes} Suppress `this` type mismatch.
*/
Comment.prototype.createNonEditableBubble_ = function() {
// TODO (#2917): It would be great if the comment could support line breaks.
this.paragraphElement_ = Bubble.textToDom(this.block_.getCommentText());
this.bubble_ = Bubble.createNonEditableBubble(
this.paragraphElement_, /** @type {!BlockSvg} */ (this.block_),
/** @type {!Coordinate} */ (this.iconXY_));
this.applyColour();
};
/**
* Dispose of the bubble.
* @private
* @suppress {checkTypes} Suppress `this` type mismatch.
*/
Comment.prototype.disposeBubble_ = function() {
if (this.onMouseUpWrapper_) {
browserEvents.unbind(this.onMouseUpWrapper_);
/**
* Mouse up event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseUpWrapper_ = null;
}
if (this.onWheelWrapper_) {
browserEvents.unbind(this.onWheelWrapper_);
/**
* Wheel event data.
* @type {?browserEvents.Data}
* @private
*/
this.onWheelWrapper_ = null;
}
if (this.onChangeWrapper_) {
browserEvents.unbind(this.onChangeWrapper_);
/**
* Change event data.
* @type {?browserEvents.Data}
* @private
*/
this.onChangeWrapper_ = null;
}
if (this.onInputWrapper_) {
browserEvents.unbind(this.onInputWrapper_);
/**
* Input event data.
* @type {?browserEvents.Data}
* @private
*/
this.onInputWrapper_ = null;
}
this.bubble_.dispose();
this.bubble_ = null;
this.textarea_ = null;
this.foreignObject_ = null;
this.paragraphElement_ = null;
};
/**
* Callback fired when an edit starts.
*
* Bring the comment to the top of the stack when clicked on. Also cache the
* current text so it can be used to fire a change event.
* @param {!Event} _e Mouse up event.
* @private
*/
Comment.prototype.startEdit_ = function(_e) {
if (this.bubble_.promote()) {
// Since the act of moving this node within the DOM causes a loss of focus,
// we need to reapply the focus.
this.textarea_.focus();
/**
* The SVG element that contains the text edit area, or null if not created.
* @type {?SVGForeignObjectElement}
* @private
*/
this.foreignObject_ = null;
/**
* The editable text area, or null if not created.
* @type {?Element}
* @private
*/
this.textarea_ = null;
/**
* The top-level node of the comment text, or null if not created.
* @type {?SVGTextElement}
* @private
*/
this.paragraphElement_ = null;
this.createIcon();
}
this.cachedText_ = this.model_.text;
};
/**
* Get the dimensions of this comment's bubble.
* @return {Size} Object with width and height properties.
*/
Comment.prototype.getBubbleSize = function() {
return this.model_.size;
};
/**
* Size this comment's bubble.
* @param {number} width Width of the bubble.
* @param {number} height Height of the bubble.
*/
Comment.prototype.setBubbleSize = function(width, height) {
if (this.bubble_) {
this.bubble_.setBubbleSize(width, height);
} else {
this.model_.size.width = width;
this.model_.size.height = height;
/**
* Draw the comment icon.
* @param {!Element} group The icon group.
* @protected
*/
drawIcon_(group) {
// Circle.
dom.createSvgElement(
Svg.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.
dom.createSvgElement(
Svg.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 mark.
dom.createSvgElement(
Svg.RECT, {
'class': 'blocklyIconSymbol',
'x': '6.8',
'y': '10.78',
'height': '2',
'width': '2',
},
group);
}
};
/**
* Update the comment's view to match the model.
* @package
*/
Comment.prototype.updateText = function() {
if (this.textarea_) {
this.textarea_.value = this.model_.text;
} else if (this.paragraphElement_) {
// Non-Editable mode.
// TODO (#2917): If 2917 gets added this will probably need to be updated.
this.paragraphElement_.firstChild.textContent = this.model_.text;
/**
* Create the editor for the comment's bubble.
* @return {!SVGElement} The top-level node of the editor.
* @private
*/
createEditor_() {
/* Create the editor. Here's the markup that will be generated in
* editable mode:
<foreignObject x="8" y="8" width="164" height="164">
<body xmlns="http://www.w3.org/1999/xhtml" class="blocklyMinimalBody">
<textarea xmlns="http://www.w3.org/1999/xhtml"
class="blocklyCommentTextarea"
style="height: 164px; width: 164px;"></textarea>
</body>
</foreignObject>
* For non-editable mode see Warning.textToDom_.
*/
this.foreignObject_ = dom.createSvgElement(
Svg.FOREIGNOBJECT, {'x': Bubble.BORDER_WIDTH, 'y': Bubble.BORDER_WIDTH},
null);
const body = document.createElementNS(dom.HTML_NS, 'body');
body.setAttribute('xmlns', dom.HTML_NS);
body.className = 'blocklyMinimalBody';
this.textarea_ = document.createElementNS(dom.HTML_NS, 'textarea');
const textarea = this.textarea_;
textarea.className = 'blocklyCommentTextarea';
textarea.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR');
textarea.value = this.model_.text;
this.resizeTextarea_();
body.appendChild(textarea);
this.foreignObject_.appendChild(body);
// Ideally this would be hooked to the focus event for the comment.
// However doing so in Firefox swallows the cursor for unknown reasons.
// So this is hooked to mouseup instead. No big deal.
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
textarea, 'mouseup', this, this.startEdit_, true, true);
// Don't zoom with mousewheel.
this.onWheelWrapper_ =
browserEvents.conditionalBind(textarea, 'wheel', this, function(e) {
e.stopPropagation();
});
this.onChangeWrapper_ = browserEvents.conditionalBind(
textarea, 'change', this,
/**
* @this {Comment}
* @param {Event} _e Unused event parameter.
*/
function(_e) {
if (this.cachedText_ !== this.model_.text) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this.block_, 'comment', null, this.cachedText_,
this.model_.text));
}
});
this.onInputWrapper_ = browserEvents.conditionalBind(
textarea, 'input', this,
/**
* @this {Comment}
* @param {Event} _e Unused event parameter.
*/
function(_e) {
this.model_.text = textarea.value;
});
setTimeout(textarea.focus.bind(textarea), 0);
return this.foreignObject_;
}
};
/**
* Dispose of this comment.
*
* If you want to receive a comment "delete" event (newValue: null), then this
* should not be called directly. Instead call block.setCommentText(null);
*/
Comment.prototype.dispose = function() {
this.block_.comment = null;
Icon.prototype.dispose.call(this);
};
/**
* Add or remove editability of the comment.
* @override
*/
updateEditable() {
super.updateEditable();
if (this.isVisible()) {
// Recreate the bubble with the correct UI.
this.disposeBubble_();
this.createBubble_();
}
}
/**
* Callback function triggered when the bubble has resized.
* Resize the text area accordingly.
* @private
*/
onBubbleResize_() {
if (!this.isVisible()) {
return;
}
this.model_.size = this.bubble_.getBubbleSize();
this.resizeTextarea_();
}
/**
* Resizes the text area to match the size defined on the model (which is
* the size of the bubble).
* @private
*/
resizeTextarea_() {
const size = this.model_.size;
const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
const widthMinusBorder = size.width - doubleBorderWidth;
const heightMinusBorder = size.height - doubleBorderWidth;
this.foreignObject_.setAttribute('width', widthMinusBorder);
this.foreignObject_.setAttribute('height', heightMinusBorder);
this.textarea_.style.width = (widthMinusBorder - 4) + 'px';
this.textarea_.style.height = (heightMinusBorder - 4) + 'px';
}
/**
* Show or hide the comment bubble.
* @param {boolean} visible True if the bubble should be visible.
*/
setVisible(visible) {
if (visible === this.isVisible()) {
return;
}
eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
this.block_, visible, 'comment'));
this.model_.pinned = visible;
if (visible) {
this.createBubble_();
} else {
this.disposeBubble_();
}
}
/**
* Show the bubble. Handles deciding if it should be editable or not.
* @private
*/
createBubble_() {
if (!this.block_.isEditable() || userAgent.IE) {
// MSIE does not support foreignobject; textareas are impossible.
// https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-svg/56e6e04c-7c8c-44dd-8100-bd745ee42034
// Always treat comments in IE as uneditable.
this.createNonEditableBubble_();
} else {
this.createEditableBubble_();
}
}
/**
* Show an editable bubble.
* @private
*/
createEditableBubble_() {
this.bubble_ = new Bubble(
/** @type {!WorkspaceSvg} */ (this.block_.workspace),
this.createEditor_(), this.block_.pathObject.svgPath,
/** @type {!Coordinate} */ (this.iconXY_), this.model_.size.width,
this.model_.size.height);
// Expose this comment's block's ID on its top-level SVG group.
this.bubble_.setSvgId(this.block_.id);
this.bubble_.registerResizeEvent(this.onBubbleResize_.bind(this));
this.applyColour();
}
/**
* Show a non-editable bubble.
* @private
* @suppress {checkTypes} Suppress `this` type mismatch.
*/
createNonEditableBubble_() {
// TODO (#2917): It would be great if the comment could support line breaks.
this.paragraphElement_ = Bubble.textToDom(this.block_.getCommentText());
this.bubble_ = Bubble.createNonEditableBubble(
this.paragraphElement_, /** @type {!BlockSvg} */ (this.block_),
/** @type {!Coordinate} */ (this.iconXY_));
this.applyColour();
}
/**
* Dispose of the bubble.
* @private
* @suppress {checkTypes} Suppress `this` type mismatch.
*/
disposeBubble_() {
if (this.onMouseUpWrapper_) {
browserEvents.unbind(this.onMouseUpWrapper_);
this.onMouseUpWrapper_ = null;
}
if (this.onWheelWrapper_) {
browserEvents.unbind(this.onWheelWrapper_);
this.onWheelWrapper_ = null;
}
if (this.onChangeWrapper_) {
browserEvents.unbind(this.onChangeWrapper_);
this.onChangeWrapper_ = null;
}
if (this.onInputWrapper_) {
browserEvents.unbind(this.onInputWrapper_);
this.onInputWrapper_ = null;
}
this.bubble_.dispose();
this.bubble_ = null;
this.textarea_ = null;
this.foreignObject_ = null;
this.paragraphElement_ = null;
}
/**
* Callback fired when an edit starts.
*
* Bring the comment to the top of the stack when clicked on. Also cache the
* current text so it can be used to fire a change event.
* @param {!Event} _e Mouse up event.
* @private
*/
startEdit_(_e) {
if (this.bubble_.promote()) {
// Since the act of moving this node within the DOM causes a loss of
// focus, we need to reapply the focus.
this.textarea_.focus();
}
this.cachedText_ = this.model_.text;
}
/**
* Get the dimensions of this comment's bubble.
* @return {Size} Object with width and height properties.
*/
getBubbleSize() {
return this.model_.size;
}
/**
* Size this comment's bubble.
* @param {number} width Width of the bubble.
* @param {number} height Height of the bubble.
*/
setBubbleSize(width, height) {
if (this.bubble_) {
this.bubble_.setBubbleSize(width, height);
} else {
this.model_.size.width = width;
this.model_.size.height = height;
}
}
/**
* Update the comment's view to match the model.
* @package
*/
updateText() {
if (this.textarea_) {
this.textarea_.value = this.model_.text;
} else if (this.paragraphElement_) {
// Non-Editable mode.
// TODO (#2917): If 2917 gets added this will probably need to be updated.
this.paragraphElement_.firstChild.textContent = this.model_.text;
}
}
/**
* Dispose of this comment.
*
* If you want to receive a comment "delete" event (newValue: null), then this
* should not be called directly. Instead call block.setCommentText(null);
*/
dispose() {
this.block_.comment = null;
Icon.prototype.dispose.call(this);
}
}
/**
* CSS for block comment. See css.js for use.
*/
Css.register(`
.blocklyCommentTextarea {
background-color: #fef49c;
border: 0;
display: block;
margin: 0;
outline: 0;
padding: 3px;
resize: none;
text-overflow: hidden;
}
.blocklyCommentTextarea {
background-color: #fef49c;
border: 0;
display: block;
margin: 0;
outline: 0;
padding: 3px;
resize: none;
text-overflow: hidden;
}
`);
exports.Comment = Comment;

View File

@@ -17,7 +17,8 @@
*/
goog.module('Blockly.common');
const {Blocks} = goog.require('Blockly.blocks');
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition, Blocks} = goog.require('Blockly.blocks');
/* eslint-disable-next-line no-unused-vars */
const {Connection} = goog.requireType('Blockly.Connection');
/* eslint-disable-next-line no-unused-vars */
@@ -210,27 +211,55 @@ const jsonInitFactory = function(jsonDef) {
* @alias Blockly.common.defineBlocksWithJsonArray
*/
const defineBlocksWithJsonArray = function(jsonArray) {
defineBlocks(createBlockDefinitionsFromJsonArray(jsonArray));
};
exports.defineBlocksWithJsonArray = defineBlocksWithJsonArray;
/**
* 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.
* @return {!Object<string, !BlockDefinition>} A map of the block
* definitions created.
* @alias Blockly.common.defineBlocksWithJsonArray
*/
const createBlockDefinitionsFromJsonArray = function(jsonArray) {
const /** @type {!Object<string,!BlockDefinition>} */ blocks = {};
for (let i = 0; i < jsonArray.length; i++) {
const elem = jsonArray[i];
if (!elem) {
console.warn(
'Block definition #' + i + ' in JSON array is ' + elem + '. ' +
'Skipping.');
} else {
const typename = elem.type;
if (!typename) {
console.warn(
'Block definition #' + i +
' in JSON array is missing a type attribute. Skipping.');
} else {
if (Blocks[typename]) {
console.warn(
'Block definition #' + i + ' in JSON array' +
' overwrites prior definition of "' + typename + '".');
}
Blocks[typename] = {init: jsonInitFactory(elem)};
}
console.warn(`Block definition #${i} in JSON array is ${elem}. Skipping`);
continue;
}
const type = elem.type;
if (!type) {
console.warn(
`Block definition #${i} in JSON array is missing a type attribute. ` +
'Skipping.');
continue;
}
blocks[type] = {init: jsonInitFactory(elem)};
}
return blocks;
};
exports.createBlockDefinitionsFromJsonArray =
createBlockDefinitionsFromJsonArray;
/**
* Add the specified block definitions to the block definitions
* dictionary (Blockly.Blocks).
* @param {!Object<string,!BlockDefinition>} blocks A map of block
* type names to block definitions.
* @alias Blockly.common.defineBlocks
*/
const defineBlocks = function(blocks) {
// Iterate over own enumerable properties.
for (const type of Object.keys(blocks)) {
const definition = blocks[type];
if (type in Blocks) {
console.warn(`Block definiton "${type}" overwrites previous definition.`);
}
Blocks[type] = definition;
}
};
exports.defineBlocksWithJsonArray = defineBlocksWithJsonArray;
exports.defineBlocks = defineBlocks;

View File

@@ -31,24 +31,179 @@ const {IPositionable} = goog.requireType('Blockly.IPositionable');
/**
* Manager for all items registered with the workspace.
* @constructor
* @alias Blockly.ComponentManager
*/
const ComponentManager = function() {
class ComponentManager {
/**
* A map of the components registered with the workspace, mapped to id.
* @type {!Object<string, !ComponentManager.ComponentDatum>}
* @private
* Creates a new ComponentManager instance.
*/
this.componentData_ = Object.create(null);
constructor() {
/**
* A map of the components registered with the workspace, mapped to id.
* @type {!Object<string, !ComponentManager.ComponentDatum>}
* @private
*/
this.componentData_ = Object.create(null);
/**
* A map of capabilities to component IDs.
* @type {!Object<string, !Array<string>>}
* @private
*/
this.capabilityToComponentIds_ = Object.create(null);
}
/**
* A map of capabilities to component IDs.
* @type {!Object<string, !Array<string>>}
* @private
* Adds a component.
* @param {!ComponentManager.ComponentDatum} componentInfo The data for
* the component to register.
* @param {boolean=} opt_allowOverrides True to prevent an error when
* overriding an already registered item.
*/
this.capabilityToComponentIds_ = Object.create(null);
};
addComponent(componentInfo, opt_allowOverrides) {
// Don't throw an error if opt_allowOverrides is true.
const id = componentInfo.component.id;
if (!opt_allowOverrides && this.componentData_[id]) {
throw Error(
'Plugin "' + id + '" with capabilities "' +
this.componentData_[id].capabilities + '" already added.');
}
this.componentData_[id] = componentInfo;
const stringCapabilities = [];
for (let i = 0; i < componentInfo.capabilities.length; i++) {
const capability = String(componentInfo.capabilities[i]).toLowerCase();
stringCapabilities.push(capability);
if (this.capabilityToComponentIds_[capability] === undefined) {
this.capabilityToComponentIds_[capability] = [id];
} else {
this.capabilityToComponentIds_[capability].push(id);
}
}
this.componentData_[id].capabilities = stringCapabilities;
}
/**
* Removes a component.
* @param {string} id The ID of the component to remove.
*/
removeComponent(id) {
const componentInfo = this.componentData_[id];
if (!componentInfo) {
return;
}
for (let i = 0; i < componentInfo.capabilities.length; i++) {
const capability = String(componentInfo.capabilities[i]).toLowerCase();
arrayUtils.removeElem(this.capabilityToComponentIds_[capability], id);
}
delete this.componentData_[id];
}
/**
* Adds a capability to a existing registered component.
* @param {string} id The ID of the component to add the capability to.
* @param {string|!ComponentManager.Capability<T>} capability The
* capability to add.
* @template T
*/
addCapability(id, capability) {
if (!this.getComponent(id)) {
throw Error(
'Cannot add capability, "' + capability + '". Plugin "' + id +
'" has not been added to the ComponentManager');
}
if (this.hasCapability(id, capability)) {
console.warn(
'Plugin "' + id + 'already has capability "' + capability + '"');
return;
}
capability = String(capability).toLowerCase();
this.componentData_[id].capabilities.push(capability);
this.capabilityToComponentIds_[capability].push(id);
}
/**
* Removes a capability from an existing registered component.
* @param {string} id The ID of the component to remove the capability from.
* @param {string|!ComponentManager.Capability<T>} capability The
* capability to remove.
* @template T
*/
removeCapability(id, capability) {
if (!this.getComponent(id)) {
throw Error(
'Cannot remove capability, "' + capability + '". Plugin "' + id +
'" has not been added to the ComponentManager');
}
if (!this.hasCapability(id, capability)) {
console.warn(
'Plugin "' + id + 'doesn\'t have capability "' + capability +
'" to remove');
return;
}
capability = String(capability).toLowerCase();
arrayUtils.removeElem(this.componentData_[id].capabilities, capability);
arrayUtils.removeElem(this.capabilityToComponentIds_[capability], id);
}
/**
* Returns whether the component with this id has the specified capability.
* @param {string} id The ID of the component to check.
* @param {string|!ComponentManager.Capability<T>} capability The
* capability to check for.
* @return {boolean} Whether the component has the capability.
* @template T
*/
hasCapability(id, capability) {
capability = String(capability).toLowerCase();
return this.componentData_[id].capabilities.indexOf(capability) !== -1;
}
/**
* Gets the component with the given ID.
* @param {string} id The ID of the component to get.
* @return {!IComponent|undefined} The component with the given name
* or undefined if not found.
*/
getComponent(id) {
return this.componentData_[id] && this.componentData_[id].component;
}
/**
* Gets all the components with the specified capability.
* @param {string|!ComponentManager.Capability<T>
* } capability The capability of the component.
* @param {boolean} sorted Whether to return list ordered by weights.
* @return {!Array<T>} The components that match the specified capability.
* @template T
*/
getComponents(capability, sorted) {
capability = String(capability).toLowerCase();
const componentIds = this.capabilityToComponentIds_[capability];
if (!componentIds) {
return [];
}
const components = [];
if (sorted) {
const componentDataList = [];
const componentData = this.componentData_;
componentIds.forEach(function(id) {
componentDataList.push(componentData[id]);
});
componentDataList.sort(function(a, b) {
return a.weight - b.weight;
});
componentDataList.forEach(function(ComponentDatum) {
components.push(ComponentDatum.component);
});
} else {
const componentData = this.componentData_;
componentIds.forEach(function(id) {
components.push(componentData[id].component);
});
}
return components;
}
}
/**
* An object storing component information.
@@ -62,179 +217,31 @@ const ComponentManager = function() {
*/
ComponentManager.ComponentDatum;
/**
* Adds a component.
* @param {!ComponentManager.ComponentDatum} componentInfo The data for
* the component to register.
* @param {boolean=} opt_allowOverrides True to prevent an error when overriding
* an already registered item.
*/
ComponentManager.prototype.addComponent = function(
componentInfo, opt_allowOverrides) {
// Don't throw an error if opt_allowOverrides is true.
const id = componentInfo.component.id;
if (!opt_allowOverrides && this.componentData_[id]) {
throw Error(
'Plugin "' + id + '" with capabilities "' +
this.componentData_[id].capabilities + '" already added.');
}
this.componentData_[id] = componentInfo;
const stringCapabilities = [];
for (let i = 0; i < componentInfo.capabilities.length; i++) {
const capability = String(componentInfo.capabilities[i]).toLowerCase();
stringCapabilities.push(capability);
if (this.capabilityToComponentIds_[capability] === undefined) {
this.capabilityToComponentIds_[capability] = [id];
} else {
this.capabilityToComponentIds_[capability].push(id);
}
}
this.componentData_[id].capabilities = stringCapabilities;
};
/**
* Removes a component.
* @param {string} id The ID of the component to remove.
*/
ComponentManager.prototype.removeComponent = function(id) {
const componentInfo = this.componentData_[id];
if (!componentInfo) {
return;
}
for (let i = 0; i < componentInfo.capabilities.length; i++) {
const capability = String(componentInfo.capabilities[i]).toLowerCase();
arrayUtils.removeElem(this.capabilityToComponentIds_[capability], id);
}
delete this.componentData_[id];
};
/**
* Adds a capability to a existing registered component.
* @param {string} id The ID of the component to add the capability to.
* @param {string|!ComponentManager.Capability<T>} capability The
* capability to add.
* @template T
*/
ComponentManager.prototype.addCapability = function(id, capability) {
if (!this.getComponent(id)) {
throw Error(
'Cannot add capability, "' + capability + '". Plugin "' + id +
'" has not been added to the ComponentManager');
}
if (this.hasCapability(id, capability)) {
console.warn(
'Plugin "' + id + 'already has capability "' + capability + '"');
return;
}
capability = String(capability).toLowerCase();
this.componentData_[id].capabilities.push(capability);
this.capabilityToComponentIds_[capability].push(id);
};
/**
* Removes a capability from an existing registered component.
* @param {string} id The ID of the component to remove the capability from.
* @param {string|!ComponentManager.Capability<T>} capability The
* capability to remove.
* @template T
*/
ComponentManager.prototype.removeCapability = function(id, capability) {
if (!this.getComponent(id)) {
throw Error(
'Cannot remove capability, "' + capability + '". Plugin "' + id +
'" has not been added to the ComponentManager');
}
if (!this.hasCapability(id, capability)) {
console.warn(
'Plugin "' + id + 'doesn\'t have capability "' + capability +
'" to remove');
return;
}
capability = String(capability).toLowerCase();
arrayUtils.removeElem(this.componentData_[id].capabilities, capability);
arrayUtils.removeElem(this.capabilityToComponentIds_[capability], id);
};
/**
* Returns whether the component with this id has the specified capability.
* @param {string} id The ID of the component to check.
* @param {string|!ComponentManager.Capability<T>} capability The
* capability to check for.
* @return {boolean} Whether the component has the capability.
* @template T
*/
ComponentManager.prototype.hasCapability = function(id, capability) {
capability = String(capability).toLowerCase();
return this.componentData_[id].capabilities.indexOf(capability) !== -1;
};
/**
* Gets the component with the given ID.
* @param {string} id The ID of the component to get.
* @return {!IComponent|undefined} The component with the given name
* or undefined if not found.
*/
ComponentManager.prototype.getComponent = function(id) {
return this.componentData_[id] && this.componentData_[id].component;
};
/**
* Gets all the components with the specified capability.
* @param {string|!ComponentManager.Capability<T>
* } capability The capability of the component.
* @param {boolean} sorted Whether to return list ordered by weights.
* @return {!Array<T>} The components that match the specified capability.
* @template T
*/
ComponentManager.prototype.getComponents = function(capability, sorted) {
capability = String(capability).toLowerCase();
const componentIds = this.capabilityToComponentIds_[capability];
if (!componentIds) {
return [];
}
const components = [];
if (sorted) {
const componentDataList = [];
const componentData = this.componentData_;
componentIds.forEach(function(id) {
componentDataList.push(componentData[id]);
});
componentDataList.sort(function(a, b) {
return a.weight - b.weight;
});
componentDataList.forEach(function(ComponentDatum) {
components.push(ComponentDatum.component);
});
} else {
const componentData = this.componentData_;
componentIds.forEach(function(id) {
components.push(componentData[id].component);
});
}
return components;
};
/**
* A name with the capability of the element stored in the generic.
* @param {string} name The name of the component capability.
* @constructor
* @template T
* @alias Blockly.ComponentManager.Capability
*/
ComponentManager.Capability = function(name) {
ComponentManager.Capability = class {
/**
* @type {string}
* @private
* @param {string} name The name of the component capability.
*/
this.name_ = name;
};
constructor(name) {
/**
* @type {string}
* @private
*/
this.name_ = name;
}
/**
* Returns the name of the capability.
* @return {string} The name.
* @override
*/
ComponentManager.Capability.prototype.toString = function() {
return this.name_;
/**
* Returns the name of the capability.
* @return {string} The name.
* @override
*/
toString() {
return this.name_;
}
};
/** @type {!ComponentManager.Capability<!IPositionable>} */

87
core/config.js Normal file
View File

@@ -0,0 +1,87 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview All the values that we expect developers to be able to change
* before injecting Blockly. Changing these values during run time is not
* generally recommended.
*/
'use strict';
/**
* All the values that we expect developers to be able to change
* before injecting Blockly. Changing these values during run time is not
* generally recommended.
* @namespace Blockly.config
*/
goog.module('Blockly.config');
/**
* All the values that we expect developers to be able to change
* before injecting Blockly.
* @typedef {{
* dragRadius: number,
* flyoutDragRadius: number,
* snapRadius: number,
* currentConnectionPreference: number,
* bumpDelay: number,
* connectingSnapRadius: number
* }}
*/
let Config; // eslint-disable-line no-unused-vars
/**
* Default snap radius.
* @type {number}
*/
const DEFAULT_SNAP_RADIUS = 28;
/**
* Object holding all the values on Blockly that we expect developers to be
* able to change.
* @type {Config}
*/
const config = {
/**
* Number of pixels the mouse must move before a drag starts.
* @alias Blockly.config.dragRadius
*/
dragRadius: 5,
/**
* Number of pixels the mouse must move before a drag/scroll starts from the
* flyout. Because the drag-intention is determined when this is reached, it
* is larger than dragRadius so that the drag-direction is clearer.
* @alias Blockly.config.flyoutDragRadius
*/
flyoutDragRadius: 10,
/**
* Maximum misalignment between connections for them to snap together.
* @alias Blockly.config.snapRadius
*/
snapRadius: DEFAULT_SNAP_RADIUS,
/**
* Maximum misalignment between connections for them to snap together.
* This should be the same as the snap radius.
* @alias Blockly.config.connectingSnapRadius
*/
connectingSnapRadius: DEFAULT_SNAP_RADIUS,
/**
* How much to prefer staying connected to the current connection over moving
* to a new connection. The current previewed connection is considered to be
* this much closer to the matching connection on the block than it actually
* is.
* @alias Blockly.config.currentConnectionPreference
*/
currentConnectionPreference: 8,
/**
* Delay in ms between trigger and bumping unconnected block out of alignment.
* @alias Blockly.config.bumpDelay
*/
bumpDelay: 250,
};
exports.config = config;

File diff suppressed because it is too large Load Diff

View File

@@ -31,282 +31,280 @@ const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
/**
* Class for connection type checking logic.
* @implements {IConnectionChecker}
* @constructor
* @alias Blockly.ConnectionChecker
*/
const ConnectionChecker = function() {};
/**
* Check whether the current connection can connect with the target
* connection.
* @param {Connection} a Connection to check compatibility with.
* @param {Connection} b Connection to check compatibility with.
* @param {boolean} isDragging True if the connection is being made by dragging
* a block.
* @param {number=} opt_distance The max allowable distance between the
* connections for drag checks.
* @return {boolean} Whether the connection is legal.
* @public
*/
ConnectionChecker.prototype.canConnect = function(
a, b, isDragging, opt_distance) {
return this.canConnectWithReason(a, b, isDragging, opt_distance) ===
Connection.CAN_CONNECT;
};
/**
* Checks whether the current connection can connect with the target
* connection, and return an error code if there are problems.
* @param {Connection} a Connection to check compatibility with.
* @param {Connection} b Connection to check compatibility with.
* @param {boolean} isDragging True if the connection is being made by dragging
* a block.
* @param {number=} opt_distance The max allowable distance between the
* connections for drag checks.
* @return {number} Connection.CAN_CONNECT if the connection is legal,
* an error code otherwise.
* @public
*/
ConnectionChecker.prototype.canConnectWithReason = function(
a, b, isDragging, opt_distance) {
const safety = this.doSafetyChecks(a, b);
if (safety !== Connection.CAN_CONNECT) {
return safety;
class ConnectionChecker {
/**
* Check whether the current connection can connect with the target
* connection.
* @param {Connection} a Connection to check compatibility with.
* @param {Connection} b Connection to check compatibility with.
* @param {boolean} isDragging True if the connection is being made by
* dragging a block.
* @param {number=} opt_distance The max allowable distance between the
* connections for drag checks.
* @return {boolean} Whether the connection is legal.
* @public
*/
canConnect(a, b, isDragging, opt_distance) {
return this.canConnectWithReason(a, b, isDragging, opt_distance) ===
Connection.CAN_CONNECT;
}
// If the safety checks passed, both connections are non-null.
const connOne = /** @type {!Connection} **/ (a);
const connTwo = /** @type {!Connection} **/ (b);
if (!this.doTypeChecks(connOne, connTwo)) {
return Connection.REASON_CHECKS_FAILED;
}
if (isDragging &&
!this.doDragChecks(
/** @type {!RenderedConnection} **/ (a),
/** @type {!RenderedConnection} **/ (b), opt_distance || 0)) {
return Connection.REASON_DRAG_CHECKS_FAILED;
}
return Connection.CAN_CONNECT;
};
/**
* Helper method that translates a connection error code into a string.
* @param {number} errorCode The error code.
* @param {Connection} a One of the two connections being checked.
* @param {Connection} b The second of the two connections being
* checked.
* @return {string} A developer-readable error string.
* @public
*/
ConnectionChecker.prototype.getErrorMessage = function(errorCode, a, b) {
switch (errorCode) {
case Connection.REASON_SELF_CONNECTION:
return 'Attempted to connect a block to itself.';
case Connection.REASON_DIFFERENT_WORKSPACES:
// Usually this means one block has been deleted.
return 'Blocks not on same workspace.';
case Connection.REASON_WRONG_TYPE:
return 'Attempt to connect incompatible types.';
case Connection.REASON_TARGET_NULL:
return 'Target connection is null.';
case Connection.REASON_CHECKS_FAILED: {
const connOne = /** @type {!Connection} **/ (a);
const connTwo = /** @type {!Connection} **/ (b);
let msg = 'Connection checks failed. ';
msg += connOne + ' expected ' + connOne.getCheck() + ', found ' +
connTwo.getCheck();
return msg;
/**
* Checks whether the current connection can connect with the target
* connection, and return an error code if there are problems.
* @param {Connection} a Connection to check compatibility with.
* @param {Connection} b Connection to check compatibility with.
* @param {boolean} isDragging True if the connection is being made by
* dragging a block.
* @param {number=} opt_distance The max allowable distance between the
* connections for drag checks.
* @return {number} Connection.CAN_CONNECT if the connection is legal,
* an error code otherwise.
* @public
*/
canConnectWithReason(a, b, isDragging, opt_distance) {
const safety = this.doSafetyChecks(a, b);
if (safety !== Connection.CAN_CONNECT) {
return safety;
}
case Connection.REASON_SHADOW_PARENT:
return 'Connecting non-shadow to shadow block.';
case Connection.REASON_DRAG_CHECKS_FAILED:
return 'Drag checks failed.';
case Connection.REASON_PREVIOUS_AND_OUTPUT:
return 'Block would have an output and a previous connection.';
default:
return 'Unknown connection failure: this should never happen!';
}
};
/**
* Check that connecting the given connections is safe, meaning that it would
* not break any of Blockly's basic assumptions (e.g. no self connections).
* @param {Connection} a The first of the connections to check.
* @param {Connection} b The second of the connections to check.
* @return {number} An enum with the reason this connection is safe or unsafe.
* @public
*/
ConnectionChecker.prototype.doSafetyChecks = function(a, b) {
if (!a || !b) {
return Connection.REASON_TARGET_NULL;
}
let superiorBlock;
let inferiorBlock;
let superiorConnection;
let inferiorConnection;
if (a.isSuperior()) {
superiorBlock = a.getSourceBlock();
inferiorBlock = b.getSourceBlock();
superiorConnection = a;
inferiorConnection = b;
} else {
inferiorBlock = a.getSourceBlock();
superiorBlock = b.getSourceBlock();
inferiorConnection = a;
superiorConnection = b;
}
if (superiorBlock === inferiorBlock) {
return Connection.REASON_SELF_CONNECTION;
} else if (
inferiorConnection.type !==
internalConstants.OPPOSITE_TYPE[superiorConnection.type]) {
return Connection.REASON_WRONG_TYPE;
} else if (superiorBlock.workspace !== inferiorBlock.workspace) {
return Connection.REASON_DIFFERENT_WORKSPACES;
} else if (superiorBlock.isShadow() && !inferiorBlock.isShadow()) {
return Connection.REASON_SHADOW_PARENT;
} else if (
inferiorConnection.type === ConnectionType.OUTPUT_VALUE &&
inferiorBlock.previousConnection &&
inferiorBlock.previousConnection.isConnected()) {
return Connection.REASON_PREVIOUS_AND_OUTPUT;
} else if (
inferiorConnection.type === ConnectionType.PREVIOUS_STATEMENT &&
inferiorBlock.outputConnection &&
inferiorBlock.outputConnection.isConnected()) {
return Connection.REASON_PREVIOUS_AND_OUTPUT;
}
return Connection.CAN_CONNECT;
};
// If the safety checks passed, both connections are non-null.
const connOne = /** @type {!Connection} **/ (a);
const connTwo = /** @type {!Connection} **/ (b);
if (!this.doTypeChecks(connOne, connTwo)) {
return Connection.REASON_CHECKS_FAILED;
}
/**
* Check whether this connection is compatible with another connection with
* respect to the value type system. E.g. square_root("Hello") is not
* compatible.
* @param {!Connection} a Connection to compare.
* @param {!Connection} b Connection to compare against.
* @return {boolean} True if the connections share a type.
* @public
*/
ConnectionChecker.prototype.doTypeChecks = function(a, b) {
const checkArrayOne = a.getCheck();
const checkArrayTwo = b.getCheck();
if (isDragging &&
!this.doDragChecks(
/** @type {!RenderedConnection} **/ (a),
/** @type {!RenderedConnection} **/ (b), opt_distance || 0)) {
return Connection.REASON_DRAG_CHECKS_FAILED;
}
if (!checkArrayOne || !checkArrayTwo) {
// One or both sides are promiscuous enough that anything will fit.
return true;
return Connection.CAN_CONNECT;
}
// Find any intersection in the check lists.
for (let i = 0; i < checkArrayOne.length; i++) {
if (checkArrayTwo.indexOf(checkArrayOne[i]) !== -1) {
/**
* Helper method that translates a connection error code into a string.
* @param {number} errorCode The error code.
* @param {Connection} a One of the two connections being checked.
* @param {Connection} b The second of the two connections being
* checked.
* @return {string} A developer-readable error string.
* @public
*/
getErrorMessage(errorCode, a, b) {
switch (errorCode) {
case Connection.REASON_SELF_CONNECTION:
return 'Attempted to connect a block to itself.';
case Connection.REASON_DIFFERENT_WORKSPACES:
// Usually this means one block has been deleted.
return 'Blocks not on same workspace.';
case Connection.REASON_WRONG_TYPE:
return 'Attempt to connect incompatible types.';
case Connection.REASON_TARGET_NULL:
return 'Target connection is null.';
case Connection.REASON_CHECKS_FAILED: {
const connOne = /** @type {!Connection} **/ (a);
const connTwo = /** @type {!Connection} **/ (b);
let msg = 'Connection checks failed. ';
msg += connOne + ' expected ' + connOne.getCheck() + ', found ' +
connTwo.getCheck();
return msg;
}
case Connection.REASON_SHADOW_PARENT:
return 'Connecting non-shadow to shadow block.';
case Connection.REASON_DRAG_CHECKS_FAILED:
return 'Drag checks failed.';
case Connection.REASON_PREVIOUS_AND_OUTPUT:
return 'Block would have an output and a previous connection.';
default:
return 'Unknown connection failure: this should never happen!';
}
}
/**
* Check that connecting the given connections is safe, meaning that it would
* not break any of Blockly's basic assumptions (e.g. no self connections).
* @param {Connection} a The first of the connections to check.
* @param {Connection} b The second of the connections to check.
* @return {number} An enum with the reason this connection is safe or unsafe.
* @public
*/
doSafetyChecks(a, b) {
if (!a || !b) {
return Connection.REASON_TARGET_NULL;
}
let superiorBlock;
let inferiorBlock;
let superiorConnection;
let inferiorConnection;
if (a.isSuperior()) {
superiorBlock = a.getSourceBlock();
inferiorBlock = b.getSourceBlock();
superiorConnection = a;
inferiorConnection = b;
} else {
inferiorBlock = a.getSourceBlock();
superiorBlock = b.getSourceBlock();
inferiorConnection = a;
superiorConnection = b;
}
if (superiorBlock === inferiorBlock) {
return Connection.REASON_SELF_CONNECTION;
} else if (
inferiorConnection.type !==
internalConstants.OPPOSITE_TYPE[superiorConnection.type]) {
return Connection.REASON_WRONG_TYPE;
} else if (superiorBlock.workspace !== inferiorBlock.workspace) {
return Connection.REASON_DIFFERENT_WORKSPACES;
} else if (superiorBlock.isShadow() && !inferiorBlock.isShadow()) {
return Connection.REASON_SHADOW_PARENT;
} else if (
inferiorConnection.type === ConnectionType.OUTPUT_VALUE &&
inferiorBlock.previousConnection &&
inferiorBlock.previousConnection.isConnected()) {
return Connection.REASON_PREVIOUS_AND_OUTPUT;
} else if (
inferiorConnection.type === ConnectionType.PREVIOUS_STATEMENT &&
inferiorBlock.outputConnection &&
inferiorBlock.outputConnection.isConnected()) {
return Connection.REASON_PREVIOUS_AND_OUTPUT;
}
return Connection.CAN_CONNECT;
}
/**
* Check whether this connection is compatible with another connection with
* respect to the value type system. E.g. square_root("Hello") is not
* compatible.
* @param {!Connection} a Connection to compare.
* @param {!Connection} b Connection to compare against.
* @return {boolean} True if the connections share a type.
* @public
*/
doTypeChecks(a, b) {
const checkArrayOne = a.getCheck();
const checkArrayTwo = b.getCheck();
if (!checkArrayOne || !checkArrayTwo) {
// One or both sides are promiscuous enough that anything will fit.
return true;
}
}
// No intersection.
return false;
};
/**
* Check whether this connection can be made by dragging.
* @param {!RenderedConnection} a Connection to compare.
* @param {!RenderedConnection} b Connection to compare against.
* @param {number} distance The maximum allowable distance between connections.
* @return {boolean} True if the connection is allowed during a drag.
* @public
*/
ConnectionChecker.prototype.doDragChecks = function(a, b, distance) {
if (a.distanceFrom(b) > distance) {
// Find any intersection in the check lists.
for (let i = 0; i < checkArrayOne.length; i++) {
if (checkArrayTwo.indexOf(checkArrayOne[i]) !== -1) {
return true;
}
}
// No intersection.
return false;
}
// Don't consider insertion markers.
if (b.getSourceBlock().isInsertionMarker()) {
return false;
}
switch (b.type) {
case ConnectionType.PREVIOUS_STATEMENT:
return this.canConnectToPrevious_(a, b);
case ConnectionType.OUTPUT_VALUE: {
// Don't offer to connect an already connected left (male) value plug to
// an available right (female) value plug.
if ((b.isConnected() && !b.targetBlock().isInsertionMarker()) ||
a.isConnected()) {
return false;
}
break;
}
case ConnectionType.INPUT_VALUE: {
// Offering to connect the left (male) of a value block to an already
// connected value pair is ok, we'll splice it in.
// However, don't offer to splice into an immovable block.
if (b.isConnected() && !b.targetBlock().isMovable() &&
!b.targetBlock().isShadow()) {
return false;
}
break;
}
case ConnectionType.NEXT_STATEMENT: {
// Don't let a block with no next connection bump other blocks out of the
// stack. But covering up a shadow block or stack of shadow blocks is
// fine. Similarly, replacing a terminal statement with another terminal
// statement is allowed.
if (b.isConnected() && !a.getSourceBlock().nextConnection &&
!b.targetBlock().isShadow() && b.targetBlock().nextConnection) {
return false;
}
break;
}
default:
// Unexpected connection type.
/**
* Check whether this connection can be made by dragging.
* @param {!RenderedConnection} a Connection to compare.
* @param {!RenderedConnection} b Connection to compare against.
* @param {number} distance The maximum allowable distance between
* connections.
* @return {boolean} True if the connection is allowed during a drag.
* @public
*/
doDragChecks(a, b, distance) {
if (a.distanceFrom(b) > distance) {
return false;
}
}
// Don't let blocks try to connect to themselves or ones they nest.
if (common.draggingConnections.indexOf(b) !== -1) {
return false;
}
// Don't consider insertion markers.
if (b.getSourceBlock().isInsertionMarker()) {
return false;
}
return true;
};
switch (b.type) {
case ConnectionType.PREVIOUS_STATEMENT:
return this.canConnectToPrevious_(a, b);
case ConnectionType.OUTPUT_VALUE: {
// Don't offer to connect an already connected left (male) value plug to
// an available right (female) value plug.
if ((b.isConnected() && !b.targetBlock().isInsertionMarker()) ||
a.isConnected()) {
return false;
}
break;
}
case ConnectionType.INPUT_VALUE: {
// Offering to connect the left (male) of a value block to an already
// connected value pair is ok, we'll splice it in.
// However, don't offer to splice into an immovable block.
if (b.isConnected() && !b.targetBlock().isMovable() &&
!b.targetBlock().isShadow()) {
return false;
}
break;
}
case ConnectionType.NEXT_STATEMENT: {
// Don't let a block with no next connection bump other blocks out of
// the stack. But covering up a shadow block or stack of shadow blocks
// is fine. Similarly, replacing a terminal statement with another
// terminal statement is allowed.
if (b.isConnected() && !a.getSourceBlock().nextConnection &&
!b.targetBlock().isShadow() && b.targetBlock().nextConnection) {
return false;
}
break;
}
default:
// Unexpected connection type.
return false;
}
/**
* Helper function for drag checking.
* @param {!Connection} a The connection to check, which must be a
* statement input or next connection.
* @param {!Connection} b A nearby connection to check, which
* must be a previous connection.
* @return {boolean} True if the connection is allowed, false otherwise.
* @protected
*/
ConnectionChecker.prototype.canConnectToPrevious_ = function(a, b) {
if (a.targetConnection) {
// This connection is already occupied.
// A next connection will never disconnect itself mid-drag.
return false;
}
// Don't let blocks try to connect to themselves or ones they nest.
if (common.draggingConnections.indexOf(b) !== -1) {
return false;
}
// Don't let blocks try to connect to themselves or ones they nest.
if (common.draggingConnections.indexOf(b) !== -1) {
return false;
}
if (!b.targetConnection) {
return true;
}
const targetBlock = b.targetBlock();
// If it is connected to a real block, game over.
if (!targetBlock.isInsertionMarker()) {
return false;
/**
* Helper function for drag checking.
* @param {!Connection} a The connection to check, which must be a
* statement input or next connection.
* @param {!Connection} b A nearby connection to check, which
* must be a previous connection.
* @return {boolean} True if the connection is allowed, false otherwise.
* @protected
*/
canConnectToPrevious_(a, b) {
if (a.targetConnection) {
// This connection is already occupied.
// A next connection will never disconnect itself mid-drag.
return false;
}
// Don't let blocks try to connect to themselves or ones they nest.
if (common.draggingConnections.indexOf(b) !== -1) {
return false;
}
if (!b.targetConnection) {
return true;
}
const targetBlock = b.targetBlock();
// If it is connected to a real block, game over.
if (!targetBlock.isInsertionMarker()) {
return false;
}
// If it's connected to an insertion marker but that insertion marker
// is the first block in a stack, it's still fine. If that insertion
// marker is in the middle of a stack, it won't work.
return !targetBlock.getPreviousBlock();
}
// If it's connected to an insertion marker but that insertion marker
// is the first block in a stack, it's still fine. If that insertion
// marker is in the middle of a stack, it won't work.
return !targetBlock.getPreviousBlock();
};
}
registry.register(
registry.Type.CONNECTION_CHECKER, registry.DEFAULT, ConnectionChecker);

View File

@@ -34,276 +34,281 @@ goog.require('Blockly.constants');
* Database of connections.
* Connections are stored in order of their vertical component. This way
* connections in an area may be looked up quickly using a binary search.
* @param {!IConnectionChecker} checker The workspace's
* connection type checker, used to decide if connections are valid during a
* drag.
* @constructor
* @alias Blockly.ConnectionDB
*/
const ConnectionDB = function(checker) {
class ConnectionDB {
/**
* Array of connections sorted by y position in workspace units.
* @type {!Array<!RenderedConnection>}
* @param {!IConnectionChecker} checker The workspace's
* connection type checker, used to decide if connections are valid during
* a drag.
*/
constructor(checker) {
/**
* Array of connections sorted by y position in workspace units.
* @type {!Array<!RenderedConnection>}
* @private
*/
this.connections_ = [];
/**
* The workspace's connection type checker, used to decide if connections
* are valid during a drag.
* @type {!IConnectionChecker}
* @private
*/
this.connectionChecker_ = checker;
}
/**
* Add a connection to the database. Should not already exist in the database.
* @param {!RenderedConnection} connection The connection to be added.
* @param {number} yPos The y position used to decide where to insert the
* connection.
* @package
*/
addConnection(connection, yPos) {
const index = this.calculateIndexForYPos_(yPos);
this.connections_.splice(index, 0, connection);
}
/**
* Finds the index of the given connection.
*
* Starts by doing a binary search to find the approximate location, then
* linearly searches nearby for the exact connection.
* @param {!RenderedConnection} conn The connection to find.
* @param {number} yPos The y position used to find the index of the
* connection.
* @return {number} The index of the connection, or -1 if the connection was
* not found.
* @private
*/
this.connections_ = [];
/**
* The workspace's connection type checker, used to decide if connections are
* valid during a drag.
* @type {!IConnectionChecker}
* @private
*/
this.connectionChecker_ = checker;
};
findIndexOfConnection_(conn, yPos) {
if (!this.connections_.length) {
return -1;
}
/**
* Add a connection to the database. Should not already exist in the database.
* @param {!RenderedConnection} connection The connection to be added.
* @param {number} yPos The y position used to decide where to insert the
* connection.
* @package
*/
ConnectionDB.prototype.addConnection = function(connection, yPos) {
const index = this.calculateIndexForYPos_(yPos);
this.connections_.splice(index, 0, connection);
};
const bestGuess = this.calculateIndexForYPos_(yPos);
if (bestGuess >= this.connections_.length) {
// Not in list
return -1;
}
/**
* Finds the index of the given connection.
*
* Starts by doing a binary search to find the approximate location, then
* linearly searches nearby for the exact connection.
* @param {!RenderedConnection} conn The connection to find.
* @param {number} yPos The y position used to find the index of the connection.
* @return {number} The index of the connection, or -1 if the connection was
* not found.
* @private
*/
ConnectionDB.prototype.findIndexOfConnection_ = function(conn, yPos) {
if (!this.connections_.length) {
yPos = conn.y;
// Walk forward and back on the y axis looking for the connection.
let pointer = bestGuess;
while (pointer >= 0 && this.connections_[pointer].y === yPos) {
if (this.connections_[pointer] === conn) {
return pointer;
}
pointer--;
}
pointer = bestGuess;
while (pointer < this.connections_.length &&
this.connections_[pointer].y === yPos) {
if (this.connections_[pointer] === conn) {
return pointer;
}
pointer++;
}
return -1;
}
const bestGuess = this.calculateIndexForYPos_(yPos);
if (bestGuess >= this.connections_.length) {
// Not in list
return -1;
}
yPos = conn.y;
// Walk forward and back on the y axis looking for the connection.
let pointer = bestGuess;
while (pointer >= 0 && this.connections_[pointer].y === yPos) {
if (this.connections_[pointer] === conn) {
return pointer;
}
pointer--;
}
pointer = bestGuess;
while (pointer < this.connections_.length &&
this.connections_[pointer].y === yPos) {
if (this.connections_[pointer] === conn) {
return pointer;
}
pointer++;
}
return -1;
};
/**
* Finds the correct index for the given y position.
* @param {number} yPos The y position used to decide where to
* insert the connection.
* @return {number} The candidate index.
* @private
*/
ConnectionDB.prototype.calculateIndexForYPos_ = function(yPos) {
if (!this.connections_.length) {
return 0;
}
let pointerMin = 0;
let pointerMax = this.connections_.length;
while (pointerMin < pointerMax) {
const pointerMid = Math.floor((pointerMin + pointerMax) / 2);
if (this.connections_[pointerMid].y < yPos) {
pointerMin = pointerMid + 1;
} else if (this.connections_[pointerMid].y > yPos) {
pointerMax = pointerMid;
} else {
pointerMin = pointerMid;
break;
}
}
return pointerMin;
};
/**
* Remove a connection from the database. Must already exist in DB.
* @param {!RenderedConnection} connection The connection to be removed.
* @param {number} yPos The y position used to find the index of the connection.
* @throws {Error} If the connection cannot be found in the database.
*/
ConnectionDB.prototype.removeConnection = function(connection, yPos) {
const index = this.findIndexOfConnection_(connection, yPos);
if (index === -1) {
throw Error('Unable to find connection in connectionDB.');
}
this.connections_.splice(index, 1);
};
/**
* Find all nearby connections to the given connection.
* Type checking does not apply, since this function is used for bumping.
* @param {!RenderedConnection} connection The connection whose
* neighbours should be returned.
* @param {number} maxRadius The maximum radius to another connection.
* @return {!Array<!RenderedConnection>} List of connections.
*/
ConnectionDB.prototype.getNeighbours = function(connection, maxRadius) {
const db = this.connections_;
const currentX = connection.x;
const currentY = connection.y;
// Binary search to find the closest y location.
let pointerMin = 0;
let pointerMax = db.length - 2;
let pointerMid = pointerMax;
while (pointerMin < pointerMid) {
if (db[pointerMid].y < currentY) {
pointerMin = pointerMid;
} else {
pointerMax = pointerMid;
}
pointerMid = Math.floor((pointerMin + pointerMax) / 2);
}
const neighbours = [];
/**
* Computes if the current connection is within the allowed radius of another
* connection.
* This function is a closure and has access to outside variables.
* @param {number} yIndex The other connection's index in the database.
* @return {boolean} True if the current connection's vertical distance from
* the other connection is less than the allowed radius.
* Finds the correct index for the given y position.
* @param {number} yPos The y position used to decide where to
* insert the connection.
* @return {number} The candidate index.
* @private
*/
function checkConnection_(yIndex) {
const dx = currentX - db[yIndex].x;
const dy = currentY - db[yIndex].y;
const r = Math.sqrt(dx * dx + dy * dy);
if (r <= maxRadius) {
neighbours.push(db[yIndex]);
calculateIndexForYPos_(yPos) {
if (!this.connections_.length) {
return 0;
}
return dy < maxRadius;
let pointerMin = 0;
let pointerMax = this.connections_.length;
while (pointerMin < pointerMax) {
const pointerMid = Math.floor((pointerMin + pointerMax) / 2);
if (this.connections_[pointerMid].y < yPos) {
pointerMin = pointerMid + 1;
} else if (this.connections_[pointerMid].y > yPos) {
pointerMax = pointerMid;
} else {
pointerMin = pointerMid;
break;
}
}
return pointerMin;
}
// Walk forward and back on the y axis looking for the closest x,y point.
pointerMin = pointerMid;
pointerMax = pointerMid;
if (db.length) {
while (pointerMin >= 0 && checkConnection_(pointerMin)) {
/**
* Remove a connection from the database. Must already exist in DB.
* @param {!RenderedConnection} connection The connection to be removed.
* @param {number} yPos The y position used to find the index of the
* connection.
* @throws {Error} If the connection cannot be found in the database.
*/
removeConnection(connection, yPos) {
const index = this.findIndexOfConnection_(connection, yPos);
if (index === -1) {
throw Error('Unable to find connection in connectionDB.');
}
this.connections_.splice(index, 1);
}
/**
* Find all nearby connections to the given connection.
* Type checking does not apply, since this function is used for bumping.
* @param {!RenderedConnection} connection The connection whose
* neighbours should be returned.
* @param {number} maxRadius The maximum radius to another connection.
* @return {!Array<!RenderedConnection>} List of connections.
*/
getNeighbours(connection, maxRadius) {
const db = this.connections_;
const currentX = connection.x;
const currentY = connection.y;
// Binary search to find the closest y location.
let pointerMin = 0;
let pointerMax = db.length - 2;
let pointerMid = pointerMax;
while (pointerMin < pointerMid) {
if (db[pointerMid].y < currentY) {
pointerMin = pointerMid;
} else {
pointerMax = pointerMid;
}
pointerMid = Math.floor((pointerMin + pointerMax) / 2);
}
const neighbours = [];
/**
* Computes if the current connection is within the allowed radius of
* another connection. This function is a closure and has access to outside
* variables.
* @param {number} yIndex The other connection's index in the database.
* @return {boolean} True if the current connection's vertical distance from
* the other connection is less than the allowed radius.
*/
function checkConnection_(yIndex) {
const dx = currentX - db[yIndex].x;
const dy = currentY - db[yIndex].y;
const r = Math.sqrt(dx * dx + dy * dy);
if (r <= maxRadius) {
neighbours.push(db[yIndex]);
}
return dy < maxRadius;
}
// Walk forward and back on the y axis looking for the closest x,y point.
pointerMin = pointerMid;
pointerMax = pointerMid;
if (db.length) {
while (pointerMin >= 0 && checkConnection_(pointerMin)) {
pointerMin--;
}
do {
pointerMax++;
} while (pointerMax < db.length && checkConnection_(pointerMax));
}
return neighbours;
}
/**
* Is the candidate connection close to the reference connection.
* Extremely fast; only looks at Y distance.
* @param {number} index Index in database of candidate connection.
* @param {number} baseY Reference connection's Y value.
* @param {number} maxRadius The maximum radius to another connection.
* @return {boolean} True if connection is in range.
* @private
*/
isInYRange_(index, baseY, maxRadius) {
return (Math.abs(this.connections_[index].y - baseY) <= maxRadius);
}
/**
* Find the closest compatible connection to this connection.
* @param {!RenderedConnection} conn The connection searching for a compatible
* mate.
* @param {number} maxRadius The maximum radius to another connection.
* @param {!Coordinate} dxy Offset between this connection's
* location in the database and the current location (as a result of
* dragging).
* @return {!{connection: RenderedConnection, radius: number}}
* Contains two properties: 'connection' which is either another
* connection or null, and 'radius' which is the distance.
*/
searchForClosest(conn, maxRadius, dxy) {
if (!this.connections_.length) {
// Don't bother.
return {connection: null, radius: maxRadius};
}
// Stash the values of x and y from before the drag.
const baseY = conn.y;
const baseX = conn.x;
conn.x = baseX + dxy.x;
conn.y = baseY + dxy.y;
// calculateIndexForYPos_ finds an index for insertion, which is always
// after any block with the same y index. We want to search both forward
// and back, so search on both sides of the index.
const closestIndex = this.calculateIndexForYPos_(conn.y);
let bestConnection = null;
let bestRadius = maxRadius;
let temp;
// Walk forward and back on the y axis looking for the closest x,y point.
let pointerMin = closestIndex - 1;
while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y, maxRadius)) {
temp = this.connections_[pointerMin];
if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) {
bestConnection = temp;
bestRadius = temp.distanceFrom(conn);
}
pointerMin--;
}
do {
let pointerMax = closestIndex;
while (pointerMax < this.connections_.length &&
this.isInYRange_(pointerMax, conn.y, maxRadius)) {
temp = this.connections_[pointerMax];
if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) {
bestConnection = temp;
bestRadius = temp.distanceFrom(conn);
}
pointerMax++;
} while (pointerMax < db.length && checkConnection_(pointerMax));
}
return neighbours;
};
/**
* Is the candidate connection close to the reference connection.
* Extremely fast; only looks at Y distance.
* @param {number} index Index in database of candidate connection.
* @param {number} baseY Reference connection's Y value.
* @param {number} maxRadius The maximum radius to another connection.
* @return {boolean} True if connection is in range.
* @private
*/
ConnectionDB.prototype.isInYRange_ = function(index, baseY, maxRadius) {
return (Math.abs(this.connections_[index].y - baseY) <= maxRadius);
};
/**
* Find the closest compatible connection to this connection.
* @param {!RenderedConnection} conn The connection searching for a compatible
* mate.
* @param {number} maxRadius The maximum radius to another connection.
* @param {!Coordinate} dxy Offset between this connection's
* location in the database and the current location (as a result of
* dragging).
* @return {!{connection: RenderedConnection, radius: number}}
* Contains two properties: 'connection' which is either another
* connection or null, and 'radius' which is the distance.
*/
ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, dxy) {
if (!this.connections_.length) {
// Don't bother.
return {connection: null, radius: maxRadius};
}
// Stash the values of x and y from before the drag.
const baseY = conn.y;
const baseX = conn.x;
conn.x = baseX + dxy.x;
conn.y = baseY + dxy.y;
// calculateIndexForYPos_ finds an index for insertion, which is always
// after any block with the same y index. We want to search both forward
// and back, so search on both sides of the index.
const closestIndex = this.calculateIndexForYPos_(conn.y);
let bestConnection = null;
let bestRadius = maxRadius;
let temp;
// Walk forward and back on the y axis looking for the closest x,y point.
let pointerMin = closestIndex - 1;
while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y, maxRadius)) {
temp = this.connections_[pointerMin];
if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) {
bestConnection = temp;
bestRadius = temp.distanceFrom(conn);
}
pointerMin--;
// Reset the values of x and y.
conn.x = baseX;
conn.y = baseY;
// If there were no valid connections, bestConnection will be null.
return {connection: bestConnection, radius: bestRadius};
}
let pointerMax = closestIndex;
while (pointerMax < this.connections_.length &&
this.isInYRange_(pointerMax, conn.y, maxRadius)) {
temp = this.connections_[pointerMax];
if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) {
bestConnection = temp;
bestRadius = temp.distanceFrom(conn);
}
pointerMax++;
/**
* Initialize a set of connection DBs for a workspace.
* @param {!IConnectionChecker} checker The workspace's
* connection checker, used to decide if connections are valid during a
* drag.
* @return {!Array<!ConnectionDB>} Array of databases.
*/
static init(checker) {
// Create four databases, one for each connection type.
const dbList = [];
dbList[ConnectionType.INPUT_VALUE] = new ConnectionDB(checker);
dbList[ConnectionType.OUTPUT_VALUE] = new ConnectionDB(checker);
dbList[ConnectionType.NEXT_STATEMENT] = new ConnectionDB(checker);
dbList[ConnectionType.PREVIOUS_STATEMENT] = new ConnectionDB(checker);
return dbList;
}
// Reset the values of x and y.
conn.x = baseX;
conn.y = baseY;
// If there were no valid connections, bestConnection will be null.
return {connection: bestConnection, radius: bestRadius};
};
/**
* Initialize a set of connection DBs for a workspace.
* @param {!IConnectionChecker} checker The workspace's
* connection checker, used to decide if connections are valid during a
* drag.
* @return {!Array<!ConnectionDB>} Array of databases.
*/
ConnectionDB.init = function(checker) {
// Create four databases, one for each connection type.
const dbList = [];
dbList[ConnectionType.INPUT_VALUE] = new ConnectionDB(checker);
dbList[ConnectionType.OUTPUT_VALUE] = new ConnectionDB(checker);
dbList[ConnectionType.NEXT_STATEMENT] = new ConnectionDB(checker);
dbList[ConnectionType.PREVIOUS_STATEMENT] = new ConnectionDB(checker);
return dbList;
};
}
exports.ConnectionDB = ConnectionDB;

View File

@@ -23,11 +23,13 @@ const clipboard = goog.require('Blockly.clipboard');
const deprecation = goog.require('Blockly.utils.deprecation');
const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
const internalConstants = goog.require('Blockly.internalConstants');
const userAgent = goog.require('Blockly.utils.userAgent');
const svgMath = goog.require('Blockly.utils.svgMath');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
const {config} = goog.require('Blockly.config');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
const {MenuItem} = goog.require('Blockly.MenuItem');
const {Menu} = goog.require('Blockly.Menu');
@@ -262,15 +264,16 @@ const callbackFactory = function(block, xml) {
eventUtils.disable();
let newBlock;
try {
newBlock = Xml.domToBlock(xml, block.workspace);
newBlock =
/** @type {!BlockSvg} */ (Xml.domToBlock(xml, block.workspace));
// Move the new block next to the old block.
const xy = block.getRelativeToSurfaceXY();
if (block.RTL) {
xy.x -= internalConstants.SNAP_RADIUS;
xy.x -= config.snapRadius;
} else {
xy.x += internalConstants.SNAP_RADIUS;
xy.x += config.snapRadius;
}
xy.y += internalConstants.SNAP_RADIUS * 2;
xy.y += config.snapRadius * 2;
newBlock.moveBy(xy.x, xy.y);
} finally {
eventUtils.enable();
@@ -339,7 +342,7 @@ exports.commentDuplicateOption = commentDuplicateOption;
* @alias Blockly.ContextMenu.workspaceCommentOption
*/
const workspaceCommentOption = function(ws, e) {
const WorkspaceCommentSvg = goog.module.get('Blockly.WorkspaceCommentSvg');
const {WorkspaceCommentSvg} = goog.module.get('Blockly.WorkspaceCommentSvg');
if (!WorkspaceCommentSvg) {
throw Error('Missing require for Blockly.WorkspaceCommentSvg');
}

View File

@@ -239,8 +239,7 @@ const addDeletableBlocks_ = function(block, deleteList) {
if (block.isDeletable()) {
Array.prototype.push.apply(deleteList, block.getDescendants(false));
} else {
const children = /* eslint-disable-next-line indent */
/** @type {!Array<!BlockSvg>} */ (block.getChildren(false));
const children = block.getChildren(false);
for (let i = 0; i < children.length; i++) {
addDeletableBlocks_(children[i], deleteList);
}

View File

@@ -25,21 +25,102 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
* Class for the registry of context menu items. This is intended to be a
* singleton. You should not create a new instance, and only access this class
* from ContextMenuRegistry.registry.
* @constructor
* @private
* @alias Blockly.ContextMenuRegistry
*/
const ContextMenuRegistry = function() {
// Singleton instance should be registered once.
ContextMenuRegistry.registry = this;
class ContextMenuRegistry {
/**
* Resets the existing singleton instance of ContextMenuRegistry.
*/
constructor() {
this.reset();
}
/**
* Registry of all registered RegistryItems, keyed by ID.
* @type {!Object<string, !ContextMenuRegistry.RegistryItem>}
* @private
* Clear and recreate the registry.
*/
this.registry_ = Object.create(null);
};
reset() {
/**
* Registry of all registered RegistryItems, keyed by ID.
* @type {!Object<string, !ContextMenuRegistry.RegistryItem>}
* @private
*/
this.registry_ = Object.create(null);
}
/**
* Registers a RegistryItem.
* @param {!ContextMenuRegistry.RegistryItem} item Context menu item to
* register.
* @throws {Error} if an item with the given ID already exists.
*/
register(item) {
if (this.registry_[item.id]) {
throw Error('Menu item with ID "' + item.id + '" is already registered.');
}
this.registry_[item.id] = item;
}
/**
* Unregisters a RegistryItem with the given ID.
* @param {string} id The ID of the RegistryItem to remove.
* @throws {Error} if an item with the given ID does not exist.
*/
unregister(id) {
if (!this.registry_[id]) {
throw new Error('Menu item with ID "' + id + '" not found.');
}
delete this.registry_[id];
}
/**
* @param {string} id The ID of the RegistryItem to get.
* @return {?ContextMenuRegistry.RegistryItem} RegistryItem or null if not
* found
*/
getItem(id) {
return this.registry_[id] || null;
}
/**
* Gets the valid context menu options for the given scope type (e.g. block or
* workspace) and scope. Blocks are only shown if the preconditionFn shows
* they should not be hidden.
* @param {!ContextMenuRegistry.ScopeType} scopeType Type of scope where menu
* should be shown (e.g. on a block or on a workspace)
* @param {!ContextMenuRegistry.Scope} scope Current scope of context menu
* (i.e., the exact workspace or block being clicked on)
* @return {!Array<!ContextMenuRegistry.ContextMenuOption>} the list of
* ContextMenuOptions
*/
getContextMenuOptions(scopeType, scope) {
const menuOptions = [];
const registry = this.registry_;
Object.keys(registry).forEach(function(id) {
const item = registry[id];
if (scopeType === item.scopeType) {
const precondition = item.preconditionFn(scope);
if (precondition !== 'hidden') {
const displayText = typeof item.displayText === 'function' ?
item.displayText(scope) :
item.displayText;
/** @type {!ContextMenuRegistry.ContextMenuOption} */
const menuOption = {
text: displayText,
enabled: (precondition === 'enabled'),
callback: item.callback,
scope: scope,
weight: item.weight,
};
menuOptions.push(menuOption);
}
}
});
menuOptions.sort(function(a, b) {
return a.weight - b.weight;
});
return menuOptions;
}
}
/**
* Where this menu item should be rendered. If the menu item should be rendered
@@ -90,85 +171,8 @@ ContextMenuRegistry.ContextMenuOption;
/**
* Singleton instance of this class. All interactions with this class should be
* done on this object.
* @type {?ContextMenuRegistry}
* @type {!ContextMenuRegistry}
*/
ContextMenuRegistry.registry = null;
/**
* Registers a RegistryItem.
* @param {!ContextMenuRegistry.RegistryItem} item Context menu item to
* register.
* @throws {Error} if an item with the given ID already exists.
*/
ContextMenuRegistry.prototype.register = function(item) {
if (this.registry_[item.id]) {
throw Error('Menu item with ID "' + item.id + '" is already registered.');
}
this.registry_[item.id] = item;
};
/**
* Unregisters a RegistryItem with the given ID.
* @param {string} id The ID of the RegistryItem to remove.
* @throws {Error} if an item with the given ID does not exist.
*/
ContextMenuRegistry.prototype.unregister = function(id) {
if (!this.registry_[id]) {
throw new Error('Menu item with ID "' + id + '" not found.');
}
delete this.registry_[id];
};
/**
* @param {string} id The ID of the RegistryItem to get.
* @return {?ContextMenuRegistry.RegistryItem} RegistryItem or null if not found
*/
ContextMenuRegistry.prototype.getItem = function(id) {
return this.registry_[id] || null;
};
/**
* Gets the valid context menu options for the given scope type (e.g. block or
* workspace) and scope. Blocks are only shown if the preconditionFn shows they
* should not be hidden.
* @param {!ContextMenuRegistry.ScopeType} scopeType Type of scope where menu
* should be shown (e.g. on a block or on a workspace)
* @param {!ContextMenuRegistry.Scope} scope Current scope of context menu
* (i.e., the exact workspace or block being clicked on)
* @return {!Array<!ContextMenuRegistry.ContextMenuOption>} the list of
* ContextMenuOptions
*/
ContextMenuRegistry.prototype.getContextMenuOptions = function(
scopeType, scope) {
const menuOptions = [];
const registry = this.registry_;
Object.keys(registry).forEach(function(id) {
const item = registry[id];
if (scopeType === item.scopeType) {
const precondition = item.preconditionFn(scope);
if (precondition !== 'hidden') {
const displayText = typeof item.displayText === 'function' ?
item.displayText(scope) :
item.displayText;
/** @type {!ContextMenuRegistry.ContextMenuOption} */
const menuOption = {
text: displayText,
enabled: (precondition === 'enabled'),
callback: item.callback,
scope: scope,
weight: item.weight,
};
menuOptions.push(menuOption);
}
}
});
menuOptions.sort(function(a, b) {
return a.weight - b.weight;
});
return menuOptions;
};
// Creates and assigns the singleton instance.
new ContextMenuRegistry();
ContextMenuRegistry.registry = new ContextMenuRegistry();
exports.ContextMenuRegistry = ContextMenuRegistry;

View File

@@ -89,480 +89,480 @@ exports.inject = inject;
* @alias Blockly.Css.content
*/
let content = (`
.blocklySvg {
background-color: #fff;
outline: none;
overflow: hidden; /* IE overflows by default. */
position: absolute;
display: block;
}
.blocklySvg {
background-color: #fff;
outline: none;
overflow: hidden; /* IE overflows by default. */
position: absolute;
display: block;
}
.blocklyWidgetDiv {
display: none;
position: absolute;
z-index: 99999; /* big value for bootstrap3 compatibility */
}
.blocklyWidgetDiv {
display: none;
position: absolute;
z-index: 99999; /* big value for bootstrap3 compatibility */
}
.injectionDiv {
height: 100%;
position: relative;
overflow: hidden; /* So blocks in drag surface disappear at edges */
touch-action: none;
}
.injectionDiv {
height: 100%;
position: relative;
overflow: hidden; /* So blocks in drag surface disappear at edges */
touch-action: none;
}
.blocklyNonSelectable {
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.blocklyNonSelectable {
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.blocklyWsDragSurface {
display: none;
position: absolute;
top: 0;
left: 0;
}
.blocklyWsDragSurface {
display: none;
position: absolute;
top: 0;
left: 0;
}
/* Added as a separate rule with multiple classes to make it more specific
than a bootstrap rule that selects svg:root. See issue #1275 for context.
/* Added as a separate rule with multiple classes to make it more specific
than a bootstrap rule that selects svg:root. See issue #1275 for context.
*/
.blocklyWsDragSurface.blocklyOverflowVisible {
overflow: visible;
}
.blocklyBlockDragSurface {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: visible !important;
z-index: 50; /* Display below toolbox, but above everything else. */
}
.blocklyBlockCanvas.blocklyCanvasTransitioning,
.blocklyBubbleCanvas.blocklyCanvasTransitioning {
transition: transform .5s;
}
.blocklyTooltipDiv {
background-color: #ffffc7;
border: 1px solid #ddc;
box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);
color: #000;
display: none;
font: 9pt sans-serif;
opacity: .9;
padding: 2px;
position: absolute;
z-index: 100000; /* big value for bootstrap3 compatibility */
}
.blocklyDropDownDiv {
position: absolute;
left: 0;
top: 0;
z-index: 1000;
display: none;
border: 1px solid;
border-color: #dadce0;
background-color: #fff;
border-radius: 2px;
padding: 4px;
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
}
.blocklyDropDownDiv.blocklyFocused {
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
}
.blocklyDropDownContent {
max-height: 300px; // @todo: spec for maximum height.
overflow: auto;
overflow-x: hidden;
position: relative;
}
.blocklyDropDownArrow {
position: absolute;
left: 0;
top: 0;
width: 16px;
height: 16px;
z-index: -1;
background-color: inherit;
border-color: inherit;
}
.blocklyDropDownButton {
display: inline-block;
float: left;
padding: 0;
margin: 4px;
border-radius: 4px;
outline: none;
border: 1px solid;
transition: box-shadow .1s;
cursor: pointer;
}
.blocklyArrowTop {
border-top: 1px solid;
border-left: 1px solid;
border-top-left-radius: 4px;
border-color: inherit;
}
.blocklyArrowBottom {
border-bottom: 1px solid;
border-right: 1px solid;
border-bottom-right-radius: 4px;
border-color: inherit;
}
.blocklyResizeSE {
cursor: se-resize;
fill: #aaa;
}
.blocklyResizeSW {
cursor: sw-resize;
fill: #aaa;
}
.blocklyResizeLine {
stroke: #515A5A;
stroke-width: 1;
}
.blocklyHighlightedConnectionPath {
fill: none;
stroke: #fc3;
stroke-width: 4px;
}
.blocklyPathLight {
fill: none;
stroke-linecap: round;
stroke-width: 1;
}
.blocklySelected>.blocklyPathLight {
display: none;
}
.blocklyDraggable {
/* backup for browsers (e.g. IE11) that don't support grab */
cursor: url("<<<PATH>>>/handopen.cur"), auto;
cursor: grab;
cursor: -webkit-grab;
}
/* backup for browsers (e.g. IE11) that don't support grabbing */
.blocklyDragging {
/* backup for browsers (e.g. IE11) that don't support grabbing */
cursor: url("<<<PATH>>>/handclosed.cur"), auto;
cursor: grabbing;
cursor: -webkit-grabbing;
}
/* Changes cursor on mouse down. Not effective in Firefox because of
https://bugzilla.mozilla.org/show_bug.cgi?id=771241 */
.blocklyDraggable:active {
/* backup for browsers (e.g. IE11) that don't support grabbing */
cursor: url("<<<PATH>>>/handclosed.cur"), auto;
cursor: grabbing;
cursor: -webkit-grabbing;
}
/* Change the cursor on the whole drag surface in case the mouse gets
ahead of block during a drag. This way the cursor is still a closed hand.
*/
.blocklyWsDragSurface.blocklyOverflowVisible {
overflow: visible;
}
.blocklyBlockDragSurface {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: visible !important;
z-index: 50; /* Display below toolbox, but above everything else. */
}
.blocklyBlockCanvas.blocklyCanvasTransitioning,
.blocklyBubbleCanvas.blocklyCanvasTransitioning {
transition: transform .5s;
}
.blocklyTooltipDiv {
background-color: #ffffc7;
border: 1px solid #ddc;
box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);
color: #000;
display: none;
font: 9pt sans-serif;
opacity: .9;
padding: 2px;
position: absolute;
z-index: 100000; /* big value for bootstrap3 compatibility */
}
.blocklyDropDownDiv {
position: absolute;
left: 0;
top: 0;
z-index: 1000;
display: none;
border: 1px solid;
border-color: #dadce0;
background-color: #fff;
border-radius: 2px;
padding: 4px;
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
}
.blocklyDropDownDiv.blocklyFocused {
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
}
.blocklyDropDownContent {
max-height: 300px; // @todo: spec for maximum height.
overflow: auto;
overflow-x: hidden;
position: relative;
}
.blocklyDropDownArrow {
position: absolute;
left: 0;
top: 0;
width: 16px;
height: 16px;
z-index: -1;
background-color: inherit;
border-color: inherit;
}
.blocklyDropDownButton {
display: inline-block;
float: left;
padding: 0;
margin: 4px;
border-radius: 4px;
outline: none;
border: 1px solid;
transition: box-shadow .1s;
cursor: pointer;
}
.blocklyArrowTop {
border-top: 1px solid;
border-left: 1px solid;
border-top-left-radius: 4px;
border-color: inherit;
}
.blocklyArrowBottom {
border-bottom: 1px solid;
border-right: 1px solid;
border-bottom-right-radius: 4px;
border-color: inherit;
}
.blocklyResizeSE {
cursor: se-resize;
fill: #aaa;
}
.blocklyResizeSW {
cursor: sw-resize;
fill: #aaa;
}
.blocklyResizeLine {
stroke: #515A5A;
stroke-width: 1;
}
.blocklyHighlightedConnectionPath {
fill: none;
stroke: #fc3;
stroke-width: 4px;
}
.blocklyPathLight {
fill: none;
stroke-linecap: round;
stroke-width: 1;
}
.blocklySelected>.blocklyPathLight {
display: none;
}
.blocklyDraggable {
/* backup for browsers (e.g. IE11) that don't support grab */
cursor: url("<<<PATH>>>/handopen.cur"), auto;
cursor: grab;
cursor: -webkit-grab;
}
/* backup for browsers (e.g. IE11) that don't support grabbing */
.blocklyDragging {
/* backup for browsers (e.g. IE11) that don't support grabbing */
cursor: url("<<<PATH>>>/handclosed.cur"), auto;
cursor: grabbing;
cursor: -webkit-grabbing;
}
/* Changes cursor on mouse down. Not effective in Firefox because of
https://bugzilla.mozilla.org/show_bug.cgi?id=771241 */
.blocklyDraggable:active {
/* backup for browsers (e.g. IE11) that don't support grabbing */
cursor: url("<<<PATH>>>/handclosed.cur"), auto;
cursor: grabbing;
cursor: -webkit-grabbing;
}
/* Change the cursor on the whole drag surface in case the mouse gets
ahead of block during a drag. This way the cursor is still a closed hand.
*/
.blocklyBlockDragSurface .blocklyDraggable {
/* backup for browsers (e.g. IE11) that don't support grabbing */
cursor: url("<<<PATH>>>/handclosed.cur"), auto;
cursor: grabbing;
cursor: -webkit-grabbing;
}
.blocklyDragging.blocklyDraggingDelete {
cursor: url("<<<PATH>>>/handdelete.cur"), auto;
}
.blocklyDragging>.blocklyPath,
.blocklyDragging>.blocklyPathLight {
fill-opacity: .8;
stroke-opacity: .8;
}
.blocklyDragging>.blocklyPathDark {
display: none;
}
.blocklyDisabled>.blocklyPath {
fill-opacity: .5;
stroke-opacity: .5;
}
.blocklyDisabled>.blocklyPathLight,
.blocklyDisabled>.blocklyPathDark {
display: none;
}
.blocklyInsertionMarker>.blocklyPath,
.blocklyInsertionMarker>.blocklyPathLight,
.blocklyInsertionMarker>.blocklyPathDark {
fill-opacity: .2;
stroke: none;
}
.blocklyMultilineText {
font-family: monospace;
}
.blocklyNonEditableText>text {
pointer-events: none;
}
.blocklyFlyout {
position: absolute;
z-index: 20;
}
.blocklyText text {
cursor: default;
}
/*
Don't allow users to select text. It gets annoying when trying to
drag a block and selected text moves instead.
*/
.blocklySvg text,
.blocklyBlockDragSurface text {
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
cursor: inherit;
}
.blocklyHidden {
display: none;
}
.blocklyFieldDropdown:not(.blocklyHidden) {
display: block;
}
.blocklyIconGroup {
cursor: default;
}
.blocklyIconGroup:not(:hover),
.blocklyIconGroupReadonly {
opacity: .6;
}
.blocklyIconShape {
fill: #00f;
stroke: #fff;
stroke-width: 1px;
}
.blocklyIconSymbol {
fill: #fff;
}
.blocklyMinimalBody {
margin: 0;
padding: 0;
}
.blocklyHtmlInput {
border: none;
border-radius: 4px;
height: 100%;
margin: 0;
outline: none;
padding: 0;
width: 100%;
text-align: center;
display: block;
box-sizing: border-box;
}
/* Edge and IE introduce a close icon when the input value is longer than a
certain length. This affects our sizing calculations of the text input.
Hiding the close icon to avoid that. */
.blocklyHtmlInput::-ms-clear {
display: none;
}
.blocklyMainBackground {
stroke-width: 1;
stroke: #c6c6c6; /* Equates to #ddd due to border being off-pixel. */
}
.blocklyMutatorBackground {
fill: #fff;
stroke: #ddd;
stroke-width: 1;
}
.blocklyFlyoutBackground {
fill: #ddd;
fill-opacity: .8;
}
.blocklyMainWorkspaceScrollbar {
z-index: 20;
}
.blocklyFlyoutScrollbar {
z-index: 30;
}
.blocklyScrollbarHorizontal,
.blocklyScrollbarVertical {
position: absolute;
outline: none;
}
.blocklyScrollbarBackground {
opacity: 0;
}
.blocklyScrollbarHandle {
fill: #ccc;
}
.blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
.blocklyScrollbarHandle:hover {
fill: #bbb;
}
/* Darken flyout scrollbars due to being on a grey background. */
/* By contrast, workspace scrollbars are on a white background. */
.blocklyFlyout .blocklyScrollbarHandle {
fill: #bbb;
}
.blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
.blocklyFlyout .blocklyScrollbarHandle:hover {
fill: #aaa;
}
.blocklyInvalidInput {
background: #faa;
}
.blocklyVerticalMarker {
stroke-width: 3px;
fill: rgba(255,255,255,.5);
pointer-events: none;
}
.blocklyComputeCanvas {
position: absolute;
width: 0;
height: 0;
}
.blocklyNoPointerEvents {
pointer-events: none;
}
.blocklyContextMenu {
border-radius: 4px;
max-height: 100%;
}
.blocklyDropdownMenu {
border-radius: 2px;
padding: 0 !important;
}
.blocklyDropdownMenu .blocklyMenuItem {
/* 28px on the left for icon or checkbox. */
padding-left: 28px;
}
/* BiDi override for the resting state. */
.blocklyDropdownMenu .blocklyMenuItemRtl {
/* Flip left/right padding for BiDi. */
padding-left: 5px;
padding-right: 28px;
}
.blocklyWidgetDiv .blocklyMenu {
background: #fff;
border: 1px solid transparent;
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
font: normal 13px Arial, sans-serif;
margin: 0;
outline: none;
padding: 4px 0;
position: absolute;
overflow-y: auto;
overflow-x: hidden;
max-height: 100%;
z-index: 20000; /* Arbitrary, but some apps depend on it... */
}
.blocklyWidgetDiv .blocklyMenu.blocklyFocused {
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
}
.blocklyDropDownDiv .blocklyMenu {
background: inherit; /* Compatibility with gapi, reset from goog-menu */
border: inherit; /* Compatibility with gapi, reset from goog-menu */
font: normal 13px "Helvetica Neue", Helvetica, sans-serif;
outline: none;
position: relative; /* Compatibility with gapi, reset from goog-menu */
z-index: 20000; /* Arbitrary, but some apps depend on it... */
}
/* State: resting. */
.blocklyMenuItem {
border: none;
color: #000;
cursor: pointer;
list-style: none;
margin: 0;
/* 7em on the right for shortcut. */
min-width: 7em;
padding: 6px 15px;
white-space: nowrap;
}
/* State: disabled. */
.blocklyMenuItemDisabled {
color: #ccc;
cursor: inherit;
}
/* State: hover. */
.blocklyMenuItemHighlight {
background-color: rgba(0,0,0,.1);
}
/* State: selected/checked. */
.blocklyMenuItemCheckbox {
height: 16px;
position: absolute;
width: 16px;
}
.blocklyMenuItemSelected .blocklyMenuItemCheckbox {
background: url(<<<PATH>>>/sprites.png) no-repeat -48px -16px;
float: left;
margin-left: -24px;
position: static; /* Scroll with the menu. */
}
.blocklyMenuItemRtl .blocklyMenuItemCheckbox {
float: right;
margin-right: -24px;
}
.blocklyBlockDragSurface .blocklyDraggable {
/* backup for browsers (e.g. IE11) that don't support grabbing */
cursor: url("<<<PATH>>>/handclosed.cur"), auto;
cursor: grabbing;
cursor: -webkit-grabbing;
}
.blocklyDragging.blocklyDraggingDelete {
cursor: url("<<<PATH>>>/handdelete.cur"), auto;
}
.blocklyDragging>.blocklyPath,
.blocklyDragging>.blocklyPathLight {
fill-opacity: .8;
stroke-opacity: .8;
}
.blocklyDragging>.blocklyPathDark {
display: none;
}
.blocklyDisabled>.blocklyPath {
fill-opacity: .5;
stroke-opacity: .5;
}
.blocklyDisabled>.blocklyPathLight,
.blocklyDisabled>.blocklyPathDark {
display: none;
}
.blocklyInsertionMarker>.blocklyPath,
.blocklyInsertionMarker>.blocklyPathLight,
.blocklyInsertionMarker>.blocklyPathDark {
fill-opacity: .2;
stroke: none;
}
.blocklyMultilineText {
font-family: monospace;
}
.blocklyNonEditableText>text {
pointer-events: none;
}
.blocklyFlyout {
position: absolute;
z-index: 20;
}
.blocklyText text {
cursor: default;
}
/*
Don't allow users to select text. It gets annoying when trying to
drag a block and selected text moves instead.
*/
.blocklySvg text,
.blocklyBlockDragSurface text {
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
cursor: inherit;
}
.blocklyHidden {
display: none;
}
.blocklyFieldDropdown:not(.blocklyHidden) {
display: block;
}
.blocklyIconGroup {
cursor: default;
}
.blocklyIconGroup:not(:hover),
.blocklyIconGroupReadonly {
opacity: .6;
}
.blocklyIconShape {
fill: #00f;
stroke: #fff;
stroke-width: 1px;
}
.blocklyIconSymbol {
fill: #fff;
}
.blocklyMinimalBody {
margin: 0;
padding: 0;
}
.blocklyHtmlInput {
border: none;
border-radius: 4px;
height: 100%;
margin: 0;
outline: none;
padding: 0;
width: 100%;
text-align: center;
display: block;
box-sizing: border-box;
}
/* Edge and IE introduce a close icon when the input value is longer than a
certain length. This affects our sizing calculations of the text input.
Hiding the close icon to avoid that. */
.blocklyHtmlInput::-ms-clear {
display: none;
}
.blocklyMainBackground {
stroke-width: 1;
stroke: #c6c6c6; /* Equates to #ddd due to border being off-pixel. */
}
.blocklyMutatorBackground {
fill: #fff;
stroke: #ddd;
stroke-width: 1;
}
.blocklyFlyoutBackground {
fill: #ddd;
fill-opacity: .8;
}
.blocklyMainWorkspaceScrollbar {
z-index: 20;
}
.blocklyFlyoutScrollbar {
z-index: 30;
}
.blocklyScrollbarHorizontal,
.blocklyScrollbarVertical {
position: absolute;
outline: none;
}
.blocklyScrollbarBackground {
opacity: 0;
}
.blocklyScrollbarHandle {
fill: #ccc;
}
.blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
.blocklyScrollbarHandle:hover {
fill: #bbb;
}
/* Darken flyout scrollbars due to being on a grey background. */
/* By contrast, workspace scrollbars are on a white background. */
.blocklyFlyout .blocklyScrollbarHandle {
fill: #bbb;
}
.blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
.blocklyFlyout .blocklyScrollbarHandle:hover {
fill: #aaa;
}
.blocklyInvalidInput {
background: #faa;
}
.blocklyVerticalMarker {
stroke-width: 3px;
fill: rgba(255,255,255,.5);
pointer-events: none;
}
.blocklyComputeCanvas {
position: absolute;
width: 0;
height: 0;
}
.blocklyNoPointerEvents {
pointer-events: none;
}
.blocklyContextMenu {
border-radius: 4px;
max-height: 100%;
}
.blocklyDropdownMenu {
border-radius: 2px;
padding: 0 !important;
}
.blocklyDropdownMenu .blocklyMenuItem {
/* 28px on the left for icon or checkbox. */
padding-left: 28px;
}
/* BiDi override for the resting state. */
.blocklyDropdownMenu .blocklyMenuItemRtl {
/* Flip left/right padding for BiDi. */
padding-left: 5px;
padding-right: 28px;
}
.blocklyWidgetDiv .blocklyMenu {
background: #fff;
border: 1px solid transparent;
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
font: normal 13px Arial, sans-serif;
margin: 0;
outline: none;
padding: 4px 0;
position: absolute;
overflow-y: auto;
overflow-x: hidden;
max-height: 100%;
z-index: 20000; /* Arbitrary, but some apps depend on it... */
}
.blocklyWidgetDiv .blocklyMenu.blocklyFocused {
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
}
.blocklyDropDownDiv .blocklyMenu {
background: inherit; /* Compatibility with gapi, reset from goog-menu */
border: inherit; /* Compatibility with gapi, reset from goog-menu */
font: normal 13px "Helvetica Neue", Helvetica, sans-serif;
outline: none;
position: relative; /* Compatibility with gapi, reset from goog-menu */
z-index: 20000; /* Arbitrary, but some apps depend on it... */
}
/* State: resting. */
.blocklyMenuItem {
border: none;
color: #000;
cursor: pointer;
list-style: none;
margin: 0;
/* 7em on the right for shortcut. */
min-width: 7em;
padding: 6px 15px;
white-space: nowrap;
}
/* State: disabled. */
.blocklyMenuItemDisabled {
color: #ccc;
cursor: inherit;
}
/* State: hover. */
.blocklyMenuItemHighlight {
background-color: rgba(0,0,0,.1);
}
/* State: selected/checked. */
.blocklyMenuItemCheckbox {
height: 16px;
position: absolute;
width: 16px;
}
.blocklyMenuItemSelected .blocklyMenuItemCheckbox {
background: url(<<<PATH>>>/sprites.png) no-repeat -48px -16px;
float: left;
margin-left: -24px;
position: static; /* Scroll with the menu. */
}
.blocklyMenuItemRtl .blocklyMenuItemCheckbox {
float: right;
margin-right: -24px;
}
`);
exports.content = content;

View File

@@ -18,7 +18,6 @@
*/
goog.module('Blockly.DeleteArea');
const object = goog.require('Blockly.utils.object');
const {BlockSvg} = goog.require('Blockly.BlockSvg');
const {DragTarget} = goog.require('Blockly.DragTarget');
/* eslint-disable-next-line no-unused-vars */
@@ -32,53 +31,57 @@ const {IDraggable} = goog.requireType('Blockly.IDraggable');
* dropped on top of it.
* @extends {DragTarget}
* @implements {IDeleteArea}
* @constructor
* @alias Blockly.DeleteArea
*/
const DeleteArea = function() {
DeleteArea.superClass_.constructor.call(this);
class DeleteArea extends DragTarget {
/**
* Constructor for DeleteArea. Should not be called directly, only by a
* subclass.
*/
constructor() {
super();
/**
* Whether the last block or bubble dragged over this delete area would be
* deleted if dropped on this component.
* This property is not updated after the block or bubble is deleted.
* @type {boolean}
* @protected
*/
this.wouldDelete_ = false;
}
/**
* Whether the last block or bubble dragged over this delete area would be
* deleted if dropped on this component.
* This property is not updated after the block or bubble is deleted.
* @type {boolean}
* Returns whether the provided block or bubble would be deleted if dropped on
* this area.
* This method should check if the element is deletable and is always called
* before onDragEnter/onDragOver/onDragExit.
* @param {!IDraggable} element The block or bubble currently being
* dragged.
* @param {boolean} couldConnect Whether the element could could connect to
* another.
* @return {boolean} Whether the element provided would be deleted if dropped
* on this area.
*/
wouldDelete(element, couldConnect) {
if (element instanceof BlockSvg) {
const block = /** @type {BlockSvg} */ (element);
const couldDeleteBlock = !block.getParent() && block.isDeletable();
this.updateWouldDelete_(couldDeleteBlock && !couldConnect);
} else {
this.updateWouldDelete_(element.isDeletable());
}
return this.wouldDelete_;
}
/**
* Updates the internal wouldDelete_ state.
* @param {boolean} wouldDelete The new value for the wouldDelete state.
* @protected
*/
this.wouldDelete_ = false;
};
object.inherits(DeleteArea, DragTarget);
/**
* Returns whether the provided block or bubble would be deleted if dropped on
* this area.
* This method should check if the element is deletable and is always called
* before onDragEnter/onDragOver/onDragExit.
* @param {!IDraggable} element The block or bubble currently being
* dragged.
* @param {boolean} couldConnect Whether the element could could connect to
* another.
* @return {boolean} Whether the element provided would be deleted if dropped on
* this area.
*/
DeleteArea.prototype.wouldDelete = function(element, couldConnect) {
if (element instanceof BlockSvg) {
const block = /** @type {BlockSvg} */ (element);
const couldDeleteBlock = !block.getParent() && block.isDeletable();
this.updateWouldDelete_(couldDeleteBlock && !couldConnect);
} else {
this.updateWouldDelete_(element.isDeletable());
updateWouldDelete_(wouldDelete) {
this.wouldDelete_ = wouldDelete;
}
return this.wouldDelete_;
};
/**
* Updates the internal wouldDelete_ state.
* @param {boolean} wouldDelete The new value for the wouldDelete state.
* @protected
*/
DeleteArea.prototype.updateWouldDelete_ = function(wouldDelete) {
this.wouldDelete_ = wouldDelete;
};
}
exports.DeleteArea = DeleteArea;

View File

@@ -30,68 +30,69 @@ const {Rect} = goog.requireType('Blockly.utils.Rect');
* Abstract class for a component with custom behaviour when a block or bubble
* is dragged over or dropped on top of it.
* @implements {IDragTarget}
* @constructor
* @alias Blockly.DragTarget
*/
const DragTarget = function() {};
class DragTarget {
/**
* Handles when a cursor with a block or bubble enters this drag target.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
*/
onDragEnter(_dragElement) {
// no-op
}
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to the Blockly injection div.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
*/
DragTarget.prototype.getClientRect;
/**
* Handles when a cursor with a block or bubble is dragged over this drag
* target.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
*/
onDragOver(_dragElement) {
// no-op
}
/**
* Handles when a cursor with a block or bubble enters this drag target.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
*/
DragTarget.prototype.onDragEnter = function(_dragElement) {
// no-op
};
/**
* Handles when a cursor with a block or bubble exits this drag target.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
*/
onDragExit(_dragElement) {
// no-op
}
/**
* Handles when a cursor with a block or bubble is dragged over this drag
* target.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
*/
DragTarget.prototype.onDragOver = function(_dragElement) {
// no-op
};
/**
* Handles when a block or bubble is dropped on this component.
* Should not handle delete here.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
*/
onDrop(_dragElement) {
// no-op
}
/**
* Handles when a cursor with a block or bubble exits this drag target.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
*/
DragTarget.prototype.onDragExit = function(_dragElement) {
// no-op
};
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to the Blockly injection div.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
*/
getClientRect() {
return null;
}
/**
* Handles when a block or bubble is dropped on this component.
* Should not handle delete here.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
*/
DragTarget.prototype.onDrop = function(_dragElement) {
// no-op
};
/**
* Returns whether the provided block or bubble should not be moved after being
* dropped on this component. If true, the element will return to where it was
* when the drag started.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
* @return {boolean} Whether the block or bubble provided should be returned to
* drag start.
*/
DragTarget.prototype.shouldPreventMove = function(_dragElement) {
return false;
};
/**
* Returns whether the provided block or bubble should not be moved after
* being dropped on this component. If true, the element will return to where
* it was when the drag started.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
* @return {boolean} Whether the block or bubble provided should be returned
* to drag start.
*/
shouldPreventMove(_dragElement) {
return false;
}
}
exports.DragTarget = DragTarget;

View File

@@ -16,7 +16,7 @@
* A div that floats on top of the workspace, for drop-down menus.
* @class
*/
goog.module('Blockly.DropDownDiv');
goog.module('Blockly.dropDownDiv');
const common = goog.require('Blockly.common');
const dom = goog.require('Blockly.utils.dom');
@@ -33,21 +33,14 @@ const {Size} = goog.requireType('Blockly.utils.Size');
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/**
* Class for drop-down div.
* @constructor
* @package
* @alias Blockly.DropDownDiv
*/
const DropDownDiv = function() {};
/**
* Arrow size in px. Should match the value in CSS
* (need to position pre-render).
* @type {number}
* @const
*/
DropDownDiv.ARROW_SIZE = 16;
const ARROW_SIZE = 16;
exports.ARROW_SIZE = ARROW_SIZE;
/**
* Drop-down border size in px. Should match the value in CSS (need to position
@@ -55,7 +48,8 @@ DropDownDiv.ARROW_SIZE = 16;
* @type {number}
* @const
*/
DropDownDiv.BORDER_SIZE = 1;
const BORDER_SIZE = 1;
exports.BORDER_SIZE = BORDER_SIZE;
/**
* Amount the arrow must be kept away from the edges of the main drop-down div,
@@ -63,93 +57,86 @@ DropDownDiv.BORDER_SIZE = 1;
* @type {number}
* @const
*/
DropDownDiv.ARROW_HORIZONTAL_PADDING = 12;
const ARROW_HORIZONTAL_PADDING = 12;
exports.ARROW_HORIZONTAL_PADDING = ARROW_HORIZONTAL_PADDING;
/**
* Amount drop-downs should be padded away from the source, in px.
* @type {number}
* @const
*/
DropDownDiv.PADDING_Y = 16;
const PADDING_Y = 16;
exports.PADDING_Y = PADDING_Y;
/**
* Length of animations in seconds.
* @type {number}
* @const
*/
DropDownDiv.ANIMATION_TIME = 0.25;
const ANIMATION_TIME = 0.25;
exports.ANIMATION_TIME = ANIMATION_TIME;
/**
* Timer for animation out, to be cleared if we need to immediately hide
* without disrupting new shows.
* @type {?number}
* @private
*/
DropDownDiv.animateOutTimer_ = null;
let animateOutTimer = null;
/**
* Callback for when the drop-down is hidden.
* @type {?Function}
* @private
*/
DropDownDiv.onHide_ = null;
let onHide = null;
/**
* A class name representing the current owner's workspace renderer.
* @type {string}
* @private
*/
DropDownDiv.rendererClassName_ = '';
let renderedClassName = '';
/**
* A class name representing the current owner's workspace theme.
* @type {string}
* @private
*/
DropDownDiv.themeClassName_ = '';
let themeClassName = '';
/**
* The content element.
* @type {!Element}
* @private
* @type {!HTMLDivElement}
*/
DropDownDiv.DIV_;
let div;
/**
* The content element.
* @type {!Element}
* @private
* @type {!HTMLDivElement}
*/
DropDownDiv.content_;
let content;
/**
* The arrow element.
* @type {!Element}
* @private
* @type {!HTMLDivElement}
*/
DropDownDiv.arrow_;
let arrow;
/**
* Drop-downs will appear within the bounds of this element if possible.
* Set in DropDownDiv.setBoundsElement.
* Set in setBoundsElement.
* @type {?Element}
* @private
*/
DropDownDiv.boundsElement_ = null;
let boundsElement = null;
/**
* The object currently using the drop-down.
* @type {?Object}
* @private
*/
DropDownDiv.owner_ = null;
let owner = null;
/**
* Whether the dropdown was positioned to a field or the source block.
* @type {?boolean}
* @private
*/
DropDownDiv.positionToField_ = null;
let positionToField = null;
/**
* Dropdown bounds info object used to encapsulate sizing information about a
@@ -163,7 +150,8 @@ DropDownDiv.positionToField_ = null;
* height:number
* }}
*/
DropDownDiv.BoundsInfo;
let BoundsInfo;
exports.BoundsInfo = BoundsInfo;
/**
* Dropdown position metrics.
@@ -178,84 +166,85 @@ DropDownDiv.BoundsInfo;
* arrowVisible:boolean
* }}
*/
DropDownDiv.PositionMetrics;
let PositionMetrics;
exports.PositionMetrics = PositionMetrics;
/**
* Create and insert the DOM element for this div.
* @package
*/
DropDownDiv.createDom = function() {
if (DropDownDiv.DIV_) {
const createDom = function() {
if (div) {
return; // Already created.
}
const containerDiv = document.createElement('div');
containerDiv.className = 'blocklyDropDownDiv';
div = /** @type {!HTMLDivElement} */ (document.createElement('div'));
div.className = 'blocklyDropDownDiv';
const parentDiv = common.getParentContainer() || document.body;
parentDiv.appendChild(containerDiv);
parentDiv.appendChild(div);
DropDownDiv.DIV_ = containerDiv;
const content = document.createElement('div');
content = /** @type {!HTMLDivElement} */ (document.createElement('div'));
content.className = 'blocklyDropDownContent';
DropDownDiv.DIV_.appendChild(content);
DropDownDiv.content_ = content;
div.appendChild(content);
const arrow = document.createElement('div');
arrow = /** @type {!HTMLDivElement} */ (document.createElement('div'));
arrow.className = 'blocklyDropDownArrow';
DropDownDiv.DIV_.appendChild(arrow);
DropDownDiv.arrow_ = arrow;
div.appendChild(arrow);
DropDownDiv.DIV_.style.opacity = 0;
div.style.opacity = 0;
// Transition animation for transform: translate() and opacity.
DropDownDiv.DIV_.style.transition = 'transform ' +
DropDownDiv.ANIMATION_TIME + 's, ' +
'opacity ' + DropDownDiv.ANIMATION_TIME + 's';
div.style.transition = 'transform ' + ANIMATION_TIME + 's, ' +
'opacity ' + ANIMATION_TIME + 's';
// Handle focusin/out events to add a visual indicator when
// a child is focused or blurred.
DropDownDiv.DIV_.addEventListener('focusin', function() {
dom.addClass(DropDownDiv.DIV_, 'blocklyFocused');
div.addEventListener('focusin', function() {
dom.addClass(div, 'blocklyFocused');
});
DropDownDiv.DIV_.addEventListener('focusout', function() {
dom.removeClass(DropDownDiv.DIV_, 'blocklyFocused');
div.addEventListener('focusout', function() {
dom.removeClass(div, 'blocklyFocused');
});
};
exports.createDom = createDom;
/**
* Set an element to maintain bounds within. Drop-downs will appear
* within the box of this element if possible.
* @param {?Element} boundsElement Element to bind drop-down to.
* @param {?Element} boundsElem Element to bind drop-down to.
*/
DropDownDiv.setBoundsElement = function(boundsElement) {
DropDownDiv.boundsElement_ = boundsElement;
const setBoundsElement = function(boundsElem) {
boundsElement = boundsElem;
};
exports.setBoundsElement = setBoundsElement;
/**
* Provide the div for inserting content into the drop-down.
* @return {!Element} Div to populate with content.
*/
DropDownDiv.getContentDiv = function() {
return DropDownDiv.content_;
const getContentDiv = function() {
return content;
};
exports.getContentDiv = getContentDiv;
/**
* Clear the content of the drop-down.
*/
DropDownDiv.clearContent = function() {
DropDownDiv.content_.textContent = '';
DropDownDiv.content_.style.width = '';
const clearContent = function() {
content.textContent = '';
content.style.width = '';
};
exports.clearContent = clearContent;
/**
* Set the colour for the drop-down.
* @param {string} backgroundColour Any CSS colour for the background.
* @param {string} borderColour Any CSS colour for the border.
*/
DropDownDiv.setColour = function(backgroundColour, borderColour) {
DropDownDiv.DIV_.style.backgroundColor = backgroundColour;
DropDownDiv.DIV_.style.borderColor = borderColour;
const setColour = function(backgroundColour, borderColour) {
div.style.backgroundColor = backgroundColour;
div.style.borderColor = borderColour;
};
exports.setColour = setColour;
/**
* Shortcut to show and place the drop-down with positioning determined
@@ -270,11 +259,12 @@ DropDownDiv.setColour = function(backgroundColour, borderColour) {
* positioning.
* @return {boolean} True if the menu rendered below block; false if above.
*/
DropDownDiv.showPositionedByBlock = function(
const showPositionedByBlock = function(
field, block, opt_onHide, opt_secondaryYOffset) {
return showPositionedByRect(
getScaledBboxOfBlock(block), field, opt_onHide, opt_secondaryYOffset);
};
exports.showPositionedByBlock = showPositionedByBlock;
/**
* Shortcut to show and place the drop-down with positioning determined
@@ -288,14 +278,13 @@ DropDownDiv.showPositionedByBlock = function(
* positioning.
* @return {boolean} True if the menu rendered below block; false if above.
*/
DropDownDiv.showPositionedByField = function(
const showPositionedByField = function(
field, opt_onHide, opt_secondaryYOffset) {
DropDownDiv.positionToField_ = true;
positionToField = true;
return showPositionedByRect(
getScaledBboxOfField(field), field, opt_onHide, opt_secondaryYOffset);
};
const internal = {};
exports.showPositionedByField = showPositionedByField;
/**
* Get the scaled bounding box of a block.
@@ -353,9 +342,9 @@ const showPositionedByRect = function(
workspace =
/** @type {!WorkspaceSvg} */ (workspace.options.parentWorkspace);
}
DropDownDiv.setBoundsElement(
setBoundsElement(
/** @type {?Element} */ (workspace.getParentSvg().parentNode));
return DropDownDiv.show(
return show(
field, sourceBlock.RTL, primaryX, primaryY, secondaryX, secondaryY,
opt_onHide);
};
@@ -368,7 +357,7 @@ const showPositionedByRect = function(
* will point there, and the container will be positioned below it.
* If we can't maintain the container bounds at the primary point, fall-back to
* the secondary point and position above.
* @param {?Object} owner The object showing the drop-down
* @param {?Object} newOwner The object showing the drop-down
* @param {boolean} rtl Right-to-left (true) or left-to-right (false).
* @param {number} primaryX Desired origin point x, in absolute px.
* @param {number} primaryY Desired origin point y, in absolute px.
@@ -381,20 +370,19 @@ const showPositionedByRect = function(
* @return {boolean} True if the menu rendered at the primary origin point.
* @package
*/
DropDownDiv.show = function(
owner, rtl, primaryX, primaryY, secondaryX, secondaryY, opt_onHide) {
DropDownDiv.owner_ = owner;
DropDownDiv.onHide_ = opt_onHide || null;
const show = function(
newOwner, rtl, primaryX, primaryY, secondaryX, secondaryY, opt_onHide) {
owner = newOwner;
onHide = opt_onHide || null;
// Set direction.
const div = DropDownDiv.DIV_;
div.style.direction = rtl ? 'rtl' : 'ltr';
const mainWorkspace =
/** @type {!WorkspaceSvg} */ (common.getMainWorkspace());
DropDownDiv.rendererClassName_ = mainWorkspace.getRenderer().getClassName();
DropDownDiv.themeClassName_ = mainWorkspace.getTheme().getClassName();
dom.addClass(div, DropDownDiv.rendererClassName_);
dom.addClass(div, DropDownDiv.themeClassName_);
renderedClassName = mainWorkspace.getRenderer().getClassName();
themeClassName = mainWorkspace.getTheme().getClassName();
dom.addClass(div, renderedClassName);
dom.addClass(div, themeClassName);
// When we change `translate` multiple times in close succession,
// Chrome may choose to wait and apply them all at once.
@@ -407,17 +395,20 @@ DropDownDiv.show = function(
return positionInternal(primaryX, primaryY, secondaryX, secondaryY);
};
exports.show = show;
const internal = {};
/**
* Get sizing info about the bounding element.
* @return {!DropDownDiv.BoundsInfo} An object containing size
* @return {!BoundsInfo} An object containing size
* information about the bounding element (bounding box and width/height).
*/
internal.getBoundsInfo = function() {
const boundPosition = style.getPageOffset(
/** @type {!Element} */ (DropDownDiv.boundsElement_));
/** @type {!Element} */ (boundsElement));
const boundSize = style.getSize(
/** @type {!Element} */ (DropDownDiv.boundsElement_));
/** @type {!Element} */ (boundsElement));
return {
left: boundPosition.x,
@@ -431,21 +422,21 @@ internal.getBoundsInfo = function() {
/**
* Helper to position the drop-down and the arrow, maintaining bounds.
* See explanation of origin points in DropDownDiv.show.
* See explanation of origin points in show.
* @param {number} primaryX Desired origin point x, in absolute px.
* @param {number} primaryY Desired origin point y, in absolute px.
* @param {number} secondaryX Secondary/alternative origin point x,
* in absolute px.
* @param {number} secondaryY Secondary/alternative origin point y,
* in absolute px.
* @return {!DropDownDiv.PositionMetrics} Various final metrics,
* @return {!PositionMetrics} Various final metrics,
* including rendered positions for drop-down and arrow.
*/
internal.getPositionMetrics = function(
primaryX, primaryY, secondaryX, secondaryY) {
const boundsInfo = internal.getBoundsInfo();
const divSize = style.getSize(
/** @type {!Element} */ (DropDownDiv.DIV_));
/** @type {!Element} */ (div));
// Can we fit in-bounds below the target?
if (primaryY + divSize.height < boundsInfo.bottom) {
@@ -472,20 +463,20 @@ internal.getPositionMetrics = function(
* Get the metrics for positioning the div below the source.
* @param {number} primaryX Desired origin point x, in absolute px.
* @param {number} primaryY Desired origin point y, in absolute px.
* @param {!DropDownDiv.BoundsInfo} boundsInfo An object containing size
* @param {!BoundsInfo} boundsInfo An object containing size
* information about the bounding element (bounding box and width/height).
* @param {!Size} divSize An object containing information about
* the size of the DropDownDiv (width & height).
* @return {!DropDownDiv.PositionMetrics} Various final metrics,
* @return {!PositionMetrics} Various final metrics,
* including rendered positions for drop-down and arrow.
*/
const getPositionBelowMetrics = function(
primaryX, primaryY, boundsInfo, divSize) {
const xCoords = DropDownDiv.getPositionX(
primaryX, boundsInfo.left, boundsInfo.right, divSize.width);
const xCoords =
getPositionX(primaryX, boundsInfo.left, boundsInfo.right, divSize.width);
const arrowY = -(DropDownDiv.ARROW_SIZE / 2 + DropDownDiv.BORDER_SIZE);
const finalY = primaryY + DropDownDiv.PADDING_Y;
const arrowY = -(ARROW_SIZE / 2 + BORDER_SIZE);
const finalY = primaryY + PADDING_Y;
return {
initialX: xCoords.divX,
@@ -505,21 +496,20 @@ const getPositionBelowMetrics = function(
* in absolute px.
* @param {number} secondaryY Secondary/alternative origin point y,
* in absolute px.
* @param {!DropDownDiv.BoundsInfo} boundsInfo An object containing size
* @param {!BoundsInfo} boundsInfo An object containing size
* information about the bounding element (bounding box and width/height).
* @param {!Size} divSize An object containing information about
* the size of the DropDownDiv (width & height).
* @return {!DropDownDiv.PositionMetrics} Various final metrics,
* @return {!PositionMetrics} Various final metrics,
* including rendered positions for drop-down and arrow.
*/
const getPositionAboveMetrics = function(
secondaryX, secondaryY, boundsInfo, divSize) {
const xCoords = DropDownDiv.getPositionX(
const xCoords = getPositionX(
secondaryX, boundsInfo.left, boundsInfo.right, divSize.width);
const arrowY = divSize.height - (DropDownDiv.BORDER_SIZE * 2) -
(DropDownDiv.ARROW_SIZE / 2);
const finalY = secondaryY - divSize.height - DropDownDiv.PADDING_Y;
const arrowY = divSize.height - (BORDER_SIZE * 2) - (ARROW_SIZE / 2);
const finalY = secondaryY - divSize.height - PADDING_Y;
const initialY = secondaryY - divSize.height; // No padding on Y.
return {
@@ -537,16 +527,16 @@ const getPositionAboveMetrics = function(
/**
* Get the metrics for positioning the div at the top of the page.
* @param {number} sourceX Desired origin point x, in absolute px.
* @param {!DropDownDiv.BoundsInfo} boundsInfo An object containing size
* @param {!BoundsInfo} boundsInfo An object containing size
* information about the bounding element (bounding box and width/height).
* @param {!Size} divSize An object containing information about
* the size of the DropDownDiv (width & height).
* @return {!DropDownDiv.PositionMetrics} Various final metrics,
* @return {!PositionMetrics} Various final metrics,
* including rendered positions for drop-down and arrow.
*/
const getPositionTopOfPageMetrics = function(sourceX, boundsInfo, divSize) {
const xCoords = DropDownDiv.getPositionX(
sourceX, boundsInfo.left, boundsInfo.right, divSize.width);
const xCoords =
getPositionX(sourceX, boundsInfo.left, boundsInfo.right, divSize.width);
// No need to provide arrow-specific information because it won't be visible.
return {
@@ -574,8 +564,7 @@ const getPositionTopOfPageMetrics = function(sourceX, boundsInfo, divSize) {
* the x positions of the left side of the DropDownDiv and the arrow.
* @package
*/
DropDownDiv.getPositionX = function(
sourceX, boundsLeft, boundsRight, divWidth) {
const getPositionX = function(sourceX, boundsLeft, boundsRight, divWidth) {
let divX = sourceX;
// Offset the topLeft coord so that the dropdowndiv is centered.
divX -= divWidth / 2;
@@ -584,77 +573,79 @@ DropDownDiv.getPositionX = function(
let arrowX = sourceX;
// Offset the arrow coord so that the arrow is centered.
arrowX -= DropDownDiv.ARROW_SIZE / 2;
arrowX -= ARROW_SIZE / 2;
// Convert the arrow position to be relative to the top left of the div.
let relativeArrowX = arrowX - divX;
const horizPadding = DropDownDiv.ARROW_HORIZONTAL_PADDING;
const horizPadding = ARROW_HORIZONTAL_PADDING;
// Clamp the arrow position so that it stays attached to the dropdowndiv.
relativeArrowX = math.clamp(
horizPadding, relativeArrowX,
divWidth - horizPadding - DropDownDiv.ARROW_SIZE);
horizPadding, relativeArrowX, divWidth - horizPadding - ARROW_SIZE);
return {arrowX: relativeArrowX, divX: divX};
};
exports.getPositionX = getPositionX;
/**
* Is the container visible?
* @return {boolean} True if visible.
*/
DropDownDiv.isVisible = function() {
return !!DropDownDiv.owner_;
const isVisible = function() {
return !!owner;
};
exports.isVisible = isVisible;
/**
* Hide the menu only if it is owned by the provided object.
* @param {?Object} owner Object which must be owning the drop-down to hide.
* @param {?Object} divOwner Object which must be owning the drop-down to hide.
* @param {boolean=} opt_withoutAnimation True if we should hide the dropdown
* without animating.
* @return {boolean} True if hidden.
*/
DropDownDiv.hideIfOwner = function(owner, opt_withoutAnimation) {
if (DropDownDiv.owner_ === owner) {
const hideIfOwner = function(divOwner, opt_withoutAnimation) {
if (owner === divOwner) {
if (opt_withoutAnimation) {
DropDownDiv.hideWithoutAnimation();
hideWithoutAnimation();
} else {
DropDownDiv.hide();
hide();
}
return true;
}
return false;
};
exports.hideIfOwner = hideIfOwner;
/**
* Hide the menu, triggering animation.
*/
DropDownDiv.hide = function() {
const hide = function() {
// Start the animation by setting the translation and fading out.
// Reset to (initialX, initialY) - i.e., no translation.
DropDownDiv.DIV_.style.transform = 'translate(0, 0)';
DropDownDiv.DIV_.style.opacity = 0;
div.style.transform = 'translate(0, 0)';
div.style.opacity = 0;
// Finish animation - reset all values to default.
DropDownDiv.animateOutTimer_ = setTimeout(function() {
DropDownDiv.hideWithoutAnimation();
}, DropDownDiv.ANIMATION_TIME * 1000);
if (DropDownDiv.onHide_) {
DropDownDiv.onHide_();
DropDownDiv.onHide_ = null;
animateOutTimer = setTimeout(function() {
hideWithoutAnimation();
}, ANIMATION_TIME * 1000);
if (onHide) {
onHide();
onHide = null;
}
};
exports.hide = hide;
/**
* Hide the menu, without animation.
*/
DropDownDiv.hideWithoutAnimation = function() {
if (!DropDownDiv.isVisible()) {
const hideWithoutAnimation = function() {
if (!isVisible()) {
return;
}
if (DropDownDiv.animateOutTimer_) {
clearTimeout(DropDownDiv.animateOutTimer_);
if (animateOutTimer) {
clearTimeout(animateOutTimer);
}
// Reset style properties in case this gets called directly
// instead of hide() - see discussion on #2551.
const div = DropDownDiv.DIV_;
div.style.transform = '';
div.style.left = '';
div.style.top = '';
@@ -663,23 +654,24 @@ DropDownDiv.hideWithoutAnimation = function() {
div.style.backgroundColor = '';
div.style.borderColor = '';
if (DropDownDiv.onHide_) {
DropDownDiv.onHide_();
DropDownDiv.onHide_ = null;
if (onHide) {
onHide();
onHide = null;
}
DropDownDiv.clearContent();
DropDownDiv.owner_ = null;
clearContent();
owner = null;
if (DropDownDiv.rendererClassName_) {
dom.removeClass(div, DropDownDiv.rendererClassName_);
DropDownDiv.rendererClassName_ = '';
if (renderedClassName) {
dom.removeClass(div, renderedClassName);
renderedClassName = '';
}
if (DropDownDiv.themeClassName_) {
dom.removeClass(div, DropDownDiv.themeClassName_);
DropDownDiv.themeClassName_ = '';
if (themeClassName) {
dom.removeClass(div, themeClassName);
themeClassName = '';
}
(/** @type {!WorkspaceSvg} */ (common.getMainWorkspace())).markFocused();
};
exports.hideWithoutAnimation = hideWithoutAnimation;
/**
* Set the dropdown div's position.
@@ -697,15 +689,15 @@ const positionInternal = function(primaryX, primaryY, secondaryX, secondaryY) {
// Update arrow CSS.
if (metrics.arrowVisible) {
DropDownDiv.arrow_.style.display = '';
DropDownDiv.arrow_.style.transform = 'translate(' + metrics.arrowX + 'px,' +
arrow.style.display = '';
arrow.style.transform = 'translate(' + metrics.arrowX + 'px,' +
metrics.arrowY + 'px) rotate(45deg)';
DropDownDiv.arrow_.setAttribute(
arrow.setAttribute(
'class',
metrics.arrowAtTop ? 'blocklyDropDownArrow blocklyArrowTop' :
'blocklyDropDownArrow blocklyArrowBottom');
} else {
DropDownDiv.arrow_.style.display = 'none';
arrow.style.display = 'none';
}
const initialX = Math.floor(metrics.initialX);
@@ -713,7 +705,6 @@ const positionInternal = function(primaryX, primaryY, secondaryX, secondaryY) {
const finalX = Math.floor(metrics.finalX);
const finalY = Math.floor(metrics.finalY);
const div = DropDownDiv.DIV_;
// First apply initial translation.
div.style.left = initialX + 'px';
div.style.top = initialY + 'px';
@@ -736,17 +727,17 @@ const positionInternal = function(primaryX, primaryY, secondaryX, secondaryY) {
* calculate the new position, it will just hide it instead.
* @package
*/
DropDownDiv.repositionForWindowResize = function() {
const repositionForWindowResize = function() {
// This condition mainly catches the dropdown div when it is being used as a
// dropdown. It is important not to close it in this case because on Android,
// when a field is focused, the soft keyboard opens triggering a window resize
// event and we want the dropdown div to stick around so users can type into
// it.
if (DropDownDiv.owner_) {
const field = /** @type {!Field} */ (DropDownDiv.owner_);
if (owner) {
const field = /** @type {!Field} */ (owner);
const block = /** @type {!BlockSvg} */ (field.getSourceBlock());
const bBox = DropDownDiv.positionToField_ ? getScaledBboxOfField(field) :
getScaledBboxOfBlock(block);
const bBox = positionToField ? getScaledBboxOfField(field) :
getScaledBboxOfBlock(block);
// If we can fit it, render below the block.
const primaryX = bBox.left + (bBox.right - bBox.left) / 2;
const primaryY = bBox.bottom;
@@ -755,10 +746,9 @@ DropDownDiv.repositionForWindowResize = function() {
const secondaryY = bBox.top;
positionInternal(primaryX, primaryY, secondaryX, secondaryY);
} else {
DropDownDiv.hide();
hide();
}
};
exports.repositionForWindowResize = repositionForWindowResize;
DropDownDiv.TEST_ONLY = internal;
exports.DropDownDiv = DropDownDiv;
exports.TEST_ONLY = internal;

View File

@@ -15,9 +15,9 @@
*/
goog.module('Blockly.Events');
const Abstract = goog.require('Blockly.Events.Abstract');
const deprecation = goog.require('Blockly.utils.deprecation');
const eventUtils = goog.require('Blockly.Events.utils');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
const {BlockBase} = goog.require('Blockly.Events.BlockBase');
const {BlockChange} = goog.require('Blockly.Events.BlockChange');
const {BlockCreate} = goog.require('Blockly.Events.BlockCreate');
@@ -47,7 +47,7 @@ const {ViewportChange} = goog.require('Blockly.Events.ViewportChange');
// Events.
exports.Abstract = Abstract;
exports.Abstract = AbstractEvent;
exports.BubbleOpen = BubbleOpen;
exports.BlockBase = BlockBase;
exports.BlockChange = BlockChange;

View File

@@ -24,98 +24,110 @@ const {Workspace} = goog.requireType('Blockly.Workspace');
/**
* Abstract class for an event.
* @constructor
* @abstract
* @alias Blockly.Events.Abstract
*/
const Abstract = function() {
class Abstract {
/**
* Whether or not the event is blank (to be populated by fromJson).
* @type {?boolean}
* @alias Blockly.Events.Abstract
*/
this.isBlank = null;
constructor() {
/**
* Whether or not the event is blank (to be populated by fromJson).
* @type {?boolean}
*/
this.isBlank = null;
/**
* The workspace identifier for this event.
* @type {string|undefined}
*/
this.workspaceId = undefined;
/**
* The workspace identifier for this event.
* @type {string|undefined}
*/
this.workspaceId = undefined;
/**
* The event group id for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
* @type {string}
*/
this.group = eventUtils.getGroup();
/**
* The event group id for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
* @type {string}
*/
this.group = eventUtils.getGroup();
/**
* Sets whether the event should be added to the undo stack.
* @type {boolean}
*/
this.recordUndo = eventUtils.getRecordUndo();
};
/**
* Sets whether the event should be added to the undo stack.
* @type {boolean}
*/
this.recordUndo = eventUtils.getRecordUndo();
/**
* Whether or not the event is a UI event.
* @type {boolean}
*/
Abstract.prototype.isUiEvent = false;
/**
* Whether or not the event is a UI event.
* @type {boolean}
*/
this.isUiEvent = false;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Abstract.prototype.toJson = function() {
const json = {'type': this.type};
if (this.group) {
json['group'] = this.group;
/**
* Type of this event.
* @type {string|undefined}
*/
this.type = undefined;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Abstract.prototype.fromJson = function(json) {
this.isBlank = false;
this.group = json['group'];
};
/**
* Does this event record any change of state?
* @return {boolean} True if null, false if something changed.
*/
Abstract.prototype.isNull = function() {
return false;
};
/**
* Run an event.
* @param {boolean} _forward True if run forward, false if run backward (undo).
*/
Abstract.prototype.run = function(_forward) {
// Defined by subclasses.
};
/**
* Get workspace the event belongs to.
* @return {!Workspace} The workspace the event belongs to.
* @throws {Error} if workspace is null.
* @protected
*/
Abstract.prototype.getEventWorkspace_ = function() {
let workspace;
if (this.workspaceId) {
const {Workspace} = goog.module.get('Blockly.Workspace');
workspace = Workspace.getById(this.workspaceId);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = {'type': this.type};
if (this.group) {
json['group'] = this.group;
}
return json;
}
if (!workspace) {
throw Error(
'Workspace is null. Event must have been generated from real' +
' Blockly events.');
}
return workspace;
};
exports = Abstract;
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
this.isBlank = false;
this.group = json['group'];
}
/**
* Does this event record any change of state?
* @return {boolean} True if null, false if something changed.
*/
isNull() {
return false;
}
/**
* Run an event.
* @param {boolean} _forward True if run forward, false if run backward
* (undo).
*/
run(_forward) {
// Defined by subclasses.
}
/**
* Get workspace the event belongs to.
* @return {!Workspace} The workspace the event belongs to.
* @throws {Error} if workspace is null.
* @protected
*/
getEventWorkspace_() {
let workspace;
if (this.workspaceId) {
const {Workspace} = goog.module.get('Blockly.Workspace');
workspace = Workspace.getById(this.workspaceId);
}
if (!workspace) {
throw Error(
'Workspace is null. Event must have been generated from real' +
' Blockly events.');
}
return workspace;
}
}
exports.Abstract = Abstract;

View File

@@ -15,55 +15,56 @@
*/
goog.module('Blockly.Events.BlockBase');
const Abstract = goog.require('Blockly.Events.Abstract');
const object = goog.require('Blockly.utils.object');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/**
* Abstract class for a block event.
* @param {!Block=} opt_block The block this event corresponds to.
* Undefined for a blank event.
* @extends {Abstract}
* @constructor
* @extends {AbstractEvent}
* @alias Blockly.Events.BlockBase
*/
const BlockBase = function(opt_block) {
BlockBase.superClass_.constructor.call(this);
this.isBlank = typeof opt_block === 'undefined';
class BlockBase extends AbstractEvent {
/**
* @param {!Block=} opt_block The block this event corresponds to.
* Undefined for a blank event.
*/
constructor(opt_block) {
super();
this.isBlank = typeof opt_block === 'undefined';
/**
* The block ID for the block this event pertains to
* @type {string}
*/
this.blockId = this.isBlank ? '' : opt_block.id;
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = this.isBlank ? '' : opt_block.workspace.id;
}
/**
* The block ID for the block this event pertains to
* @type {string}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.blockId = this.isBlank ? '' : opt_block.id;
toJson() {
const json = super.toJson();
json['blockId'] = this.blockId;
return json;
}
/**
* The workspace identifier for this event.
* @type {string}
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
this.workspaceId = this.isBlank ? '' : opt_block.workspace.id;
};
object.inherits(BlockBase, Abstract);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
BlockBase.prototype.toJson = function() {
const json = BlockBase.superClass_.toJson.call(this);
json['blockId'] = this.blockId;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
BlockBase.prototype.fromJson = function(json) {
BlockBase.superClass_.fromJson.call(this, json);
this.blockId = json['blockId'];
};
fromJson(json) {
super.fromJson(json);
this.blockId = json['blockId'];
}
}
exports.BlockBase = BlockBase;

View File

@@ -17,7 +17,6 @@ goog.module('Blockly.Events.BlockChange');
const Xml = goog.require('Blockly.Xml');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {BlockBase} = goog.require('Blockly.Events.BlockBase');
/* eslint-disable-next-line no-unused-vars */
@@ -28,145 +27,152 @@ const {Block} = goog.requireType('Blockly.Block');
/**
* Class for a block change event.
* @param {!Block=} opt_block The changed block. Undefined for a blank
* event.
* @param {string=} opt_element One of 'field', 'comment', 'disabled', etc.
* @param {?string=} opt_name Name of input or field affected, or null.
* @param {*=} opt_oldValue Previous value of element.
* @param {*=} opt_newValue New value of element.
* @extends {BlockBase}
* @constructor
* @alias Blockly.Events.BlockChange
*/
const BlockChange = function(
opt_block, opt_element, opt_name, opt_oldValue, opt_newValue) {
BlockChange.superClass_.constructor.call(this, opt_block);
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
this.element = typeof opt_element === 'undefined' ? '' : opt_element;
this.name = typeof opt_name === 'undefined' ? '' : opt_name;
this.oldValue = typeof opt_oldValue === 'undefined' ? '' : opt_oldValue;
this.newValue = typeof opt_newValue === 'undefined' ? '' : opt_newValue;
};
object.inherits(BlockChange, BlockBase);
class BlockChange extends BlockBase {
/**
* @param {!Block=} opt_block The changed block. Undefined for a blank
* event.
* @param {string=} opt_element One of 'field', 'comment', 'disabled', etc.
* @param {?string=} opt_name Name of input or field affected, or null.
* @param {*=} opt_oldValue Previous value of element.
* @param {*=} opt_newValue New value of element.
*/
constructor(opt_block, opt_element, opt_name, opt_oldValue, opt_newValue) {
super(opt_block);
/**
* Type of this event.
* @type {string}
*/
BlockChange.prototype.type = eventUtils.BLOCK_CHANGE;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.BLOCK_CHANGE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
BlockChange.prototype.toJson = function() {
const json = BlockChange.superClass_.toJson.call(this);
json['element'] = this.element;
if (this.name) {
json['name'] = this.name;
}
json['oldValue'] = this.oldValue;
json['newValue'] = this.newValue;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
BlockChange.prototype.fromJson = function(json) {
BlockChange.superClass_.fromJson.call(this, json);
this.element = json['element'];
this.name = json['name'];
this.oldValue = json['oldValue'];
this.newValue = json['newValue'];
};
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
BlockChange.prototype.isNull = function() {
return this.oldValue === this.newValue;
};
/**
* Run a change event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
BlockChange.prototype.run = function(forward) {
const workspace = this.getEventWorkspace_();
const block = workspace.getBlockById(this.blockId);
if (!block) {
console.warn('Can\'t change non-existent block: ' + this.blockId);
return;
}
if (block.mutator) {
// Close the mutator (if open) since we don't want to update it.
block.mutator.setVisible(false);
}
const value = forward ? this.newValue : this.oldValue;
switch (this.element) {
case 'field': {
const field = block.getField(this.name);
if (field) {
field.setValue(value);
} else {
console.warn('Can\'t set non-existent field: ' + this.name);
}
break;
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
case 'comment':
block.setCommentText(/** @type {string} */ (value) || null);
break;
case 'collapsed':
block.setCollapsed(!!value);
break;
case 'disabled':
block.setEnabled(!value);
break;
case 'inline':
block.setInputsInline(!!value);
break;
case 'mutation': {
const oldState = BlockChange.getExtraBlockState_(
/** @type {!BlockSvg} */ (block));
if (block.loadExtraState) {
block.loadExtraState(JSON.parse(/** @type {string} */ (value) || '{}'));
} else if (block.domToMutation) {
block.domToMutation(
Xml.textToDom(/** @type {string} */ (value) || '<mutation/>'));
}
eventUtils.fire(
new BlockChange(block, 'mutation', null, oldState, value));
break;
}
default:
console.warn('Unknown change type: ' + this.element);
this.element = typeof opt_element === 'undefined' ? '' : opt_element;
this.name = typeof opt_name === 'undefined' ? '' : opt_name;
this.oldValue = typeof opt_oldValue === 'undefined' ? '' : opt_oldValue;
this.newValue = typeof opt_newValue === 'undefined' ? '' : opt_newValue;
}
};
// TODO (#5397): Encapsulate this in the BlocklyMutationChange event when
// refactoring change events.
/**
* Returns the extra state of the given block (either as XML or a JSO, depending
* on the block's definition).
* @param {!BlockSvg} block The block to get the extra state of.
* @return {string} A stringified version of the extra state of the given block.
* @package
*/
BlockChange.getExtraBlockState_ = function(block) {
if (block.saveExtraState) {
const state = block.saveExtraState();
return state ? JSON.stringify(state) : '';
} else if (block.mutationToDom) {
const state = block.mutationToDom();
return state ? Xml.domToText(state) : '';
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
json['element'] = this.element;
if (this.name) {
json['name'] = this.name;
}
json['oldValue'] = this.oldValue;
json['newValue'] = this.newValue;
return json;
}
return '';
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.element = json['element'];
this.name = json['name'];
this.oldValue = json['oldValue'];
this.newValue = json['newValue'];
}
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
isNull() {
return this.oldValue === this.newValue;
}
/**
* Run a change event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
run(forward) {
const workspace = this.getEventWorkspace_();
const block = workspace.getBlockById(this.blockId);
if (!block) {
console.warn('Can\'t change non-existent block: ' + this.blockId);
return;
}
// Assume the block is rendered so that then we can check.
const blockSvg = /** @type {!BlockSvg} */ (block);
if (blockSvg.mutator) {
// Close the mutator (if open) since we don't want to update it.
blockSvg.mutator.setVisible(false);
}
const value = forward ? this.newValue : this.oldValue;
switch (this.element) {
case 'field': {
const field = block.getField(this.name);
if (field) {
field.setValue(value);
} else {
console.warn('Can\'t set non-existent field: ' + this.name);
}
break;
}
case 'comment':
block.setCommentText(/** @type {string} */ (value) || null);
break;
case 'collapsed':
block.setCollapsed(!!value);
break;
case 'disabled':
block.setEnabled(!value);
break;
case 'inline':
block.setInputsInline(!!value);
break;
case 'mutation': {
const oldState = BlockChange.getExtraBlockState_(
/** @type {!BlockSvg} */ (block));
if (block.loadExtraState) {
block.loadExtraState(
JSON.parse(/** @type {string} */ (value) || '{}'));
} else if (block.domToMutation) {
block.domToMutation(
Xml.textToDom(/** @type {string} */ (value) || '<mutation/>'));
}
eventUtils.fire(
new BlockChange(block, 'mutation', null, oldState, value));
break;
}
default:
console.warn('Unknown change type: ' + this.element);
}
}
// TODO (#5397): Encapsulate this in the BlocklyMutationChange event when
// refactoring change events.
/**
* Returns the extra state of the given block (either as XML or a JSO,
* depending on the block's definition).
* @param {!BlockSvg} block The block to get the extra state of.
* @return {string} A stringified version of the extra state of the given
* block.
* @package
*/
static getExtraBlockState_(block) {
if (block.saveExtraState) {
const state = block.saveExtraState();
return state ? JSON.stringify(state) : '';
} else if (block.mutationToDom) {
const state = block.mutationToDom();
return state ? Xml.domToText(state) : '';
}
return '';
}
}
registry.register(registry.Type.EVENT, eventUtils.CHANGE, BlockChange);

View File

@@ -18,7 +18,6 @@ goog.module('Blockly.Events.BlockCreate');
const Xml = goog.require('Blockly.Xml');
const blocks = goog.require('Blockly.serialization.blocks');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {BlockBase} = goog.require('Blockly.Events.BlockBase');
/* eslint-disable-next-line no-unused-vars */
@@ -27,90 +26,93 @@ const {Block} = goog.requireType('Blockly.Block');
/**
* Class for a block creation event.
* @param {!Block=} opt_block The created block. Undefined for a blank
* event.
* @extends {BlockBase}
* @constructor
* @alias Blockly.Events.BlockCreate
*/
const BlockCreate = function(opt_block) {
BlockCreate.superClass_.constructor.call(this, opt_block);
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
if (opt_block.isShadow()) {
// Moving shadow blocks is handled via disconnection.
this.recordUndo = false;
}
class BlockCreate extends BlockBase {
/**
* @param {!Block=} opt_block The created block. Undefined for a blank
* event.
*/
constructor(opt_block) {
super(opt_block);
this.xml = Xml.blockToDomWithXY(opt_block);
this.ids = eventUtils.getDescendantIds(opt_block);
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.BLOCK_CREATE;
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
if (opt_block.isShadow()) {
// Moving shadow blocks is handled via disconnection.
this.recordUndo = false;
}
this.xml = Xml.blockToDomWithXY(opt_block);
this.ids = eventUtils.getDescendantIds(opt_block);
/**
* JSON representation of the block that was just created.
* @type {!blocks.State}
*/
this.json = /** @type {!blocks.State} */ (
blocks.save(opt_block, {addCoordinates: true}));
}
/**
* JSON representation of the block that was just created.
* @type {!blocks.State}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.json = /** @type {!blocks.State} */ (
blocks.save(opt_block, {addCoordinates: true}));
};
object.inherits(BlockCreate, BlockBase);
/**
* Type of this event.
* @type {string}
*/
BlockCreate.prototype.type = eventUtils.BLOCK_CREATE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
BlockCreate.prototype.toJson = function() {
const json = BlockCreate.superClass_.toJson.call(this);
json['xml'] = Xml.domToText(this.xml);
json['ids'] = this.ids;
json['json'] = this.json;
if (!this.recordUndo) {
json['recordUndo'] = this.recordUndo;
toJson() {
const json = super.toJson();
json['xml'] = Xml.domToText(this.xml);
json['ids'] = this.ids;
json['json'] = this.json;
if (!this.recordUndo) {
json['recordUndo'] = this.recordUndo;
}
return json;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
BlockCreate.prototype.fromJson = function(json) {
BlockCreate.superClass_.fromJson.call(this, json);
this.xml = Xml.textToDom(json['xml']);
this.ids = json['ids'];
this.json = /** @type {!blocks.State} */ (json['json']);
if (json['recordUndo'] !== undefined) {
this.recordUndo = json['recordUndo'];
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.xml = Xml.textToDom(json['xml']);
this.ids = json['ids'];
this.json = /** @type {!blocks.State} */ (json['json']);
if (json['recordUndo'] !== undefined) {
this.recordUndo = json['recordUndo'];
}
}
};
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
BlockCreate.prototype.run = function(forward) {
const workspace = this.getEventWorkspace_();
if (forward) {
blocks.append(this.json, workspace);
} else {
for (let i = 0; i < this.ids.length; i++) {
const id = this.ids[i];
const block = workspace.getBlockById(id);
if (block) {
block.dispose(false);
} else if (id === this.blockId) {
// Only complain about root-level block.
console.warn('Can\'t uncreate non-existent block: ' + id);
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
run(forward) {
const workspace = this.getEventWorkspace_();
if (forward) {
blocks.append(this.json, workspace);
} else {
for (let i = 0; i < this.ids.length; i++) {
const id = this.ids[i];
const block = workspace.getBlockById(id);
if (block) {
block.dispose(false);
} else if (id === this.blockId) {
// Only complain about root-level block.
console.warn('Can\'t uncreate non-existent block: ' + id);
}
}
}
}
};
}
registry.register(registry.Type.EVENT, eventUtils.CREATE, BlockCreate);

View File

@@ -18,7 +18,6 @@ goog.module('Blockly.Events.BlockDelete');
const Xml = goog.require('Blockly.Xml');
const blocks = goog.require('Blockly.serialization.blocks');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {BlockBase} = goog.require('Blockly.Events.BlockBase');
/* eslint-disable-next-line no-unused-vars */
@@ -27,102 +26,105 @@ const {Block} = goog.requireType('Blockly.Block');
/**
* Class for a block deletion event.
* @param {!Block=} opt_block The deleted block. Undefined for a blank
* event.
* @extends {BlockBase}
* @constructor
* @alias Blockly.Events.BlockDelete
*/
const BlockDelete = function(opt_block) {
BlockDelete.superClass_.constructor.call(this, opt_block);
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
if (opt_block.getParent()) {
throw Error('Connected blocks cannot be deleted.');
}
if (opt_block.isShadow()) {
// Respawning shadow blocks is handled via disconnection.
this.recordUndo = false;
}
this.oldXml = Xml.blockToDomWithXY(opt_block);
this.ids = eventUtils.getDescendantIds(opt_block);
class BlockDelete extends BlockBase {
/**
* Was the block that was just deleted a shadow?
* @type {boolean}
* @param {!Block=} opt_block The deleted block. Undefined for a blank
* event.
*/
this.wasShadow = opt_block.isShadow();
constructor(opt_block) {
super(opt_block);
/**
* JSON representation of the block that was just deleted.
* @type {!blocks.State}
*/
this.oldJson = /** @type {!blocks.State} */ (
blocks.save(opt_block, {addCoordinates: true}));
};
object.inherits(BlockDelete, BlockBase);
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.BLOCK_DELETE;
/**
* Type of this event.
* @type {string}
*/
BlockDelete.prototype.type = eventUtils.BLOCK_DELETE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
BlockDelete.prototype.toJson = function() {
const json = BlockDelete.superClass_.toJson.call(this);
json['oldXml'] = Xml.domToText(this.oldXml);
json['ids'] = this.ids;
json['wasShadow'] = this.wasShadow;
json['oldJson'] = this.oldJson;
if (!this.recordUndo) {
json['recordUndo'] = this.recordUndo;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
BlockDelete.prototype.fromJson = function(json) {
BlockDelete.superClass_.fromJson.call(this, json);
this.oldXml = Xml.textToDom(json['oldXml']);
this.ids = json['ids'];
this.wasShadow =
json['wasShadow'] || this.oldXml.tagName.toLowerCase() === 'shadow';
this.oldJson = /** @type {!blocks.State} */ (json['oldJson']);
if (json['recordUndo'] !== undefined) {
this.recordUndo = json['recordUndo'];
}
};
/**
* Run a deletion event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
BlockDelete.prototype.run = function(forward) {
const workspace = this.getEventWorkspace_();
if (forward) {
for (let i = 0; i < this.ids.length; i++) {
const id = this.ids[i];
const block = workspace.getBlockById(id);
if (block) {
block.dispose(false);
} else if (id === this.blockId) {
// Only complain about root-level block.
console.warn('Can\'t delete non-existent block: ' + id);
}
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
} else {
blocks.append(this.oldJson, workspace);
if (opt_block.getParent()) {
throw Error('Connected blocks cannot be deleted.');
}
if (opt_block.isShadow()) {
// Respawning shadow blocks is handled via disconnection.
this.recordUndo = false;
}
this.oldXml = Xml.blockToDomWithXY(opt_block);
this.ids = eventUtils.getDescendantIds(opt_block);
/**
* Was the block that was just deleted a shadow?
* @type {boolean}
*/
this.wasShadow = opt_block.isShadow();
/**
* JSON representation of the block that was just deleted.
* @type {!blocks.State}
*/
this.oldJson = /** @type {!blocks.State} */ (
blocks.save(opt_block, {addCoordinates: true}));
}
};
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
json['oldXml'] = Xml.domToText(this.oldXml);
json['ids'] = this.ids;
json['wasShadow'] = this.wasShadow;
json['oldJson'] = this.oldJson;
if (!this.recordUndo) {
json['recordUndo'] = this.recordUndo;
}
return json;
}
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.oldXml = Xml.textToDom(json['oldXml']);
this.ids = json['ids'];
this.wasShadow =
json['wasShadow'] || this.oldXml.tagName.toLowerCase() === 'shadow';
this.oldJson = /** @type {!blocks.State} */ (json['oldJson']);
if (json['recordUndo'] !== undefined) {
this.recordUndo = json['recordUndo'];
}
}
/**
* Run a deletion event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
run(forward) {
const workspace = this.getEventWorkspace_();
if (forward) {
for (let i = 0; i < this.ids.length; i++) {
const id = this.ids[i];
const block = workspace.getBlockById(id);
if (block) {
block.dispose(false);
} else if (id === this.blockId) {
// Only complain about root-level block.
console.warn('Can\'t delete non-existent block: ' + id);
}
}
} else {
blocks.append(this.oldJson, workspace);
}
}
}
registry.register(registry.Type.EVENT, eventUtils.DELETE, BlockDelete);

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.Events.BlockDrag');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
@@ -25,63 +24,65 @@ const {UiBase} = goog.require('Blockly.Events.UiBase');
/**
* Class for a block drag event.
* @param {!Block=} opt_block The top block in the stack that is being
* dragged. Undefined for a blank event.
* @param {boolean=} opt_isStart Whether this is the start of a block drag.
* Undefined for a blank event.
* @param {!Array<!Block>=} opt_blocks The blocks affected by this
* drag. Undefined for a blank event.
* @extends {UiBase}
* @constructor
* @alias Blockly.Events.BlockDrag
*/
const BlockDrag = function(opt_block, opt_isStart, opt_blocks) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
BlockDrag.superClass_.constructor.call(this, workspaceId);
this.blockId = opt_block ? opt_block.id : null;
class BlockDrag extends UiBase {
/**
* @param {!Block=} opt_block The top block in the stack that is being
* dragged. Undefined for a blank event.
* @param {boolean=} opt_isStart Whether this is the start of a block drag.
* Undefined for a blank event.
* @param {!Array<!Block>=} opt_blocks The blocks affected by this
* drag. Undefined for a blank event.
*/
constructor(opt_block, opt_isStart, opt_blocks) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
super(workspaceId);
this.blockId = opt_block ? opt_block.id : null;
/**
* Whether this is the start of a block drag.
* @type {boolean|undefined}
*/
this.isStart = opt_isStart;
/**
* The blocks affected by this drag event.
* @type {!Array<!Block>|undefined}
*/
this.blocks = opt_blocks;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.BLOCK_DRAG;
}
/**
* Whether this is the start of a block drag.
* @type {boolean|undefined}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.isStart = opt_isStart;
toJson() {
const json = super.toJson();
json['isStart'] = this.isStart;
json['blockId'] = this.blockId;
json['blocks'] = this.blocks;
return json;
}
/**
* The blocks affected by this drag event.
* @type {!Array<!Block>|undefined}
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
this.blocks = opt_blocks;
};
object.inherits(BlockDrag, UiBase);
/**
* Type of this event.
* @type {string}
*/
BlockDrag.prototype.type = eventUtils.BLOCK_DRAG;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
BlockDrag.prototype.toJson = function() {
const json = BlockDrag.superClass_.toJson.call(this);
json['isStart'] = this.isStart;
json['blockId'] = this.blockId;
json['blocks'] = this.blocks;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
BlockDrag.prototype.fromJson = function(json) {
BlockDrag.superClass_.fromJson.call(this, json);
this.isStart = json['isStart'];
this.blockId = json['blockId'];
this.blocks = json['blocks'];
};
fromJson(json) {
super.fromJson(json);
this.isStart = json['isStart'];
this.blockId = json['blockId'];
this.blocks = json['blocks'];
}
}
registry.register(registry.Type.EVENT, eventUtils.BLOCK_DRAG, BlockDrag);

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.Events.BlockMove');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {BlockBase} = goog.require('Blockly.Events.BlockBase');
/* eslint-disable-next-line no-unused-vars */
@@ -27,168 +26,176 @@ const {Coordinate} = goog.require('Blockly.utils.Coordinate');
/**
* Class for a block move event. Created before the move.
* @param {!Block=} opt_block The moved block. Undefined for a blank
* event.
* @extends {BlockBase}
* @constructor
* @alias Blockly.Events.BlockMove
*/
const BlockMove = function(opt_block) {
BlockMove.superClass_.constructor.call(this, opt_block);
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
if (opt_block.isShadow()) {
// Moving shadow blocks is handled via disconnection.
this.recordUndo = false;
}
class BlockMove extends BlockBase {
/**
* @param {!Block=} opt_block The moved block. Undefined for a blank
* event.
*/
constructor(opt_block) {
super(opt_block);
const location = this.currentLocation_();
this.oldParentId = location.parentId;
this.oldInputName = location.inputName;
this.oldCoordinate = location.coordinate;
};
object.inherits(BlockMove, BlockBase);
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.BLOCK_MOVE;
/**
* Type of this event.
* @type {string}
*/
BlockMove.prototype.type = eventUtils.BLOCK_MOVE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
BlockMove.prototype.toJson = function() {
const json = BlockMove.superClass_.toJson.call(this);
if (this.newParentId) {
json['newParentId'] = this.newParentId;
}
if (this.newInputName) {
json['newInputName'] = this.newInputName;
}
if (this.newCoordinate) {
json['newCoordinate'] = Math.round(this.newCoordinate.x) + ',' +
Math.round(this.newCoordinate.y);
}
if (!this.recordUndo) {
json['recordUndo'] = this.recordUndo;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
BlockMove.prototype.fromJson = function(json) {
BlockMove.superClass_.fromJson.call(this, json);
this.newParentId = json['newParentId'];
this.newInputName = json['newInputName'];
if (json['newCoordinate']) {
const xy = json['newCoordinate'].split(',');
this.newCoordinate = new Coordinate(Number(xy[0]), Number(xy[1]));
}
if (json['recordUndo'] !== undefined) {
this.recordUndo = json['recordUndo'];
}
};
/**
* Record the block's new location. Called after the move.
*/
BlockMove.prototype.recordNew = function() {
const location = this.currentLocation_();
this.newParentId = location.parentId;
this.newInputName = location.inputName;
this.newCoordinate = location.coordinate;
};
/**
* Returns the parentId and input if the block is connected,
* or the XY location if disconnected.
* @return {!Object} Collection of location info.
* @private
*/
BlockMove.prototype.currentLocation_ = function() {
const workspace = this.getEventWorkspace_();
const block = workspace.getBlockById(this.blockId);
const location = {};
const parent = block.getParent();
if (parent) {
location.parentId = parent.id;
const input = parent.getInputWithBlock(block);
if (input) {
location.inputName = input.name;
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
if (opt_block.isShadow()) {
// Moving shadow blocks is handled via disconnection.
this.recordUndo = false;
}
} else {
location.coordinate = block.getRelativeToSurfaceXY();
}
return location;
};
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
BlockMove.prototype.isNull = function() {
return this.oldParentId === this.newParentId &&
this.oldInputName === this.newInputName &&
Coordinate.equals(this.oldCoordinate, this.newCoordinate);
};
const location = this.currentLocation_();
this.oldParentId = location.parentId;
this.oldInputName = location.inputName;
this.oldCoordinate = location.coordinate;
/**
* Run a move event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
BlockMove.prototype.run = function(forward) {
const workspace = this.getEventWorkspace_();
const block = workspace.getBlockById(this.blockId);
if (!block) {
console.warn('Can\'t move non-existent block: ' + this.blockId);
return;
this.newParentId = null;
this.newInputName = null;
this.newCoordinate = null;
}
const parentId = forward ? this.newParentId : this.oldParentId;
const inputName = forward ? this.newInputName : this.oldInputName;
const coordinate = forward ? this.newCoordinate : this.oldCoordinate;
let parentBlock;
if (parentId) {
parentBlock = workspace.getBlockById(parentId);
if (!parentBlock) {
console.warn('Can\'t connect to non-existent block: ' + parentId);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
if (this.newParentId) {
json['newParentId'] = this.newParentId;
}
if (this.newInputName) {
json['newInputName'] = this.newInputName;
}
if (this.newCoordinate) {
json['newCoordinate'] = Math.round(this.newCoordinate.x) + ',' +
Math.round(this.newCoordinate.y);
}
if (!this.recordUndo) {
json['recordUndo'] = this.recordUndo;
}
return json;
}
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.newParentId = json['newParentId'];
this.newInputName = json['newInputName'];
if (json['newCoordinate']) {
const xy = json['newCoordinate'].split(',');
this.newCoordinate = new Coordinate(Number(xy[0]), Number(xy[1]));
}
if (json['recordUndo'] !== undefined) {
this.recordUndo = json['recordUndo'];
}
}
/**
* Record the block's new location. Called after the move.
*/
recordNew() {
const location = this.currentLocation_();
this.newParentId = location.parentId;
this.newInputName = location.inputName;
this.newCoordinate = location.coordinate;
}
/**
* Returns the parentId and input if the block is connected,
* or the XY location if disconnected.
* @return {!Object} Collection of location info.
* @private
*/
currentLocation_() {
const workspace = this.getEventWorkspace_();
const block = workspace.getBlockById(this.blockId);
const location = {};
const parent = block.getParent();
if (parent) {
location.parentId = parent.id;
const input = parent.getInputWithBlock(block);
if (input) {
location.inputName = input.name;
}
} else {
location.coordinate = block.getRelativeToSurfaceXY();
}
return location;
}
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
isNull() {
return this.oldParentId === this.newParentId &&
this.oldInputName === this.newInputName &&
Coordinate.equals(this.oldCoordinate, this.newCoordinate);
}
/**
* Run a move event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
run(forward) {
const workspace = this.getEventWorkspace_();
const block = workspace.getBlockById(this.blockId);
if (!block) {
console.warn('Can\'t move non-existent block: ' + this.blockId);
return;
}
}
if (block.getParent()) {
block.unplug();
}
if (coordinate) {
const xy = block.getRelativeToSurfaceXY();
block.moveBy(coordinate.x - xy.x, coordinate.y - xy.y);
} else {
let blockConnection = block.outputConnection;
if (!blockConnection ||
(block.previousConnection && block.previousConnection.isConnected())) {
blockConnection = block.previousConnection;
}
let parentConnection;
const connectionType = blockConnection.type;
if (inputName) {
const input = parentBlock.getInput(inputName);
if (input) {
parentConnection = input.connection;
const parentId = forward ? this.newParentId : this.oldParentId;
const inputName = forward ? this.newInputName : this.oldInputName;
const coordinate = forward ? this.newCoordinate : this.oldCoordinate;
let parentBlock;
if (parentId) {
parentBlock = workspace.getBlockById(parentId);
if (!parentBlock) {
console.warn('Can\'t connect to non-existent block: ' + parentId);
return;
}
} else if (connectionType === ConnectionType.PREVIOUS_STATEMENT) {
parentConnection = parentBlock.nextConnection;
}
if (parentConnection) {
blockConnection.connect(parentConnection);
if (block.getParent()) {
block.unplug();
}
if (coordinate) {
const xy = block.getRelativeToSurfaceXY();
block.moveBy(coordinate.x - xy.x, coordinate.y - xy.y);
} else {
console.warn('Can\'t connect to non-existent input: ' + inputName);
let blockConnection = block.outputConnection;
if (!blockConnection ||
(block.previousConnection &&
block.previousConnection.isConnected())) {
blockConnection = block.previousConnection;
}
let parentConnection;
const connectionType = blockConnection.type;
if (inputName) {
const input = parentBlock.getInput(inputName);
if (input) {
parentConnection = input.connection;
}
} else if (connectionType === ConnectionType.PREVIOUS_STATEMENT) {
parentConnection = parentBlock.nextConnection;
}
if (parentConnection) {
blockConnection.connect(parentConnection);
} else {
console.warn('Can\'t connect to non-existent input: ' + inputName);
}
}
}
};
}
registry.register(registry.Type.EVENT, eventUtils.MOVE, BlockMove);

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.Events.BubbleOpen');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
@@ -25,64 +24,66 @@ const {UiBase} = goog.require('Blockly.Events.UiBase');
/**
* Class for a bubble open event.
* @param {BlockSvg} opt_block The associated block. Undefined for a
* blank event.
* @param {boolean=} opt_isOpen Whether the bubble is opening (false if
* closing). Undefined for a blank event.
* @param {string=} opt_bubbleType The type of bubble. One of 'mutator',
* 'comment'
* or 'warning'. Undefined for a blank event.
* @extends {UiBase}
* @constructor
* @alias Blockly.Events.BubbleOpen
*/
const BubbleOpen = function(opt_block, opt_isOpen, opt_bubbleType) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
BubbleOpen.superClass_.constructor.call(this, workspaceId);
this.blockId = opt_block ? opt_block.id : null;
class BubbleOpen extends UiBase {
/**
* @param {BlockSvg} opt_block The associated block. Undefined for a
* blank event.
* @param {boolean=} opt_isOpen Whether the bubble is opening (false if
* closing). Undefined for a blank event.
* @param {string=} opt_bubbleType The type of bubble. One of 'mutator',
* 'comment'
* or 'warning'. Undefined for a blank event.
*/
constructor(opt_block, opt_isOpen, opt_bubbleType) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
super(workspaceId);
this.blockId = opt_block ? opt_block.id : null;
/**
* Whether the bubble is opening (false if closing).
* @type {boolean|undefined}
*/
this.isOpen = opt_isOpen;
/**
* The type of bubble. One of 'mutator', 'comment', or 'warning'.
* @type {string|undefined}
*/
this.bubbleType = opt_bubbleType;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.BUBBLE_OPEN;
}
/**
* Whether the bubble is opening (false if closing).
* @type {boolean|undefined}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.isOpen = opt_isOpen;
toJson() {
const json = super.toJson();
json['isOpen'] = this.isOpen;
json['bubbleType'] = this.bubbleType;
json['blockId'] = this.blockId;
return json;
}
/**
* The type of bubble. One of 'mutator', 'comment', or 'warning'.
* @type {string|undefined}
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
this.bubbleType = opt_bubbleType;
};
object.inherits(BubbleOpen, UiBase);
/**
* Type of this event.
* @type {string}
*/
BubbleOpen.prototype.type = eventUtils.BUBBLE_OPEN;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
BubbleOpen.prototype.toJson = function() {
const json = BubbleOpen.superClass_.toJson.call(this);
json['isOpen'] = this.isOpen;
json['bubbleType'] = this.bubbleType;
json['blockId'] = this.blockId;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
BubbleOpen.prototype.fromJson = function(json) {
BubbleOpen.superClass_.fromJson.call(this, json);
this.isOpen = json['isOpen'];
this.bubbleType = json['bubbleType'];
this.blockId = json['blockId'];
};
fromJson(json) {
super.fromJson(json);
this.isOpen = json['isOpen'];
this.bubbleType = json['bubbleType'];
this.blockId = json['blockId'];
}
}
registry.register(registry.Type.EVENT, eventUtils.BUBBLE_OPEN, BubbleOpen);

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.Events.Click');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
@@ -25,58 +24,63 @@ const {UiBase} = goog.require('Blockly.Events.UiBase');
/**
* Class for a click event.
* @param {?Block=} opt_block The affected block. Null for click events
* that do not have an associated block (i.e. workspace click). Undefined
* for a blank event.
* @param {?string=} opt_workspaceId The workspace identifier for this event.
* Not used if block is passed. Undefined for a blank event.
* @param {string=} opt_targetType The type of element targeted by this click
* event. Undefined for a blank event.
* @extends {UiBase}
* @constructor
* @alias Blockly.Events.Click
*/
const Click = function(opt_block, opt_workspaceId, opt_targetType) {
const workspaceId = opt_block ? opt_block.workspace.id : opt_workspaceId;
Click.superClass_.constructor.call(this, workspaceId);
this.blockId = opt_block ? opt_block.id : null;
class Click extends UiBase {
/**
* @param {?Block=} opt_block The affected block. Null for click events
* that do not have an associated block (i.e. workspace click). Undefined
* for a blank event.
* @param {?string=} opt_workspaceId The workspace identifier for this event.
* Not used if block is passed. Undefined for a blank event.
* @param {string=} opt_targetType The type of element targeted by this click
* event. Undefined for a blank event.
*/
constructor(opt_block, opt_workspaceId, opt_targetType) {
let workspaceId = opt_block ? opt_block.workspace.id : opt_workspaceId;
if (workspaceId === null) {
workspaceId = undefined;
}
super(workspaceId);
this.blockId = opt_block ? opt_block.id : null;
/**
* The type of element targeted by this click event.
* @type {string|undefined}
*/
this.targetType = opt_targetType;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.CLICK;
}
/**
* The type of element targeted by this click event.
* @type {string|undefined}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.targetType = opt_targetType;
};
object.inherits(Click, UiBase);
/**
* Type of this event.
* @type {string}
*/
Click.prototype.type = eventUtils.CLICK;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Click.prototype.toJson = function() {
const json = Click.superClass_.toJson.call(this);
json['targetType'] = this.targetType;
if (this.blockId) {
json['blockId'] = this.blockId;
toJson() {
const json = super.toJson();
json['targetType'] = this.targetType;
if (this.blockId) {
json['blockId'] = this.blockId;
}
return json;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Click.prototype.fromJson = function(json) {
Click.superClass_.fromJson.call(this, json);
this.targetType = json['targetType'];
this.blockId = json['blockId'];
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.targetType = json['targetType'];
this.blockId = json['blockId'];
}
}
registry.register(registry.Type.EVENT, eventUtils.CLICK, Click);

View File

@@ -15,11 +15,10 @@
*/
goog.module('Blockly.Events.CommentBase');
const AbstractEvents = goog.require('Blockly.Events.Abstract');
const Xml = goog.require('Blockly.Xml');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const utilsXml = goog.require('Blockly.utils.xml');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {CommentCreate} = goog.requireType('Blockly.Events.CommentCreate');
/* eslint-disable-next-line no-unused-vars */
@@ -30,89 +29,93 @@ const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment');
/**
* Abstract class for a comment event.
* @param {!WorkspaceComment=} opt_comment The comment this event
* corresponds to. Undefined for a blank event.
* @extends {AbstractEvents}
* @constructor
* @extends {AbstractEvent}
* @alias Blockly.Events.CommentBase
*/
const CommentBase = function(opt_comment) {
class CommentBase extends AbstractEvent {
/**
* Whether or not an event is blank.
* @type {boolean}
* @param {!WorkspaceComment=} opt_comment The comment this event
* corresponds to. Undefined for a blank event.
*/
this.isBlank = typeof opt_comment === 'undefined';
constructor(opt_comment) {
super();
/**
* Whether or not an event is blank.
* @type {boolean}
*/
this.isBlank = typeof opt_comment === 'undefined';
/**
* The ID of the comment this event pertains to.
* @type {string}
*/
this.commentId = this.isBlank ? '' : opt_comment.id;
/**
* The ID of the comment this event pertains to.
* @type {string}
*/
this.commentId = this.isBlank ? '' : opt_comment.id;
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = this.isBlank ? '' : opt_comment.workspace.id;
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = this.isBlank ? '' : opt_comment.workspace.id;
/**
* The event group id for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
* @type {string}
*/
this.group = eventUtils.getGroup();
/**
* The event group id for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
* @type {string}
*/
this.group = eventUtils.getGroup();
/**
* Sets whether the event should be added to the undo stack.
* @type {boolean}
*/
this.recordUndo = eventUtils.getRecordUndo();
};
object.inherits(CommentBase, AbstractEvents);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
CommentBase.prototype.toJson = function() {
const json = CommentBase.superClass_.toJson.call(this);
if (this.commentId) {
json['commentId'] = this.commentId;
/**
* Sets whether the event should be added to the undo stack.
* @type {boolean}
*/
this.recordUndo = eventUtils.getRecordUndo();
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
CommentBase.prototype.fromJson = function(json) {
CommentBase.superClass_.fromJson.call(this, json);
this.commentId = json['commentId'];
};
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
if (this.commentId) {
json['commentId'] = this.commentId;
}
return json;
}
/**
* Helper function for Comment[Create|Delete]
* @param {!CommentCreate|!CommentDelete} event
* The event to run.
* @param {boolean} create if True then Create, if False then Delete
*/
CommentBase.CommentCreateDeleteHelper = function(event, create) {
const workspace = event.getEventWorkspace_();
if (create) {
const xmlElement = utilsXml.createElement('xml');
xmlElement.appendChild(event.xml);
Xml.domToWorkspace(xmlElement, workspace);
} else {
const comment = workspace.getCommentById(event.commentId);
if (comment) {
comment.dispose();
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.commentId = json['commentId'];
}
/**
* Helper function for Comment[Create|Delete]
* @param {!CommentCreate|!CommentDelete} event
* The event to run.
* @param {boolean} create if True then Create, if False then Delete
*/
static CommentCreateDeleteHelper(event, create) {
const workspace = event.getEventWorkspace_();
if (create) {
const xmlElement = utilsXml.createElement('xml');
xmlElement.appendChild(event.xml);
Xml.domToWorkspace(xmlElement, workspace);
} else {
// Only complain about root-level block.
console.warn('Can\'t uncreate non-existent comment: ' + event.commentId);
const comment = workspace.getCommentById(event.commentId);
if (comment) {
comment.dispose();
} else {
// Only complain about root-level block.
console.warn(
'Can\'t uncreate non-existent comment: ' + event.commentId);
}
}
}
};
}
exports.CommentBase = CommentBase;

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.Events.CommentChange');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {CommentBase} = goog.require('Blockly.Events.CommentBase');
/* eslint-disable-next-line no-unused-vars */
@@ -25,77 +24,80 @@ const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment');
/**
* Class for a comment change event.
* @param {!WorkspaceComment=} opt_comment The comment that is being
* changed. Undefined for a blank event.
* @param {string=} opt_oldContents Previous contents of the comment.
* @param {string=} opt_newContents New contents of the comment.
* @extends {CommentBase}
* @constructor
* @alias Blockly.Events.CommentChange
*/
const CommentChange = function(opt_comment, opt_oldContents, opt_newContents) {
CommentChange.superClass_.constructor.call(this, opt_comment);
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
class CommentChange extends CommentBase {
/**
* @param {!WorkspaceComment=} opt_comment The comment that is being
* changed. Undefined for a blank event.
* @param {string=} opt_oldContents Previous contents of the comment.
* @param {string=} opt_newContents New contents of the comment.
*/
constructor(opt_comment, opt_oldContents, opt_newContents) {
super(opt_comment);
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.COMMENT_CHANGE;
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
}
this.oldContents_ =
typeof opt_oldContents === 'undefined' ? '' : opt_oldContents;
this.newContents_ =
typeof opt_newContents === 'undefined' ? '' : opt_newContents;
}
this.oldContents_ =
typeof opt_oldContents === 'undefined' ? '' : opt_oldContents;
this.newContents_ =
typeof opt_newContents === 'undefined' ? '' : opt_newContents;
};
object.inherits(CommentChange, CommentBase);
/**
* Type of this event.
* @type {string}
*/
CommentChange.prototype.type = eventUtils.COMMENT_CHANGE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
CommentChange.prototype.toJson = function() {
const json = CommentChange.superClass_.toJson.call(this);
json['oldContents'] = this.oldContents_;
json['newContents'] = this.newContents_;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
CommentChange.prototype.fromJson = function(json) {
CommentChange.superClass_.fromJson.call(this, json);
this.oldContents_ = json['oldContents'];
this.newContents_ = json['newContents'];
};
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
CommentChange.prototype.isNull = function() {
return this.oldContents_ === this.newContents_;
};
/**
* Run a change event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
CommentChange.prototype.run = function(forward) {
const workspace = this.getEventWorkspace_();
const comment = workspace.getCommentById(this.commentId);
if (!comment) {
console.warn('Can\'t change non-existent comment: ' + this.commentId);
return;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
json['oldContents'] = this.oldContents_;
json['newContents'] = this.newContents_;
return json;
}
const contents = forward ? this.newContents_ : this.oldContents_;
comment.setContent(contents);
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.oldContents_ = json['oldContents'];
this.newContents_ = json['newContents'];
}
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
isNull() {
return this.oldContents_ === this.newContents_;
}
/**
* Run a change event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
run(forward) {
const workspace = this.getEventWorkspace_();
const comment = workspace.getCommentById(this.commentId);
if (!comment) {
console.warn('Can\'t change non-existent comment: ' + this.commentId);
return;
}
const contents = forward ? this.newContents_ : this.oldContents_;
comment.setContent(contents);
}
}
registry.register(
registry.Type.EVENT, eventUtils.COMMENT_CHANGE, CommentChange);

View File

@@ -17,7 +17,6 @@ goog.module('Blockly.Events.CommentCreate');
const Xml = goog.require('Blockly.Xml');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {CommentBase} = goog.require('Blockly.Events.CommentBase');
/* eslint-disable-next-line no-unused-vars */
@@ -26,55 +25,58 @@ const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment');
/**
* Class for a comment creation event.
* @param {!WorkspaceComment=} opt_comment The created comment.
* Undefined for a blank event.
* @extends {CommentBase}
* @constructor
* @alias Blockly.Events.CommentCreate
*/
const CommentCreate = function(opt_comment) {
CommentCreate.superClass_.constructor.call(this, opt_comment);
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
class CommentCreate extends CommentBase {
/**
* @param {!WorkspaceComment=} opt_comment The created comment.
* Undefined for a blank event.
*/
constructor(opt_comment) {
super(opt_comment);
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.COMMENT_CREATE;
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
}
this.xml = opt_comment.toXmlWithXY();
}
this.xml = opt_comment.toXmlWithXY();
};
object.inherits(CommentCreate, CommentBase);
// TODO (#1266): "Full" and "minimal" serialization.
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
json['xml'] = Xml.domToText(this.xml);
return json;
}
/**
* Type of this event.
* @type {string}
*/
CommentCreate.prototype.type = eventUtils.COMMENT_CREATE;
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.xml = Xml.textToDom(json['xml']);
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
// TODO (#1266): "Full" and "minimal" serialization.
CommentCreate.prototype.toJson = function() {
const json = CommentCreate.superClass_.toJson.call(this);
json['xml'] = Xml.domToText(this.xml);
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
CommentCreate.prototype.fromJson = function(json) {
CommentCreate.superClass_.fromJson.call(this, json);
this.xml = Xml.textToDom(json['xml']);
};
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
CommentCreate.prototype.run = function(forward) {
CommentBase.CommentCreateDeleteHelper(this, forward);
};
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
run(forward) {
CommentBase.CommentCreateDeleteHelper(this, forward);
}
}
registry.register(
registry.Type.EVENT, eventUtils.COMMENT_CREATE, CommentCreate);

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.Events.CommentDelete');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {CommentBase} = goog.require('Blockly.Events.CommentBase');
/* eslint-disable-next-line no-unused-vars */
@@ -25,53 +24,56 @@ const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment');
/**
* Class for a comment deletion event.
* @param {!WorkspaceComment=} opt_comment The deleted comment.
* Undefined for a blank event.
* @extends {CommentBase}
* @constructor
* @alias Blockly.Events.CommentDelete
*/
const CommentDelete = function(opt_comment) {
CommentDelete.superClass_.constructor.call(this, opt_comment);
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
class CommentDelete extends CommentBase {
/**
* @param {!WorkspaceComment=} opt_comment The deleted comment.
* Undefined for a blank event.
*/
constructor(opt_comment) {
super(opt_comment);
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.COMMENT_DELETE;
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
}
this.xml = opt_comment.toXmlWithXY();
}
this.xml = opt_comment.toXmlWithXY();
};
object.inherits(CommentDelete, CommentBase);
// TODO (#1266): "Full" and "minimal" serialization.
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
return json;
}
/**
* Type of this event.
* @type {string}
*/
CommentDelete.prototype.type = eventUtils.COMMENT_DELETE;
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
// TODO (#1266): "Full" and "minimal" serialization.
CommentDelete.prototype.toJson = function() {
const json = CommentDelete.superClass_.toJson.call(this);
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
CommentDelete.prototype.fromJson = function(json) {
CommentDelete.superClass_.fromJson.call(this, json);
};
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
CommentDelete.prototype.run = function(forward) {
CommentBase.CommentCreateDeleteHelper(this, !forward);
};
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
run(forward) {
CommentBase.CommentCreateDeleteHelper(this, !forward);
}
}
registry.register(
registry.Type.EVENT, eventUtils.COMMENT_DELETE, CommentDelete);

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.Events.CommentMove');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {CommentBase} = goog.require('Blockly.Events.CommentBase');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
@@ -26,129 +25,132 @@ const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment');
/**
* Class for a comment move event. Created before the move.
* @param {!WorkspaceComment=} opt_comment The comment that is being
* moved. Undefined for a blank event.
* @extends {CommentBase}
* @constructor
* @alias Blockly.Events.CommentMove
*/
const CommentMove = function(opt_comment) {
CommentMove.superClass_.constructor.call(this, opt_comment);
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
class CommentMove extends CommentBase {
/**
* @param {!WorkspaceComment=} opt_comment The comment that is being
* moved. Undefined for a blank event.
*/
constructor(opt_comment) {
super(opt_comment);
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.COMMENT_MOVE;
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
}
/**
* The comment that is being moved. Will be cleared after recording the new
* location.
* @type {WorkspaceComment}
*/
this.comment_ = opt_comment;
/**
* The location before the move, in workspace coordinates.
* @type {!Coordinate}
*/
this.oldCoordinate_ = opt_comment.getXY();
/**
* The location after the move, in workspace coordinates.
* @type {Coordinate}
*/
this.newCoordinate_ = null;
}
/**
* The comment that is being moved. Will be cleared after recording the new
* location.
* @type {WorkspaceComment}
* Record the comment's new location. Called after the move. Can only be
* called once.
*/
this.comment_ = opt_comment;
recordNew() {
if (!this.comment_) {
throw Error(
'Tried to record the new position of a comment on the ' +
'same event twice.');
}
this.newCoordinate_ = this.comment_.getXY();
this.comment_ = null;
}
/**
* The location before the move, in workspace coordinates.
* @type {!Coordinate}
* Override the location before the move. Use this if you don't create the
* event until the end of the move, but you know the original location.
* @param {!Coordinate} xy The location before the move,
* in workspace coordinates.
*/
this.oldCoordinate_ = opt_comment.getXY();
setOldCoordinate(xy) {
this.oldCoordinate_ = xy;
}
// TODO (#1266): "Full" and "minimal" serialization.
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
if (this.oldCoordinate_) {
json['oldCoordinate'] = Math.round(this.oldCoordinate_.x) + ',' +
Math.round(this.oldCoordinate_.y);
}
if (this.newCoordinate_) {
json['newCoordinate'] = Math.round(this.newCoordinate_.x) + ',' +
Math.round(this.newCoordinate_.y);
}
return json;
}
/**
* The location after the move, in workspace coordinates.
* @type {Coordinate}
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
this.newCoordinate_ = null;
};
object.inherits(CommentMove, CommentBase);
fromJson(json) {
super.fromJson(json);
/**
* Record the comment's new location. Called after the move. Can only be
* called once.
*/
CommentMove.prototype.recordNew = function() {
if (!this.comment_) {
throw Error(
'Tried to record the new position of a comment on the ' +
'same event twice.');
}
this.newCoordinate_ = this.comment_.getXY();
this.comment_ = null;
};
/**
* Type of this event.
* @type {string}
*/
CommentMove.prototype.type = eventUtils.COMMENT_MOVE;
/**
* Override the location before the move. Use this if you don't create the
* event until the end of the move, but you know the original location.
* @param {!Coordinate} xy The location before the move,
* in workspace coordinates.
*/
CommentMove.prototype.setOldCoordinate = function(xy) {
this.oldCoordinate_ = xy;
};
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
// TODO (#1266): "Full" and "minimal" serialization.
CommentMove.prototype.toJson = function() {
const json = CommentMove.superClass_.toJson.call(this);
if (this.oldCoordinate_) {
json['oldCoordinate'] = Math.round(this.oldCoordinate_.x) + ',' +
Math.round(this.oldCoordinate_.y);
}
if (this.newCoordinate_) {
json['newCoordinate'] = Math.round(this.newCoordinate_.x) + ',' +
Math.round(this.newCoordinate_.y);
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
CommentMove.prototype.fromJson = function(json) {
CommentMove.superClass_.fromJson.call(this, json);
if (json['oldCoordinate']) {
const xy = json['oldCoordinate'].split(',');
this.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
}
if (json['newCoordinate']) {
const xy = json['newCoordinate'].split(',');
this.newCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
}
};
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
CommentMove.prototype.isNull = function() {
return Coordinate.equals(this.oldCoordinate_, this.newCoordinate_);
};
/**
* Run a move event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
CommentMove.prototype.run = function(forward) {
const workspace = this.getEventWorkspace_();
const comment = workspace.getCommentById(this.commentId);
if (!comment) {
console.warn('Can\'t move non-existent comment: ' + this.commentId);
return;
if (json['oldCoordinate']) {
const xy = json['oldCoordinate'].split(',');
this.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
}
if (json['newCoordinate']) {
const xy = json['newCoordinate'].split(',');
this.newCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
}
}
const target = forward ? this.newCoordinate_ : this.oldCoordinate_;
// TODO: Check if the comment is being dragged, and give up if so.
const current = comment.getXY();
comment.moveBy(target.x - current.x, target.y - current.y);
};
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
isNull() {
return Coordinate.equals(this.oldCoordinate_, this.newCoordinate_);
}
/**
* Run a move event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
run(forward) {
const workspace = this.getEventWorkspace_();
const comment = workspace.getCommentById(this.commentId);
if (!comment) {
console.warn('Can\'t move non-existent comment: ' + this.commentId);
return;
}
const target = forward ? this.newCoordinate_ : this.oldCoordinate_;
// TODO: Check if the comment is being dragged, and give up if so.
const current = comment.getXY();
comment.moveBy(target.x - current.x, target.y - current.y);
}
}
registry.register(registry.Type.EVENT, eventUtils.COMMENT_MOVE, CommentMove);

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.Events.MarkerMove');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {ASTNode} = goog.require('Blockly.ASTNode');
/* eslint-disable-next-line no-unused-vars */
@@ -28,81 +27,83 @@ const {Workspace} = goog.requireType('Blockly.Workspace');
/**
* Class for a marker move event.
* @param {?Block=} opt_block The affected block. Null if current node
* is of type workspace. Undefined for a blank event.
* @param {boolean=} isCursor Whether this is a cursor event. Undefined for a
* blank event.
* @param {?ASTNode=} opt_oldNode The old node the marker used to be on.
* Undefined for a blank event.
* @param {!ASTNode=} opt_newNode The new node the marker is now on.
* Undefined for a blank event.
* @extends {UiBase}
* @constructor
* @alias Blockly.Events.MarkerMove
*/
const MarkerMove = function(opt_block, isCursor, opt_oldNode, opt_newNode) {
let workspaceId = opt_block ? opt_block.workspace.id : undefined;
if (opt_newNode && opt_newNode.getType() === ASTNode.types.WORKSPACE) {
workspaceId = (/** @type {!Workspace} */ (opt_newNode.getLocation())).id;
class MarkerMove extends UiBase {
/**
* @param {?Block=} opt_block The affected block. Null if current node
* is of type workspace. Undefined for a blank event.
* @param {boolean=} isCursor Whether this is a cursor event. Undefined for a
* blank event.
* @param {?ASTNode=} opt_oldNode The old node the marker used to be on.
* Undefined for a blank event.
* @param {!ASTNode=} opt_newNode The new node the marker is now on.
* Undefined for a blank event.
*/
constructor(opt_block, isCursor, opt_oldNode, opt_newNode) {
let workspaceId = opt_block ? opt_block.workspace.id : undefined;
if (opt_newNode && opt_newNode.getType() === ASTNode.types.WORKSPACE) {
workspaceId = (/** @type {!Workspace} */ (opt_newNode.getLocation())).id;
}
super(workspaceId);
/**
* The workspace identifier for this event.
* @type {?string}
*/
this.blockId = opt_block ? opt_block.id : null;
/**
* The old node the marker used to be on.
* @type {?ASTNode|undefined}
*/
this.oldNode = opt_oldNode;
/**
* The new node the marker is now on.
* @type {ASTNode|undefined}
*/
this.newNode = opt_newNode;
/**
* Whether this is a cursor event.
* @type {boolean|undefined}
*/
this.isCursor = isCursor;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.MARKER_MOVE;
}
MarkerMove.superClass_.constructor.call(this, workspaceId);
/**
* The workspace identifier for this event.
* @type {?string}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.blockId = opt_block ? opt_block.id : null;
toJson() {
const json = super.toJson();
json['isCursor'] = this.isCursor;
json['blockId'] = this.blockId;
json['oldNode'] = this.oldNode;
json['newNode'] = this.newNode;
return json;
}
/**
* The old node the marker used to be on.
* @type {?ASTNode|undefined}
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
this.oldNode = opt_oldNode;
/**
* The new node the marker is now on.
* @type {ASTNode|undefined}
*/
this.newNode = opt_newNode;
/**
* Whether this is a cursor event.
* @type {boolean|undefined}
*/
this.isCursor = isCursor;
};
object.inherits(MarkerMove, UiBase);
/**
* Type of this event.
* @type {string}
*/
MarkerMove.prototype.type = eventUtils.MARKER_MOVE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
MarkerMove.prototype.toJson = function() {
const json = MarkerMove.superClass_.toJson.call(this);
json['isCursor'] = this.isCursor;
json['blockId'] = this.blockId;
json['oldNode'] = this.oldNode;
json['newNode'] = this.newNode;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
MarkerMove.prototype.fromJson = function(json) {
MarkerMove.superClass_.fromJson.call(this, json);
this.isCursor = json['isCursor'];
this.blockId = json['blockId'];
this.oldNode = json['oldNode'];
this.newNode = json['newNode'];
};
fromJson(json) {
super.fromJson(json);
this.isCursor = json['isCursor'];
this.blockId = json['blockId'];
this.oldNode = json['oldNode'];
this.newNode = json['newNode'];
}
}
registry.register(registry.Type.EVENT, eventUtils.MARKER_MOVE, MarkerMove);

View File

@@ -16,66 +16,67 @@
goog.module('Blockly.Events.Selected');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {UiBase} = goog.require('Blockly.Events.UiBase');
/**
* Class for a selected event.
* @param {?string=} opt_oldElementId The ID of the previously selected
* element. Null if no element last selected. Undefined for a blank event.
* @param {?string=} opt_newElementId The ID of the selected element. Null if no
* element currently selected (deselect). Undefined for a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* Null if no element previously selected. Undefined for a blank event.
* @extends {UiBase}
* @constructor
* @alias Blockly.Events.Selected
*/
const Selected = function(opt_oldElementId, opt_newElementId, opt_workspaceId) {
Selected.superClass_.constructor.call(this, opt_workspaceId);
class Selected extends UiBase {
/**
* @param {?string=} opt_oldElementId The ID of the previously selected
* element. Null if no element last selected. Undefined for a blank event.
* @param {?string=} opt_newElementId The ID of the selected element. Null if
* no element currently selected (deselect). Undefined for a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* Null if no element previously selected. Undefined for a blank event.
*/
constructor(opt_oldElementId, opt_newElementId, opt_workspaceId) {
super(opt_workspaceId);
/**
* The id of the last selected element.
* @type {?string|undefined}
*/
this.oldElementId = opt_oldElementId;
/**
* The id of the selected element.
* @type {?string|undefined}
*/
this.newElementId = opt_newElementId;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.SELECTED;
}
/**
* The id of the last selected element.
* @type {?string|undefined}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.oldElementId = opt_oldElementId;
toJson() {
const json = super.toJson();
json['oldElementId'] = this.oldElementId;
json['newElementId'] = this.newElementId;
return json;
}
/**
* The id of the selected element.
* @type {?string|undefined}
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
this.newElementId = opt_newElementId;
};
object.inherits(Selected, UiBase);
/**
* Type of this event.
* @type {string}
*/
Selected.prototype.type = eventUtils.SELECTED;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Selected.prototype.toJson = function() {
const json = Selected.superClass_.toJson.call(this);
json['oldElementId'] = this.oldElementId;
json['newElementId'] = this.newElementId;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Selected.prototype.fromJson = function(json) {
Selected.superClass_.fromJson.call(this, json);
this.oldElementId = json['oldElementId'];
this.newElementId = json['newElementId'];
};
fromJson(json) {
super.fromJson(json);
this.oldElementId = json['oldElementId'];
this.newElementId = json['newElementId'];
}
}
registry.register(registry.Type.EVENT, eventUtils.SELECTED, Selected);

View File

@@ -16,55 +16,56 @@
goog.module('Blockly.Events.ThemeChange');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {UiBase} = goog.require('Blockly.Events.UiBase');
/**
* Class for a theme change event.
* @param {string=} opt_themeName The theme name. Undefined for a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* event. Undefined for a blank event.
* @extends {UiBase}
* @constructor
* @alias Blockly.Events.ThemeChange
*/
const ThemeChange = function(opt_themeName, opt_workspaceId) {
ThemeChange.superClass_.constructor.call(this, opt_workspaceId);
class ThemeChange extends UiBase {
/**
* @param {string=} opt_themeName The theme name. Undefined for a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* event. Undefined for a blank event.
*/
constructor(opt_themeName, opt_workspaceId) {
super(opt_workspaceId);
/**
* The theme name.
* @type {string|undefined}
*/
this.themeName = opt_themeName;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.THEME_CHANGE;
}
/**
* The theme name.
* @type {string|undefined}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.themeName = opt_themeName;
};
object.inherits(ThemeChange, UiBase);
toJson() {
const json = super.toJson();
json['themeName'] = this.themeName;
return json;
}
/**
* Type of this event.
* @type {string}
*/
ThemeChange.prototype.type = eventUtils.THEME_CHANGE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
ThemeChange.prototype.toJson = function() {
const json = ThemeChange.superClass_.toJson.call(this);
json['themeName'] = this.themeName;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
ThemeChange.prototype.fromJson = function(json) {
ThemeChange.superClass_.fromJson.call(this, json);
this.themeName = json['themeName'];
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.themeName = json['themeName'];
}
}
registry.register(registry.Type.EVENT, eventUtils.THEME_CHANGE, ThemeChange);

View File

@@ -16,66 +16,67 @@
goog.module('Blockly.Events.ToolboxItemSelect');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {UiBase} = goog.require('Blockly.Events.UiBase');
/**
* Class for a toolbox item select event.
* @param {?string=} opt_oldItem The previously selected toolbox item. Undefined
* for a blank event.
* @param {?string=} opt_newItem The newly selected toolbox item. Undefined for
* a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
* @extends {UiBase}
* @constructor
* @alias Blockly.Events.ToolboxItemSelect
*/
const ToolboxItemSelect = function(opt_oldItem, opt_newItem, opt_workspaceId) {
ToolboxItemSelect.superClass_.constructor.call(this, opt_workspaceId);
class ToolboxItemSelect extends UiBase {
/**
* @param {?string=} opt_oldItem The previously selected toolbox item.
* Undefined for a blank event.
* @param {?string=} opt_newItem The newly selected toolbox item. Undefined
* for a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
*/
constructor(opt_oldItem, opt_newItem, opt_workspaceId) {
super(opt_workspaceId);
/**
* The previously selected toolbox item.
* @type {?string|undefined}
*/
this.oldItem = opt_oldItem;
/**
* The newly selected toolbox item.
* @type {?string|undefined}
*/
this.newItem = opt_newItem;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.TOOLBOX_ITEM_SELECT;
}
/**
* The previously selected toolbox item.
* @type {?string|undefined}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.oldItem = opt_oldItem;
toJson() {
const json = super.toJson();
json['oldItem'] = this.oldItem;
json['newItem'] = this.newItem;
return json;
}
/**
* The newly selected toolbox item.
* @type {?string|undefined}
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
this.newItem = opt_newItem;
};
object.inherits(ToolboxItemSelect, UiBase);
/**
* Type of this event.
* @type {string}
*/
ToolboxItemSelect.prototype.type = eventUtils.TOOLBOX_ITEM_SELECT;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
ToolboxItemSelect.prototype.toJson = function() {
const json = ToolboxItemSelect.superClass_.toJson.call(this);
json['oldItem'] = this.oldItem;
json['newItem'] = this.newItem;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
ToolboxItemSelect.prototype.fromJson = function(json) {
ToolboxItemSelect.superClass_.fromJson.call(this, json);
this.oldItem = json['oldItem'];
this.newItem = json['newItem'];
};
fromJson(json) {
super.fromJson(json);
this.oldItem = json['oldItem'];
this.newItem = json['newItem'];
}
}
registry.register(
registry.Type.EVENT, eventUtils.TOOLBOX_ITEM_SELECT, ToolboxItemSelect);

View File

@@ -16,56 +16,57 @@
goog.module('Blockly.Events.TrashcanOpen');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {UiBase} = goog.require('Blockly.Events.UiBase');
/**
* Class for a trashcan open event.
* @param {boolean=} opt_isOpen Whether the trashcan flyout is opening (false if
* opening). Undefined for a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
* @extends {UiBase}
* @constructor
* @alias Blockly.Events.TrashcanOpen
*/
const TrashcanOpen = function(opt_isOpen, opt_workspaceId) {
TrashcanOpen.superClass_.constructor.call(this, opt_workspaceId);
class TrashcanOpen extends UiBase {
/**
* @param {boolean=} opt_isOpen Whether the trashcan flyout is opening (false
* if opening). Undefined for a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
*/
constructor(opt_isOpen, opt_workspaceId) {
super(opt_workspaceId);
/**
* Whether the trashcan flyout is opening (false if closing).
* @type {boolean|undefined}
*/
this.isOpen = opt_isOpen;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.TRASHCAN_OPEN;
}
/**
* Whether the trashcan flyout is opening (false if closing).
* @type {boolean|undefined}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.isOpen = opt_isOpen;
};
object.inherits(TrashcanOpen, UiBase);
toJson() {
const json = super.toJson();
json['isOpen'] = this.isOpen;
return json;
}
/**
* Type of this event.
* @type {string}
*/
TrashcanOpen.prototype.type = eventUtils.TRASHCAN_OPEN;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
TrashcanOpen.prototype.toJson = function() {
const json = TrashcanOpen.superClass_.toJson.call(this);
json['isOpen'] = this.isOpen;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
TrashcanOpen.prototype.fromJson = function(json) {
TrashcanOpen.superClass_.fromJson.call(this, json);
this.isOpen = json['isOpen'];
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.isOpen = json['isOpen'];
}
}
registry.register(registry.Type.EVENT, eventUtils.TRASHCAN_OPEN, TrashcanOpen);

View File

@@ -18,7 +18,6 @@
goog.module('Blockly.Events.Ui');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
@@ -27,60 +26,62 @@ const {UiBase} = goog.require('Blockly.Events.UiBase');
/**
* Class for a UI event.
* @param {?Block=} opt_block The affected block. Null for UI events
* that do not have an associated block. Undefined for a blank event.
* @param {string=} opt_element One of 'selected', 'comment', 'mutatorOpen',
* etc.
* @param {*=} opt_oldValue Previous value of element.
* @param {*=} opt_newValue New value of element.
* @extends {UiBase}
* @deprecated December 2020. Instead use a more specific UI event.
* @constructor
* @alias Blockly.Events.Ui
*/
const Ui = function(opt_block, opt_element, opt_oldValue, opt_newValue) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
Ui.superClass_.constructor.call(this, workspaceId);
class Ui extends UiBase {
/**
* @param {?Block=} opt_block The affected block. Null for UI events
* that do not have an associated block. Undefined for a blank event.
* @param {string=} opt_element One of 'selected', 'comment', 'mutatorOpen',
* etc.
* @param {*=} opt_oldValue Previous value of element.
* @param {*=} opt_newValue New value of element.
*/
constructor(opt_block, opt_element, opt_oldValue, opt_newValue) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
super(workspaceId);
this.blockId = opt_block ? opt_block.id : null;
this.element = typeof opt_element === 'undefined' ? '' : opt_element;
this.oldValue = typeof opt_oldValue === 'undefined' ? '' : opt_oldValue;
this.newValue = typeof opt_newValue === 'undefined' ? '' : opt_newValue;
};
object.inherits(Ui, UiBase);
this.blockId = opt_block ? opt_block.id : null;
this.element = typeof opt_element === 'undefined' ? '' : opt_element;
this.oldValue = typeof opt_oldValue === 'undefined' ? '' : opt_oldValue;
this.newValue = typeof opt_newValue === 'undefined' ? '' : opt_newValue;
/**
* Type of this event.
* @type {string}
*/
Ui.prototype.type = eventUtils.UI;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Ui.prototype.toJson = function() {
const json = Ui.superClass_.toJson.call(this);
json['element'] = this.element;
if (this.newValue !== undefined) {
json['newValue'] = this.newValue;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.UI;
}
if (this.blockId) {
json['blockId'] = this.blockId;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Ui.prototype.fromJson = function(json) {
Ui.superClass_.fromJson.call(this, json);
this.element = json['element'];
this.newValue = json['newValue'];
this.blockId = json['blockId'];
};
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
json['element'] = this.element;
if (this.newValue !== undefined) {
json['newValue'] = this.newValue;
}
if (this.blockId) {
json['blockId'] = this.blockId;
}
return json;
}
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.element = json['element'];
this.newValue = json['newValue'];
this.blockId = json['blockId'];
}
}
registry.register(registry.Type.EVENT, eventUtils.UI, Ui);

View File

@@ -17,8 +17,7 @@
*/
goog.module('Blockly.Events.UiBase');
const Abstract = goog.require('Blockly.Events.Abstract');
const object = goog.require('Blockly.utils.object');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
/**
@@ -27,36 +26,38 @@ const object = goog.require('Blockly.utils.object');
* editing to work (e.g. scrolling the workspace, zooming, opening toolbox
* categories).
* UI events do not undo or redo.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
* @extends {Abstract}
* @constructor
* @extends {AbstractEvent}
* @alias Blockly.Events.UiBase
*/
const UiBase = function(opt_workspaceId) {
UiBase.superClass_.constructor.call(this);
class UiBase extends AbstractEvent {
/**
* Whether or not the event is blank (to be populated by fromJson).
* @type {boolean}
* @param {string=} opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
*/
this.isBlank = typeof opt_workspaceId === 'undefined';
constructor(opt_workspaceId) {
super();
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = opt_workspaceId ? opt_workspaceId : '';
/**
* Whether or not the event is blank (to be populated by fromJson).
* @type {boolean}
*/
this.isBlank = typeof opt_workspaceId === 'undefined';
// UI events do not undo or redo.
this.recordUndo = false;
};
object.inherits(UiBase, Abstract);
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = opt_workspaceId ? opt_workspaceId : '';
/**
* Whether or not the event is a UI event.
* @type {boolean}
*/
UiBase.prototype.isUiEvent = true;
// UI events do not undo or redo.
this.recordUndo = false;
/**
* Whether or not the event is a UI event.
* @type {boolean}
*/
this.isUiEvent = true;
}
}
exports.UiBase = UiBase;

View File

@@ -15,55 +15,56 @@
*/
goog.module('Blockly.Events.VarBase');
const Abstract = goog.require('Blockly.Events.Abstract');
const object = goog.require('Blockly.utils.object');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {VariableModel} = goog.requireType('Blockly.VariableModel');
/**
* Abstract class for a variable event.
* @param {!VariableModel=} opt_variable The variable this event
* corresponds to. Undefined for a blank event.
* @extends {Abstract}
* @constructor
* @extends {AbstractEvent}
* @alias Blockly.Events.VarBase
*/
const VarBase = function(opt_variable) {
VarBase.superClass_.constructor.call(this);
this.isBlank = typeof opt_variable === 'undefined';
class VarBase extends AbstractEvent {
/**
* @param {!VariableModel=} opt_variable The variable this event
* corresponds to. Undefined for a blank event.
*/
constructor(opt_variable) {
super();
this.isBlank = typeof opt_variable === 'undefined';
/**
* The variable id for the variable this event pertains to.
* @type {string}
*/
this.varId = this.isBlank ? '' : opt_variable.getId();
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = this.isBlank ? '' : opt_variable.workspace.id;
}
/**
* The variable id for the variable this event pertains to.
* @type {string}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.varId = this.isBlank ? '' : opt_variable.getId();
toJson() {
const json = super.toJson();
json['varId'] = this.varId;
return json;
}
/**
* The workspace identifier for this event.
* @type {string}
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
this.workspaceId = this.isBlank ? '' : opt_variable.workspace.id;
};
object.inherits(VarBase, Abstract);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
VarBase.prototype.toJson = function() {
const json = VarBase.superClass_.toJson.call(this);
json['varId'] = this.varId;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
VarBase.prototype.fromJson = function(json) {
VarBase.superClass_.toJson.call(this);
this.varId = json['varId'];
};
fromJson(json) {
super.fromJson(json);
this.varId = json['varId'];
}
}
exports.VarBase = VarBase;

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.Events.VarCreate');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {VarBase} = goog.require('Blockly.Events.VarBase');
/* eslint-disable-next-line no-unused-vars */
@@ -25,62 +24,65 @@ const {VariableModel} = goog.requireType('Blockly.VariableModel');
/**
* Class for a variable creation event.
* @param {!VariableModel=} opt_variable The created variable. Undefined
* for a blank event.
* @extends {VarBase}
* @constructor
* @alias Blockly.Events.VarCreate
*/
const VarCreate = function(opt_variable) {
VarCreate.superClass_.constructor.call(this, opt_variable);
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
class VarCreate extends VarBase {
/**
* @param {!VariableModel=} opt_variable The created variable. Undefined
* for a blank event.
*/
constructor(opt_variable) {
super(opt_variable);
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.VAR_CREATE;
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
}
this.varType = opt_variable.type;
this.varName = opt_variable.name;
}
this.varType = opt_variable.type;
this.varName = opt_variable.name;
};
object.inherits(VarCreate, VarBase);
/**
* Type of this event.
* @type {string}
*/
VarCreate.prototype.type = eventUtils.VAR_CREATE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
VarCreate.prototype.toJson = function() {
const json = VarCreate.superClass_.toJson.call(this);
json['varType'] = this.varType;
json['varName'] = this.varName;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
VarCreate.prototype.fromJson = function(json) {
VarCreate.superClass_.fromJson.call(this, json);
this.varType = json['varType'];
this.varName = json['varName'];
};
/**
* Run a variable creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
VarCreate.prototype.run = function(forward) {
const workspace = this.getEventWorkspace_();
if (forward) {
workspace.createVariable(this.varName, this.varType, this.varId);
} else {
workspace.deleteVariableById(this.varId);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
json['varType'] = this.varType;
json['varName'] = this.varName;
return json;
}
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.varType = json['varType'];
this.varName = json['varName'];
}
/**
* Run a variable creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
run(forward) {
const workspace = this.getEventWorkspace_();
if (forward) {
workspace.createVariable(this.varName, this.varType, this.varId);
} else {
workspace.deleteVariableById(this.varId);
}
}
}
registry.register(registry.Type.EVENT, eventUtils.VAR_CREATE, VarCreate);

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.Events.VarDelete');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {VarBase} = goog.require('Blockly.Events.VarBase');
/* eslint-disable-next-line no-unused-vars */
@@ -25,62 +24,65 @@ const {VariableModel} = goog.requireType('Blockly.VariableModel');
/**
* Class for a variable deletion event.
* @param {!VariableModel=} opt_variable The deleted variable. Undefined
* for a blank event.
* @extends {VarBase}
* @constructor
* @alias Blockly.Events.VarDelete
*/
const VarDelete = function(opt_variable) {
VarDelete.superClass_.constructor.call(this, opt_variable);
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
class VarDelete extends VarBase {
/**
* @param {!VariableModel=} opt_variable The deleted variable. Undefined
* for a blank event.
*/
constructor(opt_variable) {
super(opt_variable);
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.VAR_DELETE;
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
}
this.varType = opt_variable.type;
this.varName = opt_variable.name;
}
this.varType = opt_variable.type;
this.varName = opt_variable.name;
};
object.inherits(VarDelete, VarBase);
/**
* Type of this event.
* @type {string}
*/
VarDelete.prototype.type = eventUtils.VAR_DELETE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
VarDelete.prototype.toJson = function() {
const json = VarDelete.superClass_.toJson.call(this);
json['varType'] = this.varType;
json['varName'] = this.varName;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
VarDelete.prototype.fromJson = function(json) {
VarDelete.superClass_.fromJson.call(this, json);
this.varType = json['varType'];
this.varName = json['varName'];
};
/**
* Run a variable deletion event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
VarDelete.prototype.run = function(forward) {
const workspace = this.getEventWorkspace_();
if (forward) {
workspace.deleteVariableById(this.varId);
} else {
workspace.createVariable(this.varName, this.varType, this.varId);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
json['varType'] = this.varType;
json['varName'] = this.varName;
return json;
}
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.varType = json['varType'];
this.varName = json['varName'];
}
/**
* Run a variable deletion event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
run(forward) {
const workspace = this.getEventWorkspace_();
if (forward) {
workspace.deleteVariableById(this.varId);
} else {
workspace.createVariable(this.varName, this.varType, this.varId);
}
}
}
registry.register(registry.Type.EVENT, eventUtils.VAR_DELETE, VarDelete);

View File

@@ -16,7 +16,6 @@
goog.module('Blockly.Events.VarRename');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {VarBase} = goog.require('Blockly.Events.VarBase');
/* eslint-disable-next-line no-unused-vars */
@@ -25,63 +24,66 @@ const {VariableModel} = goog.requireType('Blockly.VariableModel');
/**
* Class for a variable rename event.
* @param {!VariableModel=} opt_variable The renamed variable. Undefined
* for a blank event.
* @param {string=} newName The new name the variable will be changed to.
* @extends {VarBase}
* @constructor
* @alias Blockly.Events.VarRename
*/
const VarRename = function(opt_variable, newName) {
VarRename.superClass_.constructor.call(this, opt_variable);
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
class VarRename extends VarBase {
/**
* @param {!VariableModel=} opt_variable The renamed variable. Undefined
* for a blank event.
* @param {string=} newName The new name the variable will be changed to.
*/
constructor(opt_variable, newName) {
super(opt_variable);
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.VAR_RENAME;
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
}
this.oldName = opt_variable.name;
this.newName = typeof newName === 'undefined' ? '' : newName;
}
this.oldName = opt_variable.name;
this.newName = typeof newName === 'undefined' ? '' : newName;
};
object.inherits(VarRename, VarBase);
/**
* Type of this event.
* @type {string}
*/
VarRename.prototype.type = eventUtils.VAR_RENAME;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
VarRename.prototype.toJson = function() {
const json = VarRename.superClass_.toJson.call(this);
json['oldName'] = this.oldName;
json['newName'] = this.newName;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
VarRename.prototype.fromJson = function(json) {
VarRename.superClass_.fromJson.call(this, json);
this.oldName = json['oldName'];
this.newName = json['newName'];
};
/**
* Run a variable rename event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
VarRename.prototype.run = function(forward) {
const workspace = this.getEventWorkspace_();
if (forward) {
workspace.renameVariableById(this.varId, this.newName);
} else {
workspace.renameVariableById(this.varId, this.oldName);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = super.toJson();
json['oldName'] = this.oldName;
json['newName'] = this.newName;
return json;
}
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
super.fromJson(json);
this.oldName = json['oldName'];
this.newName = json['newName'];
}
/**
* Run a variable rename event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
run(forward) {
const workspace = this.getEventWorkspace_();
if (forward) {
workspace.renameVariableById(this.varId, this.newName);
} else {
workspace.renameVariableById(this.varId, this.oldName);
}
}
}
registry.register(registry.Type.EVENT, eventUtils.VAR_RENAME, VarRename);

View File

@@ -16,89 +16,90 @@
goog.module('Blockly.Events.ViewportChange');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {UiBase} = goog.require('Blockly.Events.UiBase');
/**
* Class for a viewport change event.
* @param {number=} opt_top Top-edge of the visible portion of the workspace,
* relative to the workspace origin. Undefined for a blank event.
* @param {number=} opt_left Left-edge of the visible portion of the workspace,
* relative to the workspace origin. Undefined for a blank event.
* @param {number=} opt_scale The scale of the workspace. Undefined for a blank
* event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
* @param {number=} opt_oldScale The old scale of the workspace. Undefined for a
* blank event.
* @extends {UiBase}
* @constructor
* @alias Blockly.Events.ViewportChange
*/
const ViewportChange = function(
opt_top, opt_left, opt_scale, opt_workspaceId, opt_oldScale) {
ViewportChange.superClass_.constructor.call(this, opt_workspaceId);
class ViewportChange extends UiBase {
/**
* @param {number=} opt_top Top-edge of the visible portion of the workspace,
* relative to the workspace origin. Undefined for a blank event.
* @param {number=} opt_left Left-edge of the visible portion of the
* workspace relative to the workspace origin. Undefined for a blank
* event.
* @param {number=} opt_scale The scale of the workspace. Undefined for a
* blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
* @param {number=} opt_oldScale The old scale of the workspace. Undefined for
* a blank event.
*/
constructor(opt_top, opt_left, opt_scale, opt_workspaceId, opt_oldScale) {
super(opt_workspaceId);
/**
* Top-edge of the visible portion of the workspace, relative to the
* workspace origin.
* @type {number|undefined}
*/
this.viewTop = opt_top;
/**
* Left-edge of the visible portion of the workspace, relative to the
* workspace origin.
* @type {number|undefined}
*/
this.viewLeft = opt_left;
/**
* The scale of the workspace.
* @type {number|undefined}
*/
this.scale = opt_scale;
/**
* The old scale of the workspace.
* @type {number|undefined}
*/
this.oldScale = opt_oldScale;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.VIEWPORT_CHANGE;
}
/**
* Top-edge of the visible portion of the workspace, relative to the workspace
* origin.
* @type {number|undefined}
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
this.viewTop = opt_top;
toJson() {
const json = super.toJson();
json['viewTop'] = this.viewTop;
json['viewLeft'] = this.viewLeft;
json['scale'] = this.scale;
json['oldScale'] = this.oldScale;
return json;
}
/**
* Left-edge of the visible portion of the workspace, relative to the
* workspace origin.
* @type {number|undefined}
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
this.viewLeft = opt_left;
/**
* The scale of the workspace.
* @type {number|undefined}
*/
this.scale = opt_scale;
/**
* The old scale of the workspace.
* @type {number|undefined}
*/
this.oldScale = opt_oldScale;
};
object.inherits(ViewportChange, UiBase);
/**
* Type of this event.
* @type {string}
*/
ViewportChange.prototype.type = eventUtils.VIEWPORT_CHANGE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
ViewportChange.prototype.toJson = function() {
const json = ViewportChange.superClass_.toJson.call(this);
json['viewTop'] = this.viewTop;
json['viewLeft'] = this.viewLeft;
json['scale'] = this.scale;
json['oldScale'] = this.oldScale;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
ViewportChange.prototype.fromJson = function(json) {
ViewportChange.superClass_.fromJson.call(this, json);
this.viewTop = json['viewTop'];
this.viewLeft = json['viewLeft'];
this.scale = json['scale'];
this.oldScale = json['oldScale'];
};
fromJson(json) {
super.fromJson(json);
this.viewTop = json['viewTop'];
this.viewLeft = json['viewLeft'];
this.scale = json['scale'];
this.oldScale = json['oldScale'];
}
}
registry.register(
registry.Type.EVENT, eventUtils.VIEWPORT_CHANGE, ViewportChange);

View File

@@ -17,11 +17,13 @@
*/
goog.module('Blockly.Events.utils');
/* eslint-disable-next-line no-unused-vars */
const Abstract = goog.requireType('Blockly.Events.Abstract');
const idGenerator = goog.require('Blockly.utils.idGenerator');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {Abstract} = goog.requireType('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {BlockChange} = goog.requireType('Blockly.Events.BlockChange');
/* eslint-disable-next-line no-unused-vars */
const {BlockCreate} = goog.requireType('Blockly.Events.BlockCreate');
/* eslint-disable-next-line no-unused-vars */
const {BlockMove} = goog.requireType('Blockly.Events.BlockMove');
@@ -32,7 +34,11 @@ const {CommentCreate} = goog.requireType('Blockly.Events.CommentCreate');
/* eslint-disable-next-line no-unused-vars */
const {CommentMove} = goog.requireType('Blockly.Events.CommentMove');
/* eslint-disable-next-line no-unused-vars */
const {ViewportChange} = goog.requireType('Blockly.Events.ViewportChange');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/**
@@ -307,6 +313,7 @@ exports.BUMP_EVENTS = BUMP_EVENTS;
/**
* List of events queued for firing.
* @type {!Array<!Abstract>}
*/
const FIRE_QUEUE = [];
@@ -365,7 +372,9 @@ const filter = function(queueIn, forward) {
if (!event.isNull()) {
// Treat all UI events as the same type in hash table.
const eventType = event.isUiEvent ? UI : event.type;
const key = [eventType, event.blockId, event.workspaceId].join(' ');
// TODO(#5927): Ceck whether `blockId` exists before accessing it.
const blockId = /** @type {*} */ (event).blockId;
const key = [eventType, blockId, event.workspaceId].join(' ');
const lastEntry = hash[key];
const lastEvent = lastEntry ? lastEntry.event : null;
@@ -376,22 +385,25 @@ const filter = function(queueIn, forward) {
hash[key] = {event: event, index: i};
mergedQueue.push(event);
} else if (event.type === MOVE && lastEntry.index === i - 1) {
const moveEvent = /** @type {!BlockMove} */ (event);
// Merge move events.
lastEvent.newParentId = event.newParentId;
lastEvent.newInputName = event.newInputName;
lastEvent.newCoordinate = event.newCoordinate;
lastEvent.newParentId = moveEvent.newParentId;
lastEvent.newInputName = moveEvent.newInputName;
lastEvent.newCoordinate = moveEvent.newCoordinate;
lastEntry.index = i;
} else if (
event.type === CHANGE && event.element === lastEvent.element &&
event.name === lastEvent.name) {
const changeEvent = /** @type {!BlockChange} */ (event);
// Merge change events.
lastEvent.newValue = event.newValue;
lastEvent.newValue = changeEvent.newValue;
} else if (event.type === VIEWPORT_CHANGE) {
const viewportEvent = /** @type {!ViewportChange} */ (event);
// Merge viewport change events.
lastEvent.viewTop = event.viewTop;
lastEvent.viewLeft = event.viewLeft;
lastEvent.scale = event.scale;
lastEvent.oldScale = event.oldScale;
lastEvent.viewTop = viewportEvent.viewTop;
lastEvent.viewLeft = viewportEvent.viewLeft;
lastEvent.scale = viewportEvent.scale;
lastEvent.oldScale = viewportEvent.oldScale;
} else if (event.type === CLICK && lastEvent.type === BUBBLE_OPEN) {
// Drop click events caused by opening/closing bubbles.
} else {
@@ -546,12 +558,15 @@ exports.get = get;
*/
const disableOrphans = function(event) {
if (event.type === MOVE || event.type === CREATE) {
if (!event.workspaceId) {
const blockEvent = /** @type {!BlockMove|!BlockCreate} */ (event);
if (!blockEvent.workspaceId) {
return;
}
const {Workspace} = goog.module.get('Blockly.Workspace');
const eventWorkspace = Workspace.getById(event.workspaceId);
let block = eventWorkspace.getBlockById(event.blockId);
const eventWorkspace =
/** @type {!WorkspaceSvg} */ (
Workspace.getById(blockEvent.workspaceId));
let block = eventWorkspace.getBlockById(blockEvent.blockId);
if (block) {
// Changing blocks as part of this event shouldn't be undoable.
const initialUndoFlag = recordUndo;

View File

@@ -15,10 +15,9 @@
*/
goog.module('Blockly.Events.FinishedLoading');
const Abstract = goog.require('Blockly.Events.Abstract');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
@@ -28,70 +27,65 @@ const {Workspace} = goog.requireType('Blockly.Workspace');
* Used to notify the developer when the workspace has finished loading (i.e
* domToWorkspace).
* Finished loading events do not record undo or redo.
* @param {!Workspace=} opt_workspace The workspace that has finished
* loading. Undefined for a blank event.
* @extends {Abstract}
* @constructor
* @extends {AbstractEvent}
* @alias Blockly.Events.FinishedLoading
*/
const FinishedLoading = function(opt_workspace) {
class FinishedLoading extends AbstractEvent {
/**
* Whether or not the event is blank (to be populated by fromJson).
* @type {boolean}
* @param {!Workspace=} opt_workspace The workspace that has finished
* loading. Undefined for a blank event.
*/
this.isBlank = typeof opt_workspace === 'undefined';
constructor(opt_workspace) {
super();
/**
* Whether or not the event is blank (to be populated by fromJson).
* @type {boolean}
*/
this.isBlank = typeof opt_workspace === 'undefined';
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = opt_workspace ? opt_workspace.id : '';
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = opt_workspace ? opt_workspace.id : '';
/**
* The event group ID for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
* @type {string}
*/
this.group = eventUtils.getGroup();
// Workspace events do not undo or redo.
this.recordUndo = false;
// Workspace events do not undo or redo.
this.recordUndo = false;
};
object.inherits(FinishedLoading, Abstract);
/**
* Type of this event.
* @type {string}
*/
FinishedLoading.prototype.type = eventUtils.FINISHED_LOADING;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
FinishedLoading.prototype.toJson = function() {
const json = {
'type': this.type,
};
if (this.group) {
json['group'] = this.group;
/**
* Type of this event.
* @type {string}
*/
this.type = eventUtils.FINISHED_LOADING;
}
if (this.workspaceId) {
json['workspaceId'] = this.workspaceId;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
FinishedLoading.prototype.fromJson = function(json) {
this.isBlank = false;
this.workspaceId = json['workspaceId'];
this.group = json['group'];
};
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
toJson() {
const json = {
'type': this.type,
};
if (this.group) {
json['group'] = this.group;
}
if (this.workspaceId) {
json['workspaceId'] = this.workspaceId;
}
return json;
}
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
fromJson(json) {
this.isBlank = false;
this.workspaceId = json['workspaceId'];
this.group = json['group'];
}
}
registry.register(
registry.Type.EVENT, eventUtils.FINISHED_LOADING, FinishedLoading);

View File

@@ -24,6 +24,7 @@ goog.module('Blockly.Extensions');
const parsing = goog.require('Blockly.utils.parsing');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
goog.requireType('Blockly.Mutator');
@@ -454,7 +455,7 @@ exports.buildTooltipForDropdown = buildTooltipForDropdown;
const checkDropdownOptionsInTable = function(block, dropdownName, lookupTable) {
// Validate all dropdown options have values.
const dropdown = block.getField(dropdownName);
if (!dropdown.isOptionListDynamic()) {
if (dropdown instanceof FieldDropdown && !dropdown.isOptionListDynamic()) {
const options = dropdown.getOptions();
for (let i = 0; i < options.length; i++) {
const optionKey = options[i][1]; // label, then value
@@ -512,11 +513,11 @@ exports.buildTooltipWithFieldText = buildTooltipWithFieldText;
* @this {Block}
*/
const extensionParentTooltip = function() {
this.tooltipWhenNotConnected = this.tooltip;
const tooltipWhenNotConnected = this.tooltip;
this.setTooltip(function() {
const parent = this.getParent();
return (parent && parent.getInputsInline() && parent.tooltip) ||
this.tooltipWhenNotConnected;
tooltipWhenNotConnected;
}.bind(this));
};
register('parent_tooltip_when_inline', extensionParentTooltip);

File diff suppressed because it is too large Load Diff

View File

@@ -19,110 +19,493 @@ const Css = goog.require('Blockly.Css');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const browserEvents = goog.require('Blockly.browserEvents');
const dom = goog.require('Blockly.utils.dom');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const math = goog.require('Blockly.utils.math');
const object = goog.require('Blockly.utils.object');
const userAgent = goog.require('Blockly.utils.userAgent');
const {DropDownDiv} = goog.require('Blockly.DropDownDiv');
const {Field} = goog.require('Blockly.Field');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
const {KeyCodes} = goog.require('Blockly.utils.KeyCodes');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Svg} = goog.require('Blockly.utils.Svg');
/**
* Class for an editable angle field.
* @param {string|number=} opt_value The initial value of the field. Should cast
* to a number. Defaults to 0.
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a number & returns a
* validated number, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/angle#creation}
* for a list of properties this parameter supports.
* @extends {FieldTextInput}
* @constructor
* @alias Blockly.FieldAngle
*/
const FieldAngle = function(opt_value, opt_validator, opt_config) {
class FieldAngle extends FieldTextInput {
/**
* Should the angle increase as the angle picker is moved clockwise (true)
* or counterclockwise (false)
* @see FieldAngle.CLOCKWISE
* @type {boolean}
* @param {(string|number|!Sentinel)=} opt_value The initial value of
* the field. Should cast to a number. Defaults to 0.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a number & returns a
* validated number, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/angle#creation}
* for a list of properties this parameter supports.
*/
constructor(opt_value, opt_validator, opt_config) {
super(Field.SKIP_SETUP);
/**
* Should the angle increase as the angle picker is moved clockwise (true)
* or counterclockwise (false)
* @see FieldAngle.CLOCKWISE
* @type {boolean}
* @private
*/
this.clockwise_ = FieldAngle.CLOCKWISE;
/**
* The offset of zero degrees (and all other angles).
* @see FieldAngle.OFFSET
* @type {number}
* @private
*/
this.offset_ = FieldAngle.OFFSET;
/**
* The maximum angle to allow before wrapping.
* @see FieldAngle.WRAP
* @type {number}
* @private
*/
this.wrap_ = FieldAngle.WRAP;
/**
* The amount to round angles to when using a mouse or keyboard nav input.
* @see FieldAngle.ROUND
* @type {number}
* @private
*/
this.round_ = FieldAngle.ROUND;
/**
* The angle picker's SVG element.
* @type {?SVGElement}
* @private
*/
this.editor_ = null;
/**
* The angle picker's gauge path depending on the value.
* @type {?SVGElement}
*/
this.gauge_ = null;
/**
* The angle picker's line drawn representing the value's angle.
* @type {?SVGElement}
*/
this.line_ = null;
/**
* The degree symbol for this field.
* @type {SVGTSpanElement}
* @protected
*/
this.symbol_ = null;
/**
* Wrapper click event data.
* @type {?browserEvents.Data}
* @private
*/
this.clickWrapper_ = null;
/**
* Surface click event data.
* @type {?browserEvents.Data}
* @private
*/
this.clickSurfaceWrapper_ = null;
/**
* Surface mouse move event data.
* @type {?browserEvents.Data}
* @private
*/
this.moveSurfaceWrapper_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) this.configure_(opt_config);
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
}
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
configure_(config) {
super.configure_(config);
switch (config['mode']) {
case 'compass':
this.clockwise_ = true;
this.offset_ = 90;
break;
case 'protractor':
// This is the default mode, so we could do nothing. But just to
// future-proof, we'll set it anyway.
this.clockwise_ = false;
this.offset_ = 0;
break;
}
// Allow individual settings to override the mode setting.
const clockwise = config['clockwise'];
if (typeof clockwise === 'boolean') {
this.clockwise_ = clockwise;
}
// If these are passed as null then we should leave them on the default.
let offset = config['offset'];
if (offset !== null) {
offset = Number(offset);
if (!isNaN(offset)) {
this.offset_ = offset;
}
}
let wrap = config['wrap'];
if (wrap !== null) {
wrap = Number(wrap);
if (!isNaN(wrap)) {
this.wrap_ = wrap;
}
}
let round = config['round'];
if (round !== null) {
round = Number(round);
if (!isNaN(round)) {
this.round_ = round;
}
}
}
/**
* Create the block UI for this field.
* @package
*/
initView() {
super.initView();
// Add the degree symbol to the left of the number, even in RTL (issue
// #2380)
this.symbol_ = dom.createSvgElement(Svg.TSPAN, {}, null);
this.symbol_.appendChild(document.createTextNode('\u00B0'));
this.textElement_.appendChild(this.symbol_);
}
/**
* Updates the graph when the field rerenders.
* @protected
* @override
*/
render_() {
super.render_();
this.updateGraph_();
}
/**
* Create and show the angle field's editor.
* @param {Event=} opt_e Optional mouse event that triggered the field to
* open, or undefined if triggered programmatically.
* @protected
*/
showEditor_(opt_e) {
// Mobile browsers have issues with in-line textareas (focus & keyboards).
const noFocus = userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD;
super.showEditor_(opt_e, noFocus);
this.dropdownCreate_();
dropDownDiv.getContentDiv().appendChild(this.editor_);
dropDownDiv.setColour(
this.sourceBlock_.style.colourPrimary,
this.sourceBlock_.style.colourTertiary);
dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this));
this.updateGraph_();
}
/**
* Create the angle dropdown editor.
* @private
*/
this.clockwise_ = FieldAngle.CLOCKWISE;
dropdownCreate_() {
const svg = dom.createSvgElement(
Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
'xmlns:xlink': dom.XLINK_NS,
'version': '1.1',
'height': (FieldAngle.HALF * 2) + 'px',
'width': (FieldAngle.HALF * 2) + 'px',
'style': 'touch-action: none',
},
null);
const circle = dom.createSvgElement(
Svg.CIRCLE, {
'cx': FieldAngle.HALF,
'cy': FieldAngle.HALF,
'r': FieldAngle.RADIUS,
'class': 'blocklyAngleCircle',
},
svg);
this.gauge_ =
dom.createSvgElement(Svg.PATH, {'class': 'blocklyAngleGauge'}, svg);
this.line_ = dom.createSvgElement(
Svg.LINE, {
'x1': FieldAngle.HALF,
'y1': FieldAngle.HALF,
'class': 'blocklyAngleLine',
},
svg);
// Draw markers around the edge.
for (let angle = 0; angle < 360; angle += 15) {
dom.createSvgElement(
Svg.LINE, {
'x1': FieldAngle.HALF + FieldAngle.RADIUS,
'y1': FieldAngle.HALF,
'x2': FieldAngle.HALF + FieldAngle.RADIUS -
(angle % 45 === 0 ? 10 : 5),
'y2': FieldAngle.HALF,
'class': 'blocklyAngleMarks',
'transform': 'rotate(' + angle + ',' + FieldAngle.HALF + ',' +
FieldAngle.HALF + ')',
},
svg);
}
// The angle picker is different from other fields in that it updates on
// mousemove even if it's not in the middle of a drag. In future we may
// change this behaviour.
this.clickWrapper_ =
browserEvents.conditionalBind(svg, 'click', this, this.hide_);
// On touch devices, the picker's value is only updated with a drag. Add
// a click handler on the drag surface to update the value if the surface
// is clicked.
this.clickSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'click', this, this.onMouseMove_, true, true);
this.moveSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'mousemove', this, this.onMouseMove_, true, true);
this.editor_ = svg;
}
/**
* The offset of zero degrees (and all other angles).
* @see FieldAngle.OFFSET
* @type {number}
* Disposes of events and DOM-references belonging to the angle editor.
* @private
*/
this.offset_ = FieldAngle.OFFSET;
dropdownDispose_() {
if (this.clickWrapper_) {
browserEvents.unbind(this.clickWrapper_);
this.clickWrapper_ = null;
}
if (this.clickSurfaceWrapper_) {
browserEvents.unbind(this.clickSurfaceWrapper_);
this.clickSurfaceWrapper_ = null;
}
if (this.moveSurfaceWrapper_) {
browserEvents.unbind(this.moveSurfaceWrapper_);
this.moveSurfaceWrapper_ = null;
}
this.gauge_ = null;
this.line_ = null;
}
/**
* The maximum angle to allow before wrapping.
* @see FieldAngle.WRAP
* @type {number}
* Hide the editor.
* @private
*/
this.wrap_ = FieldAngle.WRAP;
hide_() {
dropDownDiv.hideIfOwner(this);
WidgetDiv.hide();
}
/**
* The amount to round angles to when using a mouse or keyboard nav input.
* @see FieldAngle.ROUND
* @type {number}
* Set the angle to match the mouse's position.
* @param {!Event} e Mouse move event.
* @protected
*/
onMouseMove_(e) {
// Calculate angle.
const bBox = this.gauge_.ownerSVGElement.getBoundingClientRect();
const dx = e.clientX - bBox.left - FieldAngle.HALF;
const dy = e.clientY - bBox.top - FieldAngle.HALF;
let angle = Math.atan(-dy / dx);
if (isNaN(angle)) {
// This shouldn't happen, but let's not let this error propagate further.
return;
}
angle = math.toDegrees(angle);
// 0: East, 90: North, 180: West, 270: South.
if (dx < 0) {
angle += 180;
} else if (dy > 0) {
angle += 360;
}
// Do offsetting.
if (this.clockwise_) {
angle = this.offset_ + 360 - angle;
} else {
angle = 360 - (this.offset_ - angle);
}
this.displayMouseOrKeyboardValue_(angle);
}
/**
* Handles and displays values that are input via mouse or arrow key input.
* These values need to be rounded and wrapped before being displayed so
* that the text input's value is appropriate.
* @param {number} angle New angle.
* @private
*/
this.round_ = FieldAngle.ROUND;
FieldAngle.superClass_.constructor.call(
this, opt_value, opt_validator, opt_config);
displayMouseOrKeyboardValue_(angle) {
if (this.round_) {
angle = Math.round(angle / this.round_) * this.round_;
}
angle = this.wrapValue_(angle);
if (angle !== this.value_) {
this.setEditorValue_(angle);
}
}
/**
* The angle picker's SVG element.
* @type {?SVGElement}
* Redraw the graph with the current angle.
* @private
*/
this.editor_ = null;
updateGraph_() {
if (!this.gauge_) {
return;
}
// Always display the input (i.e. getText) even if it is invalid.
let angleDegrees = Number(this.getText()) + this.offset_;
angleDegrees %= 360;
let angleRadians = math.toRadians(angleDegrees);
const path = ['M ', FieldAngle.HALF, ',', FieldAngle.HALF];
let x2 = FieldAngle.HALF;
let y2 = FieldAngle.HALF;
if (!isNaN(angleRadians)) {
const clockwiseFlag = Number(this.clockwise_);
const angle1 = math.toRadians(this.offset_);
const x1 = Math.cos(angle1) * FieldAngle.RADIUS;
const y1 = Math.sin(angle1) * -FieldAngle.RADIUS;
if (clockwiseFlag) {
angleRadians = 2 * angle1 - angleRadians;
}
x2 += Math.cos(angleRadians) * FieldAngle.RADIUS;
y2 -= Math.sin(angleRadians) * FieldAngle.RADIUS;
// Don't ask how the flag calculations work. They just do.
let largeFlag =
Math.abs(Math.floor((angleRadians - angle1) / Math.PI) % 2);
if (clockwiseFlag) {
largeFlag = 1 - largeFlag;
}
path.push(
' l ', x1, ',', y1, ' A ', FieldAngle.RADIUS, ',', FieldAngle.RADIUS,
' 0 ', largeFlag, ' ', clockwiseFlag, ' ', x2, ',', y2, ' z');
}
this.gauge_.setAttribute('d', path.join(''));
this.line_.setAttribute('x2', x2);
this.line_.setAttribute('y2', y2);
}
/**
* The angle picker's gauge path depending on the value.
* @type {?SVGElement}
* Handle key down to the editor.
* @param {!Event} e Keyboard event.
* @protected
* @override
*/
this.gauge_ = null;
onHtmlInputKeyDown_(e) {
super.onHtmlInputKeyDown_(e);
let multiplier;
if (e.keyCode === KeyCodes.LEFT) {
// decrement (increment in RTL)
multiplier = this.sourceBlock_.RTL ? 1 : -1;
} else if (e.keyCode === KeyCodes.RIGHT) {
// increment (decrement in RTL)
multiplier = this.sourceBlock_.RTL ? -1 : 1;
} else if (e.keyCode === KeyCodes.DOWN) {
// decrement
multiplier = -1;
} else if (e.keyCode === KeyCodes.UP) {
// increment
multiplier = 1;
}
if (multiplier) {
const value = /** @type {number} */ (this.getValue());
this.displayMouseOrKeyboardValue_(value + (multiplier * this.round_));
e.preventDefault();
e.stopPropagation();
}
}
/**
* The angle picker's line drawn representing the value's angle.
* @type {?SVGElement}
* Ensure that the input value is a valid angle.
* @param {*=} opt_newValue The input value.
* @return {?number} A valid angle, or null if invalid.
* @protected
* @override
*/
this.line_ = null;
doClassValidation_(opt_newValue) {
const value = Number(opt_newValue);
if (isNaN(value) || !isFinite(value)) {
return null;
}
return this.wrapValue_(value);
}
/**
* Wrapper click event data.
* @type {?browserEvents.Data}
* Wraps the value so that it is in the range (-360 + wrap, wrap).
* @param {number} value The value to wrap.
* @return {number} The wrapped value.
* @private
*/
this.clickWrapper_ = null;
wrapValue_(value) {
value %= 360;
if (value < 0) {
value += 360;
}
if (value > this.wrap_) {
value -= 360;
}
return value;
}
/**
* Surface click event data.
* @type {?browserEvents.Data}
* @private
* Construct a FieldAngle from a JSON arg object.
* @param {!Object} options A JSON object with options (angle).
* @return {!FieldAngle} The new field instance.
* @package
* @nocollapse
* @override
*/
this.clickSurfaceWrapper_ = null;
/**
* Surface mouse move event data.
* @type {?browserEvents.Data}
* @private
*/
this.moveSurfaceWrapper_ = null;
};
object.inherits(FieldAngle, FieldTextInput);
static fromJson(options) {
// `this` might be a subclass of FieldAngle if that class doesn't override
// the static fromJson method.
return new this(options['angle'], undefined, options);
}
}
/**
* The default value for this field.
@@ -131,26 +514,6 @@ object.inherits(FieldAngle, FieldTextInput);
*/
FieldAngle.prototype.DEFAULT_VALUE = 0;
/**
* Construct a FieldAngle from a JSON arg object.
* @param {!Object} options A JSON object with options (angle).
* @return {!FieldAngle} The new field instance.
* @package
* @nocollapse
*/
FieldAngle.fromJson = function(options) {
// `this` might be a subclass of FieldAngle if that class doesn't override
// the static fromJson method.
return new this(options['angle'], undefined, options);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
FieldAngle.prototype.SERIALIZABLE = true;
/**
* The default amount to round angles to when using a mouse or keyboard nav
* input. Must be a positive integer to support keyboard navigation.
@@ -193,377 +556,34 @@ FieldAngle.WRAP = 360;
*/
FieldAngle.RADIUS = FieldAngle.HALF - 1;
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
FieldAngle.prototype.configure_ = function(config) {
FieldAngle.superClass_.configure_.call(this, config);
switch (config['mode']) {
case 'compass':
this.clockwise_ = true;
this.offset_ = 90;
break;
case 'protractor':
// This is the default mode, so we could do nothing. But just to
// future-proof, we'll set it anyway.
this.clockwise_ = false;
this.offset_ = 0;
break;
}
// Allow individual settings to override the mode setting.
const clockwise = config['clockwise'];
if (typeof clockwise === 'boolean') {
this.clockwise_ = clockwise;
}
// If these are passed as null then we should leave them on the default.
let offset = config['offset'];
if (offset !== null) {
offset = Number(offset);
if (!isNaN(offset)) {
this.offset_ = offset;
}
}
let wrap = config['wrap'];
if (wrap !== null) {
wrap = Number(wrap);
if (!isNaN(wrap)) {
this.wrap_ = wrap;
}
}
let round = config['round'];
if (round !== null) {
round = Number(round);
if (!isNaN(round)) {
this.round_ = round;
}
}
};
/**
* Create the block UI for this field.
* @package
*/
FieldAngle.prototype.initView = function() {
FieldAngle.superClass_.initView.call(this);
// Add the degree symbol to the left of the number, even in RTL (issue #2380)
this.symbol_ = dom.createSvgElement(Svg.TSPAN, {}, null);
this.symbol_.appendChild(document.createTextNode('\u00B0'));
this.textElement_.appendChild(this.symbol_);
};
/**
* Updates the graph when the field rerenders.
* @protected
* @override
*/
FieldAngle.prototype.render_ = function() {
FieldAngle.superClass_.render_.call(this);
this.updateGraph_();
};
/**
* Create and show the angle field's editor.
* @param {Event=} opt_e Optional mouse event that triggered the field to open,
* or undefined if triggered programmatically.
* @protected
*/
FieldAngle.prototype.showEditor_ = function(opt_e) {
// Mobile browsers have issues with in-line textareas (focus & keyboards).
const noFocus = userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD;
FieldAngle.superClass_.showEditor_.call(this, opt_e, noFocus);
this.dropdownCreate_();
DropDownDiv.getContentDiv().appendChild(this.editor_);
DropDownDiv.setColour(
this.sourceBlock_.style.colourPrimary,
this.sourceBlock_.style.colourTertiary);
DropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this));
this.updateGraph_();
};
/**
* Create the angle dropdown editor.
* @private
*/
FieldAngle.prototype.dropdownCreate_ = function() {
const svg = dom.createSvgElement(
Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
'xmlns:xlink': dom.XLINK_NS,
'version': '1.1',
'height': (FieldAngle.HALF * 2) + 'px',
'width': (FieldAngle.HALF * 2) + 'px',
'style': 'touch-action: none',
},
null);
const circle = dom.createSvgElement(
Svg.CIRCLE, {
'cx': FieldAngle.HALF,
'cy': FieldAngle.HALF,
'r': FieldAngle.RADIUS,
'class': 'blocklyAngleCircle',
},
svg);
this.gauge_ =
dom.createSvgElement(Svg.PATH, {'class': 'blocklyAngleGauge'}, svg);
this.line_ = dom.createSvgElement(
Svg.LINE, {
'x1': FieldAngle.HALF,
'y1': FieldAngle.HALF,
'class': 'blocklyAngleLine',
},
svg);
// Draw markers around the edge.
for (let angle = 0; angle < 360; angle += 15) {
dom.createSvgElement(
Svg.LINE, {
'x1': FieldAngle.HALF + FieldAngle.RADIUS,
'y1': FieldAngle.HALF,
'x2':
FieldAngle.HALF + FieldAngle.RADIUS - (angle % 45 === 0 ? 10 : 5),
'y2': FieldAngle.HALF,
'class': 'blocklyAngleMarks',
'transform': 'rotate(' + angle + ',' + FieldAngle.HALF + ',' +
FieldAngle.HALF + ')',
},
svg);
}
// The angle picker is different from other fields in that it updates on
// mousemove even if it's not in the middle of a drag. In future we may
// change this behaviour.
this.clickWrapper_ =
browserEvents.conditionalBind(svg, 'click', this, this.hide_);
// On touch devices, the picker's value is only updated with a drag. Add
// a click handler on the drag surface to update the value if the surface
// is clicked.
this.clickSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'click', this, this.onMouseMove_, true, true);
this.moveSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'mousemove', this, this.onMouseMove_, true, true);
this.editor_ = svg;
};
/**
* Disposes of events and DOM-references belonging to the angle editor.
* @private
*/
FieldAngle.prototype.dropdownDispose_ = function() {
if (this.clickWrapper_) {
browserEvents.unbind(this.clickWrapper_);
this.clickWrapper_ = null;
}
if (this.clickSurfaceWrapper_) {
browserEvents.unbind(this.clickSurfaceWrapper_);
this.clickSurfaceWrapper_ = null;
}
if (this.moveSurfaceWrapper_) {
browserEvents.unbind(this.moveSurfaceWrapper_);
this.moveSurfaceWrapper_ = null;
}
this.gauge_ = null;
this.line_ = null;
};
/**
* Hide the editor.
* @private
*/
FieldAngle.prototype.hide_ = function() {
DropDownDiv.hideIfOwner(this);
WidgetDiv.hide();
};
/**
* Set the angle to match the mouse's position.
* @param {!Event} e Mouse move event.
* @protected
*/
FieldAngle.prototype.onMouseMove_ = function(e) {
// Calculate angle.
const bBox = this.gauge_.ownerSVGElement.getBoundingClientRect();
const dx = e.clientX - bBox.left - FieldAngle.HALF;
const dy = e.clientY - bBox.top - FieldAngle.HALF;
let angle = Math.atan(-dy / dx);
if (isNaN(angle)) {
// This shouldn't happen, but let's not let this error propagate further.
return;
}
angle = math.toDegrees(angle);
// 0: East, 90: North, 180: West, 270: South.
if (dx < 0) {
angle += 180;
} else if (dy > 0) {
angle += 360;
}
// Do offsetting.
if (this.clockwise_) {
angle = this.offset_ + 360 - angle;
} else {
angle = 360 - (this.offset_ - angle);
}
this.displayMouseOrKeyboardValue_(angle);
};
/**
* Handles and displays values that are input via mouse or arrow key input.
* These values need to be rounded and wrapped before being displayed so
* that the text input's value is appropriate.
* @param {number} angle New angle.
* @private
*/
FieldAngle.prototype.displayMouseOrKeyboardValue_ = function(angle) {
if (this.round_) {
angle = Math.round(angle / this.round_) * this.round_;
}
angle = this.wrapValue_(angle);
if (angle !== this.value_) {
this.setEditorValue_(angle);
}
};
/**
* Redraw the graph with the current angle.
* @private
*/
FieldAngle.prototype.updateGraph_ = function() {
if (!this.gauge_) {
return;
}
// Always display the input (i.e. getText) even if it is invalid.
let angleDegrees = Number(this.getText()) + this.offset_;
angleDegrees %= 360;
let angleRadians = math.toRadians(angleDegrees);
const path = ['M ', FieldAngle.HALF, ',', FieldAngle.HALF];
let x2 = FieldAngle.HALF;
let y2 = FieldAngle.HALF;
if (!isNaN(angleRadians)) {
const clockwiseFlag = Number(this.clockwise_);
const angle1 = math.toRadians(this.offset_);
const x1 = Math.cos(angle1) * FieldAngle.RADIUS;
const y1 = Math.sin(angle1) * -FieldAngle.RADIUS;
if (clockwiseFlag) {
angleRadians = 2 * angle1 - angleRadians;
}
x2 += Math.cos(angleRadians) * FieldAngle.RADIUS;
y2 -= Math.sin(angleRadians) * FieldAngle.RADIUS;
// Don't ask how the flag calculations work. They just do.
let largeFlag = Math.abs(Math.floor((angleRadians - angle1) / Math.PI) % 2);
if (clockwiseFlag) {
largeFlag = 1 - largeFlag;
}
path.push(
' l ', x1, ',', y1, ' A ', FieldAngle.RADIUS, ',', FieldAngle.RADIUS,
' 0 ', largeFlag, ' ', clockwiseFlag, ' ', x2, ',', y2, ' z');
}
this.gauge_.setAttribute('d', path.join(''));
this.line_.setAttribute('x2', x2);
this.line_.setAttribute('y2', y2);
};
/**
* Handle key down to the editor.
* @param {!Event} e Keyboard event.
* @protected
* @override
*/
FieldAngle.prototype.onHtmlInputKeyDown_ = function(e) {
FieldAngle.superClass_.onHtmlInputKeyDown_.call(this, e);
let multiplier;
if (e.keyCode === KeyCodes.LEFT) {
// decrement (increment in RTL)
multiplier = this.sourceBlock_.RTL ? 1 : -1;
} else if (e.keyCode === KeyCodes.RIGHT) {
// increment (decrement in RTL)
multiplier = this.sourceBlock_.RTL ? -1 : 1;
} else if (e.keyCode === KeyCodes.DOWN) {
// decrement
multiplier = -1;
} else if (e.keyCode === KeyCodes.UP) {
// increment
multiplier = 1;
}
if (multiplier) {
const value = /** @type {number} */ (this.getValue());
this.displayMouseOrKeyboardValue_(value + (multiplier * this.round_));
e.preventDefault();
e.stopPropagation();
}
};
/**
* Ensure that the input value is a valid angle.
* @param {*=} opt_newValue The input value.
* @return {?number} A valid angle, or null if invalid.
* @protected
* @override
*/
FieldAngle.prototype.doClassValidation_ = function(opt_newValue) {
const value = Number(opt_newValue);
if (isNaN(value) || !isFinite(value)) {
return null;
}
return this.wrapValue_(value);
};
/**
* Wraps the value so that it is in the range (-360 + wrap, wrap).
* @param {number} value The value to wrap.
* @return {number} The wrapped value.
* @private
*/
FieldAngle.prototype.wrapValue_ = function(value) {
value %= 360;
if (value < 0) {
value += 360;
}
if (value > this.wrap_) {
value -= 360;
}
return value;
};
/**
* CSS for angle field. See css.js for use.
*/
Css.register(`
.blocklyAngleCircle {
stroke: #444;
stroke-width: 1;
fill: #ddd;
fill-opacity: .8;
}
.blocklyAngleCircle {
stroke: #444;
stroke-width: 1;
fill: #ddd;
fill-opacity: .8;
}
.blocklyAngleMarks {
stroke: #444;
stroke-width: 1;
}
.blocklyAngleMarks {
stroke: #444;
stroke-width: 1;
}
.blocklyAngleGauge {
fill: #f88;
fill-opacity: .8;
pointer-events: none;
}
.blocklyAngleGauge {
fill: #f88;
fill-opacity: .8;
pointer-events: none;
}
.blocklyAngleLine {
stroke: #f00;
stroke-width: 2;
stroke-linecap: round;
pointer-events: none;
}
.blocklyAngleLine {
stroke: #f00;
stroke-width: 2;
stroke-linecap: round;
pointer-events: none;
}
`);
fieldRegistry.register('field_angle', FieldAngle);

View File

@@ -17,41 +17,224 @@ goog.module('Blockly.FieldCheckbox');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const object = goog.require('Blockly.utils.object');
const {Field} = goog.require('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockChange');
/**
* Class for a checkbox field.
* @param {string|boolean=} opt_value The initial value of the field. Should
* either be 'TRUE', 'FALSE' or a boolean. Defaults to 'FALSE'.
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a value ('TRUE' or 'FALSE') &
* returns a validated value ('TRUE' or 'FALSE'), or null to abort the
* change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/checkbox#creation}
* for a list of properties this parameter supports.
* @extends {Field}
* @constructor
* @alias Blockly.FieldCheckbox
*/
const FieldCheckbox = function(opt_value, opt_validator, opt_config) {
class FieldCheckbox extends Field {
/**
* Character for the check mark. Used to apply a different check mark
* character to individual fields.
* @type {?string}
* @param {(string|boolean|!Sentinel)=} opt_value The initial value of
* the field. Should either be 'TRUE', 'FALSE' or a boolean. Defaults to
* 'FALSE'.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a value ('TRUE' or 'FALSE') &
* returns a validated value ('TRUE' or 'FALSE'), or null to abort the
* change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/checkbox#creation}
* for a list of properties this parameter supports.
*/
constructor(opt_value, opt_validator, opt_config) {
super(Field.SKIP_SETUP);
/**
* Character for the check mark. Used to apply a different check mark
* character to individual fields.
* @type {string}
* @private
*/
this.checkChar_ = FieldCheckbox.CHECK_CHAR;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
/**
* Mouse cursor style when over the hotspot that initiates editability.
* @type {string}
*/
this.CURSOR = 'default';
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) this.configure_(opt_config);
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
}
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
configure_(config) {
super.configure_(config);
if (config['checkCharacter']) {
this.checkChar_ = config['checkCharacter'];
}
}
/**
* Saves this field's value.
* @return {*} The boolean value held by this field.
* @override
* @package
*/
saveState() {
const legacyState = this.saveLegacyState(FieldCheckbox);
if (legacyState !== null) {
return legacyState;
}
return this.getValueBoolean();
}
/**
* Create the block UI for this checkbox.
* @package
*/
initView() {
super.initView();
dom.addClass(
/** @type {!SVGTextElement} **/ (this.textElement_), 'blocklyCheckbox');
this.textElement_.style.display = this.value_ ? 'block' : 'none';
}
/**
* @override
*/
render_() {
if (this.textContent_) {
this.textContent_.nodeValue = this.getDisplayText_();
}
this.updateSize_(this.getConstants().FIELD_CHECKBOX_X_OFFSET);
}
/**
* @override
*/
getDisplayText_() {
return this.checkChar_;
}
/**
* Set the character used for the check mark.
* @param {?string} character The character to use for the check mark, or
* null to use the default.
*/
setCheckCharacter(character) {
this.checkChar_ = character || FieldCheckbox.CHECK_CHAR;
this.forceRerender();
}
/**
* Toggle the state of the checkbox on click.
* @protected
*/
showEditor_() {
this.setValue(!this.value_);
}
/**
* Ensure that the input value is valid ('TRUE' or 'FALSE').
* @param {*=} opt_newValue The input value.
* @return {?string} A valid value ('TRUE' or 'FALSE), or null if invalid.
* @protected
*/
doClassValidation_(opt_newValue) {
if (opt_newValue === true || opt_newValue === 'TRUE') {
return 'TRUE';
}
if (opt_newValue === false || opt_newValue === 'FALSE') {
return 'FALSE';
}
return null;
}
/**
* Update the value of the field, and update the checkElement.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a either 'TRUE' or 'FALSE'.
* @protected
*/
doValueUpdate_(newValue) {
this.value_ = this.convertValueToBool_(newValue);
// Update visual.
if (this.textElement_) {
this.textElement_.style.display = this.value_ ? 'block' : 'none';
}
}
/**
* Get the value of this field, either 'TRUE' or 'FALSE'.
* @return {string} The value of this field.
*/
getValue() {
return this.value_ ? 'TRUE' : 'FALSE';
}
/**
* Get the boolean value of this field.
* @return {boolean} The boolean value of this field.
*/
getValueBoolean() {
return /** @type {boolean} */ (this.value_);
}
/**
* Get the text of this field. Used when the block is collapsed.
* @return {string} Text representing the value of this field
* ('true' or 'false').
*/
getText() {
return String(this.convertValueToBool_(this.value_));
}
/**
* Convert a value into a pure boolean.
*
* Converts 'TRUE' to true and 'FALSE' to false correctly, everything else
* is cast to a boolean.
* @param {*} value The value to convert.
* @return {boolean} The converted value.
* @private
*/
this.checkChar_ = null;
convertValueToBool_(value) {
if (typeof value === 'string') {
return value === 'TRUE';
} else {
return !!value;
}
}
FieldCheckbox.superClass_.constructor.call(
this, opt_value, opt_validator, opt_config);
};
object.inherits(FieldCheckbox, Field);
/**
* Construct a FieldCheckbox from a JSON arg object.
* @param {!Object} options A JSON object with options (checked).
* @return {!FieldCheckbox} The new field instance.
* @package
* @nocollapse
*/
static fromJson(options) {
// `this` might be a subclass of FieldCheckbox if that class doesn't
// 'override' the static fromJson method.
return new this(options['checked'], undefined, options);
}
}
/**
* The default value for this field.
@@ -60,19 +243,6 @@ object.inherits(FieldCheckbox, Field);
*/
FieldCheckbox.prototype.DEFAULT_VALUE = false;
/**
* Construct a FieldCheckbox from a JSON arg object.
* @param {!Object} options A JSON object with options (checked).
* @return {!FieldCheckbox} The new field instance.
* @package
* @nocollapse
*/
FieldCheckbox.fromJson = function(options) {
// `this` might be a subclass of FieldCheckbox if that class doesn't override
// the static fromJson method.
return new this(options['checked'], undefined, options);
};
/**
* Default character for the checkmark.
* @type {string}
@@ -80,164 +250,6 @@ FieldCheckbox.fromJson = function(options) {
*/
FieldCheckbox.CHECK_CHAR = '\u2713';
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
FieldCheckbox.prototype.SERIALIZABLE = true;
/**
* Mouse cursor style when over the hotspot that initiates editability.
*/
FieldCheckbox.prototype.CURSOR = 'default';
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
FieldCheckbox.prototype.configure_ = function(config) {
FieldCheckbox.superClass_.configure_.call(this, config);
if (config['checkCharacter']) {
this.checkChar_ = config['checkCharacter'];
}
};
/**
* Saves this field's value.
* @return {*} The boolean value held by this field.
* @override
* @package
*/
FieldCheckbox.prototype.saveState = function() {
const legacyState = this.saveLegacyState(FieldCheckbox);
if (legacyState !== null) {
return legacyState;
}
return this.getValueBoolean();
};
/**
* Create the block UI for this checkbox.
* @package
*/
FieldCheckbox.prototype.initView = function() {
FieldCheckbox.superClass_.initView.call(this);
dom.addClass(
/** @type {!SVGTextElement} **/ (this.textElement_), 'blocklyCheckbox');
this.textElement_.style.display = this.value_ ? 'block' : 'none';
};
/**
* @override
*/
FieldCheckbox.prototype.render_ = function() {
if (this.textContent_) {
this.textContent_.nodeValue = this.getDisplayText_();
}
this.updateSize_(this.getConstants().FIELD_CHECKBOX_X_OFFSET);
};
/**
* @override
*/
FieldCheckbox.prototype.getDisplayText_ = function() {
return this.checkChar_ || FieldCheckbox.CHECK_CHAR;
};
/**
* Set the character used for the check mark.
* @param {?string} character The character to use for the check mark, or
* null to use the default.
*/
FieldCheckbox.prototype.setCheckCharacter = function(character) {
this.checkChar_ = character;
this.forceRerender();
};
/**
* Toggle the state of the checkbox on click.
* @protected
*/
FieldCheckbox.prototype.showEditor_ = function() {
this.setValue(!this.value_);
};
/**
* Ensure that the input value is valid ('TRUE' or 'FALSE').
* @param {*=} opt_newValue The input value.
* @return {?string} A valid value ('TRUE' or 'FALSE), or null if invalid.
* @protected
*/
FieldCheckbox.prototype.doClassValidation_ = function(opt_newValue) {
if (opt_newValue === true || opt_newValue === 'TRUE') {
return 'TRUE';
}
if (opt_newValue === false || opt_newValue === 'FALSE') {
return 'FALSE';
}
return null;
};
/**
* Update the value of the field, and update the checkElement.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a either 'TRUE' or 'FALSE'.
* @protected
*/
FieldCheckbox.prototype.doValueUpdate_ = function(newValue) {
this.value_ = this.convertValueToBool_(newValue);
// Update visual.
if (this.textElement_) {
this.textElement_.style.display = this.value_ ? 'block' : 'none';
}
};
/**
* Get the value of this field, either 'TRUE' or 'FALSE'.
* @return {string} The value of this field.
*/
FieldCheckbox.prototype.getValue = function() {
return this.value_ ? 'TRUE' : 'FALSE';
};
/**
* Get the boolean value of this field.
* @return {boolean} The boolean value of this field.
*/
FieldCheckbox.prototype.getValueBoolean = function() {
return /** @type {boolean} */ (this.value_);
};
/**
* Get the text of this field. Used when the block is collapsed.
* @return {string} Text representing the value of this field
* ('true' or 'false').
*/
FieldCheckbox.prototype.getText = function() {
return String(this.convertValueToBool_(this.value_));
};
/**
* Convert a value into a pure boolean.
*
* Converts 'TRUE' to true and 'FALSE' to false correctly, everything else
* is cast to a boolean.
* @param {*} value The value to convert.
* @return {boolean} The converted value.
* @private
*/
FieldCheckbox.prototype.convertValueToBool_ = function(value) {
if (typeof value === 'string') {
return value === 'TRUE';
} else {
return !!value;
}
};
fieldRegistry.register('field_checkbox', FieldCheckbox);
exports.FieldCheckbox = FieldCheckbox;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -17,108 +17,272 @@ goog.module('Blockly.FieldImage');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const object = goog.require('Blockly.utils.object');
const parsing = goog.require('Blockly.utils.parsing');
const {Field} = goog.require('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Size} = goog.require('Blockly.utils.Size');
const {Svg} = goog.require('Blockly.utils.Svg');
/**
* Class for an image on a block.
* @param {string} src The URL of the image.
* @param {!(string|number)} width Width of the image.
* @param {!(string|number)} height Height of the image.
* @param {string=} opt_alt Optional alt text for when block is collapsed.
* @param {function(!FieldImage)=} opt_onClick Optional function to be
* called when the image is clicked. If opt_onClick is defined, opt_alt must
* also be defined.
* @param {boolean=} opt_flipRtl Whether to flip the icon in RTL.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/image#creation}
* for a list of properties this parameter supports.
* @extends {Field}
* @constructor
* @alias Blockly.FieldImage
*/
const FieldImage = function(
src, width, height, opt_alt, opt_onClick, opt_flipRtl, opt_config) {
// Return early.
if (!src) {
throw Error('Src value of an image field is required');
}
src = parsing.replaceMessageReferences(src);
const imageHeight = Number(parsing.replaceMessageReferences(height));
const imageWidth = Number(parsing.replaceMessageReferences(width));
if (isNaN(imageHeight) || isNaN(imageWidth)) {
throw Error(
'Height and width values of an image field must cast to' +
' numbers.');
}
if (imageHeight <= 0 || imageWidth <= 0) {
throw Error(
'Height and width values of an image field must be greater' +
' than 0.');
}
// Initialize configurable properties.
class FieldImage extends Field {
/**
* Whether to flip this image in RTL.
* @type {boolean}
* @private
* @param {string|!Sentinel} src The URL of the image.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {!(string|number)} width Width of the image.
* @param {!(string|number)} height Height of the image.
* @param {string=} opt_alt Optional alt text for when block is collapsed.
* @param {function(!FieldImage)=} opt_onClick Optional function to be
* called when the image is clicked. If opt_onClick is defined, opt_alt
* must also be defined.
* @param {boolean=} opt_flipRtl Whether to flip the icon in RTL.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/image#creation}
* for a list of properties this parameter supports.
*/
this.flipRtl_ = false;
constructor(
src, width, height, opt_alt, opt_onClick, opt_flipRtl, opt_config) {
super(Field.SKIP_SETUP);
/**
* Alt text of this image.
* @type {string}
* @private
*/
this.altText_ = '';
// Return early.
if (!src) {
throw Error('Src value of an image field is required');
}
const imageHeight = Number(parsing.replaceMessageReferences(height));
const imageWidth = Number(parsing.replaceMessageReferences(width));
if (isNaN(imageHeight) || isNaN(imageWidth)) {
throw Error(
'Height and width values of an image field must cast to' +
' numbers.');
}
if (imageHeight <= 0 || imageWidth <= 0) {
throw Error(
'Height and width values of an image field must be greater' +
' than 0.');
}
FieldImage.superClass_.constructor.call(this, src, null, opt_config);
/**
* The size of the area rendered by the field.
* @type {Size}
* @protected
* @override
*/
this.size_ = new Size(imageWidth, imageHeight + FieldImage.Y_PADDING);
if (!opt_config) { // If the config wasn't passed, do old configuration.
this.flipRtl_ = !!opt_flipRtl;
this.altText_ = parsing.replaceMessageReferences(opt_alt) || '';
/**
* Store the image height, since it is different from the field height.
* @type {number}
* @private
*/
this.imageHeight_ = imageHeight;
/**
* The function to be called when this field is clicked.
* @type {?function(!FieldImage)}
* @private
*/
this.clickHandler_ = null;
if (typeof opt_onClick === 'function') {
this.clickHandler_ = opt_onClick;
}
/**
* The rendered field's image element.
* @type {SVGImageElement}
* @private
*/
this.imageElement_ = null;
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
* @const
*/
this.EDITABLE = false;
/**
* Used to tell if the field needs to be rendered the next time the block is
* rendered. Image fields are statically sized, and only need to be
* rendered at initialization.
* @type {boolean}
* @protected
*/
this.isDirty_ = false;
/**
* Whether to flip this image in RTL.
* @type {boolean}
* @private
*/
this.flipRtl_ = false;
/**
* Alt text of this image.
* @type {string}
* @private
*/
this.altText_ = '';
if (src === Field.SKIP_SETUP) return;
if (opt_config) {
this.configure_(opt_config);
} else {
this.flipRtl_ = !!opt_flipRtl;
this.altText_ = parsing.replaceMessageReferences(opt_alt) || '';
}
this.setValue(parsing.replaceMessageReferences(src));
}
// Initialize other properties.
/**
* The size of the area rendered by the field.
* @type {Size}
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
this.size_ = new Size(imageWidth, imageHeight + FieldImage.Y_PADDING);
/**
* Store the image height, since it is different from the field height.
* @type {number}
* @private
*/
this.imageHeight_ = imageHeight;
/**
* The function to be called when this field is clicked.
* @type {?function(!FieldImage)}
* @private
*/
this.clickHandler_ = null;
if (typeof opt_onClick === 'function') {
this.clickHandler_ = opt_onClick;
configure_(config) {
super.configure_(config);
this.flipRtl_ = !!config['flipRtl'];
this.altText_ = parsing.replaceMessageReferences(config['alt']) || '';
}
/**
* The rendered field's image element.
* @type {SVGImageElement}
* @private
* Create the block UI for this image.
* @package
*/
this.imageElement_ = null;
};
object.inherits(FieldImage, Field);
initView() {
this.imageElement_ = dom.createSvgElement(
Svg.IMAGE, {
'height': this.imageHeight_ + 'px',
'width': this.size_.width + 'px',
'alt': this.altText_,
},
this.fieldGroup_);
this.imageElement_.setAttributeNS(
dom.XLINK_NS, 'xlink:href', /** @type {string} */ (this.value_));
if (this.clickHandler_) {
this.imageElement_.style.cursor = 'pointer';
}
}
/**
* @override
*/
updateSize_() {
// NOP
}
/**
* Ensure that the input value (the source URL) is a string.
* @param {*=} opt_newValue The input value.
* @return {?string} A string, or null if invalid.
* @protected
*/
doClassValidation_(opt_newValue) {
if (typeof opt_newValue !== 'string') {
return null;
}
return opt_newValue;
}
/**
* Update the value of this image field, and update the displayed image.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a string.
* @protected
*/
doValueUpdate_(newValue) {
this.value_ = newValue;
if (this.imageElement_) {
this.imageElement_.setAttributeNS(
dom.XLINK_NS, 'xlink:href', String(this.value_));
}
}
/**
* Get whether to flip this image in RTL
* @return {boolean} True if we should flip in RTL.
* @override
*/
getFlipRtl() {
return this.flipRtl_;
}
/**
* Set the alt text of this image.
* @param {?string} alt New alt text.
* @public
*/
setAlt(alt) {
if (alt === this.altText_) {
return;
}
this.altText_ = alt || '';
if (this.imageElement_) {
this.imageElement_.setAttribute('alt', this.altText_);
}
}
/**
* If field click is called, and click handler defined,
* call the handler.
* @protected
*/
showEditor_() {
if (this.clickHandler_) {
this.clickHandler_(this);
}
}
/**
* Set the function that is called when this image is clicked.
* @param {?function(!FieldImage)} func The function that is called
* when the image is clicked, or null to remove.
*/
setOnClickHandler(func) {
this.clickHandler_ = func;
}
/**
* Use the `getText_` developer hook to override the field's text
* representation.
* Return the image alt text instead.
* @return {?string} The image alt text.
* @protected
* @override
*/
getText_() {
return this.altText_;
}
/**
* Construct a FieldImage from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (src, width, height,
* alt, and flipRtl).
* @return {!FieldImage} The new field instance.
* @package
* @nocollapse
*/
static fromJson(options) {
// `this` might be a subclass of FieldImage if that class doesn't override
// the static fromJson method.
return new this(
options['src'], options['width'], options['height'], undefined,
undefined, undefined, options);
}
}
/**
* The default value for this field.
@@ -127,23 +291,6 @@ object.inherits(FieldImage, Field);
*/
FieldImage.prototype.DEFAULT_VALUE = '';
/**
* Construct a FieldImage from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (src, width, height,
* alt, and flipRtl).
* @return {!FieldImage} The new field instance.
* @package
* @nocollapse
*/
FieldImage.fromJson = function(options) {
// `this` might be a subclass of FieldImage if that class doesn't override
// the static fromJson method.
return new this(
options['src'], options['width'], options['height'], undefined, undefined,
undefined, options);
};
/**
* Vertical padding below the image, which is included in the reported height of
* the field.
@@ -152,144 +299,6 @@ FieldImage.fromJson = function(options) {
*/
FieldImage.Y_PADDING = 1;
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
*/
FieldImage.prototype.EDITABLE = false;
/**
* Used to tell if the field needs to be rendered the next time the block is
* rendered. Image fields are statically sized, and only need to be
* rendered at initialization.
* @type {boolean}
* @protected
*/
FieldImage.prototype.isDirty_ = false;
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
FieldImage.prototype.configure_ = function(config) {
FieldImage.superClass_.configure_.call(this, config);
this.flipRtl_ = !!config['flipRtl'];
this.altText_ = parsing.replaceMessageReferences(config['alt']) || '';
};
/**
* Create the block UI for this image.
* @package
*/
FieldImage.prototype.initView = function() {
this.imageElement_ = dom.createSvgElement(
Svg.IMAGE, {
'height': this.imageHeight_ + 'px',
'width': this.size_.width + 'px',
'alt': this.altText_,
},
this.fieldGroup_);
this.imageElement_.setAttributeNS(
dom.XLINK_NS, 'xlink:href', /** @type {string} */ (this.value_));
if (this.clickHandler_) {
this.imageElement_.style.cursor = 'pointer';
}
};
/**
* @override
*/
FieldImage.prototype.updateSize_ = function() {
// NOP
};
/**
* Ensure that the input value (the source URL) is a string.
* @param {*=} opt_newValue The input value.
* @return {?string} A string, or null if invalid.
* @protected
*/
FieldImage.prototype.doClassValidation_ = function(opt_newValue) {
if (typeof opt_newValue !== 'string') {
return null;
}
return opt_newValue;
};
/**
* Update the value of this image field, and update the displayed image.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a string.
* @protected
*/
FieldImage.prototype.doValueUpdate_ = function(newValue) {
this.value_ = newValue;
if (this.imageElement_) {
this.imageElement_.setAttributeNS(
dom.XLINK_NS, 'xlink:href', String(this.value_));
}
};
/**
* Get whether to flip this image in RTL
* @return {boolean} True if we should flip in RTL.
* @override
*/
FieldImage.prototype.getFlipRtl = function() {
return this.flipRtl_;
};
/**
* Set the alt text of this image.
* @param {?string} alt New alt text.
* @public
*/
FieldImage.prototype.setAlt = function(alt) {
if (alt === this.altText_) {
return;
}
this.altText_ = alt || '';
if (this.imageElement_) {
this.imageElement_.setAttribute('alt', this.altText_);
}
};
/**
* If field click is called, and click handler defined,
* call the handler.
* @protected
*/
FieldImage.prototype.showEditor_ = function() {
if (this.clickHandler_) {
this.clickHandler_(this);
}
};
/**
* Set the function that is called when this image is clicked.
* @param {?function(!FieldImage)} func The function that is called
* when the image is clicked, or null to remove.
*/
FieldImage.prototype.setOnClickHandler = function(func) {
this.clickHandler_ = func;
};
/**
* Use the `getText_` developer hook to override the field's text
* representation.
* Return the image alt text instead.
* @return {?string} The image alt text.
* @protected
* @override
*/
FieldImage.prototype.getText_ = function() {
return this.altText_;
};
fieldRegistry.register('field_image', FieldImage);
exports.FieldImage = FieldImage;

View File

@@ -19,39 +19,123 @@ goog.module('Blockly.FieldLabel');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const object = goog.require('Blockly.utils.object');
const parsing = goog.require('Blockly.utils.parsing');
const {Field} = goog.require('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
/**
* Class for a non-editable, non-serializable text field.
* @param {string=} opt_value The initial value of the field. Should cast to a
* string. Defaults to an empty string if null or undefined.
* @param {string=} opt_class Optional CSS class for the field's text.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label#creation}
* for a list of properties this parameter supports.
* @extends {Field}
* @constructor
* @alias Blockly.FieldLabel
*/
const FieldLabel = function(opt_value, opt_class, opt_config) {
class FieldLabel extends Field {
/**
* The html class name to use for this field.
* @type {?string}
* @private
* @param {(string|!Sentinel)=} opt_value The initial value of the
* field. Should cast to a string. Defaults to an empty string if null or
* undefined.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {string=} opt_class Optional CSS class for the field's text.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label#creation}
* for a list of properties this parameter supports.
*/
this.class_ = null;
constructor(opt_value, opt_class, opt_config) {
super(Field.SKIP_SETUP);
FieldLabel.superClass_.constructor.call(this, opt_value, null, opt_config);
/**
* The html class name to use for this field.
* @type {?string}
* @private
*/
this.class_ = null;
if (!opt_config) { // If the config was not passed use old configuration.
this.class_ = opt_class || null;
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
*/
this.EDITABLE = false;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) {
this.configure_(opt_config);
} else {
this.class_ = opt_class || null;
}
this.setValue(opt_value);
}
};
object.inherits(FieldLabel, Field);
/**
* @override
*/
configure_(config) {
super.configure_(config);
this.class_ = config['class'];
}
/**
* Create block UI for this label.
* @package
*/
initView() {
this.createTextElement_();
if (this.class_) {
dom.addClass(
/** @type {!SVGTextElement} */ (this.textElement_), this.class_);
}
}
/**
* Ensure that the input value casts to a valid string.
* @param {*=} opt_newValue The input value.
* @return {?string} A valid string, or null if invalid.
* @protected
*/
doClassValidation_(opt_newValue) {
if (opt_newValue === null || opt_newValue === undefined) {
return null;
}
return String(opt_newValue);
}
/**
* Set the CSS class applied to the field's textElement_.
* @param {?string} cssClass The new CSS class name, or null to remove.
*/
setClass(cssClass) {
if (this.textElement_) {
// This check isn't necessary, but it's faster than letting removeClass
// figure it out.
if (this.class_) {
dom.removeClass(this.textElement_, this.class_);
}
if (cssClass) {
dom.addClass(this.textElement_, cssClass);
}
}
this.class_ = cssClass;
}
/**
* Construct a FieldLabel from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and class).
* @return {!FieldLabel} The new field instance.
* @package
* @nocollapse
*/
static fromJson(options) {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldLabel if that class doesn't override
// the static fromJson method.
return new this(text, undefined, options);
}
}
/**
* The default value for this field.
@@ -60,79 +144,6 @@ object.inherits(FieldLabel, Field);
*/
FieldLabel.prototype.DEFAULT_VALUE = '';
/**
* Construct a FieldLabel from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and class).
* @return {!FieldLabel} The new field instance.
* @package
* @nocollapse
*/
FieldLabel.fromJson = function(options) {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldLabel if that class doesn't override
// the static fromJson method.
return new this(text, undefined, options);
};
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
*/
FieldLabel.prototype.EDITABLE = false;
/**
* @override
*/
FieldLabel.prototype.configure_ = function(config) {
FieldLabel.superClass_.configure_.call(this, config);
this.class_ = config['class'];
};
/**
* Create block UI for this label.
* @package
*/
FieldLabel.prototype.initView = function() {
this.createTextElement_();
if (this.class_) {
dom.addClass(
/** @type {!SVGTextElement} */ (this.textElement_), this.class_);
}
};
/**
* Ensure that the input value casts to a valid string.
* @param {*=} opt_newValue The input value.
* @return {?string} A valid string, or null if invalid.
* @protected
*/
FieldLabel.prototype.doClassValidation_ = function(opt_newValue) {
if (opt_newValue === null || opt_newValue === undefined) {
return null;
}
return String(opt_newValue);
};
/**
* Set the CSS class applied to the field's textElement_.
* @param {?string} cssClass The new CSS class name, or null to remove.
*/
FieldLabel.prototype.setClass = function(cssClass) {
if (this.textElement_) {
// This check isn't necessary, but it's faster than letting removeClass
// figure it out.
if (this.class_) {
dom.removeClass(this.textElement_, this.class_);
}
if (cssClass) {
dom.addClass(this.textElement_, cssClass);
}
}
this.class_ = cssClass;
};
fieldRegistry.register('field_label', FieldLabel);
exports.FieldLabel = FieldLabel;

View File

@@ -20,59 +20,60 @@
goog.module('Blockly.FieldLabelSerializable');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const object = goog.require('Blockly.utils.object');
const parsing = goog.require('Blockly.utils.parsing');
const {FieldLabel} = goog.require('Blockly.FieldLabel');
/**
* Class for a non-editable, serializable text field.
* @param {*} opt_value The initial value of the field. Should cast to a
* string. Defaults to an empty string if null or undefined.
* @param {string=} opt_class Optional CSS class for the field's text.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label-serializable#creation}
* for a list of properties this parameter supports.
* @extends {FieldLabel}
* @constructor
*
* @alias Blockly.FieldLabelSerializable
*/
const FieldLabelSerializable = function(opt_value, opt_class, opt_config) {
FieldLabelSerializable.superClass_.constructor.call(
this, opt_value, opt_class, opt_config);
};
object.inherits(FieldLabelSerializable, FieldLabel);
class FieldLabelSerializable extends FieldLabel {
/**
* @param {string=} opt_value The initial value of the field. Should cast to a
* string. Defaults to an empty string if null or undefined.
* @param {string=} opt_class Optional CSS class for the field's text.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label-serializable#creation}
* for a list of properties this parameter supports.
*/
constructor(opt_value, opt_class, opt_config) {
super(String(opt_value ?? ''), opt_class, opt_config);
/**
* Construct a FieldLabelSerializable from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and class).
* @return {!FieldLabelSerializable} The new field instance.
* @package
* @nocollapse
*/
FieldLabelSerializable.fromJson = function(options) {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldLabelSerializable if that class doesn't
// override the static fromJson method.
return new this(text, undefined, options);
};
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
*/
this.EDITABLE = false;
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
*/
FieldLabelSerializable.prototype.EDITABLE = false;
/**
* Serializable fields are saved by the XML renderer, non-serializable
* fields are not. This field should be serialized, but only edited
* programmatically.
* @type {boolean}
*/
this.SERIALIZABLE = true;
}
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. This field should be serialized, but only edited programmatically.
* @type {boolean}
*/
FieldLabelSerializable.prototype.SERIALIZABLE = true;
/**
* Construct a FieldLabelSerializable from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and class).
* @return {!FieldLabelSerializable} The new field instance.
* @package
* @nocollapse
* @override
*/
static fromJson(options) {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldLabelSerializable if that class
// doesn't override the static fromJson method.
return new this(text, undefined, options);
}
}
fieldRegistry.register('field_label_serializable', FieldLabelSerializable);

View File

@@ -20,428 +20,442 @@ const WidgetDiv = goog.require('Blockly.WidgetDiv');
const aria = goog.require('Blockly.utils.aria');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const object = goog.require('Blockly.utils.object');
const parsing = goog.require('Blockly.utils.parsing');
const userAgent = goog.require('Blockly.utils.userAgent');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
const {Field} = goog.require('Blockly.Field');
const {KeyCodes} = goog.require('Blockly.utils.KeyCodes');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Svg} = goog.require('Blockly.utils.Svg');
/**
* Class for an editable text area field.
* @param {string=} opt_value The initial content of the field. Should cast to a
* string. Defaults to an empty string if null or undefined.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns either the accepted text, a replacement
* text, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/multiline-text-input#creation}
* for a list of properties this parameter supports.
* @extends {FieldTextInput}
* @constructor
* @alias Blockly.FieldMultilineInput
*/
const FieldMultilineInput = function(opt_value, opt_validator, opt_config) {
FieldMultilineInput.superClass_.constructor.call(
this, opt_value, opt_validator, opt_config);
class FieldMultilineInput extends FieldTextInput {
/**
* The SVG group element that will contain a text element for each text row
* when initialized.
* @type {SVGGElement}
* @param {(string|!Sentinel)=} opt_value The initial content of the
* field. Should cast to a string. Defaults to an empty string if null or
* undefined.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns either the accepted text, a replacement
* text, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/multiline-text-input#creation}
* for a list of properties this parameter supports.
*/
this.textGroup_ = null;
constructor(opt_value, opt_validator, opt_config) {
super(Field.SKIP_SETUP);
/**
* The SVG group element that will contain a text element for each text row
* when initialized.
* @type {SVGGElement}
*/
this.textGroup_ = null;
/**
* Defines the maximum number of lines of field.
* If exceeded, scrolling functionality is enabled.
* @type {number}
* @protected
*/
this.maxLines_ = Infinity;
/**
* Whether Y overflow is currently occurring.
* @type {boolean}
* @protected
*/
this.isOverflowedY_ = false;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) this.configure_(opt_config);
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
}
/**
* Defines the maximum number of lines of field.
* If exceeded, scrolling functionality is enabled.
* @type {number}
* @override
*/
configure_(config) {
super.configure_(config);
config.maxLines && this.setMaxLines(config.maxLines);
}
/**
* Serializes this field's value to XML. Should only be called by Blockly.Xml.
* @param {!Element} fieldElement The element to populate with info about the
* field's state.
* @return {!Element} The element containing info about the field's state.
* @package
*/
toXml(fieldElement) {
// Replace '\n' characters with HTML-escaped equivalent '&#10'. This is
// needed so the plain-text representation of the XML produced by
// `Blockly.Xml.domToText` will appear on a single line (this is a
// limitation of the plain-text format).
fieldElement.textContent = this.getValue().replace(/\n/g, '&#10;');
return fieldElement;
}
/**
* Sets the field's value based on the given XML element. Should only be
* called by Blockly.Xml.
* @param {!Element} fieldElement The element containing info about the
* field's state.
* @package
*/
fromXml(fieldElement) {
this.setValue(fieldElement.textContent.replace(/&#10;/g, '\n'));
}
/**
* Saves this field's value.
* @return {*} The state of this field.
* @package
*/
saveState() {
const legacyState = this.saveLegacyState(FieldMultilineInput);
if (legacyState !== null) {
return legacyState;
}
return this.getValue();
}
/**
* Sets the field's value based on the given state.
* @param {*} state The state of the variable to assign to this variable
* field.
* @override
* @package
*/
loadState(state) {
if (this.loadLegacyState(Field, state)) {
return;
}
this.setValue(state);
}
/**
* Create the block UI for this field.
* @package
*/
initView() {
this.createBorderRect_();
this.textGroup_ = dom.createSvgElement(
Svg.G, {
'class': 'blocklyEditableText',
},
this.fieldGroup_);
}
/**
* 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.
* @protected
* @override
*/
getDisplayText_() {
let textLines = this.getText();
if (!textLines) {
// Prevent the field from disappearing if empty.
return Field.NBSP;
}
const lines = textLines.split('\n');
textLines = '';
const displayLinesNumber =
this.isOverflowedY_ ? this.maxLines_ : lines.length;
for (let i = 0; i < displayLinesNumber; i++) {
let text = lines[i];
if (text.length > this.maxDisplayLength) {
// Truncate displayed string and add an ellipsis ('...').
text = text.substring(0, this.maxDisplayLength - 4) + '...';
} else if (this.isOverflowedY_ && i === displayLinesNumber - 1) {
text = text.substring(0, text.length - 3) + '...';
}
// Replace whitespace with non-breaking spaces so the text doesn't
// collapse.
text = text.replace(/\s/g, Field.NBSP);
textLines += text;
if (i !== displayLinesNumber - 1) {
textLines += '\n';
}
}
if (this.sourceBlock_.RTL) {
// The SVG is LTR, force value to be RTL.
textLines += '\u200F';
}
return textLines;
}
/**
* Called by setValue if the text input is valid. Updates the value of the
* field, and updates the text of the field if it is not currently being
* edited (i.e. handled by the htmlInput_). Is being redefined here to update
* overflow state of the field.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a string.
* @protected
*/
this.maxLines_ = Infinity;
doValueUpdate_(newValue) {
super.doValueUpdate_(newValue);
this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_;
}
/**
* Whether Y overflow is currently occurring.
* @type {boolean}
* Updates the text of the textElement.
* @protected
*/
this.isOverflowedY_ = false;
};
object.inherits(FieldMultilineInput, FieldTextInput);
/**
* @override
*/
FieldMultilineInput.prototype.configure_ = function(config) {
FieldMultilineInput.superClass_.configure_.call(this, config);
config.maxLines && this.setMaxLines(config.maxLines);
};
/**
* Construct a FieldMultilineInput from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and spellcheck).
* @return {!FieldMultilineInput} The new field instance.
* @package
* @nocollapse
*/
FieldMultilineInput.fromJson = function(options) {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldMultilineInput if that class doesn't
// override the static fromJson method.
return new this(text, undefined, options);
};
/**
* Serializes this field's value to XML. Should only be called by Blockly.Xml.
* @param {!Element} fieldElement The element to populate with info about the
* field's state.
* @return {!Element} The element containing info about the field's state.
* @package
*/
FieldMultilineInput.prototype.toXml = function(fieldElement) {
// Replace '\n' characters with HTML-escaped equivalent '&#10'. This is
// needed so the plain-text representation of the XML produced by
// `Blockly.Xml.domToText` will appear on a single line (this is a limitation
// of the plain-text format).
fieldElement.textContent = this.getValue().replace(/\n/g, '&#10;');
return fieldElement;
};
/**
* Sets the field's value based on the given XML element. Should only be
* called by Blockly.Xml.
* @param {!Element} fieldElement The element containing info about the
* field's state.
* @package
*/
FieldMultilineInput.prototype.fromXml = function(fieldElement) {
this.setValue(fieldElement.textContent.replace(/&#10;/g, '\n'));
};
/**
* Saves this field's value.
* @return {*} The state of this field.
* @package
*/
FieldMultilineInput.prototype.saveState = function() {
const legacyState = this.saveLegacyState(FieldMultilineInput);
if (legacyState !== null) {
return legacyState;
}
return this.getValue();
};
/**
* Sets the field's value based on the given state.
* @param {*} state The state of the variable to assign to this variable field.
* @override
* @package
*/
FieldMultilineInput.prototype.loadState = function(state) {
if (this.loadLegacyState(Field, state)) {
return;
}
this.setValue(state);
};
/**
* Create the block UI for this field.
* @package
*/
FieldMultilineInput.prototype.initView = function() {
this.createBorderRect_();
this.textGroup_ = dom.createSvgElement(
Svg.G, {
'class': 'blocklyEditableText',
},
this.fieldGroup_);
};
/**
* 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.
* @protected
* @override
*/
FieldMultilineInput.prototype.getDisplayText_ = function() {
let textLines = this.getText();
if (!textLines) {
// Prevent the field from disappearing if empty.
return Field.NBSP;
}
const lines = textLines.split('\n');
textLines = '';
const displayLinesNumber =
this.isOverflowedY_ ? this.maxLines_ : lines.length;
for (let i = 0; i < displayLinesNumber; i++) {
let text = lines[i];
if (text.length > this.maxDisplayLength) {
// Truncate displayed string and add an ellipsis ('...').
text = text.substring(0, this.maxDisplayLength - 4) + '...';
} else if (this.isOverflowedY_ && i === displayLinesNumber - 1) {
text = text.substring(0, text.length - 3) + '...';
render_() {
// Remove all text group children.
let currentChild;
while ((currentChild = this.textGroup_.firstChild)) {
this.textGroup_.removeChild(currentChild);
}
// Replace whitespace with non-breaking spaces so the text doesn't collapse.
text = text.replace(/\s/g, Field.NBSP);
textLines += text;
if (i !== displayLinesNumber - 1) {
textLines += '\n';
// Add in text elements into the group.
const lines = this.getDisplayText_().split('\n');
let y = 0;
for (let i = 0; i < lines.length; i++) {
const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
const span = dom.createSvgElement(
Svg.TEXT, {
'class': 'blocklyText blocklyMultilineText',
'x': this.getConstants().FIELD_BORDER_RECT_X_PADDING,
'y': y + this.getConstants().FIELD_BORDER_RECT_Y_PADDING,
'dy': this.getConstants().FIELD_TEXT_BASELINE,
},
this.textGroup_);
span.appendChild(document.createTextNode(lines[i]));
y += lineHeight;
}
if (this.isBeingEdited_) {
const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
if (this.isOverflowedY_) {
dom.addClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
} else {
dom.removeClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
}
}
this.updateSize_();
if (this.isBeingEdited_) {
if (this.sourceBlock_.RTL) {
// in RTL, we need to let the browser reflow before resizing
// in order to get the correct bounding box of the borderRect
// avoiding issue #2777.
setTimeout(this.resizeEditor_.bind(this), 0);
} else {
this.resizeEditor_();
}
const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
if (!this.isTextValid_) {
dom.addClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, true);
} else {
dom.removeClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, false);
}
}
}
if (this.sourceBlock_.RTL) {
// The SVG is LTR, force value to be RTL.
textLines += '\u200F';
}
return textLines;
};
/**
* Called by setValue if the text input is valid. Updates the value of the
* field, and updates the text of the field if it is not currently being
* edited (i.e. handled by the htmlInput_). Is being redefined here to update
* overflow state of the field.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a string.
* @protected
*/
FieldMultilineInput.prototype.doValueUpdate_ = function(newValue) {
FieldMultilineInput.superClass_.doValueUpdate_.call(this, newValue);
this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_;
};
/**
* Updates the size of the field based on the text.
* @protected
*/
updateSize_() {
const nodes = this.textGroup_.childNodes;
let totalWidth = 0;
let totalHeight = 0;
for (let i = 0; i < nodes.length; i++) {
const tspan = /** @type {!Element} */ (nodes[i]);
const textWidth = dom.getTextWidth(tspan);
if (textWidth > totalWidth) {
totalWidth = textWidth;
}
totalHeight += this.getConstants().FIELD_TEXT_HEIGHT +
(i > 0 ? this.getConstants().FIELD_BORDER_RECT_Y_PADDING : 0);
}
if (this.isBeingEdited_) {
// The default width is based on the longest line in the display text,
// but when it's being edited, width should be calculated based on the
// absolute longest line, even if it would be truncated after editing.
// Otherwise we would get wrong editor width when there are more
// lines than this.maxLines_.
const actualEditorLines = this.value_.split('\n');
const dummyTextElement = dom.createSvgElement(
Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'});
const fontSize = this.getConstants().FIELD_TEXT_FONTSIZE;
const fontWeight = this.getConstants().FIELD_TEXT_FONTWEIGHT;
const fontFamily = this.getConstants().FIELD_TEXT_FONTFAMILY;
/**
* Updates the text of the textElement.
* @protected
*/
FieldMultilineInput.prototype.render_ = function() {
// Remove all text group children.
let currentChild;
while ((currentChild = this.textGroup_.firstChild)) {
this.textGroup_.removeChild(currentChild);
for (let i = 0; i < actualEditorLines.length; i++) {
if (actualEditorLines[i].length > this.maxDisplayLength) {
actualEditorLines[i] =
actualEditorLines[i].substring(0, this.maxDisplayLength);
}
dummyTextElement.textContent = actualEditorLines[i];
const lineWidth = dom.getFastTextWidth(
dummyTextElement, fontSize, fontWeight, fontFamily);
if (lineWidth > totalWidth) {
totalWidth = lineWidth;
}
}
const scrollbarWidth =
this.htmlInput_.offsetWidth - this.htmlInput_.clientWidth;
totalWidth += scrollbarWidth;
}
if (this.borderRect_) {
totalHeight += this.getConstants().FIELD_BORDER_RECT_Y_PADDING * 2;
totalWidth += this.getConstants().FIELD_BORDER_RECT_X_PADDING * 2;
this.borderRect_.setAttribute('width', totalWidth);
this.borderRect_.setAttribute('height', totalHeight);
}
this.size_.width = totalWidth;
this.size_.height = totalHeight;
this.positionBorderRect_();
}
// Add in text elements into the group.
const lines = this.getDisplayText_().split('\n');
let y = 0;
for (let i = 0; i < lines.length; i++) {
/**
* Show the inline free-text editor on top of the text.
* Overrides the default behaviour to force rerender in order to
* correct block size, based on editor text.
* @param {Event=} _opt_e Optional mouse event that triggered the field to
* open, or undefined if triggered programmatically.
* @param {boolean=} opt_quietInput True if editor should be created without
* focus. Defaults to false.
* @override
*/
showEditor_(_opt_e, opt_quietInput) {
super.showEditor_(_opt_e, opt_quietInput);
this.forceRerender();
}
/**
* Create the text input editor widget.
* @return {!HTMLTextAreaElement} The newly created text input editor.
* @protected
*/
widgetCreate_() {
const div = WidgetDiv.getDiv();
const scale = this.workspace_.getScale();
const htmlInput =
/** @type {HTMLTextAreaElement} */ (document.createElement('textarea'));
htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
htmlInput.setAttribute('spellcheck', this.spellcheck_);
const fontSize = (this.getConstants().FIELD_TEXT_FONTSIZE * scale) + 'pt';
div.style.fontSize = fontSize;
htmlInput.style.fontSize = fontSize;
const borderRadius = (FieldTextInput.BORDERRADIUS * scale) + 'px';
htmlInput.style.borderRadius = borderRadius;
const paddingX = this.getConstants().FIELD_BORDER_RECT_X_PADDING * scale;
const paddingY =
this.getConstants().FIELD_BORDER_RECT_Y_PADDING * scale / 2;
htmlInput.style.padding = paddingY + 'px ' + paddingX + 'px ' + paddingY +
'px ' + paddingX + 'px';
const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
const span = dom.createSvgElement(
Svg.TEXT, {
'class': 'blocklyText blocklyMultilineText',
'x': this.getConstants().FIELD_BORDER_RECT_X_PADDING,
'y': y + this.getConstants().FIELD_BORDER_RECT_Y_PADDING,
'dy': this.getConstants().FIELD_TEXT_BASELINE,
},
this.textGroup_);
span.appendChild(document.createTextNode(lines[i]));
y += lineHeight;
}
htmlInput.style.lineHeight = (lineHeight * scale) + 'px';
if (this.isBeingEdited_) {
const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
if (this.isOverflowedY_) {
dom.addClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
} else {
dom.removeClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
}
}
div.appendChild(htmlInput);
this.updateSize_();
if (this.isBeingEdited_) {
if (this.sourceBlock_.RTL) {
// in RTL, we need to let the browser reflow before resizing
// in order to get the correct bounding box of the borderRect
// avoiding issue #2777.
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
htmlInput.untypedDefaultValue_ = this.value_;
htmlInput.oldValue_ = null;
if (userAgent.GECKO) {
// In FF, ensure the browser reflows before resizing to avoid issue #2777.
setTimeout(this.resizeEditor_.bind(this), 0);
} else {
this.resizeEditor_();
}
const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
if (!this.isTextValid_) {
dom.addClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, true);
} else {
dom.removeClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, false);
this.bindInputEvents_(htmlInput);
return htmlInput;
}
/**
* Sets the maxLines config for this field.
* @param {number} maxLines Defines the maximum number of lines allowed,
* before scrolling functionality is enabled.
*/
setMaxLines(maxLines) {
if (typeof maxLines === 'number' && maxLines > 0 &&
maxLines !== this.maxLines_) {
this.maxLines_ = maxLines;
this.forceRerender();
}
}
};
/**
* Updates the size of the field based on the text.
* @protected
*/
FieldMultilineInput.prototype.updateSize_ = function() {
const nodes = this.textGroup_.childNodes;
let totalWidth = 0;
let totalHeight = 0;
for (let i = 0; i < nodes.length; i++) {
const tspan = /** @type {!Element} */ (nodes[i]);
const textWidth = dom.getTextWidth(tspan);
if (textWidth > totalWidth) {
totalWidth = textWidth;
/**
* Returns the maxLines config of this field.
* @return {number} The maxLines config value.
*/
getMaxLines() {
return this.maxLines_;
}
/**
* Handle key down to the editor. Override the text input definition of this
* so as to not close the editor when enter is typed in.
* @param {!Event} e Keyboard event.
* @protected
*/
onHtmlInputKeyDown_(e) {
if (e.keyCode !== KeyCodes.ENTER) {
super.onHtmlInputKeyDown_(e);
}
totalHeight += this.getConstants().FIELD_TEXT_HEIGHT +
(i > 0 ? this.getConstants().FIELD_BORDER_RECT_Y_PADDING : 0);
}
if (this.isBeingEdited_) {
// The default width is based on the longest line in the display text,
// but when it's being edited, width should be calculated based on the
// absolute longest line, even if it would be truncated after editing.
// Otherwise we would get wrong editor width when there are more
// lines than this.maxLines_.
const actualEditorLines = this.value_.split('\n');
const dummyTextElement = dom.createSvgElement(
Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'});
const fontSize = this.getConstants().FIELD_TEXT_FONTSIZE;
const fontWeight = this.getConstants().FIELD_TEXT_FONTWEIGHT;
const fontFamily = this.getConstants().FIELD_TEXT_FONTFAMILY;
for (let i = 0; i < actualEditorLines.length; i++) {
if (actualEditorLines[i].length > this.maxDisplayLength) {
actualEditorLines[i] =
actualEditorLines[i].substring(0, this.maxDisplayLength);
}
dummyTextElement.textContent = actualEditorLines[i];
const lineWidth = dom.getFastTextWidth(
dummyTextElement, fontSize, fontWeight, fontFamily);
if (lineWidth > totalWidth) {
totalWidth = lineWidth;
}
}
const scrollbarWidth =
this.htmlInput_.offsetWidth - this.htmlInput_.clientWidth;
totalWidth += scrollbarWidth;
}
if (this.borderRect_) {
totalHeight += this.getConstants().FIELD_BORDER_RECT_Y_PADDING * 2;
totalWidth += this.getConstants().FIELD_BORDER_RECT_X_PADDING * 2;
this.borderRect_.setAttribute('width', totalWidth);
this.borderRect_.setAttribute('height', totalHeight);
}
this.size_.width = totalWidth;
this.size_.height = totalHeight;
this.positionBorderRect_();
};
/**
* Show the inline free-text editor on top of the text.
* Overrides the default behaviour to force rerender in order to
* correct block size, based on editor text.
* @param {Event=} _opt_e Optional mouse event that triggered the field to open,
* or undefined if triggered programmatically.
* @param {boolean=} opt_quietInput True if editor should be created without
* focus. Defaults to false.
* @override
*/
FieldMultilineInput.prototype.showEditor_ = function(_opt_e, opt_quietInput) {
FieldMultilineInput.superClass_.showEditor_.call(
this, _opt_e, opt_quietInput);
this.forceRerender();
};
/**
* Create the text input editor widget.
* @return {!HTMLTextAreaElement} The newly created text input editor.
* @protected
*/
FieldMultilineInput.prototype.widgetCreate_ = function() {
const div = WidgetDiv.getDiv();
const scale = this.workspace_.getScale();
const htmlInput =
/** @type {HTMLTextAreaElement} */ (document.createElement('textarea'));
htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
htmlInput.setAttribute('spellcheck', this.spellcheck_);
const fontSize = (this.getConstants().FIELD_TEXT_FONTSIZE * scale) + 'pt';
div.style.fontSize = fontSize;
htmlInput.style.fontSize = fontSize;
const borderRadius = (FieldTextInput.BORDERRADIUS * scale) + 'px';
htmlInput.style.borderRadius = borderRadius;
const paddingX = this.getConstants().FIELD_BORDER_RECT_X_PADDING * scale;
const paddingY = this.getConstants().FIELD_BORDER_RECT_Y_PADDING * scale / 2;
htmlInput.style.padding =
paddingY + 'px ' + paddingX + 'px ' + paddingY + 'px ' + paddingX + 'px';
const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
htmlInput.style.lineHeight = (lineHeight * scale) + 'px';
div.appendChild(htmlInput);
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
htmlInput.untypedDefaultValue_ = this.value_;
htmlInput.oldValue_ = null;
if (userAgent.GECKO) {
// In FF, ensure the browser reflows before resizing to avoid issue #2777.
setTimeout(this.resizeEditor_.bind(this), 0);
} else {
this.resizeEditor_();
}
this.bindInputEvents_(htmlInput);
return htmlInput;
};
/**
* Sets the maxLines config for this field.
* @param {number} maxLines Defines the maximum number of lines allowed,
* before scrolling functionality is enabled.
*/
FieldMultilineInput.prototype.setMaxLines = function(maxLines) {
if (typeof maxLines === 'number' && maxLines > 0 &&
maxLines !== this.maxLines_) {
this.maxLines_ = maxLines;
this.forceRerender();
/**
* Construct a FieldMultilineInput from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and spellcheck).
* @return {!FieldMultilineInput} The new field instance.
* @package
* @nocollapse
* @override
*/
static fromJson(options) {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldMultilineInput if that class doesn't
// override the static fromJson method.
return new this(text, undefined, options);
}
};
/**
* Returns the maxLines config of this field.
* @return {number} The maxLines config value.
*/
FieldMultilineInput.prototype.getMaxLines = function() {
return this.maxLines_;
};
/**
* Handle key down to the editor. Override the text input definition of this
* so as to not close the editor when enter is typed in.
* @param {!Event} e Keyboard event.
* @protected
*/
FieldMultilineInput.prototype.onHtmlInputKeyDown_ = function(e) {
if (e.keyCode !== KeyCodes.ENTER) {
FieldMultilineInput.superClass_.onHtmlInputKeyDown_.call(this, e);
}
};
}
/**
* CSS for multiline field. See css.js for use.
*/
Css.register(`
.blocklyHtmlTextAreaInput {
font-family: monospace;
resize: none;
overflow: hidden;
height: 100%;
text-align: left;
}
.blocklyHtmlTextAreaInput {
font-family: monospace;
resize: none;
overflow: hidden;
height: 100%;
text-align: left;
}
.blocklyHtmlTextAreaInputOverflowedY {
overflow-y: scroll;
}
.blocklyHtmlTextAreaInputOverflowedY {
overflow-y: scroll;
}
`);
fieldRegistry.register('field_multilinetext', FieldMultilineInput);

View File

@@ -17,67 +17,316 @@ goog.module('Blockly.FieldNumber');
const aria = goog.require('Blockly.utils.aria');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const object = goog.require('Blockly.utils.object');
const {Field} = goog.require('Blockly.Field');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
/**
* Class for an editable number field.
* @param {string|number=} opt_value The initial value of the field. Should cast
* to a number. Defaults to 0.
* @param {?(string|number)=} opt_min Minimum value.
* @param {?(string|number)=} opt_max Maximum value.
* @param {?(string|number)=} opt_precision Precision for value.
* @param {?Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a number & returns a validated
* number, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/number#creation}
* for a list of properties this parameter supports.
* @extends {FieldTextInput}
* @constructor
* @alias Blockly.FieldNumber
*/
const FieldNumber = function(
opt_value, opt_min, opt_max, opt_precision, opt_validator, opt_config) {
class FieldNumber extends FieldTextInput {
/**
* The minimum value this number field can contain.
* @type {number}
* @protected
* @param {(string|number|!Sentinel)=} opt_value The initial value of
* the field. Should cast to a number. Defaults to 0.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {?(string|number)=} opt_min Minimum value. Will only be used if
* opt_config is not provided.
* @param {?(string|number)=} opt_max Maximum value. Will only be used if
* opt_config is not provided.
* @param {?(string|number)=} opt_precision Precision for value. Will only be
* used if opt_config is not provided.
* @param {?Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a number & returns a validated
* number, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/number#creation}
* for a list of properties this parameter supports.
*/
this.min_ = -Infinity;
constructor(
opt_value, opt_min, opt_max, opt_precision, opt_validator, opt_config) {
// Pass SENTINEL so that we can define properties before value validation.
super(Field.SKIP_SETUP);
/**
* The minimum value this number field can contain.
* @type {number}
* @protected
*/
this.min_ = -Infinity;
/**
* The maximum value this number field can contain.
* @type {number}
* @protected
*/
this.max_ = Infinity;
/**
* The multiple to which this fields value is rounded.
* @type {number}
* @protected
*/
this.precision_ = 0;
/**
* The number of decimal places to allow, or null to allow any number of
* decimal digits.
* @type {?number}
* @private
*/
this.decimalPlaces_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) {
this.configure_(opt_config);
} else {
this.setConstraints(opt_min, opt_max, opt_precision);
}
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
}
/**
* The maximum value this number field can contain.
* @type {number}
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
this.max_ = Infinity;
configure_(config) {
super.configure_(config);
this.setMinInternal_(config['min']);
this.setMaxInternal_(config['max']);
this.setPrecisionInternal_(config['precision']);
}
/**
* The multiple to which this fields value is rounded.
* @type {number}
* @protected
* Set the maximum, minimum and precision constraints on this field.
* Any of these properties may be undefined or NaN to be disabled.
* Setting precision (usually a power of 10) enforces a minimum step between
* values. That is, the user's value will rounded to the closest multiple of
* precision. The least significant digit place is inferred from the
* precision. Integers values can be enforces by choosing an integer
* precision.
* @param {?(number|string|undefined)} min Minimum value.
* @param {?(number|string|undefined)} max Maximum value.
* @param {?(number|string|undefined)} precision Precision for value.
*/
this.precision_ = 0;
setConstraints(min, max, precision) {
this.setMinInternal_(min);
this.setMaxInternal_(max);
this.setPrecisionInternal_(precision);
this.setValue(this.getValue());
}
/**
* The number of decimal places to allow, or null to allow any number of
* decimal digits.
* @type {?number}
* Sets the minimum value this field can contain. Updates the value to
* reflect.
* @param {?(number|string|undefined)} min Minimum value.
*/
setMin(min) {
this.setMinInternal_(min);
this.setValue(this.getValue());
}
/**
* Sets the minimum value this field can contain. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} min Minimum value.
* @private
*/
this.decimalPlaces_ = null;
FieldNumber.superClass_.constructor.call(
this, opt_value, opt_validator, opt_config);
if (!opt_config) { // Only do one kind of configuration or the other.
this.setConstraints(opt_min, opt_max, opt_precision);
setMinInternal_(min) {
if (min == null) {
this.min_ = -Infinity;
} else {
min = Number(min);
if (!isNaN(min)) {
this.min_ = min;
}
}
}
};
object.inherits(FieldNumber, FieldTextInput);
/**
* Returns the current minimum value this field can contain. Default is
* -Infinity.
* @return {number} The current minimum value this field can contain.
*/
getMin() {
return this.min_;
}
/**
* Sets the maximum value this field can contain. Updates the value to
* reflect.
* @param {?(number|string|undefined)} max Maximum value.
*/
setMax(max) {
this.setMaxInternal_(max);
this.setValue(this.getValue());
}
/**
* Sets the maximum value this field can contain. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} max Maximum value.
* @private
*/
setMaxInternal_(max) {
if (max == null) {
this.max_ = Infinity;
} else {
max = Number(max);
if (!isNaN(max)) {
this.max_ = max;
}
}
}
/**
* Returns the current maximum value this field can contain. Default is
* Infinity.
* @return {number} The current maximum value this field can contain.
*/
getMax() {
return this.max_;
}
/**
* Sets the precision of this field's value, i.e. the number to which the
* value is rounded. Updates the field to reflect.
* @param {?(number|string|undefined)} precision The number to which the
* field's value is rounded.
*/
setPrecision(precision) {
this.setPrecisionInternal_(precision);
this.setValue(this.getValue());
}
/**
* Sets the precision of this field's value. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} precision The number to which the
* field's value is rounded.
* @private
*/
setPrecisionInternal_(precision) {
this.precision_ = Number(precision) || 0;
let precisionString = String(this.precision_);
if (precisionString.indexOf('e') !== -1) {
// String() is fast. But it turns .0000001 into '1e-7'.
// Use the much slower toLocaleString to access all the digits.
precisionString =
this.precision_.toLocaleString('en-US', {maximumFractionDigits: 20});
}
const decimalIndex = precisionString.indexOf('.');
if (decimalIndex === -1) {
// If the precision is 0 (float) allow any number of decimals,
// otherwise allow none.
this.decimalPlaces_ = precision ? 0 : null;
} else {
this.decimalPlaces_ = precisionString.length - decimalIndex - 1;
}
}
/**
* Returns the current precision of this field. The precision being the
* number to which the field's value is rounded. A precision of 0 means that
* the value is not rounded.
* @return {number} The number to which this field's value is rounded.
*/
getPrecision() {
return this.precision_;
}
/**
* Ensure that the input value is a valid number (must fulfill the
* constraints placed on the field).
* @param {*=} opt_newValue The input value.
* @return {?number} A valid number, or null if invalid.
* @protected
* @override
*/
doClassValidation_(opt_newValue) {
if (opt_newValue === null) {
return null;
}
// Clean up text.
let newValue = String(opt_newValue);
// TODO: Handle cases like 'ten', '1.203,14', etc.
// 'O' is sometimes mistaken for '0' by inexperienced users.
newValue = newValue.replace(/O/ig, '0');
// Strip out thousands separators.
newValue = newValue.replace(/,/g, '');
// Ignore case of 'Infinity'.
newValue = newValue.replace(/infinity/i, 'Infinity');
// Clean up number.
let n = Number(newValue || 0);
if (isNaN(n)) {
// Invalid number.
return null;
}
// Get the value in range.
n = Math.min(Math.max(n, this.min_), this.max_);
// Round to nearest multiple of precision.
if (this.precision_ && isFinite(n)) {
n = Math.round(n / this.precision_) * this.precision_;
}
// Clean up floating point errors.
if (this.decimalPlaces_ !== null) {
n = Number(n.toFixed(this.decimalPlaces_));
}
return n;
}
/**
* Create the number input editor widget.
* @return {!HTMLElement} The newly created number input editor.
* @protected
* @override
*/
widgetCreate_() {
const htmlInput = super.widgetCreate_();
// Set the accessibility state
if (this.min_ > -Infinity) {
aria.setState(htmlInput, aria.State.VALUEMIN, this.min_);
}
if (this.max_ < Infinity) {
aria.setState(htmlInput, aria.State.VALUEMAX, this.max_);
}
return htmlInput;
}
/**
* Construct a FieldNumber from a JSON arg object.
* @param {!Object} options A JSON object with options (value, min, max, and
* precision).
* @return {!FieldNumber} The new field instance.
* @package
* @nocollapse
* @override
*/
static fromJson(options) {
// `this` might be a subclass of FieldNumber if that class doesn't override
// the static fromJson method.
return new this(
options['value'], undefined, undefined, undefined, undefined, options);
}
}
/**
* The default value for this field.
@@ -86,236 +335,6 @@ object.inherits(FieldNumber, FieldTextInput);
*/
FieldNumber.prototype.DEFAULT_VALUE = 0;
/**
* Construct a FieldNumber from a JSON arg object.
* @param {!Object} options A JSON object with options (value, min, max, and
* precision).
* @return {!FieldNumber} The new field instance.
* @package
* @nocollapse
*/
FieldNumber.fromJson = function(options) {
// `this` might be a subclass of FieldNumber if that class doesn't override
// the static fromJson method.
return new this(
options['value'], undefined, undefined, undefined, undefined, options);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
FieldNumber.prototype.SERIALIZABLE = true;
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
FieldNumber.prototype.configure_ = function(config) {
FieldNumber.superClass_.configure_.call(this, config);
this.setMinInternal_(config['min']);
this.setMaxInternal_(config['max']);
this.setPrecisionInternal_(config['precision']);
};
/**
* Set the maximum, minimum and precision constraints on this field.
* Any of these properties may be undefined or NaN to be disabled.
* Setting precision (usually a power of 10) enforces a minimum step between
* values. That is, the user's value will rounded to the closest multiple of
* precision. The least significant digit place is inferred from the precision.
* Integers values can be enforces by choosing an integer precision.
* @param {?(number|string|undefined)} min Minimum value.
* @param {?(number|string|undefined)} max Maximum value.
* @param {?(number|string|undefined)} precision Precision for value.
*/
FieldNumber.prototype.setConstraints = function(min, max, precision) {
this.setMinInternal_(min);
this.setMaxInternal_(max);
this.setPrecisionInternal_(precision);
this.setValue(this.getValue());
};
/**
* Sets the minimum value this field can contain. Updates the value to reflect.
* @param {?(number|string|undefined)} min Minimum value.
*/
FieldNumber.prototype.setMin = function(min) {
this.setMinInternal_(min);
this.setValue(this.getValue());
};
/**
* Sets the minimum value this field can contain. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} min Minimum value.
* @private
*/
FieldNumber.prototype.setMinInternal_ = function(min) {
if (min == null) {
this.min_ = -Infinity;
} else {
min = Number(min);
if (!isNaN(min)) {
this.min_ = min;
}
}
};
/**
* Returns the current minimum value this field can contain. Default is
* -Infinity.
* @return {number} The current minimum value this field can contain.
*/
FieldNumber.prototype.getMin = function() {
return this.min_;
};
/**
* Sets the maximum value this field can contain. Updates the value to reflect.
* @param {?(number|string|undefined)} max Maximum value.
*/
FieldNumber.prototype.setMax = function(max) {
this.setMaxInternal_(max);
this.setValue(this.getValue());
};
/**
* Sets the maximum value this field can contain. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} max Maximum value.
* @private
*/
FieldNumber.prototype.setMaxInternal_ = function(max) {
if (max == null) {
this.max_ = Infinity;
} else {
max = Number(max);
if (!isNaN(max)) {
this.max_ = max;
}
}
};
/**
* Returns the current maximum value this field can contain. Default is
* Infinity.
* @return {number} The current maximum value this field can contain.
*/
FieldNumber.prototype.getMax = function() {
return this.max_;
};
/**
* Sets the precision of this field's value, i.e. the number to which the
* value is rounded. Updates the field to reflect.
* @param {?(number|string|undefined)} precision The number to which the
* field's value is rounded.
*/
FieldNumber.prototype.setPrecision = function(precision) {
this.setPrecisionInternal_(precision);
this.setValue(this.getValue());
};
/**
* Sets the precision of this field's value. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} precision The number to which the
* field's value is rounded.
* @private
*/
FieldNumber.prototype.setPrecisionInternal_ = function(precision) {
this.precision_ = Number(precision) || 0;
let precisionString = String(this.precision_);
if (precisionString.indexOf('e') !== -1) {
// String() is fast. But it turns .0000001 into '1e-7'.
// Use the much slower toLocaleString to access all the digits.
precisionString =
this.precision_.toLocaleString('en-US', {maximumFractionDigits: 20});
}
const decimalIndex = precisionString.indexOf('.');
if (decimalIndex === -1) {
// If the precision is 0 (float) allow any number of decimals,
// otherwise allow none.
this.decimalPlaces_ = precision ? 0 : null;
} else {
this.decimalPlaces_ = precisionString.length - decimalIndex - 1;
}
};
/**
* Returns the current precision of this field. The precision being the
* number to which the field's value is rounded. A precision of 0 means that
* the value is not rounded.
* @return {number} The number to which this field's value is rounded.
*/
FieldNumber.prototype.getPrecision = function() {
return this.precision_;
};
/**
* Ensure that the input value is a valid number (must fulfill the
* constraints placed on the field).
* @param {*=} opt_newValue The input value.
* @return {?number} A valid number, or null if invalid.
* @protected
* @override
*/
FieldNumber.prototype.doClassValidation_ = function(opt_newValue) {
if (opt_newValue === null) {
return null;
}
// Clean up text.
let newValue = String(opt_newValue);
// TODO: Handle cases like 'ten', '1.203,14', etc.
// 'O' is sometimes mistaken for '0' by inexperienced users.
newValue = newValue.replace(/O/ig, '0');
// Strip out thousands separators.
newValue = newValue.replace(/,/g, '');
// Ignore case of 'Infinity'.
newValue = newValue.replace(/infinity/i, 'Infinity');
// Clean up number.
let n = Number(newValue || 0);
if (isNaN(n)) {
// Invalid number.
return null;
}
// Get the value in range.
n = Math.min(Math.max(n, this.min_), this.max_);
// Round to nearest multiple of precision.
if (this.precision_ && isFinite(n)) {
n = Math.round(n / this.precision_) * this.precision_;
}
// Clean up floating point errors.
if (this.decimalPlaces_ !== null) {
n = Number(n.toFixed(this.decimalPlaces_));
}
return n;
};
/**
* Create the number input editor widget.
* @return {!HTMLElement} The newly created number input editor.
* @protected
* @override
*/
FieldNumber.prototype.widgetCreate_ = function() {
const htmlInput = FieldNumber.superClass_.widgetCreate_.call(this);
// Set the accessibility state
if (this.min_ > -Infinity) {
aria.setState(htmlInput, aria.State.VALUEMIN, this.min_);
}
if (this.max_ < Infinity) {
aria.setState(htmlInput, aria.State.VALUEMAX, this.max_);
}
return htmlInput;
};
fieldRegistry.register('field_number', FieldNumber);
exports.FieldNumber = FieldNumber;

File diff suppressed because it is too large Load Diff

View File

@@ -19,16 +19,18 @@ const Variables = goog.require('Blockly.Variables');
const Xml = goog.require('Blockly.Xml');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const internalConstants = goog.require('Blockly.internalConstants');
const object = goog.require('Blockly.utils.object');
const parsing = goog.require('Blockly.utils.parsing');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {Field} = goog.require('Blockly.Field');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
/* eslint-disable-next-line no-unused-vars */
const {MenuItem} = goog.requireType('Blockly.MenuItem');
/* eslint-disable-next-line no-unused-vars */
const {Menu} = goog.requireType('Blockly.Menu');
const {Msg} = goog.require('Blockly.Msg');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Size} = goog.require('Blockly.utils.Size');
const {VariableModel} = goog.require('Blockly.VariableModel');
/** @suppress {extraRequire} */
@@ -37,483 +39,519 @@ goog.require('Blockly.Events.BlockChange');
/**
* Class for a variable's dropdown field.
* @param {?string} varName The default name for the variable. If null,
* a unique variable name will be generated.
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a variable ID & returns a
* validated variable ID, or null to abort the change.
* @param {Array<string>=} opt_variableTypes A list of the types of variables
* to include in the dropdown.
* @param {string=} opt_defaultType The type of variable to create if this
* field's value is not explicitly set. Defaults to ''.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/variable#creation}
* for a list of properties this parameter supports.
* @extends {FieldDropdown}
* @constructor
* @alias Blockly.FieldVariable
*/
const FieldVariable = function(
varName, opt_validator, opt_variableTypes, opt_defaultType, opt_config) {
// The FieldDropdown constructor expects the field's initial value to be
// the first entry in the menu generator, which it may or may not be.
// Just do the relevant parts of the constructor.
class FieldVariable extends FieldDropdown {
/**
* @param {?string|!Sentinel} varName The default name for the variable.
* If null, a unique variable name will be generated.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a variable ID & returns a
* validated variable ID, or null to abort the change.
* @param {Array<string>=} opt_variableTypes A list of the types of variables
* to include in the dropdown. Will only be used if opt_config is not
* provided.
* @param {string=} opt_defaultType The type of variable to create if this
* field's value is not explicitly set. Defaults to ''. Will only be used
* if opt_config is not provided.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/variable#creation}
* for a list of properties this parameter supports.
*/
constructor(
varName, opt_validator, opt_variableTypes, opt_defaultType, opt_config) {
super(Field.SKIP_SETUP);
/**
* An array of options for a dropdown list,
* or a function which generates these options.
* @type {(!Array<!Array>|
* !function(this:FieldDropdown): !Array<!Array>)}
* @protected
*/
this.menuGenerator_ = FieldVariable.dropdownCreate;
/**
* The initial variable name passed to this field's constructor, or an
* empty string if a name wasn't provided. Used to create the initial
* variable.
* @type {string}
*/
this.defaultVariableName = typeof varName === 'string' ? varName : '';
/**
* The type of the default variable for this field.
* @type {string}
* @private
*/
this.defaultType_ = '';
/**
* All of the types of variables that will be available in this field's
* dropdown.
* @type {?Array<string>}
*/
this.variableTypes = [];
/**
* The size of the area rendered by the field.
* @type {Size}
* @protected
* @override
*/
this.size_ = new Size(0, 0);
/**
* The variable model associated with this field.
* @type {?VariableModel}
* @private
*/
this.variable_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
if (varName === Field.SKIP_SETUP) return;
if (opt_config) {
this.configure_(opt_config);
} else {
this.setTypes_(opt_variableTypes, opt_defaultType);
}
if (opt_validator) this.setValidator(opt_validator);
}
/**
* An array of options for a dropdown list,
* or a function which generates these options.
* @type {(!Array<!Array>|
* !function(this:FieldDropdown): !Array<!Array>)}
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
*/
this.menuGenerator_ = FieldVariable.dropdownCreate;
configure_(config) {
super.configure_(config);
this.setTypes_(config['variableTypes'], config['defaultType']);
}
/**
* The initial variable name passed to this field's constructor, or an
* empty string if a name wasn't provided. Used to create the initial
* variable.
* @type {string}
* Initialize the model for this field if it has not already been initialized.
* If the value has not been set to a variable by the first render, we make up
* a variable rather than let the value be invalid.
* @package
*/
this.defaultVariableName = typeof varName === 'string' ? varName : '';
initModel() {
if (this.variable_) {
return; // Initialization already happened.
}
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, null, this.defaultVariableName,
this.defaultType_);
// Don't call setValue because we don't want to cause a rerender.
this.doValueUpdate_(variable.getId());
}
/**
* The size of the area rendered by the field.
* @type {Size}
* @protected
* @override
*/
this.size_ = new Size(0, 0);
opt_config && this.configure_(opt_config);
opt_validator && this.setValidator(opt_validator);
if (!opt_config) { // Only do one kind of configuration or the other.
this.setTypes_(opt_variableTypes, opt_defaultType);
}
};
object.inherits(FieldVariable, FieldDropdown);
/**
* Construct a FieldVariable from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (variable,
* variableTypes, and defaultType).
* @return {!FieldVariable} The new field instance.
* @package
* @nocollapse
*/
FieldVariable.fromJson = function(options) {
const varName = parsing.replaceMessageReferences(options['variable']);
// `this` might be a subclass of FieldVariable if that class doesn't override
// the static fromJson method.
return new this(varName, undefined, undefined, undefined, options);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
FieldVariable.prototype.SERIALIZABLE = true;
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
*/
FieldVariable.prototype.configure_ = function(config) {
FieldVariable.superClass_.configure_.call(this, config);
this.setTypes_(config['variableTypes'], config['defaultType']);
};
/**
* Initialize the model for this field if it has not already been initialized.
* If the value has not been set to a variable by the first render, we make up a
* variable rather than let the value be invalid.
* @package
*/
FieldVariable.prototype.initModel = function() {
if (this.variable_) {
return; // Initialization already happened.
}
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, null, this.defaultVariableName,
this.defaultType_);
// Don't call setValue because we don't want to cause a rerender.
this.doValueUpdate_(variable.getId());
};
/**
* @override
*/
FieldVariable.prototype.shouldAddBorderRect_ = function() {
return FieldVariable.superClass_.shouldAddBorderRect_.call(this) &&
(!this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
this.sourceBlock_.type !== 'variables_get');
};
/**
* Initialize this field based on the given XML.
* @param {!Element} fieldElement The element containing information about the
* variable field's state.
*/
FieldVariable.prototype.fromXml = function(fieldElement) {
const id = fieldElement.getAttribute('id');
const variableName = fieldElement.textContent;
// 'variabletype' should be lowercase, but until July 2019 it was sometimes
// recorded as 'variableType'. Thus we need to check for both.
const variableType = fieldElement.getAttribute('variabletype') ||
fieldElement.getAttribute('variableType') || '';
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, id, variableName, variableType);
// This should never happen :)
if (variableType !== null && variableType !== variable.type) {
throw Error(
'Serialized variable type with id \'' + variable.getId() +
'\' had type ' + variable.type + ', and ' +
'does not match variable field that references it: ' +
Xml.domToText(fieldElement) + '.');
shouldAddBorderRect_() {
return super.shouldAddBorderRect_() &&
(!this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
this.sourceBlock_.type !== 'variables_get');
}
this.setValue(variable.getId());
};
/**
* Initialize this field based on the given XML.
* @param {!Element} fieldElement The element containing information about the
* variable field's state.
*/
fromXml(fieldElement) {
const id = fieldElement.getAttribute('id');
const variableName = fieldElement.textContent;
// 'variabletype' should be lowercase, but until July 2019 it was sometimes
// recorded as 'variableType'. Thus we need to check for both.
const variableType = fieldElement.getAttribute('variabletype') ||
fieldElement.getAttribute('variableType') || '';
/**
* Serialize this field to XML.
* @param {!Element} fieldElement The element to populate with info about the
* field's state.
* @return {!Element} The element containing info about the field's state.
*/
FieldVariable.prototype.toXml = function(fieldElement) {
// Make sure the variable is initialized.
this.initModel();
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, id, variableName, variableType);
fieldElement.id = this.variable_.getId();
fieldElement.textContent = this.variable_.name;
if (this.variable_.type) {
fieldElement.setAttribute('variabletype', this.variable_.type);
}
return fieldElement;
};
/**
* Saves this field's value.
* @param {boolean=} doFullSerialization If true, the variable field will
* serialize the full state of the field being referenced (ie ID, name,
* and type) rather than just a reference to it (ie ID).
* @return {*} The state of the variable field.
* @override
* @package
*/
FieldVariable.prototype.saveState = function(doFullSerialization) {
const legacyState = this.saveLegacyState(FieldVariable);
if (legacyState !== null) {
return legacyState;
}
// Make sure the variable is initialized.
this.initModel();
const state = {'id': this.variable_.getId()};
if (doFullSerialization) {
state['name'] = this.variable_.name;
state['type'] = this.variable_.type;
}
return state;
};
/**
* Sets the field's value based on the given state.
* @param {*} state The state of the variable to assign to this variable field.
* @override
* @package
*/
FieldVariable.prototype.loadState = function(state) {
if (this.loadLegacyState(FieldVariable, state)) {
return;
}
// This is necessary so that blocks in the flyout can have custom var names.
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, state['id'] || null, state['name'],
state['type'] || '');
this.setValue(variable.getId());
};
/**
* Attach this field to a block.
* @param {!Block} block The block containing this field.
*/
FieldVariable.prototype.setSourceBlock = function(block) {
if (block.isShadow()) {
throw Error('Variable fields are not allowed to exist on shadow blocks.');
}
FieldVariable.superClass_.setSourceBlock.call(this, block);
};
/**
* Get the variable's ID.
* @return {string} Current variable's ID.
*/
FieldVariable.prototype.getValue = function() {
return this.variable_ ? this.variable_.getId() : null;
};
/**
* Get the text from this field, which is the selected variable's name.
* @return {string} The selected variable's name, or the empty string if no
* variable is selected.
*/
FieldVariable.prototype.getText = function() {
return this.variable_ ? this.variable_.name : '';
};
/**
* Get the variable model for the selected variable.
* Not guaranteed to be in the variable map on the workspace (e.g. if accessed
* after the variable has been deleted).
* @return {?VariableModel} The selected variable, or null if none was
* selected.
* @package
*/
FieldVariable.prototype.getVariable = function() {
return this.variable_;
};
/**
* Gets the validation function for this field, or null if not set.
* Returns null if the variable is not set, because validators should not
* run on the initial setValue call, because the field won't be attached to
* a block and workspace at that point.
* @return {?Function} Validation function, or null.
*/
FieldVariable.prototype.getValidator = function() {
// Validators shouldn't operate on the initial setValue call.
// Normally this is achieved by calling setValidator after setValue, but
// this is not a possibility with variable fields.
if (this.variable_) {
return this.validator_;
}
return null;
};
/**
* Ensure that the ID belongs to a valid variable of an allowed type.
* @param {*=} opt_newValue The ID of the new variable to set.
* @return {?string} The validated ID, or null if invalid.
* @protected
*/
FieldVariable.prototype.doClassValidation_ = function(opt_newValue) {
if (opt_newValue === null) {
return null;
}
const newId = /** @type {string} */ (opt_newValue);
const variable = Variables.getVariable(this.sourceBlock_.workspace, newId);
if (!variable) {
console.warn(
'Variable id doesn\'t point to a real variable! ' +
'ID was ' + newId);
return null;
}
// Type Checks.
const type = variable.type;
if (!this.typeIsAllowed_(type)) {
console.warn('Variable type doesn\'t match this field! Type was ' + type);
return null;
}
return newId;
};
/**
* Update the value of this variable field, as well as its variable and text.
*
* The variable ID should be valid at this point, but if a variable field
* validator returns a bad ID, this could break.
* @param {*} newId The value to be saved.
* @protected
*/
FieldVariable.prototype.doValueUpdate_ = function(newId) {
this.variable_ = Variables.getVariable(
this.sourceBlock_.workspace, /** @type {string} */ (newId));
FieldVariable.superClass_.doValueUpdate_.call(this, newId);
};
/**
* Check whether the given variable type is allowed on this field.
* @param {string} type The type to check.
* @return {boolean} True if the type is in the list of allowed types.
* @private
*/
FieldVariable.prototype.typeIsAllowed_ = function(type) {
const typeList = this.getVariableTypes_();
if (!typeList) {
return true; // If it's null, all types are valid.
}
for (let i = 0; i < typeList.length; i++) {
if (type === typeList[i]) {
return true;
// This should never happen :)
if (variableType !== null && variableType !== variable.type) {
throw Error(
'Serialized variable type with id \'' + variable.getId() +
'\' had type ' + variable.type + ', and ' +
'does not match variable field that references it: ' +
Xml.domToText(fieldElement) + '.');
}
}
return false;
};
/**
* Return a list of variable types to include in the dropdown.
* @return {!Array<string>} Array of variable types.
* @throws {Error} if variableTypes is an empty array.
* @private
*/
FieldVariable.prototype.getVariableTypes_ = function() {
// TODO (#1513): Try to avoid calling this every time the field is edited.
let variableTypes = this.variableTypes;
if (variableTypes === null) {
// If variableTypes is null, return all variable types.
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
return this.sourceBlock_.workspace.getVariableTypes();
this.setValue(variable.getId());
}
/**
* Serialize this field to XML.
* @param {!Element} fieldElement The element to populate with info about the
* field's state.
* @return {!Element} The element containing info about the field's state.
*/
toXml(fieldElement) {
// Make sure the variable is initialized.
this.initModel();
fieldElement.id = this.variable_.getId();
fieldElement.textContent = this.variable_.name;
if (this.variable_.type) {
fieldElement.setAttribute('variabletype', this.variable_.type);
}
return fieldElement;
}
variableTypes = variableTypes || [''];
if (variableTypes.length === 0) {
// Throw an error if variableTypes is an empty list.
const name = this.getText();
throw Error(
'\'variableTypes\' of field variable ' + name + ' was an empty list');
}
return variableTypes;
};
/**
* Parse the optional arguments representing the allowed variable types and the
* default variable type.
* @param {Array<string>=} opt_variableTypes A list of the types of variables
* to include in the dropdown. If null or undefined, variables of all types
* will be displayed in the dropdown.
* @param {string=} opt_defaultType The type of the variable to create if this
* field's value is not explicitly set. Defaults to ''.
* @private
*/
FieldVariable.prototype.setTypes_ = function(
opt_variableTypes, opt_defaultType) {
// If you expected that the default type would be the same as the only entry
// in the variable types array, tell the Blockly team by commenting on #1499.
const defaultType = opt_defaultType || '';
let variableTypes;
// Set the allowable variable types. Null means all types on the workspace.
if (opt_variableTypes === null || opt_variableTypes === undefined) {
variableTypes = null;
} else if (Array.isArray(opt_variableTypes)) {
variableTypes = opt_variableTypes;
// Make sure the default type is valid.
let isInArray = false;
for (let i = 0; i < variableTypes.length; i++) {
if (variableTypes[i] === defaultType) {
isInArray = true;
/**
* Saves this field's value.
* @param {boolean=} doFullSerialization If true, the variable field will
* serialize the full state of the field being referenced (ie ID, name,
* and type) rather than just a reference to it (ie ID).
* @return {*} The state of the variable field.
* @override
* @package
*/
saveState(doFullSerialization) {
const legacyState = this.saveLegacyState(FieldVariable);
if (legacyState !== null) {
return legacyState;
}
// Make sure the variable is initialized.
this.initModel();
const state = {'id': this.variable_.getId()};
if (doFullSerialization) {
state['name'] = this.variable_.name;
state['type'] = this.variable_.type;
}
return state;
}
/**
* Sets the field's value based on the given state.
* @param {*} state The state of the variable to assign to this variable
* field.
* @override
* @package
*/
loadState(state) {
if (this.loadLegacyState(FieldVariable, state)) {
return;
}
// This is necessary so that blocks in the flyout can have custom var names.
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, state['id'] || null, state['name'],
state['type'] || '');
this.setValue(variable.getId());
}
/**
* Attach this field to a block.
* @param {!Block} block The block containing this field.
*/
setSourceBlock(block) {
if (block.isShadow()) {
throw Error('Variable fields are not allowed to exist on shadow blocks.');
}
super.setSourceBlock(block);
}
/**
* Get the variable's ID.
* @return {?string} Current variable's ID.
*/
getValue() {
return this.variable_ ? this.variable_.getId() : null;
}
/**
* Get the text from this field, which is the selected variable's name.
* @return {string} The selected variable's name, or the empty string if no
* variable is selected.
*/
getText() {
return this.variable_ ? this.variable_.name : '';
}
/**
* Get the variable model for the selected variable.
* Not guaranteed to be in the variable map on the workspace (e.g. if accessed
* after the variable has been deleted).
* @return {?VariableModel} The selected variable, or null if none was
* selected.
* @package
*/
getVariable() {
return this.variable_;
}
/**
* Gets the validation function for this field, or null if not set.
* Returns null if the variable is not set, because validators should not
* run on the initial setValue call, because the field won't be attached to
* a block and workspace at that point.
* @return {?Function} Validation function, or null.
*/
getValidator() {
// Validators shouldn't operate on the initial setValue call.
// Normally this is achieved by calling setValidator after setValue, but
// this is not a possibility with variable fields.
if (this.variable_) {
return this.validator_;
}
return null;
}
/**
* Ensure that the ID belongs to a valid variable of an allowed type.
* @param {*=} opt_newValue The ID of the new variable to set.
* @return {?string} The validated ID, or null if invalid.
* @protected
*/
doClassValidation_(opt_newValue) {
if (opt_newValue === null) {
return null;
}
const newId = /** @type {string} */ (opt_newValue);
const variable = Variables.getVariable(this.sourceBlock_.workspace, newId);
if (!variable) {
console.warn(
'Variable id doesn\'t point to a real variable! ' +
'ID was ' + newId);
return null;
}
// Type Checks.
const type = variable.type;
if (!this.typeIsAllowed_(type)) {
console.warn(
'Variable type doesn\'t match this field! Type was ' + type);
return null;
}
return newId;
}
/**
* Update the value of this variable field, as well as its variable and text.
*
* The variable ID should be valid at this point, but if a variable field
* validator returns a bad ID, this could break.
* @param {*} newId The value to be saved.
* @protected
*/
doValueUpdate_(newId) {
this.variable_ = Variables.getVariable(
this.sourceBlock_.workspace, /** @type {string} */ (newId));
super.doValueUpdate_(newId);
}
/**
* Check whether the given variable type is allowed on this field.
* @param {string} type The type to check.
* @return {boolean} True if the type is in the list of allowed types.
* @private
*/
typeIsAllowed_(type) {
const typeList = this.getVariableTypes_();
if (!typeList) {
return true; // If it's null, all types are valid.
}
for (let i = 0; i < typeList.length; i++) {
if (type === typeList[i]) {
return true;
}
}
if (!isInArray) {
return false;
}
/**
* Return a list of variable types to include in the dropdown.
* @return {!Array<string>} Array of variable types.
* @throws {Error} if variableTypes is an empty array.
* @private
*/
getVariableTypes_() {
// TODO (#1513): Try to avoid calling this every time the field is edited.
let variableTypes = this.variableTypes;
if (variableTypes === null) {
// If variableTypes is null, return all variable types.
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
return this.sourceBlock_.workspace.getVariableTypes();
}
}
variableTypes = variableTypes || [''];
if (variableTypes.length === 0) {
// Throw an error if variableTypes is an empty list.
const name = this.getText();
throw Error(
'Invalid default type \'' + defaultType + '\' in ' +
'the definition of a FieldVariable');
'\'variableTypes\' of field variable ' + name + ' was an empty list');
}
} else {
throw Error(
'\'variableTypes\' was not an array in the definition of ' +
'a FieldVariable');
return variableTypes;
}
// Only update the field once all checks pass.
this.defaultType_ = defaultType;
this.variableTypes = variableTypes;
};
/**
* Refreshes the name of the variable by grabbing the name of the model.
* Used when a variable gets renamed, but the ID stays the same. Should only
* be called by the block.
* @package
*/
FieldVariable.prototype.refreshVariableName = function() {
this.forceRerender();
};
/**
* Return a sorted list of variable names for variable dropdown menus.
* Include a special option at the end for creating a new variable name.
* @return {!Array<!Array>} Array of variable names/id tuples.
* @this {FieldVariable}
*/
FieldVariable.dropdownCreate = function() {
if (!this.variable_) {
throw Error(
'Tried to call dropdownCreate on a variable field with no' +
' variable selected.');
}
const name = this.getText();
let variableModelList = [];
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
const variableTypes = this.getVariableTypes_();
// Get a copy of the list, so that adding rename and new variable options
// doesn't modify the workspace's list.
for (let i = 0; i < variableTypes.length; i++) {
const variableType = variableTypes[i];
const variables =
this.sourceBlock_.workspace.getVariablesOfType(variableType);
variableModelList = variableModelList.concat(variables);
/**
* Parse the optional arguments representing the allowed variable types and
* the default variable type.
* @param {Array<string>=} opt_variableTypes A list of the types of variables
* to include in the dropdown. If null or undefined, variables of all
* types will be displayed in the dropdown.
* @param {string=} opt_defaultType The type of the variable to create if this
* field's value is not explicitly set. Defaults to ''.
* @private
*/
setTypes_(opt_variableTypes, opt_defaultType) {
// If you expected that the default type would be the same as the only entry
// in the variable types array, tell the Blockly team by commenting on
// #1499.
const defaultType = opt_defaultType || '';
let variableTypes;
// Set the allowable variable types. Null means all types on the workspace.
if (opt_variableTypes === null || opt_variableTypes === undefined) {
variableTypes = null;
} else if (Array.isArray(opt_variableTypes)) {
variableTypes = opt_variableTypes;
// Make sure the default type is valid.
let isInArray = false;
for (let i = 0; i < variableTypes.length; i++) {
if (variableTypes[i] === defaultType) {
isInArray = true;
}
}
if (!isInArray) {
throw Error(
'Invalid default type \'' + defaultType + '\' in ' +
'the definition of a FieldVariable');
}
} else {
throw Error(
'\'variableTypes\' was not an array in the definition of ' +
'a FieldVariable');
}
}
variableModelList.sort(VariableModel.compareByName);
const options = [];
for (let i = 0; i < variableModelList.length; i++) {
// Set the UUID as the internal representation of the variable.
options[i] = [variableModelList[i].name, variableModelList[i].getId()];
}
options.push([Msg['RENAME_VARIABLE'], internalConstants.RENAME_VARIABLE_ID]);
if (Msg['DELETE_VARIABLE']) {
options.push([
Msg['DELETE_VARIABLE'].replace('%1', name),
internalConstants.DELETE_VARIABLE_ID,
]);
// Only update the field once all checks pass.
this.defaultType_ = defaultType;
this.variableTypes = variableTypes;
}
return options;
};
/**
* Refreshes the name of the variable by grabbing the name of the model.
* Used when a variable gets renamed, but the ID stays the same. Should only
* be called by the block.
* @override
* @package
*/
refreshVariableName() {
this.forceRerender();
}
/**
* 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 {!Menu} menu The Menu component clicked.
* @param {!MenuItem} menuItem The MenuItem selected within menu.
* @protected
*/
FieldVariable.prototype.onItemSelected_ = function(menu, menuItem) {
const id = menuItem.getValue();
// Handle special cases.
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
if (id === internalConstants.RENAME_VARIABLE_ID) {
// Rename variable.
Variables.renameVariable(this.sourceBlock_.workspace, this.variable_);
return;
} else if (id === internalConstants.DELETE_VARIABLE_ID) {
// Delete variable.
this.sourceBlock_.workspace.deleteVariableById(this.variable_.getId());
return;
/**
* 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 {!Menu} menu The Menu component clicked.
* @param {!MenuItem} menuItem The MenuItem selected within menu.
* @protected
*/
onItemSelected_(menu, menuItem) {
const id = menuItem.getValue();
// Handle special cases.
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
if (id === internalConstants.RENAME_VARIABLE_ID) {
// Rename variable.
Variables.renameVariable(
this.sourceBlock_.workspace,
/** @type {!VariableModel} */ (this.variable_));
return;
} else if (id === internalConstants.DELETE_VARIABLE_ID) {
// Delete variable.
this.sourceBlock_.workspace.deleteVariableById(this.variable_.getId());
return;
}
}
// Handle unspecial case.
this.setValue(id);
}
// Handle unspecial case.
this.setValue(id);
};
/**
* Overrides referencesVariables(), indicating this field refers to a variable.
* @return {boolean} True.
* @package
* @override
*/
FieldVariable.prototype.referencesVariables = function() {
return true;
};
/**
* Overrides referencesVariables(), indicating this field refers to a
* variable.
* @return {boolean} True.
* @package
* @override
*/
referencesVariables() {
return true;
}
/**
* Construct a FieldVariable from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (variable,
* variableTypes, and defaultType).
* @return {!FieldVariable} The new field instance.
* @package
* @nocollapse
* @override
*/
static fromJson(options) {
const varName = parsing.replaceMessageReferences(options['variable']);
// `this` might be a subclass of FieldVariable if that class doesn't
// override the static fromJson method.
return new this(varName, undefined, undefined, undefined, options);
}
/**
* Return a sorted list of variable names for variable dropdown menus.
* Include a special option at the end for creating a new variable name.
* @return {!Array<!Array>} Array of variable names/id tuples.
* @this {FieldVariable}
*/
static dropdownCreate() {
if (!this.variable_) {
throw Error(
'Tried to call dropdownCreate on a variable field with no' +
' variable selected.');
}
const name = this.getText();
let variableModelList = [];
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
const variableTypes = this.getVariableTypes_();
// Get a copy of the list, so that adding rename and new variable options
// doesn't modify the workspace's list.
for (let i = 0; i < variableTypes.length; i++) {
const variableType = variableTypes[i];
const variables =
this.sourceBlock_.workspace.getVariablesOfType(variableType);
variableModelList = variableModelList.concat(variables);
}
}
variableModelList.sort(VariableModel.compareByName);
const options = [];
for (let i = 0; i < variableModelList.length; i++) {
// Set the UUID as the internal representation of the variable.
options[i] = [variableModelList[i].name, variableModelList[i].getId()];
}
options.push(
[Msg['RENAME_VARIABLE'], internalConstants.RENAME_VARIABLE_ID]);
if (Msg['DELETE_VARIABLE']) {
options.push([
Msg['DELETE_VARIABLE'].replace('%1', name),
internalConstants.DELETE_VARIABLE_ID,
]);
}
return options;
}
}
fieldRegistry.register('field_variable', FieldVariable);

File diff suppressed because it is too large Load Diff

View File

@@ -29,311 +29,328 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/**
* Class for a button in the flyout.
* @param {!WorkspaceSvg} workspace The workspace in which to place this
* button.
* @param {!WorkspaceSvg} targetWorkspace The flyout's target workspace.
* @param {!toolbox.ButtonOrLabelInfo} json
* The JSON specifying the label/button.
* @param {boolean} isLabel Whether this button should be styled as a label.
* @constructor
* @package
* Class for a button or label in the flyout.
* @alias Blockly.FlyoutButton
*/
const FlyoutButton = function(workspace, targetWorkspace, json, isLabel) {
// Labels behave the same as buttons, but are styled differently.
class FlyoutButton {
/**
* @param {!WorkspaceSvg} workspace The workspace in which to place this
* button.
* @param {!WorkspaceSvg} targetWorkspace The flyout's target workspace.
* @param {!toolbox.ButtonOrLabelInfo} json
* The JSON specifying the label/button.
* @param {boolean} isLabel Whether this button should be styled as a label.
* @package
*/
constructor(workspace, targetWorkspace, json, isLabel) {
/**
* @type {!WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* @type {!WorkspaceSvg}
* @private
*/
this.targetWorkspace_ = targetWorkspace;
/**
* @type {string}
* @private
*/
this.text_ = json['text'];
/**
* @type {!Coordinate}
* @private
*/
this.position_ = new Coordinate(0, 0);
/**
* Whether this button should be styled as a label.
* Labels behave the same as buttons, but are styled differently.
* @type {boolean}
* @private
*/
this.isLabel_ = isLabel;
/**
* The key to the function called when this button is clicked.
* @type {string}
* @private
*/
this.callbackKey_ = json['callbackKey'] ||
/* Check the lower case version too to satisfy IE */
json['callbackkey'];
/**
* If specified, a CSS class to add to this button.
* @type {?string}
* @private
*/
this.cssClass_ = json['web-class'] || null;
/**
* Mouse up event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseUpWrapper_ = null;
/**
* The JSON specifying the label / button.
* @type {!toolbox.ButtonOrLabelInfo}
*/
this.info = json;
/**
* The width of the button's rect.
* @type {number}
*/
this.width = 0;
/**
* The height of the button's rect.
* @type {number}
*/
this.height = 0;
/**
* The root SVG group for the button or label.
* @type {?SVGGElement}
* @private
*/
this.svgGroup_ = null;
/**
* The SVG element with the text of the label or button.
* @type {?SVGTextElement}
* @private
*/
this.svgText_ = null;
}
/**
* @type {!WorkspaceSvg}
* Create the button elements.
* @return {!SVGElement} The button's SVG group.
*/
createDom() {
let cssClass = this.isLabel_ ? 'blocklyFlyoutLabel' : 'blocklyFlyoutButton';
if (this.cssClass_) {
cssClass += ' ' + this.cssClass_;
}
this.svgGroup_ = dom.createSvgElement(
Svg.G, {'class': cssClass}, this.workspace_.getCanvas());
let shadow;
if (!this.isLabel_) {
// Shadow rectangle (light source does not mirror in RTL).
shadow = dom.createSvgElement(
Svg.RECT, {
'class': 'blocklyFlyoutButtonShadow',
'rx': 4,
'ry': 4,
'x': 1,
'y': 1,
},
this.svgGroup_);
}
// Background rectangle.
const rect = dom.createSvgElement(
Svg.RECT, {
'class': this.isLabel_ ? 'blocklyFlyoutLabelBackground' :
'blocklyFlyoutButtonBackground',
'rx': 4,
'ry': 4,
},
this.svgGroup_);
const svgText = dom.createSvgElement(
Svg.TEXT, {
'class': this.isLabel_ ? 'blocklyFlyoutLabelText' : 'blocklyText',
'x': 0,
'y': 0,
'text-anchor': 'middle',
},
this.svgGroup_);
let text = parsing.replaceMessageReferences(this.text_);
if (this.workspace_.RTL) {
// Force text to be RTL by adding an RLM.
text += '\u200F';
}
svgText.textContent = text;
if (this.isLabel_) {
this.svgText_ = svgText;
this.workspace_.getThemeManager().subscribe(
this.svgText_, 'flyoutForegroundColour', 'fill');
}
const fontSize = style.getComputedStyle(svgText, 'fontSize');
const fontWeight = style.getComputedStyle(svgText, 'fontWeight');
const fontFamily = style.getComputedStyle(svgText, 'fontFamily');
this.width = dom.getFastTextWidthWithSizeString(
svgText, fontSize, fontWeight, fontFamily);
const fontMetrics =
dom.measureFontMetrics(text, fontSize, fontWeight, fontFamily);
this.height = fontMetrics.height;
if (!this.isLabel_) {
this.width += 2 * FlyoutButton.TEXT_MARGIN_X;
this.height += 2 * FlyoutButton.TEXT_MARGIN_Y;
shadow.setAttribute('width', this.width);
shadow.setAttribute('height', this.height);
}
rect.setAttribute('width', this.width);
rect.setAttribute('height', this.height);
svgText.setAttribute('x', this.width / 2);
svgText.setAttribute(
'y', this.height / 2 - fontMetrics.height / 2 + fontMetrics.baseline);
this.updateTransform_();
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
this.svgGroup_, 'mouseup', this, this.onMouseUp_);
return this.svgGroup_;
}
/**
* Correctly position the flyout button and make it visible.
*/
show() {
this.updateTransform_();
this.svgGroup_.setAttribute('display', 'block');
}
/**
* Update SVG attributes to match internal state.
* @private
*/
this.workspace_ = workspace;
updateTransform_() {
this.svgGroup_.setAttribute(
'transform',
'translate(' + this.position_.x + ',' + this.position_.y + ')');
}
/**
* @type {!WorkspaceSvg}
* Move the button to the given x, y coordinates.
* @param {number} x The new x coordinate.
* @param {number} y The new y coordinate.
*/
moveTo(x, y) {
this.position_.x = x;
this.position_.y = y;
this.updateTransform_();
}
/**
* @return {boolean} Whether or not the button is a label.
*/
isLabel() {
return this.isLabel_;
}
/**
* Location of the button.
* @return {!Coordinate} x, y coordinates.
* @package
*/
getPosition() {
return this.position_;
}
/**
* @return {string} Text of the button.
*/
getButtonText() {
return this.text_;
}
/**
* Get the button's target workspace.
* @return {!WorkspaceSvg} The target workspace of the flyout where this
* button resides.
*/
getTargetWorkspace() {
return this.targetWorkspace_;
}
/**
* Dispose of this button.
*/
dispose() {
if (this.onMouseUpWrapper_) {
browserEvents.unbind(this.onMouseUpWrapper_);
}
if (this.svgGroup_) {
dom.removeNode(this.svgGroup_);
}
if (this.svgText_) {
this.workspace_.getThemeManager().unsubscribe(this.svgText_);
}
}
/**
* Do something when the button is clicked.
* @param {!Event} e Mouse up event.
* @private
*/
this.targetWorkspace_ = targetWorkspace;
onMouseUp_(e) {
const gesture = this.targetWorkspace_.getGesture(e);
if (gesture) {
gesture.cancel();
}
/**
* @type {string}
* @private
*/
this.text_ = json['text'];
/**
* @type {!Coordinate}
* @private
*/
this.position_ = new Coordinate(0, 0);
/**
* Whether this button should be styled as a label.
* @type {boolean}
* @private
*/
this.isLabel_ = isLabel;
/**
* The key to the function called when this button is clicked.
* @type {string}
* @private
*/
this.callbackKey_ = json['callbackKey'] ||
/* Check the lower case version too to satisfy IE */
json['callbackkey'];
/**
* If specified, a CSS class to add to this button.
* @type {?string}
* @private
*/
this.cssClass_ = json['web-class'] || null;
/**
* Mouse up event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseUpWrapper_ = null;
/**
* The JSON specifying the label / button.
* @type {!toolbox.ButtonOrLabelInfo}
*/
this.info = json;
};
if (this.isLabel_ && this.callbackKey_) {
console.warn(
'Labels should not have callbacks. Label text: ' + this.text_);
} else if (
!this.isLabel_ &&
!(this.callbackKey_ &&
this.targetWorkspace_.getButtonCallback(this.callbackKey_))) {
console.warn('Buttons should have callbacks. Button text: ' + this.text_);
} else if (!this.isLabel_) {
this.targetWorkspace_.getButtonCallback(this.callbackKey_)(this);
}
}
}
/**
* The horizontal margin around the text in the button.
*/
FlyoutButton.MARGIN_X = 5;
FlyoutButton.TEXT_MARGIN_X = 5;
/**
* The vertical margin around the text in the button.
*/
FlyoutButton.MARGIN_Y = 2;
/**
* The width of the button's rect.
* @type {number}
*/
FlyoutButton.prototype.width = 0;
/**
* The height of the button's rect.
* @type {number}
*/
FlyoutButton.prototype.height = 0;
/**
* Create the button elements.
* @return {!SVGElement} The button's SVG group.
*/
FlyoutButton.prototype.createDom = function() {
let cssClass = this.isLabel_ ? 'blocklyFlyoutLabel' : 'blocklyFlyoutButton';
if (this.cssClass_) {
cssClass += ' ' + this.cssClass_;
}
this.svgGroup_ = dom.createSvgElement(
Svg.G, {'class': cssClass}, this.workspace_.getCanvas());
let shadow;
if (!this.isLabel_) {
// Shadow rectangle (light source does not mirror in RTL).
shadow = dom.createSvgElement(
Svg.RECT, {
'class': 'blocklyFlyoutButtonShadow',
'rx': 4,
'ry': 4,
'x': 1,
'y': 1,
},
this.svgGroup_);
}
// Background rectangle.
const rect = dom.createSvgElement(
Svg.RECT, {
'class': this.isLabel_ ? 'blocklyFlyoutLabelBackground' :
'blocklyFlyoutButtonBackground',
'rx': 4,
'ry': 4,
},
this.svgGroup_);
const svgText = dom.createSvgElement(
Svg.TEXT, {
'class': this.isLabel_ ? 'blocklyFlyoutLabelText' : 'blocklyText',
'x': 0,
'y': 0,
'text-anchor': 'middle',
},
this.svgGroup_);
let text = parsing.replaceMessageReferences(this.text_);
if (this.workspace_.RTL) {
// Force text to be RTL by adding an RLM.
text += '\u200F';
}
svgText.textContent = text;
if (this.isLabel_) {
this.svgText_ = svgText;
this.workspace_.getThemeManager().subscribe(
this.svgText_, 'flyoutForegroundColour', 'fill');
}
const fontSize = style.getComputedStyle(svgText, 'fontSize');
const fontWeight = style.getComputedStyle(svgText, 'fontWeight');
const fontFamily = style.getComputedStyle(svgText, 'fontFamily');
this.width = dom.getFastTextWidthWithSizeString(
svgText, fontSize, fontWeight, fontFamily);
const fontMetrics =
dom.measureFontMetrics(text, fontSize, fontWeight, fontFamily);
this.height = fontMetrics.height;
if (!this.isLabel_) {
this.width += 2 * FlyoutButton.MARGIN_X;
this.height += 2 * FlyoutButton.MARGIN_Y;
shadow.setAttribute('width', this.width);
shadow.setAttribute('height', this.height);
}
rect.setAttribute('width', this.width);
rect.setAttribute('height', this.height);
svgText.setAttribute('x', this.width / 2);
svgText.setAttribute(
'y', this.height / 2 - fontMetrics.height / 2 + fontMetrics.baseline);
this.updateTransform_();
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
this.svgGroup_, 'mouseup', this, this.onMouseUp_);
return this.svgGroup_;
};
/**
* Correctly position the flyout button and make it visible.
*/
FlyoutButton.prototype.show = function() {
this.updateTransform_();
this.svgGroup_.setAttribute('display', 'block');
};
/**
* Update SVG attributes to match internal state.
* @private
*/
FlyoutButton.prototype.updateTransform_ = function() {
this.svgGroup_.setAttribute(
'transform',
'translate(' + this.position_.x + ',' + this.position_.y + ')');
};
/**
* Move the button to the given x, y coordinates.
* @param {number} x The new x coordinate.
* @param {number} y The new y coordinate.
*/
FlyoutButton.prototype.moveTo = function(x, y) {
this.position_.x = x;
this.position_.y = y;
this.updateTransform_();
};
/**
* @return {boolean} Whether or not the button is a label.
*/
FlyoutButton.prototype.isLabel = function() {
return this.isLabel_;
};
/**
* Location of the button.
* @return {!Coordinate} x, y coordinates.
* @package
*/
FlyoutButton.prototype.getPosition = function() {
return this.position_;
};
/**
* @return {string} Text of the button.
*/
FlyoutButton.prototype.getButtonText = function() {
return this.text_;
};
/**
* Get the button's target workspace.
* @return {!WorkspaceSvg} The target workspace of the flyout where this
* button resides.
*/
FlyoutButton.prototype.getTargetWorkspace = function() {
return this.targetWorkspace_;
};
/**
* Dispose of this button.
*/
FlyoutButton.prototype.dispose = function() {
if (this.onMouseUpWrapper_) {
browserEvents.unbind(this.onMouseUpWrapper_);
}
if (this.svgGroup_) {
dom.removeNode(this.svgGroup_);
}
if (this.svgText_) {
this.workspace_.getThemeManager().unsubscribe(this.svgText_);
}
};
/**
* Do something when the button is clicked.
* @param {!Event} e Mouse up event.
* @private
*/
FlyoutButton.prototype.onMouseUp_ = function(e) {
const gesture = this.targetWorkspace_.getGesture(e);
if (gesture) {
gesture.cancel();
}
if (this.isLabel_ && this.callbackKey_) {
console.warn('Labels should not have callbacks. Label text: ' + this.text_);
} else if (
!this.isLabel_ &&
!(this.callbackKey_ &&
this.targetWorkspace_.getButtonCallback(this.callbackKey_))) {
console.warn('Buttons should have callbacks. Button text: ' + this.text_);
} else if (!this.isLabel_) {
this.targetWorkspace_.getButtonCallback(this.callbackKey_)(this);
}
};
FlyoutButton.TEXT_MARGIN_Y = 2;
/**
* CSS for buttons and labels. See css.js for use.
*/
Css.register(`
.blocklyFlyoutButton {
fill: #888;
cursor: default;
}
.blocklyFlyoutButton {
fill: #888;
cursor: default;
}
.blocklyFlyoutButtonShadow {
fill: #666;
}
.blocklyFlyoutButtonShadow {
fill: #666;
}
.blocklyFlyoutButton:hover {
fill: #aaa;
}
.blocklyFlyoutButton:hover {
fill: #aaa;
}
.blocklyFlyoutLabel {
cursor: default;
}
.blocklyFlyoutLabel {
cursor: default;
}
.blocklyFlyoutLabelBackground {
opacity: 0;
}
.blocklyFlyoutLabelBackground {
opacity: 0;
}
`);
exports.FlyoutButton = FlyoutButton;

View File

@@ -17,12 +17,11 @@ goog.module('Blockly.HorizontalFlyout');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const browserEvents = goog.require('Blockly.browserEvents');
const object = goog.require('Blockly.utils.object');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const registry = goog.require('Blockly.registry');
const toolbox = goog.require('Blockly.utils.toolbox');
/* eslint-disable-next-line no-unused-vars */
const {Coordinate} = goog.requireType('Blockly.utils.Coordinate');
const {DropDownDiv} = goog.require('Blockly.DropDownDiv');
const {Flyout} = goog.require('Blockly.Flyout');
/* eslint-disable-next-line no-unused-vars */
const {Options} = goog.requireType('Blockly.Options');
@@ -32,355 +31,358 @@ const {Scrollbar} = goog.require('Blockly.Scrollbar');
/**
* Class for a flyout.
* @param {!Options} workspaceOptions Dictionary of options for the
* workspace.
* @extends {Flyout}
* @constructor
* @alias Blockly.HorizontalFlyout
*/
const HorizontalFlyout = function(workspaceOptions) {
HorizontalFlyout.superClass_.constructor.call(this, workspaceOptions);
this.horizontalLayout = true;
};
object.inherits(HorizontalFlyout, Flyout);
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!{x:number,y:number}} xyRatio Contains a y property which is a float
* between 0 and 1 specifying the degree of scrolling and a
* similar x property.
* @protected
*/
HorizontalFlyout.prototype.setMetrics_ = function(xyRatio) {
if (!this.isVisible()) {
return;
class HorizontalFlyout extends Flyout {
/**
* @param {!Options} workspaceOptions Dictionary of options for the
* workspace.
*/
constructor(workspaceOptions) {
super(workspaceOptions);
this.horizontalLayout = true;
}
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!{x:number,y:number}} xyRatio Contains a y property which is a
* float between 0 and 1 specifying the degree of scrolling and a similar
* x property.
* @protected
*/
setMetrics_(xyRatio) {
if (!this.isVisible()) {
return;
}
if (typeof xyRatio.x === 'number') {
this.workspace_.scrollX =
-(scrollMetrics.left +
(scrollMetrics.width - viewMetrics.width) * xyRatio.x);
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
if (typeof xyRatio.x === 'number') {
this.workspace_.scrollX =
-(scrollMetrics.left +
(scrollMetrics.width - viewMetrics.width) * xyRatio.x);
}
this.workspace_.translate(
this.workspace_.scrollX + absoluteMetrics.left,
this.workspace_.scrollY + absoluteMetrics.top);
}
this.workspace_.translate(
this.workspace_.scrollX + absoluteMetrics.left,
this.workspace_.scrollY + absoluteMetrics.top);
};
/**
* Calculates the x coordinate for the flyout position.
* @return {number} X coordinate.
*/
HorizontalFlyout.prototype.getX = function() {
// X is always 0 since this is a horizontal flyout.
return 0;
};
/**
* Calculates the y coordinate for the flyout position.
* @return {number} Y coordinate.
*/
HorizontalFlyout.prototype.getY = function() {
if (!this.isVisible()) {
/**
* Calculates the x coordinate for the flyout position.
* @return {number} X coordinate.
*/
getX() {
// X is always 0 since this is a horizontal flyout.
return 0;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const toolboxMetrics = metricsManager.getToolboxMetrics();
let y = 0;
const atTop = this.toolboxPosition_ === toolbox.Position.TOP;
// If this flyout is not the trashcan flyout (e.g. toolbox or mutator).
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_) {
// If there is a category toolbox.
if (this.targetWorkspace.getToolbox()) {
if (atTop) {
y = toolboxMetrics.height;
/**
* Calculates the y coordinate for the flyout position.
* @return {number} Y coordinate.
*/
getY() {
if (!this.isVisible()) {
return 0;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const toolboxMetrics = metricsManager.getToolboxMetrics();
let y = 0;
const atTop = this.toolboxPosition_ === toolbox.Position.TOP;
// If this flyout is not the trashcan flyout (e.g. toolbox or mutator).
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_) {
// If there is a category toolbox.
if (this.targetWorkspace.getToolbox()) {
if (atTop) {
y = toolboxMetrics.height;
} else {
y = viewMetrics.height - this.height_;
}
// Simple (flyout-only) toolbox.
} else {
y = viewMetrics.height - this.height_;
if (atTop) {
y = 0;
} else {
// The simple flyout does not cover the workspace.
y = viewMetrics.height;
}
}
// Simple (flyout-only) toolbox.
// Trashcan flyout is opposite the main flyout.
} else {
if (atTop) {
y = 0;
} else {
// The simple flyout does not cover the workspace.
y = viewMetrics.height;
// Because the anchor point of the flyout is on the top, but we want
// to align the bottom edge of the flyout with the bottom edge of the
// blocklyDiv, we calculate the full height of the div minus the height
// of the flyout.
y = viewMetrics.height + absoluteMetrics.top - this.height_;
}
}
// Trashcan flyout is opposite the main flyout.
} else {
return y;
}
/**
* Move the flyout to the edge of the workspace.
*/
position() {
if (!this.isVisible() || !this.targetWorkspace.isVisible()) {
return;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
// Record the width for workspace metrics.
this.width_ = targetWorkspaceViewMetrics.width;
const edgeWidth = targetWorkspaceViewMetrics.width - 2 * this.CORNER_RADIUS;
const edgeHeight = this.height_ - this.CORNER_RADIUS;
this.setBackgroundPath_(edgeWidth, edgeHeight);
const x = this.getX();
const y = this.getY();
this.positionAt_(this.width_, this.height_, x, y);
}
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
setBackgroundPath_(width, height) {
const atTop = this.toolboxPosition_ === toolbox.Position.TOP;
// Start at top left.
const path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)];
if (atTop) {
y = 0;
// Top.
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', -width);
// Left.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('z');
} else {
// Because the anchor point of the flyout is on the top, but we want
// to align the bottom edge of the flyout with the bottom edge of the
// blocklyDiv, we calculate the full height of the div minus the height
// of the flyout.
y = viewMetrics.height + absoluteMetrics.top - this.height_;
// Top.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, -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);
// Bottom.
path.push('h', -width - 2 * this.CORNER_RADIUS);
// Left.
path.push('z');
}
this.svgBackground_.setAttribute('d', path.join(' '));
}
/**
* Scroll the flyout to the top.
*/
scrollToStart() {
this.workspace_.scrollbar.setX(this.RTL ? Infinity : 0);
}
/**
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @protected
*/
wheel_(e) {
const scrollDelta = browserEvents.getScrollDeltaPixels(e);
const delta = scrollDelta.x || scrollDelta.y;
if (delta) {
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const pos = (viewMetrics.left - scrollMetrics.left) + delta;
this.workspace_.scrollbar.setX(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and
// dropDownDiv.
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
}
/**
* Lay out the blocks in the flyout.
* @param {!Array<!Object>} contents The blocks and buttons to lay out.
* @param {!Array<number>} gaps The visible gaps between blocks.
* @protected
*/
layout_(contents, gaps) {
this.workspace_.scale = this.targetWorkspace.scale;
const margin = this.MARGIN;
let cursorX = margin + this.tabWidth_;
const cursorY = margin;
if (this.RTL) {
contents = contents.reverse();
}
for (let i = 0, item; (item = contents[i]); i++) {
if (item.type === 'block') {
const block = item.block;
const allBlocks = block.getDescendants(false);
for (let j = 0, child; (child = allBlocks[j]); j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such
// a block.
child.isInFlyout = true;
}
block.render();
const root = block.getSvgRoot();
const blockHW = block.getHeightWidth();
// Figure out where to place the block.
const tab = block.outputConnection ? this.tabWidth_ : 0;
let moveX;
if (this.RTL) {
moveX = cursorX + blockHW.width;
} else {
moveX = cursorX - tab;
}
block.moveBy(moveX, cursorY);
const rect = this.createRect_(block, moveX, cursorY, blockHW, i);
cursorX += (blockHW.width + gaps[i]);
this.addBlockListeners_(root, block, rect);
} else if (item.type === 'button') {
this.initFlyoutButton_(item.button, cursorX, cursorY);
cursorX += (item.button.width + gaps[i]);
}
}
}
return y;
};
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} True if the drag is toward the workspace.
* @package
*/
isDragTowardWorkspace(currentDragDeltaXY) {
const dx = currentDragDeltaXY.x;
const dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
const dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
/**
* Move the flyout to the edge of the workspace.
*/
HorizontalFlyout.prototype.position = function() {
if (!this.isVisible() || !this.targetWorkspace.isVisible()) {
return;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
// Record the width for workspace metrics.
this.width_ = targetWorkspaceViewMetrics.width;
const edgeWidth = targetWorkspaceViewMetrics.width - 2 * this.CORNER_RADIUS;
const edgeHeight = this.height_ - this.CORNER_RADIUS;
this.setBackgroundPath_(edgeWidth, edgeHeight);
const x = this.getX();
const y = this.getY();
this.positionAt_(this.width_, this.height_, x, y);
};
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
HorizontalFlyout.prototype.setBackgroundPath_ = function(width, height) {
const atTop = this.toolboxPosition_ === toolbox.Position.TOP;
// Start at top left.
const path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)];
if (atTop) {
// Top.
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', -width);
// Left.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('z');
} else {
// Top.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, -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);
// Bottom.
path.push('h', -width - 2 * this.CORNER_RADIUS);
// Left.
path.push('z');
}
this.svgBackground_.setAttribute('d', path.join(' '));
};
/**
* Scroll the flyout to the top.
*/
HorizontalFlyout.prototype.scrollToStart = function() {
this.workspace_.scrollbar.setX(this.RTL ? Infinity : 0);
};
/**
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @protected
*/
HorizontalFlyout.prototype.wheel_ = function(e) {
const scrollDelta = browserEvents.getScrollDeltaPixels(e);
const delta = scrollDelta.x || scrollDelta.y;
if (delta) {
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const pos = (viewMetrics.left - scrollMetrics.left) + delta;
this.workspace_.scrollbar.setX(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and DropDownDiv.
WidgetDiv.hide();
DropDownDiv.hideWithoutAnimation();
const range = this.dragAngleRange_;
// Check for up or down dragging.
if ((dragDirection < 90 + range && dragDirection > 90 - range) ||
(dragDirection > -90 - range && dragDirection < -90 + range)) {
return true;
}
return false;
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
};
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to viewport.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
*/
getClientRect() {
if (!this.svgGroup_ || this.autoClose || !this.isVisible()) {
// The bounding rectangle won't compute correctly if the flyout is closed
// and auto-close flyouts aren't valid drag targets (or delete areas).
return null;
}
/**
* Lay out the blocks in the flyout.
* @param {!Array<!Object>} contents The blocks and buttons to lay out.
* @param {!Array<number>} gaps The visible gaps between blocks.
* @protected
*/
HorizontalFlyout.prototype.layout_ = function(contents, gaps) {
this.workspace_.scale = this.targetWorkspace.scale;
const margin = this.MARGIN;
let cursorX = margin + this.tabWidth_;
const cursorY = margin;
if (this.RTL) {
contents = contents.reverse();
}
const flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown
// flyout area are still deleted. Must be larger than the largest screen
// size, but be smaller than half Number.MAX_SAFE_INTEGER (not available on
// IE).
const BIG_NUM = 1000000000;
const top = flyoutRect.top;
for (let i = 0, item; (item = contents[i]); i++) {
if (item.type === 'block') {
const block = item.block;
const allBlocks = block.getDescendants(false);
for (let j = 0, child; (child = allBlocks[j]); j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such a
// block.
child.isInFlyout = true;
}
block.render();
const root = block.getSvgRoot();
const blockHW = block.getHeightWidth();
// Figure out where to place the block.
const tab = block.outputConnection ? this.tabWidth_ : 0;
let moveX;
if (this.RTL) {
moveX = cursorX + blockHW.width;
} else {
moveX = cursorX - tab;
}
block.moveBy(moveX, cursorY);
const rect = this.createRect_(block, moveX, cursorY, blockHW, i);
cursorX += (blockHW.width + gaps[i]);
this.addBlockListeners_(root, block, rect);
} else if (item.type === 'button') {
this.initFlyoutButton_(item.button, cursorX, cursorY);
cursorX += (item.button.width + gaps[i]);
if (this.toolboxPosition_ === toolbox.Position.TOP) {
const height = flyoutRect.height;
return new Rect(-BIG_NUM, top + height, -BIG_NUM, BIG_NUM);
} else { // Bottom.
return new Rect(top, BIG_NUM, -BIG_NUM, BIG_NUM);
}
}
};
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} True if the drag is toward the workspace.
* @package
*/
HorizontalFlyout.prototype.isDragTowardWorkspace = function(
currentDragDeltaXY) {
const dx = currentDragDeltaXY.x;
const dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
const dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
const range = this.dragAngleRange_;
// Check for up or down dragging.
if ((dragDirection < 90 + range && dragDirection > 90 - range) ||
(dragDirection > -90 - range && dragDirection < -90 + range)) {
return true;
}
return false;
};
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to viewport.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
*/
HorizontalFlyout.prototype.getClientRect = function() {
if (!this.svgGroup_ || this.autoClose || !this.isVisible()) {
// The bounding rectangle won't compute correctly if the flyout is closed
// and auto-close flyouts aren't valid drag targets (or delete areas).
return null;
}
const flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout
// area are still deleted. Must be larger than the largest screen size,
// but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE).
const BIG_NUM = 1000000000;
const top = flyoutRect.top;
if (this.toolboxPosition_ === toolbox.Position.TOP) {
const height = flyoutRect.height;
return new Rect(-BIG_NUM, top + height, -BIG_NUM, BIG_NUM);
} else { // Bottom.
return new Rect(top, BIG_NUM, -BIG_NUM, BIG_NUM);
}
};
/**
* Compute height of flyout. toolbox.Position mat under each block.
* For RTL: Lay out the blocks right-aligned.
* @protected
*/
HorizontalFlyout.prototype.reflowInternal_ = function() {
this.workspace_.scale = this.getFlyoutScale();
let flyoutHeight = 0;
const blocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; (block = blocks[i]); i++) {
flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height);
}
const buttons = this.buttons_;
for (let i = 0, button; (button = buttons[i]); i++) {
flyoutHeight = Math.max(flyoutHeight, button.height);
}
flyoutHeight += this.MARGIN * 1.5;
flyoutHeight *= this.workspace_.scale;
flyoutHeight += Scrollbar.scrollbarThickness;
if (this.height_ !== flyoutHeight) {
/**
* Compute height of flyout. toolbox.Position mat under each block.
* For RTL: Lay out the blocks right-aligned.
* @protected
*/
reflowInternal_() {
this.workspace_.scale = this.getFlyoutScale();
let flyoutHeight = 0;
const blocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; (block = blocks[i]); i++) {
if (block.flyoutRect_) {
this.moveRectToBlock_(block.flyoutRect_, block);
flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height);
}
const buttons = this.buttons_;
for (let i = 0, button; (button = buttons[i]); i++) {
flyoutHeight = Math.max(flyoutHeight, button.height);
}
flyoutHeight += this.MARGIN * 1.5;
flyoutHeight *= this.workspace_.scale;
flyoutHeight += Scrollbar.scrollbarThickness;
if (this.height_ !== flyoutHeight) {
for (let i = 0, block; (block = blocks[i]); i++) {
if (this.rectMap_.has(block)) {
this.moveRectToBlock_(this.rectMap_.get(block), block);
}
}
}
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_ &&
this.toolboxPosition_ === toolbox.Position.TOP &&
!this.targetWorkspace.getToolbox()) {
// This flyout is a simple toolbox. Reposition the workspace so that (0,0)
// is in the correct position relative to the new absolute edge (ie
// toolbox edge).
this.targetWorkspace.translate(
this.targetWorkspace.scrollX,
this.targetWorkspace.scrollY + flyoutHeight);
}
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_ &&
this.toolboxPosition_ === toolbox.Position.TOP &&
!this.targetWorkspace.getToolbox()) {
// This flyout is a simple toolbox. Reposition the workspace so that
// (0,0) is in the correct position relative to the new absolute edge
// (ie toolbox edge).
this.targetWorkspace.translate(
this.targetWorkspace.scrollX,
this.targetWorkspace.scrollY + flyoutHeight);
}
// Record the height for workspace metrics and .position.
this.height_ = flyoutHeight;
this.position();
this.targetWorkspace.recordDragTargets();
// Record the height for workspace metrics and .position.
this.height_ = flyoutHeight;
this.position();
this.targetWorkspace.recordDragTargets();
}
}
};
}
registry.register(
registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX, registry.DEFAULT,

View File

@@ -15,7 +15,6 @@
*/
goog.module('Blockly.FlyoutMetricsManager');
const object = goog.require('Blockly.utils.object');
/* eslint-disable-next-line no-unused-vars */
const {IFlyout} = goog.requireType('Blockly.IFlyout');
const {MetricsManager} = goog.require('Blockly.MetricsManager');
@@ -26,81 +25,82 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/**
* Calculates metrics for a flyout's workspace.
* The metrics are mainly used to size scrollbars for the flyout.
* @param {!WorkspaceSvg} workspace The flyout's workspace.
* @param {!IFlyout} flyout The flyout.
* @extends {MetricsManager}
* @constructor
* @alias Blockly.FlyoutMetricsManager
*/
const FlyoutMetricsManager = function(workspace, flyout) {
class FlyoutMetricsManager extends MetricsManager {
/**
* The flyout that owns the workspace to calculate metrics for.
* @type {!IFlyout}
* @protected
* @param {!WorkspaceSvg} workspace The flyout's workspace.
* @param {!IFlyout} flyout The flyout.
*/
this.flyout_ = flyout;
constructor(workspace, flyout) {
super(workspace);
FlyoutMetricsManager.superClass_.constructor.call(this, workspace);
};
object.inherits(FlyoutMetricsManager, MetricsManager);
/**
* Gets the bounding box of the blocks on the flyout's workspace.
* This is in workspace coordinates.
* @return {!SVGRect|{height: number, y: number, width: number, x: number}} The
* bounding box of the blocks on the workspace.
* @private
*/
FlyoutMetricsManager.prototype.getBoundingBox_ = function() {
let blockBoundingBox;
try {
blockBoundingBox = this.workspace_.getCanvas().getBBox();
} catch (e) {
// Firefox has trouble with hidden elements (Bug 528969).
// 2021 Update: It looks like this was fixed around Firefox 77 released in
// 2020.
blockBoundingBox = {height: 0, y: 0, width: 0, x: 0};
/**
* The flyout that owns the workspace to calculate metrics for.
* @type {!IFlyout}
* @protected
*/
this.flyout_ = flyout;
}
return blockBoundingBox;
};
/**
* @override
*/
FlyoutMetricsManager.prototype.getContentMetrics = function(
opt_getWorkspaceCoordinates) {
// The bounding box is in workspace coordinates.
const blockBoundingBox = this.getBoundingBox_();
const scale = opt_getWorkspaceCoordinates ? 1 : this.workspace_.scale;
/**
* Gets the bounding box of the blocks on the flyout's workspace.
* This is in workspace coordinates.
* @return {!SVGRect|{height: number, y: number, width: number, x: number}}
* The bounding box of the blocks on the workspace.
* @private
*/
getBoundingBox_() {
let blockBoundingBox;
try {
blockBoundingBox = this.workspace_.getCanvas().getBBox();
} catch (e) {
// Firefox has trouble with hidden elements (Bug 528969).
// 2021 Update: It looks like this was fixed around Firefox 77 released in
// 2020.
blockBoundingBox = {height: 0, y: 0, width: 0, x: 0};
}
return blockBoundingBox;
}
return {
height: blockBoundingBox.height * scale,
width: blockBoundingBox.width * scale,
top: blockBoundingBox.y * scale,
left: blockBoundingBox.x * scale,
};
};
/**
* @override
*/
getContentMetrics(opt_getWorkspaceCoordinates) {
// The bounding box is in workspace coordinates.
const blockBoundingBox = this.getBoundingBox_();
const scale = opt_getWorkspaceCoordinates ? 1 : this.workspace_.scale;
/**
* @override
*/
FlyoutMetricsManager.prototype.getScrollMetrics = function(
opt_getWorkspaceCoordinates, opt_viewMetrics, opt_contentMetrics) {
const contentMetrics = opt_contentMetrics || this.getContentMetrics();
const margin = this.flyout_.MARGIN * this.workspace_.scale;
const scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1;
return {
height: blockBoundingBox.height * scale,
width: blockBoundingBox.width * scale,
top: blockBoundingBox.y * scale,
left: blockBoundingBox.x * scale,
};
}
// The left padding isn't just the margin. Some blocks are also offset by
// tabWidth so that value and statement blocks line up.
// The contentMetrics.left value is equivalent to the variable left padding.
const leftPadding = contentMetrics.left;
/**
* @override
*/
getScrollMetrics(
opt_getWorkspaceCoordinates, opt_viewMetrics, opt_contentMetrics) {
const contentMetrics = opt_contentMetrics || this.getContentMetrics();
const margin = this.flyout_.MARGIN * this.workspace_.scale;
const scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1;
return {
height: (contentMetrics.height + 2 * margin) / scale,
width: (contentMetrics.width + leftPadding + margin) / scale,
top: 0,
left: 0,
};
};
// The left padding isn't just the margin. Some blocks are also offset by
// tabWidth so that value and statement blocks line up.
// The contentMetrics.left value is equivalent to the variable left padding.
const leftPadding = contentMetrics.left;
return {
height: (contentMetrics.height + 2 * margin) / scale,
width: (contentMetrics.width + leftPadding + margin) / scale,
top: 0,
left: 0,
};
}
}
exports.FlyoutMetricsManager = FlyoutMetricsManager;

View File

@@ -17,12 +17,11 @@ goog.module('Blockly.VerticalFlyout');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const browserEvents = goog.require('Blockly.browserEvents');
const object = goog.require('Blockly.utils.object');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const registry = goog.require('Blockly.registry');
const toolbox = goog.require('Blockly.utils.toolbox');
/* eslint-disable-next-line no-unused-vars */
const {Coordinate} = goog.requireType('Blockly.utils.Coordinate');
const {DropDownDiv} = goog.require('Blockly.DropDownDiv');
const {Flyout} = goog.require('Blockly.Flyout');
/* eslint-disable-next-line no-unused-vars */
const {Options} = goog.requireType('Blockly.Options');
@@ -36,16 +35,353 @@ goog.require('Blockly.constants');
/**
* Class for a flyout.
* @param {!Options} workspaceOptions Dictionary of options for the
* workspace.
* @extends {Flyout}
* @constructor
* @alias Blockly.VerticalFlyout
*/
const VerticalFlyout = function(workspaceOptions) {
VerticalFlyout.superClass_.constructor.call(this, workspaceOptions);
};
object.inherits(VerticalFlyout, Flyout);
class VerticalFlyout extends Flyout {
/**
* @param {!Options} workspaceOptions Dictionary of options for the
* workspace.
*/
constructor(workspaceOptions) {
super(workspaceOptions);
}
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!{x:number,y:number}} xyRatio Contains a y property which is a
* float between 0 and 1 specifying the degree of scrolling and a similar
* x property.
* @protected
*/
setMetrics_(xyRatio) {
if (!this.isVisible()) {
return;
}
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
if (typeof xyRatio.y === 'number') {
this.workspace_.scrollY =
-(scrollMetrics.top +
(scrollMetrics.height - viewMetrics.height) * xyRatio.y);
}
this.workspace_.translate(
this.workspace_.scrollX + absoluteMetrics.left,
this.workspace_.scrollY + absoluteMetrics.top);
}
/**
* Calculates the x coordinate for the flyout position.
* @return {number} X coordinate.
*/
getX() {
if (!this.isVisible()) {
return 0;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const toolboxMetrics = metricsManager.getToolboxMetrics();
let x = 0;
// If this flyout is not the trashcan flyout (e.g. toolbox or mutator).
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_) {
// If there is a category toolbox.
if (this.targetWorkspace.getToolbox()) {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = toolboxMetrics.width;
} else {
x = viewMetrics.width - this.width_;
}
// Simple (flyout-only) toolbox.
} else {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = 0;
} else {
// The simple flyout does not cover the workspace.
x = viewMetrics.width;
}
}
// Trashcan flyout is opposite the main flyout.
} else {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = 0;
} else {
// Because the anchor point of the flyout is on the left, but we want
// to align the right edge of the flyout with the right edge of the
// blocklyDiv, we calculate the full width of the div minus the width
// of the flyout.
x = viewMetrics.width + absoluteMetrics.left - this.width_;
}
}
return x;
}
/**
* Calculates the y coordinate for the flyout position.
* @return {number} Y coordinate.
*/
getY() {
// Y is always 0 since this is a vertical flyout.
return 0;
}
/**
* Move the flyout to the edge of the workspace.
*/
position() {
if (!this.isVisible() || !this.targetWorkspace.isVisible()) {
return;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
// Record the height for workspace metrics.
this.height_ = targetWorkspaceViewMetrics.height;
const edgeWidth = this.width_ - this.CORNER_RADIUS;
const edgeHeight =
targetWorkspaceViewMetrics.height - 2 * this.CORNER_RADIUS;
this.setBackgroundPath_(edgeWidth, edgeHeight);
const x = this.getX();
const y = this.getY();
this.positionAt_(this.width_, this.height_, x, y);
}
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
setBackgroundPath_(width, height) {
const atRight = this.toolboxPosition_ === toolbox.Position.RIGHT;
const totalWidth = width + this.CORNER_RADIUS;
// Decide whether to start on the left or right.
const path = ['M ' + (atRight ? totalWidth : 0) + ',0'];
// Top.
path.push('h', atRight ? -width : width);
// Rounded corner.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, atRight ? 0 : 1,
atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS, this.CORNER_RADIUS);
// Side closest to workspace.
path.push('v', Math.max(0, height));
// Rounded corner.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, atRight ? 0 : 1,
atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, this.CORNER_RADIUS);
// Bottom.
path.push('h', atRight ? width : -width);
path.push('z');
this.svgBackground_.setAttribute('d', path.join(' '));
}
/**
* Scroll the flyout to the top.
*/
scrollToStart() {
this.workspace_.scrollbar.setY(0);
}
/**
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @protected
*/
wheel_(e) {
const scrollDelta = browserEvents.getScrollDeltaPixels(e);
if (scrollDelta.y) {
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const pos = (viewMetrics.top - scrollMetrics.top) + scrollDelta.y;
this.workspace_.scrollbar.setY(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and
// dropDownDiv.
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
}
/**
* Lay out the blocks in the flyout.
* @param {!Array<!Object>} contents The blocks and buttons to lay out.
* @param {!Array<number>} gaps The visible gaps between blocks.
* @protected
*/
layout_(contents, gaps) {
this.workspace_.scale = this.targetWorkspace.scale;
const margin = this.MARGIN;
const cursorX = this.RTL ? margin : margin + this.tabWidth_;
let cursorY = margin;
for (let i = 0, item; (item = contents[i]); i++) {
if (item.type === 'block') {
const block = item.block;
const allBlocks = block.getDescendants(false);
for (let j = 0, child; (child = allBlocks[j]); j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such
// a block.
child.isInFlyout = true;
}
block.render();
const root = block.getSvgRoot();
const blockHW = block.getHeightWidth();
const moveX =
block.outputConnection ? cursorX - this.tabWidth_ : cursorX;
block.moveBy(moveX, cursorY);
const rect = this.createRect_(
block, this.RTL ? moveX - blockHW.width : moveX, cursorY, blockHW,
i);
this.addBlockListeners_(root, block, rect);
cursorY += blockHW.height + gaps[i];
} else if (item.type === 'button') {
this.initFlyoutButton_(item.button, cursorX, cursorY);
cursorY += item.button.height + gaps[i];
}
}
}
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} True if the drag is toward the workspace.
* @package
*/
isDragTowardWorkspace(currentDragDeltaXY) {
const dx = currentDragDeltaXY.x;
const dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
const dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
const range = this.dragAngleRange_;
// Check for left or right dragging.
if ((dragDirection < range && dragDirection > -range) ||
(dragDirection < -180 + range || dragDirection > 180 - range)) {
return true;
}
return false;
}
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to viewport.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
*/
getClientRect() {
if (!this.svgGroup_ || this.autoClose || !this.isVisible()) {
// The bounding rectangle won't compute correctly if the flyout is closed
// and auto-close flyouts aren't valid drag targets (or delete areas).
return null;
}
const flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown
// flyout area are still deleted. Must be larger than the largest screen
// size, but be smaller than half Number.MAX_SAFE_INTEGER (not available on
// IE).
const BIG_NUM = 1000000000;
const left = flyoutRect.left;
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
const width = flyoutRect.width;
return new Rect(-BIG_NUM, BIG_NUM, -BIG_NUM, left + width);
} else { // Right
return new Rect(-BIG_NUM, BIG_NUM, left, BIG_NUM);
}
}
/**
* Compute width of flyout. toolbox.Position mat under each block.
* For RTL: Lay out the blocks and buttons to be right-aligned.
* @protected
*/
reflowInternal_() {
this.workspace_.scale = this.getFlyoutScale();
let flyoutWidth = 0;
const blocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; (block = blocks[i]); i++) {
let width = block.getHeightWidth().width;
if (block.outputConnection) {
width -= this.tabWidth_;
}
flyoutWidth = Math.max(flyoutWidth, width);
}
for (let i = 0, button; (button = this.buttons_[i]); i++) {
flyoutWidth = Math.max(flyoutWidth, button.width);
}
flyoutWidth += this.MARGIN * 1.5 + this.tabWidth_;
flyoutWidth *= this.workspace_.scale;
flyoutWidth += Scrollbar.scrollbarThickness;
if (this.width_ !== flyoutWidth) {
for (let i = 0, block; (block = blocks[i]); i++) {
if (this.RTL) {
// With the flyoutWidth known, right-align the blocks.
const oldX = block.getRelativeToSurfaceXY().x;
let newX = flyoutWidth / this.workspace_.scale - this.MARGIN;
if (!block.outputConnection) {
newX -= this.tabWidth_;
}
block.moveBy(newX - oldX, 0);
}
if (this.rectMap_.has(block)) {
this.moveRectToBlock_(this.rectMap_.get(block), block);
}
}
if (this.RTL) {
// With the flyoutWidth known, right-align the buttons.
for (let i = 0, button; (button = this.buttons_[i]); i++) {
const y = button.getPosition().y;
const x = flyoutWidth / this.workspace_.scale - button.width -
this.MARGIN - this.tabWidth_;
button.moveTo(x, y);
}
}
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_ &&
this.toolboxPosition_ === toolbox.Position.LEFT &&
!this.targetWorkspace.getToolbox()) {
// This flyout is a simple toolbox. Reposition the workspace so that
// (0,0) is in the correct position relative to the new absolute edge
// (ie toolbox edge).
this.targetWorkspace.translate(
this.targetWorkspace.scrollX + flyoutWidth,
this.targetWorkspace.scrollY);
}
// Record the width for workspace metrics and .position.
this.width_ = flyoutWidth;
this.position();
this.targetWorkspace.recordDragTargets();
}
}
}
/**
* The name of the vertical flyout in the registry.
@@ -53,336 +389,6 @@ object.inherits(VerticalFlyout, Flyout);
*/
VerticalFlyout.registryName = 'verticalFlyout';
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!{x:number,y:number}} xyRatio Contains a y property which is a float
* between 0 and 1 specifying the degree of scrolling and a
* similar x property.
* @protected
*/
VerticalFlyout.prototype.setMetrics_ = function(xyRatio) {
if (!this.isVisible()) {
return;
}
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
if (typeof xyRatio.y === 'number') {
this.workspace_.scrollY =
-(scrollMetrics.top +
(scrollMetrics.height - viewMetrics.height) * xyRatio.y);
}
this.workspace_.translate(
this.workspace_.scrollX + absoluteMetrics.left,
this.workspace_.scrollY + absoluteMetrics.top);
};
/**
* Calculates the x coordinate for the flyout position.
* @return {number} X coordinate.
*/
VerticalFlyout.prototype.getX = function() {
if (!this.isVisible()) {
return 0;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const toolboxMetrics = metricsManager.getToolboxMetrics();
let x = 0;
// If this flyout is not the trashcan flyout (e.g. toolbox or mutator).
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_) {
// If there is a category toolbox.
if (this.targetWorkspace.getToolbox()) {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = toolboxMetrics.width;
} else {
x = viewMetrics.width - this.width_;
}
// Simple (flyout-only) toolbox.
} else {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = 0;
} else {
// The simple flyout does not cover the workspace.
x = viewMetrics.width;
}
}
// Trashcan flyout is opposite the main flyout.
} else {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = 0;
} else {
// Because the anchor point of the flyout is on the left, but we want
// to align the right edge of the flyout with the right edge of the
// blocklyDiv, we calculate the full width of the div minus the width
// of the flyout.
x = viewMetrics.width + absoluteMetrics.left - this.width_;
}
}
return x;
};
/**
* Calculates the y coordinate for the flyout position.
* @return {number} Y coordinate.
*/
VerticalFlyout.prototype.getY = function() {
// Y is always 0 since this is a vertical flyout.
return 0;
};
/**
* Move the flyout to the edge of the workspace.
*/
VerticalFlyout.prototype.position = function() {
if (!this.isVisible() || !this.targetWorkspace.isVisible()) {
return;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
// Record the height for workspace metrics.
this.height_ = targetWorkspaceViewMetrics.height;
const edgeWidth = this.width_ - this.CORNER_RADIUS;
const edgeHeight = targetWorkspaceViewMetrics.height - 2 * this.CORNER_RADIUS;
this.setBackgroundPath_(edgeWidth, edgeHeight);
const x = this.getX();
const y = this.getY();
this.positionAt_(this.width_, this.height_, x, y);
};
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
VerticalFlyout.prototype.setBackgroundPath_ = function(width, height) {
const atRight = this.toolboxPosition_ === toolbox.Position.RIGHT;
const totalWidth = width + this.CORNER_RADIUS;
// Decide whether to start on the left or right.
const path = ['M ' + (atRight ? totalWidth : 0) + ',0'];
// Top.
path.push('h', atRight ? -width : width);
// Rounded corner.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, atRight ? 0 : 1,
atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS, this.CORNER_RADIUS);
// Side closest to workspace.
path.push('v', Math.max(0, height));
// Rounded corner.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, atRight ? 0 : 1,
atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, this.CORNER_RADIUS);
// Bottom.
path.push('h', atRight ? width : -width);
path.push('z');
this.svgBackground_.setAttribute('d', path.join(' '));
};
/**
* Scroll the flyout to the top.
*/
VerticalFlyout.prototype.scrollToStart = function() {
this.workspace_.scrollbar.setY(0);
};
/**
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @protected
*/
VerticalFlyout.prototype.wheel_ = function(e) {
const scrollDelta = browserEvents.getScrollDeltaPixels(e);
if (scrollDelta.y) {
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const pos = (viewMetrics.top - scrollMetrics.top) + scrollDelta.y;
this.workspace_.scrollbar.setY(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and DropDownDiv.
WidgetDiv.hide();
DropDownDiv.hideWithoutAnimation();
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
};
/**
* Lay out the blocks in the flyout.
* @param {!Array<!Object>} contents The blocks and buttons to lay out.
* @param {!Array<number>} gaps The visible gaps between blocks.
* @protected
*/
VerticalFlyout.prototype.layout_ = function(contents, gaps) {
this.workspace_.scale = this.targetWorkspace.scale;
const margin = this.MARGIN;
const cursorX = this.RTL ? margin : margin + this.tabWidth_;
let cursorY = margin;
for (let i = 0, item; (item = contents[i]); i++) {
if (item.type === 'block') {
const block = item.block;
const allBlocks = block.getDescendants(false);
for (let j = 0, child; (child = allBlocks[j]); j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such a
// block.
child.isInFlyout = true;
}
block.render();
const root = block.getSvgRoot();
const blockHW = block.getHeightWidth();
const moveX = block.outputConnection ? cursorX - this.tabWidth_ : cursorX;
block.moveBy(moveX, cursorY);
const rect = this.createRect_(
block, this.RTL ? moveX - blockHW.width : moveX, cursorY, blockHW, i);
this.addBlockListeners_(root, block, rect);
cursorY += blockHW.height + gaps[i];
} else if (item.type === 'button') {
this.initFlyoutButton_(item.button, cursorX, cursorY);
cursorY += item.button.height + gaps[i];
}
}
};
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} True if the drag is toward the workspace.
* @package
*/
VerticalFlyout.prototype.isDragTowardWorkspace = function(currentDragDeltaXY) {
const dx = currentDragDeltaXY.x;
const dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
const dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
const range = this.dragAngleRange_;
// Check for left or right dragging.
if ((dragDirection < range && dragDirection > -range) ||
(dragDirection < -180 + range || dragDirection > 180 - range)) {
return true;
}
return false;
};
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to viewport.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
*/
VerticalFlyout.prototype.getClientRect = function() {
if (!this.svgGroup_ || this.autoClose || !this.isVisible()) {
// The bounding rectangle won't compute correctly if the flyout is closed
// and auto-close flyouts aren't valid drag targets (or delete areas).
return null;
}
const flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout
// area are still deleted. Must be larger than the largest screen size,
// but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE).
const BIG_NUM = 1000000000;
const left = flyoutRect.left;
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
const width = flyoutRect.width;
return new Rect(-BIG_NUM, BIG_NUM, -BIG_NUM, left + width);
} else { // Right
return new Rect(-BIG_NUM, BIG_NUM, left, BIG_NUM);
}
};
/**
* Compute width of flyout. toolbox.Position mat under each block.
* For RTL: Lay out the blocks and buttons to be right-aligned.
* @protected
*/
VerticalFlyout.prototype.reflowInternal_ = function() {
this.workspace_.scale = this.getFlyoutScale();
let flyoutWidth = 0;
const blocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; (block = blocks[i]); i++) {
let width = block.getHeightWidth().width;
if (block.outputConnection) {
width -= this.tabWidth_;
}
flyoutWidth = Math.max(flyoutWidth, width);
}
for (let i = 0, button; (button = this.buttons_[i]); i++) {
flyoutWidth = Math.max(flyoutWidth, button.width);
}
flyoutWidth += this.MARGIN * 1.5 + this.tabWidth_;
flyoutWidth *= this.workspace_.scale;
flyoutWidth += Scrollbar.scrollbarThickness;
if (this.width_ !== flyoutWidth) {
for (let i = 0, block; (block = blocks[i]); i++) {
if (this.RTL) {
// With the flyoutWidth known, right-align the blocks.
const oldX = block.getRelativeToSurfaceXY().x;
let newX = flyoutWidth / this.workspace_.scale - this.MARGIN;
if (!block.outputConnection) {
newX -= this.tabWidth_;
}
block.moveBy(newX - oldX, 0);
}
if (block.flyoutRect_) {
this.moveRectToBlock_(block.flyoutRect_, block);
}
}
if (this.RTL) {
// With the flyoutWidth known, right-align the buttons.
for (let i = 0, button; (button = this.buttons_[i]); i++) {
const y = button.getPosition().y;
const x = flyoutWidth / this.workspace_.scale - button.width -
this.MARGIN - this.tabWidth_;
button.moveTo(x, y);
}
}
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_ &&
this.toolboxPosition_ === toolbox.Position.LEFT &&
!this.targetWorkspace.getToolbox()) {
// This flyout is a simple toolbox. Reposition the workspace so that (0,0)
// is in the correct position relative to the new absolute edge (ie
// toolbox edge).
this.targetWorkspace.translate(
this.targetWorkspace.scrollX + flyoutWidth,
this.targetWorkspace.scrollY);
}
// Record the width for workspace metrics and .position.
this.width_ = flyoutWidth;
this.position();
this.targetWorkspace.recordDragTargets();
}
};
registry.register(
registry.Type.FLYOUTS_VERTICAL_TOOLBOX, registry.DEFAULT, VerticalFlyout);

View File

@@ -29,390 +29,508 @@ const {Workspace} = goog.requireType('Blockly.Workspace');
/**
* Class for a code generator that translates the blocks into a language.
* @param {string} name Language name of this generator.
* @constructor
* @unrestricted
* @alias Blockly.Generator
*/
const Generator = function(name) {
this.name_ = name;
this.FUNCTION_NAME_PLACEHOLDER_REGEXP_ =
new RegExp(this.FUNCTION_NAME_PLACEHOLDER_, 'g');
};
class Generator {
/**
* @param {string} name Language name of this generator.
*/
constructor(name) {
this.name_ = name;
/**
* Arbitrary code to inject into locations that risk causing infinite loops.
* Any instances of '%1' will be replaced by the block ID that failed.
* E.g. ' checkTimeout(%1);\n'
* @type {?string}
*/
Generator.prototype.INFINITE_LOOP_TRAP = null;
/**
* This is used as a placeholder in functions defined using
* Generator.provideFunction_. It must not be legal code that could
* legitimately appear in a function definition (or comment), and it must
* not confuse the regular expression parser.
* @type {string}
* @protected
*/
this.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}';
/**
* Arbitrary code to inject before every statement.
* Any instances of '%1' will be replaced by the block ID of the statement.
* E.g. 'highlight(%1);\n'
* @type {?string}
*/
Generator.prototype.STATEMENT_PREFIX = null;
this.FUNCTION_NAME_PLACEHOLDER_REGEXP_ =
new RegExp(this.FUNCTION_NAME_PLACEHOLDER_, 'g');
/**
* Arbitrary code to inject after every statement.
* Any instances of '%1' will be replaced by the block ID of the statement.
* E.g. 'highlight(%1);\n'
* @type {?string}
*/
Generator.prototype.STATEMENT_SUFFIX = null;
/**
* Arbitrary code to inject into locations that risk causing infinite loops.
* Any instances of '%1' will be replaced by the block ID that failed.
* E.g. ' checkTimeout(%1);\n'
* @type {?string}
*/
this.INFINITE_LOOP_TRAP = null;
/**
* The method of indenting. Defaults to two spaces, but language generators
* may override this to increase indent or change to tabs.
* @type {string}
*/
Generator.prototype.INDENT = ' ';
/**
* Arbitrary code to inject before every statement.
* Any instances of '%1' will be replaced by the block ID of the statement.
* E.g. 'highlight(%1);\n'
* @type {?string}
*/
this.STATEMENT_PREFIX = null;
/**
* Maximum length for a comment before wrapping. Does not account for
* indenting level.
* @type {number}
*/
Generator.prototype.COMMENT_WRAP = 60;
/**
* Arbitrary code to inject after every statement.
* Any instances of '%1' will be replaced by the block ID of the statement.
* E.g. 'highlight(%1);\n'
* @type {?string}
*/
this.STATEMENT_SUFFIX = null;
/**
* List of outer-inner pairings that do NOT require parentheses.
* @type {!Array<!Array<number>>}
*/
Generator.prototype.ORDER_OVERRIDES = [];
/**
* The method of indenting. Defaults to two spaces, but language generators
* may override this to increase indent or change to tabs.
* @type {string}
*/
this.INDENT = ' ';
/**
* Whether the init method has been called.
* Generators that set this flag to false after creation and true in init
* will cause blockToCode to emit a warning if the generator has not been
* initialized. If this flag is untouched, it will have no effect.
* @type {?boolean}
*/
Generator.prototype.isInitialized = null;
/**
* Maximum length for a comment before wrapping. Does not account for
* indenting level.
* @type {number}
*/
this.COMMENT_WRAP = 60;
/**
* Generate code for all blocks in the workspace to the specified language.
* @param {!Workspace=} workspace Workspace to generate code from.
* @return {string} Generated code.
*/
Generator.prototype.workspaceToCode = function(workspace) {
if (!workspace) {
// Backwards compatibility from before there could be multiple workspaces.
console.warn('No workspace specified in workspaceToCode call. Guessing.');
workspace = common.getMainWorkspace();
/**
* List of outer-inner pairings that do NOT require parentheses.
* @type {!Array<!Array<number>>}
*/
this.ORDER_OVERRIDES = [];
/**
* Whether the init method has been called.
* Generators that set this flag to false after creation and true in init
* will cause blockToCode to emit a warning if the generator has not been
* initialized. If this flag is untouched, it will have no effect.
* @type {?boolean}
*/
this.isInitialized = null;
/**
* Comma-separated list of reserved words.
* @type {string}
* @protected
*/
this.RESERVED_WORDS_ = '';
/**
* A dictionary of definitions to be printed before the code.
* @type {!Object|undefined}
* @protected
*/
this.definitions_ = undefined;
/**
* A dictionary mapping desired function names in definitions_ to actual
* function names (to avoid collisions with user functions).
* @type {!Object|undefined}
* @protected
*/
this.functionNames_ = undefined;
/**
* A database of variable and procedure names.
* @type {!Names|undefined}
* @protected
*/
this.nameDB_ = undefined;
}
let code = [];
this.init(workspace);
const blocks = workspace.getTopBlocks(true);
for (let i = 0, block; (block = blocks[i]); i++) {
let line = this.blockToCode(block);
if (Array.isArray(line)) {
// Value blocks return tuples of code and operator order.
// Top-level blocks don't care about operator order.
line = line[0];
/**
* Generate code for all blocks in the workspace to the specified language.
* @param {!Workspace=} workspace Workspace to generate code from.
* @return {string} Generated code.
*/
workspaceToCode(workspace) {
if (!workspace) {
// Backwards compatibility from before there could be multiple workspaces.
console.warn(
'No workspace specified in workspaceToCode call. Guessing.');
workspace = common.getMainWorkspace();
}
if (line) {
if (block.outputConnection) {
// This block is a naked value. Ask the language's code generator if
// it wants to append a semicolon, or something.
line = this.scrubNakedValue(line);
if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) {
line = this.injectId(this.STATEMENT_PREFIX, block) + line;
let code = [];
this.init(workspace);
const blocks = workspace.getTopBlocks(true);
for (let i = 0, block; (block = blocks[i]); i++) {
let line = this.blockToCode(block);
if (Array.isArray(line)) {
// Value blocks return tuples of code and operator order.
// Top-level blocks don't care about operator order.
line = line[0];
}
if (line) {
if (block.outputConnection) {
// This block is a naked value. Ask the language's code generator if
// it wants to append a semicolon, or something.
line = this.scrubNakedValue(line);
if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) {
line = this.injectId(this.STATEMENT_PREFIX, block) + line;
}
if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) {
line = line + this.injectId(this.STATEMENT_SUFFIX, block);
}
}
if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) {
line = line + this.injectId(this.STATEMENT_SUFFIX, block);
code.push(line);
}
}
code = code.join('\n'); // Blank line between each section.
code = this.finish(code);
// Final scrubbing of whitespace.
code = code.replace(/^\s+\n/, '');
code = code.replace(/\n\s+$/, '\n');
code = code.replace(/[ \t]+\n/g, '\n');
return code;
}
// The following are some helpful functions which can be used by multiple
// languages.
/**
* Prepend a common prefix onto each line of code.
* Intended for indenting code or adding comment markers.
* @param {string} text The lines of code.
* @param {string} prefix The common prefix.
* @return {string} The prefixed lines of code.
*/
prefixLines(text, prefix) {
return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix);
}
/**
* Recursively spider a tree of blocks, returning all their comments.
* @param {!Block} block The block from which to start spidering.
* @return {string} Concatenated list of comments.
*/
allNestedComments(block) {
const comments = [];
const blocks = block.getDescendants(true);
for (let i = 0; i < blocks.length; i++) {
const comment = blocks[i].getCommentText();
if (comment) {
comments.push(comment);
}
}
// Append an empty string to create a trailing line break when joined.
if (comments.length) {
comments.push('');
}
return comments.join('\n');
}
/**
* Generate code for the specified block (and attached blocks).
* The generator must be initialized before calling this function.
* @param {?Block} block The block to generate code for.
* @param {boolean=} opt_thisOnly True to generate code for only this
* statement.
* @return {string|!Array} For statement blocks, the generated code.
* For value blocks, an array containing the generated code and an
* operator order value. Returns '' if block is null.
*/
blockToCode(block, opt_thisOnly) {
if (this.isInitialized === false) {
console.warn(
'Generator init was not called before blockToCode was called.');
}
if (!block) {
return '';
}
if (!block.isEnabled()) {
// Skip past this block if it is disabled.
return opt_thisOnly ? '' : this.blockToCode(block.getNextBlock());
}
if (block.isInsertionMarker()) {
// Skip past insertion markers.
return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]);
}
const func = this[block.type];
if (typeof func !== 'function') {
throw Error(
'Language "' + this.name_ + '" does not know how to generate ' +
'code for block type "' + block.type + '".');
}
// First argument to func.call is the value of 'this' in the generator.
// Prior to 24 September 2013 'this' was the only way to access the block.
// The current preferred method of accessing the block is through the second
// argument to func.call, which becomes the first parameter to the
// generator.
let code = func.call(block, block);
if (Array.isArray(code)) {
// Value blocks return tuples of code and operator order.
if (!block.outputConnection) {
throw TypeError('Expecting string from statement block: ' + block.type);
}
return [this.scrub_(block, code[0], opt_thisOnly), code[1]];
} else if (typeof code === 'string') {
if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) {
code = this.injectId(this.STATEMENT_PREFIX, block) + code;
}
if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) {
code = code + this.injectId(this.STATEMENT_SUFFIX, block);
}
return this.scrub_(block, code, opt_thisOnly);
} else if (code === null) {
// Block has handled code generation itself.
return '';
}
throw SyntaxError('Invalid code generated: ' + code);
}
/**
* Generate code representing the specified value input.
* @param {!Block} block The block containing the input.
* @param {string} name The name of the input.
* @param {number} outerOrder The maximum binding strength (minimum order
* value) of any operators adjacent to "block".
* @return {string} Generated code or '' if no blocks are connected or the
* specified input does not exist.
*/
valueToCode(block, name, outerOrder) {
if (isNaN(outerOrder)) {
throw TypeError('Expecting valid order from block: ' + block.type);
}
const targetBlock = block.getInputTargetBlock(name);
if (!targetBlock) {
return '';
}
const tuple = this.blockToCode(targetBlock);
if (tuple === '') {
// Disabled block.
return '';
}
// Value blocks must return code and order of operations info.
// Statement blocks must only return code.
if (!Array.isArray(tuple)) {
throw TypeError('Expecting tuple from value block: ' + targetBlock.type);
}
let code = tuple[0];
const innerOrder = tuple[1];
if (isNaN(innerOrder)) {
throw TypeError(
'Expecting valid order from value block: ' + targetBlock.type);
}
if (!code) {
return '';
}
// Add parentheses if needed.
let parensNeeded = false;
const outerOrderClass = Math.floor(outerOrder);
const innerOrderClass = Math.floor(innerOrder);
if (outerOrderClass <= innerOrderClass) {
if (outerOrderClass === innerOrderClass &&
(outerOrderClass === 0 || outerOrderClass === 99)) {
// Don't generate parens around NONE-NONE and ATOMIC-ATOMIC pairs.
// 0 is the atomic order, 99 is the none order. No parentheses needed.
// 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 stronger than the operators
// inside this code. To prevent the code from being pulled apart,
// wrap the code in parentheses.
parensNeeded = true;
// Check for special exceptions.
for (let i = 0; i < this.ORDER_OVERRIDES.length; i++) {
if (this.ORDER_OVERRIDES[i][0] === outerOrder &&
this.ORDER_OVERRIDES[i][1] === innerOrder) {
parensNeeded = false;
break;
}
}
}
code.push(line);
}
}
code = code.join('\n'); // Blank line between each section.
code = this.finish(code);
// Final scrubbing of whitespace.
code = code.replace(/^\s+\n/, '');
code = code.replace(/\n\s+$/, '\n');
code = code.replace(/[ \t]+\n/g, '\n');
return code;
};
// The following are some helpful functions which can be used by multiple
// languages.
/**
* Prepend a common prefix onto each line of code.
* Intended for indenting code or adding comment markers.
* @param {string} text The lines of code.
* @param {string} prefix The common prefix.
* @return {string} The prefixed lines of code.
*/
Generator.prototype.prefixLines = function(text, prefix) {
return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix);
};
/**
* Recursively spider a tree of blocks, returning all their comments.
* @param {!Block} block The block from which to start spidering.
* @return {string} Concatenated list of comments.
*/
Generator.prototype.allNestedComments = function(block) {
const comments = [];
const blocks = block.getDescendants(true);
for (let i = 0; i < blocks.length; i++) {
const comment = blocks[i].getCommentText();
if (comment) {
comments.push(comment);
if (parensNeeded) {
// Technically, this should be handled on a language-by-language basis.
// However all known (sane) languages use parentheses for grouping.
code = '(' + code + ')';
}
}
// Append an empty string to create a trailing line break when joined.
if (comments.length) {
comments.push('');
}
return comments.join('\n');
};
/**
* Generate code for the specified block (and attached blocks).
* The generator must be initialized before calling this function.
* @param {Block} block The block to generate code for.
* @param {boolean=} opt_thisOnly True to generate code for only this statement.
* @return {string|!Array} For statement blocks, the generated code.
* For value blocks, an array containing the generated code and an
* operator order value. Returns '' if block is null.
*/
Generator.prototype.blockToCode = function(block, opt_thisOnly) {
if (this.isInitialized === false) {
console.warn(
'Generator init was not called before blockToCode was called.');
}
if (!block) {
return '';
}
if (!block.isEnabled()) {
// Skip past this block if it is disabled.
return opt_thisOnly ? '' : this.blockToCode(block.getNextBlock());
}
if (block.isInsertionMarker()) {
// Skip past insertion markers.
return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]);
return code;
}
const func = this[block.type];
if (typeof func !== 'function') {
throw Error(
'Language "' + this.name_ + '" does not know how to generate ' +
'code for block type "' + block.type + '".');
}
// First argument to func.call is the value of 'this' in the generator.
// Prior to 24 September 2013 'this' was the only way to access the block.
// The current preferred method of accessing the block is through the second
// argument to func.call, which becomes the first parameter to the generator.
let code = func.call(block, block);
if (Array.isArray(code)) {
// Value blocks return tuples of code and operator order.
if (!block.outputConnection) {
throw TypeError('Expecting string from statement block: ' + block.type);
/**
* Generate a code string representing the blocks attached to the named
* statement input. Indent the code.
* This is mainly used in generators. When trying to generate code to evaluate
* look at using workspaceToCode or blockToCode.
* @param {!Block} block The block containing the input.
* @param {string} name The name of the input.
* @return {string} Generated code or '' if no blocks are connected.
*/
statementToCode(block, name) {
const targetBlock = block.getInputTargetBlock(name);
let code = this.blockToCode(targetBlock);
// Value blocks must return code and order of operations info.
// Statement blocks must only return code.
if (typeof code !== 'string') {
throw TypeError(
'Expecting code from statement block: ' +
(targetBlock && targetBlock.type));
}
return [this.scrub_(block, code[0], opt_thisOnly), code[1]];
} else if (typeof code === 'string') {
if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) {
code = this.injectId(this.STATEMENT_PREFIX, block) + code;
if (code) {
code = this.prefixLines(/** @type {string} */ (code), this.INDENT);
}
return code;
}
/**
* Add an infinite loop trap to the contents of a loop.
* Add statement suffix at the start of the loop block (right after the loop
* statement executes), and a statement prefix to the end of the loop block
* (right before the loop statement executes).
* @param {string} branch Code for loop contents.
* @param {!Block} block Enclosing block.
* @return {string} Loop contents, with infinite loop trap added.
*/
addLoopTrap(branch, block) {
if (this.INFINITE_LOOP_TRAP) {
branch = this.prefixLines(
this.injectId(this.INFINITE_LOOP_TRAP, block), this.INDENT) +
branch;
}
if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) {
code = code + this.injectId(this.STATEMENT_SUFFIX, block);
branch = this.prefixLines(
this.injectId(this.STATEMENT_SUFFIX, block), this.INDENT) +
branch;
}
return this.scrub_(block, code, opt_thisOnly);
} else if (code === null) {
// Block has handled code generation itself.
return '';
}
throw SyntaxError('Invalid code generated: ' + code);
};
/**
* Generate code representing the specified value input.
* @param {!Block} block The block containing the input.
* @param {string} name The name of the input.
* @param {number} outerOrder The maximum binding strength (minimum order value)
* of any operators adjacent to "block".
* @return {string} Generated code or '' if no blocks are connected or the
* specified input does not exist.
*/
Generator.prototype.valueToCode = function(block, name, outerOrder) {
if (isNaN(outerOrder)) {
throw TypeError('Expecting valid order from block: ' + block.type);
}
const targetBlock = block.getInputTargetBlock(name);
if (!targetBlock) {
return '';
}
const tuple = this.blockToCode(targetBlock);
if (tuple === '') {
// Disabled block.
return '';
}
// Value blocks must return code and order of operations info.
// Statement blocks must only return code.
if (!Array.isArray(tuple)) {
throw TypeError('Expecting tuple from value block: ' + targetBlock.type);
}
let code = tuple[0];
const innerOrder = tuple[1];
if (isNaN(innerOrder)) {
throw TypeError(
'Expecting valid order from value block: ' + targetBlock.type);
}
if (!code) {
return '';
if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) {
branch = branch +
this.prefixLines(
this.injectId(this.STATEMENT_PREFIX, block), this.INDENT);
}
return branch;
}
// Add parentheses if needed.
let parensNeeded = false;
const outerOrderClass = Math.floor(outerOrder);
const innerOrderClass = Math.floor(innerOrder);
if (outerOrderClass <= innerOrderClass) {
if (outerOrderClass === innerOrderClass &&
(outerOrderClass === 0 || outerOrderClass === 99)) {
// Don't generate parens around NONE-NONE and ATOMIC-ATOMIC pairs.
// 0 is the atomic order, 99 is the none order. No parentheses needed.
// 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 stronger than the operators
// inside this code. To prevent the code from being pulled apart,
// wrap the code in parentheses.
parensNeeded = true;
// Check for special exceptions.
for (let i = 0; i < this.ORDER_OVERRIDES.length; i++) {
if (this.ORDER_OVERRIDES[i][0] === outerOrder &&
this.ORDER_OVERRIDES[i][1] === innerOrder) {
parensNeeded = false;
break;
}
/**
* Inject a block ID into a message to replace '%1'.
* Used for STATEMENT_PREFIX, STATEMENT_SUFFIX, and INFINITE_LOOP_TRAP.
* @param {string} msg Code snippet with '%1'.
* @param {!Block} block Block which has an ID.
* @return {string} Code snippet with ID.
*/
injectId(msg, block) {
const id = block.id.replace(/\$/g, '$$$$'); // Issue 251.
return msg.replace(/%1/g, '\'' + id + '\'');
}
/**
* Add one or more words to the list of reserved words for this language.
* @param {string} words Comma-separated list of words to add to the list.
* No spaces. Duplicates are ok.
*/
addReservedWords(words) {
this.RESERVED_WORDS_ += words + ',';
}
/**
* Define a developer-defined function (not a user-defined procedure) to be
* included in the generated code. Used for creating private helper
* functions. The first time this is called with a given desiredName, the code
* is saved and an actual name is generated. Subsequent calls with the same
* desiredName have no effect but have the same return value.
*
* It is up to the caller to make sure the same desiredName is not
* used for different helper functions (e.g. use "colourRandom" and
* "listRandom", not "random"). There is no danger of colliding with reserved
* words, or user-defined variable or procedure names.
*
* The code gets output when Generator.finish() is called.
*
* @param {string} desiredName The desired name of the function
* (e.g. mathIsPrime).
* @param {!Array<string>|string} code A list of statements or one multi-line
* code string. Use ' ' for indents (they will be replaced).
* @return {string} The actual name of the new function. This may differ
* from desiredName if the former has already been taken by the user.
* @protected
*/
provideFunction_(desiredName, code) {
if (!this.definitions_[desiredName]) {
const functionName =
this.nameDB_.getDistinctName(desiredName, NameType.PROCEDURE);
this.functionNames_[desiredName] = functionName;
if (Array.isArray(code)) {
code = code.join('\n');
}
let codeText = code.trim().replace(
this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName);
// Change all ' ' indents into the desired indent.
// To avoid an infinite loop of replacements, change all indents to '\0'
// character first, then replace them all with the indent.
// We are assuming that no provided functions contain a literal null char.
let oldCodeText;
while (oldCodeText !== codeText) {
oldCodeText = codeText;
codeText = codeText.replace(/^(( {2})*) {2}/gm, '$1\0');
}
codeText = codeText.replace(/\0/g, this.INDENT);
this.definitions_[desiredName] = codeText;
}
return this.functionNames_[desiredName];
}
if (parensNeeded) {
// Technically, this should be handled on a language-by-language basis.
// However all known (sane) languages use parentheses for grouping.
code = '(' + code + ')';
/**
* Hook for code to run before code generation starts.
* Subclasses may override this, e.g. to initialise the database of variable
* names.
* @param {!Workspace} _workspace Workspace to generate code from.
*/
init(_workspace) {
// Optionally override
// Create a dictionary of definitions to be printed before the code.
this.definitions_ = Object.create(null);
// Create a dictionary mapping desired developer-defined function names in
// definitions_ to actual function names (to avoid collisions with
// user-defined procedures).
this.functionNames_ = Object.create(null);
}
return code;
};
/**
* Generate a code string representing the blocks attached to the named
* statement input. Indent the code.
* This is mainly used in generators. When trying to generate code to evaluate
* look at using workspaceToCode or blockToCode.
* @param {!Block} block The block containing the input.
* @param {string} name The name of the input.
* @return {string} Generated code or '' if no blocks are connected.
*/
Generator.prototype.statementToCode = function(block, name) {
const targetBlock = block.getInputTargetBlock(name);
let code = this.blockToCode(targetBlock);
// Value blocks must return code and order of operations info.
// Statement blocks must only return code.
if (typeof code !== 'string') {
throw TypeError(
'Expecting code from statement block: ' +
(targetBlock && targetBlock.type));
/**
* 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 {!Block} _block The current block.
* @param {string} code The code created for this block.
* @param {boolean=} _opt_thisOnly True to generate code for only this
* statement.
* @return {string} Code with comments and subsequent blocks added.
* @protected
*/
scrub_(_block, code, _opt_thisOnly) {
// Optionally override
return code;
}
if (code) {
code = this.prefixLines(/** @type {string} */ (code), this.INDENT);
/**
* Hook for code to run at end of code generation.
* Subclasses may override this, e.g. to prepend the generated code with
* import statements or variable definitions.
* @param {string} code Generated code.
* @return {string} Completed code.
*/
finish(code) {
// Optionally override
// Clean up temporary data.
delete this.definitions_;
delete this.functionNames_;
return code;
}
return code;
};
/**
* Add an infinite loop trap to the contents of a loop.
* Add statement suffix at the start of the loop block (right after the loop
* statement executes), and a statement prefix to the end of the loop block
* (right before the loop statement executes).
* @param {string} branch Code for loop contents.
* @param {!Block} block Enclosing block.
* @return {string} Loop contents, with infinite loop trap added.
*/
Generator.prototype.addLoopTrap = function(branch, block) {
if (this.INFINITE_LOOP_TRAP) {
branch = this.prefixLines(
this.injectId(this.INFINITE_LOOP_TRAP, block), this.INDENT) +
branch;
/**
* 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.
*/
scrubNakedValue(line) {
// Optionally override
return line;
}
if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) {
branch = this.prefixLines(
this.injectId(this.STATEMENT_SUFFIX, block), this.INDENT) +
branch;
}
if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) {
branch = branch +
this.prefixLines(
this.injectId(this.STATEMENT_PREFIX, block), this.INDENT);
}
return branch;
};
/**
* Inject a block ID into a message to replace '%1'.
* Used for STATEMENT_PREFIX, STATEMENT_SUFFIX, and INFINITE_LOOP_TRAP.
* @param {string} msg Code snippet with '%1'.
* @param {!Block} block Block which has an ID.
* @return {string} Code snippet with ID.
*/
Generator.prototype.injectId = function(msg, block) {
const id = block.id.replace(/\$/g, '$$$$'); // Issue 251.
return msg.replace(/%1/g, '\'' + id + '\'');
};
/**
* Comma-separated list of reserved words.
* @type {string}
* @protected
*/
Generator.prototype.RESERVED_WORDS_ = '';
/**
* Add one or more words to the list of reserved words for this language.
* @param {string} words Comma-separated list of words to add to the list.
* No spaces. Duplicates are ok.
*/
Generator.prototype.addReservedWords = function(words) {
this.RESERVED_WORDS_ += words + ',';
};
/**
* This is used as a placeholder in functions defined using
* Generator.provideFunction_. It must not be legal code that could
* legitimately appear in a function definition (or comment), and it must
* not confuse the regular expression parser.
* @type {string}
* @protected
*/
Generator.prototype.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}';
/**
* A dictionary of definitions to be printed before the code.
* @type {!Object|undefined}
* @protected
*/
Generator.prototype.definitions_;
/**
* A dictionary mapping desired function names in definitions_ to actual
* function names (to avoid collisions with user functions).
* @type {!Object|undefined}
* @protected
*/
Generator.prototype.functionNames_;
/**
* A database of variable and procedure names.
* @type {!Names|undefined}
* @protected
*/
Generator.prototype.nameDB_;
}
Object.defineProperties(Generator.prototype, {
/**
@@ -443,109 +561,4 @@ Object.defineProperties(Generator.prototype, {
},
});
/**
* Define a developer-defined function (not a user-defined procedure) to be
* included in the generated code. Used for creating private helper functions.
* The first time this is called with a given desiredName, the code is
* saved and an actual name is generated. Subsequent calls with the
* same desiredName have no effect but have the same return value.
*
* It is up to the caller to make sure the same desiredName is not
* used for different helper functions (e.g. use "colourRandom" and
* "listRandom", not "random"). There is no danger of colliding with reserved
* words, or user-defined variable or procedure names.
*
* The code gets output when Generator.finish() is called.
*
* @param {string} desiredName The desired name of the function
* (e.g. mathIsPrime).
* @param {!Array<string>} code A list of statements. Use ' ' for indents.
* @return {string} The actual name of the new function. This may differ
* from desiredName if the former has already been taken by the user.
* @protected
*/
Generator.prototype.provideFunction_ = function(desiredName, code) {
if (!this.definitions_[desiredName]) {
const functionName =
this.nameDB_.getDistinctName(desiredName, NameType.PROCEDURE);
this.functionNames_[desiredName] = functionName;
let codeText = code.join('\n').replace(
this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName);
// Change all ' ' indents into the desired indent.
// To avoid an infinite loop of replacements, change all indents to '\0'
// character first, then replace them all with the indent.
// We are assuming that no provided functions contain a literal null char.
let oldCodeText;
while (oldCodeText !== codeText) {
oldCodeText = codeText;
codeText = codeText.replace(/^(( {2})*) {2}/gm, '$1\0');
}
codeText = codeText.replace(/\0/g, this.INDENT);
this.definitions_[desiredName] = codeText;
}
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 {!Workspace} _workspace Workspace to generate code from.
*/
Generator.prototype.init = function(_workspace) {
// Optionally override
// Create a dictionary of definitions to be printed before the code.
this.definitions_ = Object.create(null);
// Create a dictionary mapping desired developer-defined function names in
// definitions_ to actual function names (to avoid collisions with
// user-defined procedures).
this.functionNames_ = Object.create(null);
};
/**
* 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 {!Block} _block The current block.
* @param {string} code The code created for this block.
* @param {boolean=} _opt_thisOnly True to generate code for only this
* statement.
* @return {string} Code with comments and subsequent blocks added.
* @protected
*/
Generator.prototype.scrub_ = function(_block, code, _opt_thisOnly) {
// Optionally override
return code;
};
/**
* Hook for code to run at end of code generation.
* Subclasses may override this, e.g. to prepend the generated code with import
* statements or variable definitions.
* @param {string} code Generated code.
* @return {string} Completed code.
*/
Generator.prototype.finish = function(code) {
// Optionally override
// Clean up temporary data.
delete this.definitions_;
delete this.functionNames_;
return code;
};
/**
* 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.
*/
Generator.prototype.scrubNakedValue = function(line) {
// Optionally override
return line;
};
exports.Generator = Generator;

File diff suppressed because it is too large Load Diff

View File

@@ -24,200 +24,203 @@ const {Svg} = goog.require('Blockly.utils.Svg');
/**
* Class for a workspace's grid.
* @param {!SVGElement} pattern The grid's SVG pattern, created during
* injection.
* @param {!Object} options A dictionary of normalized options for the grid.
* See grid documentation:
* https://developers.google.com/blockly/guides/configure/web/grid
* @constructor
* @alias Blockly.Grid
*/
const Grid = function(pattern, options) {
class Grid {
/**
* The grid's SVG pattern, created during injection.
* @type {!SVGElement}
* @private
* @param {!SVGElement} pattern The grid's SVG pattern, created during
* injection.
* @param {!Object} options A dictionary of normalized options for the grid.
* See grid documentation:
* https://developers.google.com/blockly/guides/configure/web/grid
*/
this.gridPattern_ = pattern;
constructor(pattern, options) {
/**
* The scale of the grid, used to set stroke width on grid lines.
* This should always be the same as the workspace scale.
* @type {number}
* @private
*/
this.scale_ = 1;
/**
* The spacing of the grid lines (in px).
* @type {number}
* @private
*/
this.spacing_ = options['spacing'];
/**
* The grid's SVG pattern, created during injection.
* @type {!SVGElement}
* @private
*/
this.gridPattern_ = pattern;
/**
* How long the grid lines should be (in px).
* @type {number}
* @private
*/
this.length_ = options['length'];
/**
* The spacing of the grid lines (in px).
* @type {number}
* @private
*/
this.spacing_ = options['spacing'];
/**
* The horizontal grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line1_ = /** @type {SVGElement} */ (pattern.firstChild);
/**
* How long the grid lines should be (in px).
* @type {number}
* @private
*/
this.length_ = options['length'];
/**
* The vertical grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line2_ =
this.line1_ && (/** @type {SVGElement} */ (this.line1_.nextSibling));
/**
* The horizontal grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line1_ = /** @type {SVGElement} */ (pattern.firstChild);
/**
* Whether blocks should snap to the grid.
* @type {boolean}
* @private
*/
this.snapToGrid_ = options['snap'];
};
/**
* The vertical grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line2_ =
this.line1_ && (/** @type {SVGElement} */ (this.line1_.nextSibling));
/**
* The scale of the grid, used to set stroke width on grid lines.
* This should always be the same as the workspace scale.
* @type {number}
* @private
*/
Grid.prototype.scale_ = 1;
/**
* Dispose of this grid and unlink from the DOM.
* @package
* @suppress {checkTypes}
*/
Grid.prototype.dispose = function() {
this.gridPattern_ = null;
};
/**
* Whether blocks should snap to the grid, based on the initial configuration.
* @return {boolean} True if blocks should snap, false otherwise.
* @package
*/
Grid.prototype.shouldSnap = function() {
return this.snapToGrid_;
};
/**
* Get the spacing of the grid points (in px).
* @return {number} The spacing of the grid points.
* @package
*/
Grid.prototype.getSpacing = function() {
return this.spacing_;
};
/**
* Get the ID of the pattern element, which should be randomized to avoid
* conflicts with other Blockly instances on the page.
* @return {string} The pattern ID.
* @package
*/
Grid.prototype.getPatternId = function() {
return this.gridPattern_.id;
};
/**
* Update the grid with a new scale.
* @param {number} scale The new workspace scale.
* @package
*/
Grid.prototype.update = function(scale) {
this.scale_ = scale;
// MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
const safeSpacing = (this.spacing_ * scale) || 100;
this.gridPattern_.setAttribute('width', safeSpacing);
this.gridPattern_.setAttribute('height', safeSpacing);
let half = Math.floor(this.spacing_ / 2) + 0.5;
let start = half - this.length_ / 2;
let end = half + this.length_ / 2;
half *= scale;
start *= scale;
end *= scale;
this.setLineAttributes_(this.line1_, scale, start, end, half, half);
this.setLineAttributes_(this.line2_, scale, half, half, start, end);
};
/**
* Set the attributes on one of the lines in the grid. Use this to update the
* length and stroke width of the grid lines.
* @param {SVGElement} line Which line to update.
* @param {number} width The new stroke size (in px).
* @param {number} x1 The new x start position of the line (in px).
* @param {number} x2 The new x end position of the line (in px).
* @param {number} y1 The new y start position of the line (in px).
* @param {number} y2 The new y end position of the line (in px).
* @private
*/
Grid.prototype.setLineAttributes_ = function(line, width, x1, x2, y1, y2) {
if (line) {
line.setAttribute('stroke-width', width);
line.setAttribute('x1', x1);
line.setAttribute('y1', y1);
line.setAttribute('x2', x2);
line.setAttribute('y2', y2);
/**
* Whether blocks should snap to the grid.
* @type {boolean}
* @private
*/
this.snapToGrid_ = options['snap'];
}
};
/**
* Move the grid to a new x and y position, and make sure that change is
* visible.
* @param {number} x The new x position of the grid (in px).
* @param {number} y The new y position of the grid (in px).
* @package
*/
Grid.prototype.moveTo = function(x, y) {
this.gridPattern_.setAttribute('x', x);
this.gridPattern_.setAttribute('y', y);
if (userAgent.IE || userAgent.EDGE) {
// IE/Edge doesn't notice that the x/y offsets have changed.
// Force an update.
this.update(this.scale_);
/**
* Dispose of this grid and unlink from the DOM.
* @package
* @suppress {checkTypes}
*/
dispose() {
this.gridPattern_ = null;
}
};
/**
* Create the DOM for the grid described by options.
* @param {string} rnd A random ID to append to the pattern's ID.
* @param {!Object} gridOptions The object containing grid configuration.
* @param {!SVGElement} defs The root SVG element for this workspace's defs.
* @return {!SVGElement} The SVG element for the grid pattern.
* @package
*/
Grid.createDom = function(rnd, gridOptions, defs) {
/*
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
<rect stroke="#888" />
<rect stroke="#888" />
</pattern>
*/
const gridPattern = dom.createSvgElement(
Svg.PATTERN,
{'id': 'blocklyGridPattern' + rnd, 'patternUnits': 'userSpaceOnUse'},
defs);
if (gridOptions['length'] > 0 && gridOptions['spacing'] > 0) {
dom.createSvgElement(
Svg.LINE, {'stroke': gridOptions['colour']}, gridPattern);
if (gridOptions['length'] > 1) {
/**
* Whether blocks should snap to the grid, based on the initial configuration.
* @return {boolean} True if blocks should snap, false otherwise.
* @package
*/
shouldSnap() {
return this.snapToGrid_;
}
/**
* Get the spacing of the grid points (in px).
* @return {number} The spacing of the grid points.
* @package
*/
getSpacing() {
return this.spacing_;
}
/**
* Get the ID of the pattern element, which should be randomized to avoid
* conflicts with other Blockly instances on the page.
* @return {string} The pattern ID.
* @package
*/
getPatternId() {
return this.gridPattern_.id;
}
/**
* Update the grid with a new scale.
* @param {number} scale The new workspace scale.
* @package
*/
update(scale) {
this.scale_ = scale;
// MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
const safeSpacing = (this.spacing_ * scale) || 100;
this.gridPattern_.setAttribute('width', safeSpacing);
this.gridPattern_.setAttribute('height', safeSpacing);
let half = Math.floor(this.spacing_ / 2) + 0.5;
let start = half - this.length_ / 2;
let end = half + this.length_ / 2;
half *= scale;
start *= scale;
end *= scale;
this.setLineAttributes_(this.line1_, scale, start, end, half, half);
this.setLineAttributes_(this.line2_, scale, half, half, start, end);
}
/**
* Set the attributes on one of the lines in the grid. Use this to update the
* length and stroke width of the grid lines.
* @param {SVGElement} line Which line to update.
* @param {number} width The new stroke size (in px).
* @param {number} x1 The new x start position of the line (in px).
* @param {number} x2 The new x end position of the line (in px).
* @param {number} y1 The new y start position of the line (in px).
* @param {number} y2 The new y end position of the line (in px).
* @private
*/
setLineAttributes_(line, width, x1, x2, y1, y2) {
if (line) {
line.setAttribute('stroke-width', width);
line.setAttribute('x1', x1);
line.setAttribute('y1', y1);
line.setAttribute('x2', x2);
line.setAttribute('y2', y2);
}
}
/**
* Move the grid to a new x and y position, and make sure that change is
* visible.
* @param {number} x The new x position of the grid (in px).
* @param {number} y The new y position of the grid (in px).
* @package
*/
moveTo(x, y) {
this.gridPattern_.setAttribute('x', x);
this.gridPattern_.setAttribute('y', y);
if (userAgent.IE || userAgent.EDGE) {
// IE/Edge doesn't notice that the x/y offsets have changed.
// Force an update.
this.update(this.scale_);
}
}
/**
* Create the DOM for the grid described by options.
* @param {string} rnd A random ID to append to the pattern's ID.
* @param {!Object} gridOptions The object containing grid configuration.
* @param {!SVGElement} defs The root SVG element for this workspace's defs.
* @return {!SVGElement} The SVG element for the grid pattern.
* @package
*/
static createDom(rnd, gridOptions, defs) {
/*
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
<rect stroke="#888" />
<rect stroke="#888" />
</pattern>
*/
const gridPattern = dom.createSvgElement(
Svg.PATTERN,
{'id': 'blocklyGridPattern' + rnd, 'patternUnits': 'userSpaceOnUse'},
defs);
if (gridOptions['length'] > 0 && gridOptions['spacing'] > 0) {
dom.createSvgElement(
Svg.LINE, {'stroke': gridOptions['colour']}, gridPattern);
if (gridOptions['length'] > 1) {
dom.createSvgElement(
Svg.LINE, {'stroke': gridOptions['colour']}, gridPattern);
}
// x1, y1, x1, x2 properties will be set later in update.
} else {
// Edge 16 doesn't handle empty patterns
dom.createSvgElement(Svg.LINE, {}, gridPattern);
}
// x1, y1, x1, x2 properties will be set later in update.
} else {
// Edge 16 doesn't handle empty patterns
dom.createSvgElement(Svg.LINE, {}, gridPattern);
return gridPattern;
}
return gridPattern;
};
}
exports.Grid = Grid;

View File

@@ -29,188 +29,197 @@ const {Svg} = goog.require('Blockly.utils.Svg');
/**
* Class for an icon.
* @param {BlockSvg} block The block associated with this icon.
* @constructor
* @abstract
* @alias Blockly.Icon
*/
const Icon = function(block) {
class Icon {
/**
* The block this icon is attached to.
* @type {BlockSvg}
* @param {BlockSvg} block The block associated with this icon.
*/
constructor(block) {
/**
* The block this icon is attached to.
* @type {BlockSvg}
* @protected
*/
this.block_ = block;
/**
* The icon SVG group.
* @type {?SVGGElement}
*/
this.iconGroup_ = null;
/**
* Whether this icon gets hidden when the block is collapsed.
* @type {boolean}
*/
this.collapseHidden = true;
/**
* Height and width of icons.
* @const
*/
this.SIZE = 17;
/**
* Bubble UI (if visible).
* @type {?Bubble}
* @protected
*/
this.bubble_ = null;
/**
* Absolute coordinate of icon's center.
* @type {?Coordinate}
* @protected
*/
this.iconXY_ = null;
}
/**
* Create the icon on the block.
*/
createIcon() {
if (this.iconGroup_) {
// Icon already exists.
return;
}
/* Here's the markup that will be generated:
<g class="blocklyIconGroup">
...
</g>
*/
this.iconGroup_ =
dom.createSvgElement(Svg.G, {'class': 'blocklyIconGroup'}, null);
if (this.block_.isInFlyout) {
dom.addClass(
/** @type {!Element} */ (this.iconGroup_),
'blocklyIconGroupReadonly');
}
this.drawIcon_(this.iconGroup_);
this.block_.getSvgRoot().appendChild(this.iconGroup_);
browserEvents.conditionalBind(
this.iconGroup_, 'mouseup', this, this.iconClick_);
this.updateEditable();
}
/**
* Dispose of this icon.
*/
dispose() {
// Dispose of and unlink the icon.
dom.removeNode(this.iconGroup_);
this.iconGroup_ = null;
// Dispose of and unlink the bubble.
this.setVisible(false);
this.block_ = null;
}
/**
* Add or remove the UI indicating if this icon may be clicked or not.
*/
updateEditable() {
// No-op on the base class.
}
/**
* Is the associated bubble visible?
* @return {boolean} True if the bubble is visible.
*/
isVisible() {
return !!this.bubble_;
}
/**
* Clicking on the icon toggles if the bubble is visible.
* @param {!Event} e Mouse click event.
* @protected
*/
this.block_ = block;
iconClick_(e) {
if (this.block_.workspace.isDragging()) {
// Drag operation is concluding. Don't open the editor.
return;
}
if (!this.block_.isInFlyout && !browserEvents.isRightButton(e)) {
this.setVisible(!this.isVisible());
}
}
/**
* The icon SVG group.
* @type {?SVGGElement}
* Change the colour of the associated bubble to match its block.
*/
this.iconGroup_ = null;
};
/**
* Does this icon get hidden when the block is collapsed.
*/
Icon.prototype.collapseHidden = true;
/**
* Height and width of icons.
* @const
*/
Icon.prototype.SIZE = 17;
/**
* Bubble UI (if visible).
* @type {?Bubble}
* @protected
*/
Icon.prototype.bubble_ = null;
/**
* Absolute coordinate of icon's center.
* @type {?Coordinate}
* @protected
*/
Icon.prototype.iconXY_ = null;
/**
* Create the icon on the block.
*/
Icon.prototype.createIcon = function() {
if (this.iconGroup_) {
// Icon already exists.
return;
applyColour() {
if (this.isVisible()) {
this.bubble_.setColour(this.block_.style.colourPrimary);
}
}
/* Here's the markup that will be generated:
<g class="blocklyIconGroup">
...
</g>
*/
this.iconGroup_ =
dom.createSvgElement(Svg.G, {'class': 'blocklyIconGroup'}, null);
if (this.block_.isInFlyout) {
dom.addClass(
/** @type {!Element} */ (this.iconGroup_), 'blocklyIconGroupReadonly');
/**
* Notification that the icon has moved. Update the arrow accordingly.
* @param {!Coordinate} xy Absolute location in workspace coordinates.
*/
setIconLocation(xy) {
this.iconXY_ = xy;
if (this.isVisible()) {
this.bubble_.setAnchorLocation(xy);
}
}
this.drawIcon_(this.iconGroup_);
this.block_.getSvgRoot().appendChild(this.iconGroup_);
browserEvents.conditionalBind(
this.iconGroup_, 'mouseup', this, this.iconClick_);
this.updateEditable();
};
/**
* Dispose of this icon.
*/
Icon.prototype.dispose = function() {
// Dispose of and unlink the icon.
dom.removeNode(this.iconGroup_);
this.iconGroup_ = null;
// Dispose of and unlink the bubble.
this.setVisible(false);
this.block_ = null;
};
/**
* Add or remove the UI indicating if this icon may be clicked or not.
*/
Icon.prototype.updateEditable = function() {
// No-op on the base class.
};
/**
* Is the associated bubble visible?
* @return {boolean} True if the bubble is visible.
*/
Icon.prototype.isVisible = function() {
return !!this.bubble_;
};
/**
* Clicking on the icon toggles if the bubble is visible.
* @param {!Event} e Mouse click event.
* @protected
*/
Icon.prototype.iconClick_ = function(e) {
if (this.block_.workspace.isDragging()) {
// Drag operation is concluding. Don't open the editor.
return;
/**
* Notification that the icon has moved, but we don't really know where.
* Recompute the icon's location from scratch.
*/
computeIconLocation() {
// Find coordinates for the centre of the icon and update the arrow.
const blockXY = this.block_.getRelativeToSurfaceXY();
const iconXY = svgMath.getRelativeXY(
/** @type {!SVGElement} */ (this.iconGroup_));
const newXY = new Coordinate(
blockXY.x + iconXY.x + this.SIZE / 2,
blockXY.y + iconXY.y + this.SIZE / 2);
if (!Coordinate.equals(this.getIconLocation(), newXY)) {
this.setIconLocation(newXY);
}
}
if (!this.block_.isInFlyout && !browserEvents.isRightButton(e)) {
this.setVisible(!this.isVisible());
/**
* Returns the center of the block's icon relative to the surface.
* @return {?Coordinate} Object with x and y properties in
* workspace coordinates.
*/
getIconLocation() {
return this.iconXY_;
}
};
/**
* Change the colour of the associated bubble to match its block.
*/
Icon.prototype.applyColour = function() {
if (this.isVisible()) {
this.bubble_.setColour(this.block_.style.colourPrimary);
/**
* Get the size of the icon as used for rendering.
* This differs from the actual size of the icon, because it bulges slightly
* out of its row rather than increasing the height of its row.
* @return {!Size} Height and width.
*/
getCorrectedSize() {
// TODO (#2562): Remove getCorrectedSize.
return new Size(this.SIZE, this.SIZE - 2);
}
};
/**
* Notification that the icon has moved. Update the arrow accordingly.
* @param {!Coordinate} xy Absolute location in workspace coordinates.
*/
Icon.prototype.setIconLocation = function(xy) {
this.iconXY_ = xy;
if (this.isVisible()) {
this.bubble_.setAnchorLocation(xy);
/**
* Draw the icon.
* @param {!Element} _group The icon group.
* @protected
*/
drawIcon_(_group) {
// No-op on base class.
}
};
/**
* Notification that the icon has moved, but we don't really know where.
* Recompute the icon's location from scratch.
*/
Icon.prototype.computeIconLocation = function() {
// Find coordinates for the centre of the icon and update the arrow.
const blockXY = this.block_.getRelativeToSurfaceXY();
const iconXY = svgMath.getRelativeXY(
/** @type {!SVGElement} */ (this.iconGroup_));
const newXY = new Coordinate(
blockXY.x + iconXY.x + this.SIZE / 2,
blockXY.y + iconXY.y + this.SIZE / 2);
if (!Coordinate.equals(this.getIconLocation(), newXY)) {
this.setIconLocation(newXY);
/**
* Show or hide the icon.
* @param {boolean} _visible True if the icon should be visible.
*/
setVisible(_visible) {
// No-op on base class
}
};
/**
* Returns the center of the block's icon relative to the surface.
* @return {?Coordinate} Object with x and y properties in
* workspace coordinates.
*/
Icon.prototype.getIconLocation = function() {
return this.iconXY_;
};
/**
* Get the size of the icon as used for rendering.
* This differs from the actual size of the icon, because it bulges slightly
* out of its row rather than increasing the height of its row.
* @return {!Size} Height and width.
*/
// TODO (#2562): Remove getCorrectedSize.
Icon.prototype.getCorrectedSize = function() {
return new Size(Icon.prototype.SIZE, Icon.prototype.SIZE - 2);
};
/**
* Draw the icon.
* @param {!Element} group The icon group.
* @protected
*/
Icon.prototype.drawIcon_;
/**
* Show or hide the icon.
* @param {boolean} visible True if the icon should be visible.
*/
Icon.prototype.setVisible;
}
exports.Icon = Icon;

View File

@@ -24,11 +24,11 @@ const browserEvents = goog.require('Blockly.browserEvents');
const bumpObjects = goog.require('Blockly.bumpObjects');
const common = goog.require('Blockly.common');
const dom = goog.require('Blockly.utils.dom');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const userAgent = goog.require('Blockly.utils.userAgent');
const {BlockDragSurfaceSvg} = goog.require('Blockly.BlockDragSurfaceSvg');
/* eslint-disable-next-line no-unused-vars */
const {BlocklyOptions} = goog.requireType('Blockly.BlocklyOptions');
const {DropDownDiv} = goog.require('Blockly.DropDownDiv');
const {Grid} = goog.require('Blockly.Grid');
const {Msg} = goog.require('Blockly.Msg');
const {Options} = goog.require('Blockly.Options');
@@ -59,7 +59,8 @@ const inject = function(container, opt_options) {
}
const options =
new Options(opt_options || (/** @type {!BlocklyOptions} */ ({})));
const subContainer = document.createElement('div');
const subContainer =
/** @type {!HTMLDivElement} */ (document.createElement('div'));
subContainer.className = 'injectionDiv';
subContainer.tabIndex = 0;
aria.setState(subContainer, aria.State.LABEL, Msg['WORKSPACE_ARIA_LABEL']);
@@ -192,7 +193,7 @@ const createMainWorkspace = function(
// The SVG is now fully assembled.
common.svgResize(mainWorkspace);
WidgetDiv.createDom();
DropDownDiv.createDom();
dropDownDiv.createDom();
Tooltip.createDom();
return mainWorkspace;
};
@@ -274,7 +275,8 @@ const init = function(mainWorkspace) {
// TODO (https://github.com/google/blockly/issues/1998) handle cases where there
// are multiple workspaces and non-main workspaces are able to accept input.
const onKeyDown = function(e) {
const mainWorkspace = common.getMainWorkspace();
const mainWorkspace =
/** @type {!WorkspaceSvg} */ (common.getMainWorkspace());
if (!mainWorkspace) {
return;
}
@@ -337,7 +339,7 @@ const bindDocumentEvents = function() {
/**
* Load sounds for the given workspace.
* @param {string} pathToMedia The path to the media directory.
* @param {!Workspace} workspace The workspace to load sounds for.
* @param {!WorkspaceSvg} workspace The workspace to load sounds for.
*/
const loadSounds = function(pathToMedia, workspace) {
const audioMgr = workspace.getAudioManager();

View File

@@ -15,18 +15,6 @@
*/
goog.module('Blockly.Input');
/**
* Enum for alignment of inputs.
* @enum {number}
* @alias Blockly.Input.Align
*/
const Align = {
LEFT: -1,
CENTRE: 0,
RIGHT: 1,
};
exports.Align = Align;
const fieldRegistry = goog.require('Blockly.fieldRegistry');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
@@ -42,285 +30,306 @@ const {inputTypes} = goog.require('Blockly.inputTypes');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldLabel');
/**
* Class for an input with an optional field.
* @param {number} type The type of the input.
* @param {string} name Language-neutral identifier which may used to find this
* input again.
* @param {!Block} block The block containing this input.
* @param {Connection} connection Optional connection for this input.
* @constructor
* @alias Blockly.Input
*/
const Input = function(type, name, block, connection) {
if (type !== inputTypes.DUMMY && !name) {
throw Error('Value inputs and statement inputs must have non-empty name.');
}
/** @type {number} */
this.type = type;
/** @type {string} */
this.name = name;
class Input {
/**
* @type {!Block}
* @private
* @param {number} type The type of the input.
* @param {string} name Language-neutral identifier which may used to find
* this input again.
* @param {!Block} block The block containing this input.
* @param {Connection} connection Optional connection for this input.
*/
this.sourceBlock_ = block;
/** @type {Connection} */
this.connection = connection;
/** @type {!Array<!Field>} */
this.fieldRow = [];
};
constructor(type, name, block, connection) {
if (type !== inputTypes.DUMMY && !name) {
throw Error(
'Value inputs and statement inputs must have non-empty name.');
}
/** @type {number} */
this.type = type;
/** @type {string} */
this.name = name;
/**
* @type {!Block}
* @private
*/
this.sourceBlock_ = block;
/** @type {Connection} */
this.connection = connection;
/** @type {!Array<!Field>} */
this.fieldRow = [];
/**
* Alignment of input's fields (left, right or centre).
* @type {number}
*/
Input.prototype.align = Align.LEFT;
/**
* Alignment of input's fields (left, right or centre).
* @type {number}
*/
this.align = Align.LEFT;
/**
* Is the input visible?
* @type {boolean}
* @private
*/
Input.prototype.visible_ = true;
/**
* Get the source block for this input.
* @return {?Block} The source block, or null if there is none.
*/
Input.prototype.getSourceBlock = function() {
return this.sourceBlock_;
};
/**
* Add a field (or label from string), and all prefix and suffix fields, to the
* end of the input's field row.
* @param {string|!Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to find
* this field again. Should be unique to the host block.
* @return {!Input} The input being append to (to allow chaining).
*/
Input.prototype.appendField = function(field, opt_name) {
this.insertFieldAt(this.fieldRow.length, field, opt_name);
return this;
};
/**
* Inserts a field (or label from string), and all prefix and suffix fields, at
* the location of the input's field row.
* @param {number} index The index at which to insert field.
* @param {string|!Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to find
* this field again. Should be unique to the host block.
* @return {number} The index following the last inserted field.
*/
Input.prototype.insertFieldAt = function(index, field, opt_name) {
if (index < 0 || index > this.fieldRow.length) {
throw Error('index ' + index + ' out of bounds.');
/**
* Is the input visible?
* @type {boolean}
* @private
*/
this.visible_ = true;
}
// Falsy field values don't generate a field, unless the field is an empty
// string and named.
if (!field && !(field === '' && opt_name)) {
/**
* Get the source block for this input.
* @return {?Block} The source block, or null if there is none.
*/
getSourceBlock() {
return this.sourceBlock_;
}
/**
* Add a field (or label from string), and all prefix and suffix fields, to
* the end of the input's field row.
* @param {string|!Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to
* find this field again. Should be unique to the host block.
* @return {!Input} The input being append to (to allow chaining).
*/
appendField(field, opt_name) {
this.insertFieldAt(this.fieldRow.length, field, opt_name);
return this;
}
/**
* Inserts a field (or label from string), and all prefix and suffix fields,
* at the location of the input's field row.
* @param {number} index The index at which to insert field.
* @param {string|!Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to
* find this field again. Should be unique to the host block.
* @return {number} The index following the last inserted field.
*/
insertFieldAt(index, field, opt_name) {
if (index < 0 || index > this.fieldRow.length) {
throw Error('index ' + index + ' out of bounds.');
}
// Falsy field values don't generate a field, unless the field is an empty
// string and named.
if (!field && !(field === '' && opt_name)) {
return index;
}
// Generate a FieldLabel when given a plain text field.
if (typeof field === 'string') {
field = /** @type {!Field} **/ (fieldRegistry.fromJson({
'type': 'field_label',
'text': field,
}));
}
field.setSourceBlock(this.sourceBlock_);
if (this.sourceBlock_.rendered) {
field.init();
field.applyColour();
}
field.name = opt_name;
field.setVisible(this.isVisible());
if (field.prefixField) {
// Add any prefix.
index = this.insertFieldAt(index, field.prefixField);
}
// Add the field to the field row.
this.fieldRow.splice(index, 0, field);
index++;
if (field.suffixField) {
// Add any suffix.
index = this.insertFieldAt(index, field.suffixField);
}
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
// Adding a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
}
return index;
}
// Generate a FieldLabel when given a plain text field.
if (typeof field === 'string') {
field = /** @type {!Field} **/ (fieldRegistry.fromJson({
'type': 'field_label',
'text': field,
}));
}
field.setSourceBlock(this.sourceBlock_);
if (this.sourceBlock_.rendered) {
field.init();
field.applyColour();
}
field.name = opt_name;
field.setVisible(this.isVisible());
if (field.prefixField) {
// Add any prefix.
index = this.insertFieldAt(index, field.prefixField);
}
// Add the field to the field row.
this.fieldRow.splice(index, 0, field);
index++;
if (field.suffixField) {
// Add any suffix.
index = this.insertFieldAt(index, field.suffixField);
}
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
// Adding a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
}
return index;
};
/**
* Remove a field from this input.
* @param {string} name The name of the field.
* @param {boolean=} opt_quiet True to prevent an error if field is not present.
* @return {boolean} True if operation succeeds, false if field is not present
* and opt_quiet is true.
* @throws {Error} if the field is not present and opt_quiet is false.
*/
Input.prototype.removeField = function(name, opt_quiet) {
for (let i = 0, field; (field = this.fieldRow[i]); i++) {
if (field.name === name) {
field.dispose();
this.fieldRow.splice(i, 1);
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
// Removing a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
/**
* Remove a field from this input.
* @param {string} name The name of the field.
* @param {boolean=} opt_quiet True to prevent an error if field is not
* present.
* @return {boolean} True if operation succeeds, false if field is not present
* and opt_quiet is true.
* @throws {Error} if the field is not present and opt_quiet is false.
*/
removeField(name, opt_quiet) {
for (let i = 0, field; (field = this.fieldRow[i]); i++) {
if (field.name === name) {
field.dispose();
this.fieldRow.splice(i, 1);
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
// Removing a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
}
return true;
}
return true;
}
if (opt_quiet) {
return false;
}
throw Error('Field "' + name + '" not found.');
}
if (opt_quiet) {
return false;
/**
* Gets whether this input is visible or not.
* @return {boolean} True if visible.
*/
isVisible() {
return this.visible_;
}
throw Error('Field "' + name + '" not found.');
};
/**
* Gets whether this input is visible or not.
* @return {boolean} True if visible.
*/
Input.prototype.isVisible = function() {
return this.visible_;
};
/**
* Sets whether this input is visible or not.
* Should only be used to collapse/uncollapse a block.
* @param {boolean} visible True if visible.
* @return {!Array<!BlockSvg>} List of blocks to render.
* @package
*/
setVisible(visible) {
// Note: Currently there are only unit tests for block.setCollapsed()
// because this function is package. If this function goes back to being a
// public API tests (lots of tests) should be added.
let renderList = [];
if (this.visible_ === visible) {
return renderList;
}
this.visible_ = visible;
/**
* Sets whether this input is visible or not.
* Should only be used to collapse/uncollapse a block.
* @param {boolean} visible True if visible.
* @return {!Array<!BlockSvg>} List of blocks to render.
* @package
*/
Input.prototype.setVisible = function(visible) {
// Note: Currently there are only unit tests for block.setCollapsed()
// because this function is package. If this function goes back to being a
// public API tests (lots of tests) should be added.
let renderList = [];
if (this.visible_ === visible) {
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
field.setVisible(visible);
}
if (this.connection) {
this.connection =
/** @type {!RenderedConnection} */ (this.connection);
// Has a connection.
if (visible) {
renderList = this.connection.startTrackingAll();
} else {
this.connection.stopTrackingAll();
}
const child = this.connection.targetBlock();
if (child) {
child.getSvgRoot().style.display = visible ? 'block' : 'none';
}
}
return renderList;
}
this.visible_ = visible;
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
field.setVisible(visible);
}
if (this.connection) {
this.connection =
/** @type {!RenderedConnection} */ (this.connection);
// Has a connection.
if (visible) {
renderList = this.connection.startTrackingAll();
} else {
this.connection.stopTrackingAll();
}
const child = this.connection.targetBlock();
if (child) {
child.getSvgRoot().style.display = visible ? 'block' : 'none';
/**
* Mark all fields on this input as dirty.
* @package
*/
markDirty() {
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
field.markDirty();
}
}
return renderList;
};
/**
* Change a connection's compatibility.
* @param {string|Array<string>|null} check Compatible value type or
* list of value types. Null if all types are compatible.
* @return {!Input} The input being modified (to allow chaining).
*/
setCheck(check) {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
this.connection.setCheck(check);
return this;
}
/**
* Change the alignment of the connection's field(s).
* @param {number} align One of the values of Align
* In RTL mode directions are reversed, and Align.RIGHT aligns to the left.
* @return {!Input} The input being modified (to allow chaining).
*/
setAlign(align) {
this.align = align;
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
}
return this;
}
/**
* Changes the connection's shadow block.
* @param {?Element} shadow DOM representation of a block or null.
* @return {!Input} The input being modified (to allow chaining).
*/
setShadowDom(shadow) {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
this.connection.setShadowDom(shadow);
return this;
}
/**
* Returns the XML representation of the connection's shadow block.
* @return {?Element} Shadow DOM representation of a block or null.
*/
getShadowDom() {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
return this.connection.getShadowDom();
}
/**
* Initialize the fields on this input.
*/
init() {
if (!this.sourceBlock_.workspace.rendered) {
return; // Headless blocks don't need fields initialized.
}
for (let i = 0; i < this.fieldRow.length; i++) {
this.fieldRow[i].init();
}
}
/**
* Sever all links to this input.
* @suppress {checkTypes}
*/
dispose() {
for (let i = 0, field; (field = this.fieldRow[i]); i++) {
field.dispose();
}
if (this.connection) {
this.connection.dispose();
}
this.sourceBlock_ = null;
}
}
/**
* Mark all fields on this input as dirty.
* @package
* Enum for alignment of inputs.
* @enum {number}
* @alias Blockly.Input.Align
*/
Input.prototype.markDirty = function() {
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
field.markDirty();
}
const Align = {
LEFT: -1,
CENTRE: 0,
RIGHT: 1,
};
exports.Align = Align;
/**
* Change a connection's compatibility.
* @param {string|Array<string>|null} check Compatible value type or
* list of value types. Null if all types are compatible.
* @return {!Input} The input being modified (to allow chaining).
*/
Input.prototype.setCheck = function(check) {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
this.connection.setCheck(check);
return this;
};
/**
* Change the alignment of the connection's field(s).
* @param {number} align One of the values of Align
* In RTL mode directions are reversed, and Align.RIGHT aligns to the left.
* @return {!Input} The input being modified (to allow chaining).
*/
Input.prototype.setAlign = function(align) {
this.align = align;
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
}
return this;
};
/**
* Changes the connection's shadow block.
* @param {?Element} shadow DOM representation of a block or null.
* @return {!Input} The input being modified (to allow chaining).
*/
Input.prototype.setShadowDom = function(shadow) {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
this.connection.setShadowDom(shadow);
return this;
};
/**
* Returns the XML representation of the connection's shadow block.
* @return {?Element} Shadow DOM representation of a block or null.
*/
Input.prototype.getShadowDom = function() {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
return this.connection.getShadowDom();
};
/**
* Initialize the fields on this input.
*/
Input.prototype.init = function() {
if (!this.sourceBlock_.workspace.rendered) {
return; // Headless blocks don't need fields initialized.
}
for (let i = 0; i < this.fieldRow.length; i++) {
this.fieldRow[i].init();
}
};
/**
* Sever all links to this input.
* @suppress {checkTypes}
*/
Input.prototype.dispose = function() {
for (let i = 0, field; (field = this.fieldRow[i]); i++) {
field.dispose();
}
if (this.connection) {
this.connection.dispose();
}
this.sourceBlock_ = null;
};
// Add Align to Input so that `Blockly.Input.Align` is publicly accessible.
Input.Align = Align;
exports.Input = Input;

File diff suppressed because it is too large Load Diff

View File

@@ -21,76 +21,6 @@ goog.module('Blockly.internalConstants');
const {ConnectionType} = goog.require('Blockly.ConnectionType');
/**
* The multiplier for scroll wheel deltas using the line delta mode.
* @type {number}
* @alias Blockly.internalConstants.LINE_MODE_MULTIPLIER
*/
const LINE_MODE_MULTIPLIER = 40;
exports.LINE_MODE_MULTIPLIER = LINE_MODE_MULTIPLIER;
/**
* The multiplier for scroll wheel deltas using the page delta mode.
* @type {number}
* @alias Blockly.internalConstants.PAGE_MODE_MULTIPLIER
*/
const PAGE_MODE_MULTIPLIER = 125;
exports.PAGE_MODE_MULTIPLIER = PAGE_MODE_MULTIPLIER;
/**
* Number of pixels the mouse must move before a drag starts.
* @alias Blockly.internalConstants.DRAG_RADIUS
*/
const DRAG_RADIUS = 5;
exports.DRAG_RADIUS = DRAG_RADIUS;
/**
* Number of pixels the mouse must move before a drag/scroll starts from the
* flyout. Because the drag-intention is determined when this is reached, it is
* larger than DRAG_RADIUS so that the drag-direction is clearer.
* @alias Blockly.internalConstants.FLYOUT_DRAG_RADIUS
*/
const FLYOUT_DRAG_RADIUS = 10;
exports.FLYOUT_DRAG_RADIUS = FLYOUT_DRAG_RADIUS;
/**
* Maximum misalignment between connections for them to snap together.
* @alias Blockly.internalConstants.SNAP_RADIUS
*/
const SNAP_RADIUS = 28;
exports.SNAP_RADIUS = SNAP_RADIUS;
/**
* Maximum misalignment between connections for them to snap together,
* when a connection is already highlighted.
* @alias Blockly.internalConstants.CONNECTING_SNAP_RADIUS
*/
const CONNECTING_SNAP_RADIUS = SNAP_RADIUS;
exports.CONNECTING_SNAP_RADIUS = CONNECTING_SNAP_RADIUS;
/**
* How much to prefer staying connected to the current connection over moving to
* a new connection. The current previewed connection is considered to be this
* much closer to the matching connection on the block than it actually is.
* @alias Blockly.internalConstants.CURRENT_CONNECTION_PREFERENCE
*/
const CURRENT_CONNECTION_PREFERENCE = 8;
exports.CURRENT_CONNECTION_PREFERENCE = CURRENT_CONNECTION_PREFERENCE;
/**
* Delay in ms between trigger and bumping unconnected block out of alignment.
* @alias Blockly.internalConstants.BUMP_DELAY
*/
const BUMP_DELAY = 250;
exports.BUMP_DELAY = BUMP_DELAY;
/**
* Maximum randomness in workspace units for bumping a block.
* @alias Blockly.internalConstants.BUMP_RANDOMNESS
*/
const BUMP_RANDOMNESS = 10;
exports.BUMP_RANDOMNESS = BUMP_RANDOMNESS;
/**
* Number of characters to truncate a collapsed block to.
* @alias Blockly.internalConstants.COLLAPSE_CHARS
@@ -98,21 +28,6 @@ exports.BUMP_RANDOMNESS = BUMP_RANDOMNESS;
const COLLAPSE_CHARS = 30;
exports.COLLAPSE_CHARS = COLLAPSE_CHARS;
/**
* Length in ms for a touch to become a long press.
* @alias Blockly.internalConstants.LONGPRESS
*/
const LONGPRESS = 750;
exports.LONGPRESS = LONGPRESS;
/**
* Prevent a sound from playing if another sound preceded it within this many
* milliseconds.
* @alias Blockly.internalConstants.SOUND_LIMIT
*/
const SOUND_LIMIT = 100;
exports.SOUND_LIMIT = SOUND_LIMIT;
/**
* When dragging a block out of a stack, split the stack in two (true), or drag
* out the block healing the stack (false).
@@ -121,50 +36,6 @@ exports.SOUND_LIMIT = SOUND_LIMIT;
const DRAG_STACK = true;
exports.DRAG_STACK = DRAG_STACK;
/**
* Sprited icons and images.
* @alias Blockly.internalConstants.SPRITE
*/
const SPRITE = {
width: 96,
height: 124,
url: 'sprites.png',
};
exports.SPRITE = SPRITE;
/**
* ENUM for no drag operation.
* @const
* @alias Blockly.internalConstants.DRAG_NONE
*/
const DRAG_NONE = 0;
exports.DRAG_NONE = DRAG_NONE;
/**
* ENUM for inside the sticky DRAG_RADIUS.
* @const
* @alias Blockly.internalConstants.DRAG_STICKY
*/
const DRAG_STICKY = 1;
exports.DRAG_STICKY = DRAG_STICKY;
/**
* ENUM for inside the non-sticky DRAG_RADIUS, for differentiating between
* clicks and drags.
* @const
* @alias Blockly.internalConstants.DRAG_BEGIN
*/
const DRAG_BEGIN = 1;
exports.DRAG_BEGIN = DRAG_BEGIN;
/**
* ENUM for freely draggable (outside the DRAG_RADIUS, if one applies).
* @const
* @alias Blockly.internalConstants.DRAG_FREE
*/
const DRAG_FREE = 2;
exports.DRAG_FREE = DRAG_FREE;
/**
* Lookup table for determining the opposite type of a connection.
* @const

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,6 @@
*/
goog.module('Blockly.BasicCursor');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {ASTNode} = goog.require('Blockly.ASTNode');
const {Cursor} = goog.require('Blockly.Cursor');
@@ -27,14 +26,192 @@ const {Cursor} = goog.require('Blockly.Cursor');
* Class for a basic cursor.
* This will allow the user to get to all nodes in the AST by hitting next or
* previous.
* @constructor
* @extends {Cursor}
* @alias Blockly.BasicCursor
*/
const BasicCursor = function() {
BasicCursor.superClass_.constructor.call(this);
};
object.inherits(BasicCursor, Cursor);
class BasicCursor extends Cursor {
/**
* @alias Blockly.BasicCursor
*/
constructor() {
super();
}
/**
* Find the next node in the pre order traversal.
* @return {?ASTNode} The next node, or null if the current node is
* not set or there is no next value.
* @override
*/
next() {
const curNode = this.getCurNode();
if (!curNode) {
return null;
}
const newNode = this.getNextNode_(curNode, this.validNode_);
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
}
/**
* For a basic cursor we only have the ability to go next and previous, so
* in will also allow the user to get to the next node in the pre order
* traversal.
* @return {?ASTNode} The next node, or null if the current node is
* not set or there is no next value.
* @override
*/
in() {
return this.next();
}
/**
* Find the previous node in the pre order traversal.
* @return {?ASTNode} The previous node, or null if the current node
* is not set or there is no previous value.
* @override
*/
prev() {
const curNode = this.getCurNode();
if (!curNode) {
return null;
}
const newNode = this.getPreviousNode_(curNode, this.validNode_);
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
}
/**
* For a basic cursor we only have the ability to go next and previous, so
* out will allow the user to get to the previous node in the pre order
* traversal.
* @return {?ASTNode} The previous node, or null if the current node is
* not set or there is no previous value.
* @override
*/
out() {
return this.prev();
}
/**
* Uses pre order traversal to navigate the Blockly AST. This will allow
* a user to easily navigate the entire Blockly AST without having to go in
* and out levels on the tree.
* @param {?ASTNode} node The current position in the AST.
* @param {!function(ASTNode) : boolean} isValid A function true/false
* depending on whether the given node should be traversed.
* @return {?ASTNode} The next node in the traversal.
* @protected
*/
getNextNode_(node, isValid) {
if (!node) {
return null;
}
const newNode = node.in() || node.next();
if (isValid(newNode)) {
return newNode;
} else if (newNode) {
return this.getNextNode_(newNode, isValid);
}
const siblingOrParent = this.findSiblingOrParent_(node.out());
if (isValid(siblingOrParent)) {
return siblingOrParent;
} else if (siblingOrParent) {
return this.getNextNode_(siblingOrParent, isValid);
}
return null;
}
/**
* Reverses the pre order traversal in order to find the previous node. This
* will allow a user to easily navigate the entire Blockly AST without having
* to go in and out levels on the tree.
* @param {?ASTNode} node The current position in the AST.
* @param {!function(ASTNode) : boolean} isValid A function true/false
* depending on whether the given node should be traversed.
* @return {?ASTNode} The previous node in the traversal or null if no
* previous node exists.
* @protected
*/
getPreviousNode_(node, isValid) {
if (!node) {
return null;
}
let newNode = node.prev();
if (newNode) {
newNode = this.getRightMostChild_(newNode);
} else {
newNode = node.out();
}
if (isValid(newNode)) {
return newNode;
} else if (newNode) {
return this.getPreviousNode_(newNode, isValid);
}
return null;
}
/**
* Decides what nodes to traverse and which ones to skip. Currently, it
* skips output, stack and workspace nodes.
* @param {?ASTNode} node The AST node to check whether it is valid.
* @return {boolean} True if the node should be visited, false otherwise.
* @protected
*/
validNode_(node) {
let isValid = false;
const type = node && node.getType();
if (type === ASTNode.types.OUTPUT || type === ASTNode.types.INPUT ||
type === ASTNode.types.FIELD || type === ASTNode.types.NEXT ||
type === ASTNode.types.PREVIOUS || type === ASTNode.types.WORKSPACE) {
isValid = true;
}
return isValid;
}
/**
* From the given node find either the next valid sibling or parent.
* @param {?ASTNode} node The current position in the AST.
* @return {?ASTNode} The parent AST node or null if there are no
* valid parents.
* @private
*/
findSiblingOrParent_(node) {
if (!node) {
return null;
}
const nextNode = node.next();
if (nextNode) {
return nextNode;
}
return this.findSiblingOrParent_(node.out());
}
/**
* Get the right most child of a node.
* @param {?ASTNode} node The node to find the right most child of.
* @return {?ASTNode} The right most child of the given node, or the node
* if no child exists.
* @private
*/
getRightMostChild_(node) {
if (!node.in()) {
return node;
}
let newNode = node.in();
while (newNode.next()) {
newNode = newNode.next();
}
return this.getRightMostChild_(newNode);
}
}
/**
* Name used for registering a basic cursor.
@@ -42,182 +219,6 @@ object.inherits(BasicCursor, Cursor);
*/
BasicCursor.registrationName = 'basicCursor';
/**
* Find the next node in the pre order traversal.
* @return {?ASTNode} The next node, or null if the current node is
* not set or there is no next value.
* @override
*/
BasicCursor.prototype.next = function() {
const curNode = this.getCurNode();
if (!curNode) {
return null;
}
const newNode = this.getNextNode_(curNode, this.validNode_);
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
};
/**
* For a basic cursor we only have the ability to go next and previous, so
* in will also allow the user to get to the next node in the pre order
* traversal.
* @return {?ASTNode} The next node, or null if the current node is
* not set or there is no next value.
* @override
*/
BasicCursor.prototype.in = function() {
return this.next();
};
/**
* Find the previous node in the pre order traversal.
* @return {?ASTNode} The previous node, or null if the current node
* is not set or there is no previous value.
* @override
*/
BasicCursor.prototype.prev = function() {
const curNode = this.getCurNode();
if (!curNode) {
return null;
}
const newNode = this.getPreviousNode_(curNode, this.validNode_);
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
};
/**
* For a basic cursor we only have the ability to go next and previous, so
* out will allow the user to get to the previous node in the pre order
* traversal.
* @return {?ASTNode} The previous node, or null if the current node is
* not set or there is no previous value.
* @override
*/
BasicCursor.prototype.out = function() {
return this.prev();
};
/**
* Uses pre order traversal to navigate the Blockly AST. This will allow
* a user to easily navigate the entire Blockly AST without having to go in
* and out levels on the tree.
* @param {?ASTNode} node The current position in the AST.
* @param {!function(ASTNode) : boolean} isValid A function true/false
* depending on whether the given node should be traversed.
* @return {?ASTNode} The next node in the traversal.
* @protected
*/
BasicCursor.prototype.getNextNode_ = function(node, isValid) {
if (!node) {
return null;
}
const newNode = node.in() || node.next();
if (isValid(newNode)) {
return newNode;
} else if (newNode) {
return this.getNextNode_(newNode, isValid);
}
const siblingOrParent = this.findSiblingOrParent_(node.out());
if (isValid(siblingOrParent)) {
return siblingOrParent;
} else if (siblingOrParent) {
return this.getNextNode_(siblingOrParent, isValid);
}
return null;
};
/**
* Reverses the pre order traversal in order to find the previous node. This
* will allow a user to easily navigate the entire Blockly AST without having to
* go in and out levels on the tree.
* @param {?ASTNode} node The current position in the AST.
* @param {!function(ASTNode) : boolean} isValid A function true/false
* depending on whether the given node should be traversed.
* @return {?ASTNode} The previous node in the traversal or null if no
* previous node exists.
* @protected
*/
BasicCursor.prototype.getPreviousNode_ = function(node, isValid) {
if (!node) {
return null;
}
let newNode = node.prev();
if (newNode) {
newNode = this.getRightMostChild_(newNode);
} else {
newNode = node.out();
}
if (isValid(newNode)) {
return newNode;
} else if (newNode) {
return this.getPreviousNode_(newNode, isValid);
}
return null;
};
/**
* Decides what nodes to traverse and which ones to skip. Currently, it
* skips output, stack and workspace nodes.
* @param {?ASTNode} node The AST node to check whether it is valid.
* @return {boolean} True if the node should be visited, false otherwise.
* @protected
*/
BasicCursor.prototype.validNode_ = function(node) {
let isValid = false;
const type = node && node.getType();
if (type === ASTNode.types.OUTPUT || type === ASTNode.types.INPUT ||
type === ASTNode.types.FIELD || type === ASTNode.types.NEXT ||
type === ASTNode.types.PREVIOUS || type === ASTNode.types.WORKSPACE) {
isValid = true;
}
return isValid;
};
/**
* From the given node find either the next valid sibling or parent.
* @param {?ASTNode} node The current position in the AST.
* @return {?ASTNode} The parent AST node or null if there are no
* valid parents.
* @private
*/
BasicCursor.prototype.findSiblingOrParent_ = function(node) {
if (!node) {
return null;
}
const nextNode = node.next();
if (nextNode) {
return nextNode;
}
return this.findSiblingOrParent_(node.out());
};
/**
* Get the right most child of a node.
* @param {?ASTNode} node The node to find the right most child of.
* @return {?ASTNode} The right most child of the given node, or the node
* if no child exists.
* @private
*/
BasicCursor.prototype.getRightMostChild_ = function(node) {
if (!node.in()) {
return node;
}
let newNode = node.in();
while (newNode.next()) {
newNode = newNode.next();
}
return this.getRightMostChild_(newNode);
};
registry.register(
registry.Type.CURSOR, BasicCursor.registrationName, BasicCursor);

View File

@@ -17,7 +17,6 @@
*/
goog.module('Blockly.Cursor');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const {ASTNode} = goog.require('Blockly.ASTNode');
const {Marker} = goog.require('Blockly.Marker');
@@ -25,117 +24,120 @@ const {Marker} = goog.require('Blockly.Marker');
/**
* Class for a cursor.
* A cursor controls how a user navigates the Blockly AST.
* @constructor
* @extends {Marker}
* @alias Blockly.Cursor
*/
const Cursor = function() {
Cursor.superClass_.constructor.call(this);
class Cursor extends Marker {
/**
* @alias Blockly.Cursor
*/
constructor() {
super();
/**
* @override
*/
this.type = 'cursor';
}
/**
* @override
* Find the next connection, field, or block.
* @return {ASTNode} The next element, or null if the current node is
* not set or there is no next value.
* @public
*/
this.type = 'cursor';
};
object.inherits(Cursor, Marker);
next() {
const curNode = this.getCurNode();
if (!curNode) {
return null;
}
/**
* Find the next connection, field, or block.
* @return {ASTNode} The next element, or null if the current node is
* not set or there is no next value.
* @public
*/
Cursor.prototype.next = function() {
const curNode = this.getCurNode();
if (!curNode) {
return null;
let newNode = curNode.next();
while (newNode && newNode.next() &&
(newNode.getType() === ASTNode.types.NEXT ||
newNode.getType() === ASTNode.types.BLOCK)) {
newNode = newNode.next();
}
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
}
let newNode = curNode.next();
while (newNode && newNode.next() &&
(newNode.getType() === ASTNode.types.NEXT ||
newNode.getType() === ASTNode.types.BLOCK)) {
newNode = newNode.next();
/**
* Find the in connection or field.
* @return {ASTNode} The in element, or null if the current node is
* not set or there is no in value.
* @public
*/
in() {
let curNode = this.getCurNode();
if (!curNode) {
return null;
}
// If we are on a previous or output connection, go to the block level
// before performing next operation.
if (curNode.getType() === ASTNode.types.PREVIOUS ||
curNode.getType() === ASTNode.types.OUTPUT) {
curNode = curNode.next();
}
const newNode = curNode.in();
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
}
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
};
/**
* Find the previous connection, field, or block.
* @return {ASTNode} The previous element, or null if the current node
* is not set or there is no previous value.
* @public
*/
prev() {
const curNode = this.getCurNode();
if (!curNode) {
return null;
}
let newNode = curNode.prev();
/**
* Find the in connection or field.
* @return {ASTNode} The in element, or null if the current node is
* not set or there is no in value.
* @public
*/
Cursor.prototype.in = function() {
let curNode = this.getCurNode();
if (!curNode) {
return null;
}
// If we are on a previous or output connection, go to the block level before
// performing next operation.
if (curNode.getType() === ASTNode.types.PREVIOUS ||
curNode.getType() === ASTNode.types.OUTPUT) {
curNode = curNode.next();
}
const newNode = curNode.in();
while (newNode && newNode.prev() &&
(newNode.getType() === ASTNode.types.NEXT ||
newNode.getType() === ASTNode.types.BLOCK)) {
newNode = newNode.prev();
}
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
};
/**
* Find the previous connection, field, or block.
* @return {ASTNode} The previous element, or null if the current node
* is not set or there is no previous value.
* @public
*/
Cursor.prototype.prev = function() {
const curNode = this.getCurNode();
if (!curNode) {
return null;
}
let newNode = curNode.prev();
while (newNode && newNode.prev() &&
(newNode.getType() === ASTNode.types.NEXT ||
newNode.getType() === ASTNode.types.BLOCK)) {
newNode = newNode.prev();
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
}
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
};
/**
* Find the out connection, field, or block.
* @return {ASTNode} The out element, or null if the current node is
* not set or there is no out value.
* @public
*/
out() {
const curNode = this.getCurNode();
if (!curNode) {
return null;
}
let newNode = curNode.out();
/**
* Find the out connection, field, or block.
* @return {ASTNode} The out element, or null if the current node is
* not set or there is no out value.
* @public
*/
Cursor.prototype.out = function() {
const curNode = this.getCurNode();
if (!curNode) {
return null;
}
let newNode = curNode.out();
if (newNode && newNode.getType() === ASTNode.types.BLOCK) {
newNode = newNode.prev() || newNode;
}
if (newNode && newNode.getType() === ASTNode.types.BLOCK) {
newNode = newNode.prev() || newNode;
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
}
if (newNode) {
this.setCurNode(newNode);
}
return newNode;
};
}
registry.register(registry.Type.CURSOR, registry.DEFAULT, Cursor);

View File

@@ -26,105 +26,109 @@ const {MarkerSvg} = goog.requireType('Blockly.blockRendering.MarkerSvg');
/**
* Class for a marker.
* This is used in keyboard navigation to save a location in the Blockly AST.
* @constructor
* @alias Blockly.Marker
*/
const Marker = function() {
class Marker {
/**
* The colour of the marker.
* @type {?string}
* Constructs a new Marker instance.
*/
this.colour = null;
constructor() {
/**
* The colour of the marker.
* @type {?string}
*/
this.colour = null;
/**
* The current location of the marker.
* @type {ASTNode}
* @private
*/
this.curNode_ = null;
/**
* The object in charge of drawing the visual representation of the current
* node.
* @type {MarkerSvg}
* @private
*/
this.drawer_ = null;
/**
* The type of the marker.
* @type {string}
*/
this.type = 'marker';
}
/**
* The current location of the marker.
* @type {ASTNode}
* @private
* Sets the object in charge of drawing the marker.
* @param {MarkerSvg} drawer The object in charge of
* drawing the marker.
*/
this.curNode_ = null;
setDrawer(drawer) {
this.drawer_ = drawer;
}
/**
* The object in charge of drawing the visual representation of the current
* node.
* @type {MarkerSvg}
* @private
* Get the current drawer for the marker.
* @return {MarkerSvg} The object in charge of drawing
* the marker.
*/
this.drawer_ = null;
getDrawer() {
return this.drawer_;
}
/**
* The type of the marker.
* @type {string}
* Gets the current location of the marker.
* @return {ASTNode} The current field, connection, or block the marker
* is on.
*/
this.type = 'marker';
};
/**
* Sets the object in charge of drawing the marker.
* @param {MarkerSvg} drawer The object in charge of
* drawing the marker.
*/
Marker.prototype.setDrawer = function(drawer) {
this.drawer_ = drawer;
};
/**
* Get the current drawer for the marker.
* @return {MarkerSvg} The object in charge of drawing
* the marker.
*/
Marker.prototype.getDrawer = function() {
return this.drawer_;
};
/**
* Gets the current location of the marker.
* @return {ASTNode} The current field, connection, or block the marker
* is on.
*/
Marker.prototype.getCurNode = function() {
return this.curNode_;
};
/**
* Set the location of the marker and call the update method.
* Setting isStack to true will only work if the newLocation is the top most
* output or previous connection on a stack.
* @param {ASTNode} newNode The new location of the marker.
*/
Marker.prototype.setCurNode = function(newNode) {
const oldNode = this.curNode_;
this.curNode_ = newNode;
if (this.drawer_) {
this.drawer_.draw(oldNode, this.curNode_);
getCurNode() {
return this.curNode_;
}
};
/**
* Redraw the current marker.
* @package
*/
Marker.prototype.draw = function() {
if (this.drawer_) {
this.drawer_.draw(this.curNode_, this.curNode_);
/**
* Set the location of the marker and call the update method.
* Setting isStack to true will only work if the newLocation is the top most
* output or previous connection on a stack.
* @param {ASTNode} newNode The new location of the marker.
*/
setCurNode(newNode) {
const oldNode = this.curNode_;
this.curNode_ = newNode;
if (this.drawer_) {
this.drawer_.draw(oldNode, this.curNode_);
}
}
};
/**
* Hide the marker SVG.
*/
Marker.prototype.hide = function() {
if (this.drawer_) {
this.drawer_.hide();
/**
* Redraw the current marker.
* @package
*/
draw() {
if (this.drawer_) {
this.drawer_.draw(this.curNode_, this.curNode_);
}
}
};
/**
* Dispose of this marker.
*/
Marker.prototype.dispose = function() {
if (this.getDrawer()) {
this.getDrawer().dispose();
/**
* Hide the marker SVG.
*/
hide() {
if (this.drawer_) {
this.drawer_.hide();
}
}
};
/**
* Dispose of this marker.
*/
dispose() {
if (this.getDrawer()) {
this.getDrawer().dispose();
}
}
}
exports.Marker = Marker;

View File

@@ -17,7 +17,6 @@
*/
goog.module('Blockly.TabNavigateCursor');
const object = goog.require('Blockly.utils.object');
const {ASTNode} = goog.require('Blockly.ASTNode');
const {BasicCursor} = goog.require('Blockly.BasicCursor');
/* eslint-disable-next-line no-unused-vars */
@@ -26,32 +25,28 @@ const {Field} = goog.requireType('Blockly.Field');
/**
* A cursor for navigating between tab navigable fields.
* @constructor
* @extends {BasicCursor}
* @alias Blockly.TabNavigateCursor
*/
const TabNavigateCursor = function() {
TabNavigateCursor.superClass_.constructor.call(this);
};
object.inherits(TabNavigateCursor, BasicCursor);
/**
* Skip all nodes except for tab navigable fields.
* @param {?ASTNode} node The AST node to check whether it is valid.
* @return {boolean} True if the node should be visited, false otherwise.
* @override
*/
TabNavigateCursor.prototype.validNode_ = function(node) {
let isValid = false;
const type = node && node.getType();
if (node) {
const location = /** @type {Field} */ (node.getLocation());
if (type === ASTNode.types.FIELD && location && location.isTabNavigable() &&
location.isClickable()) {
isValid = true;
class TabNavigateCursor extends BasicCursor {
/**
* Skip all nodes except for tab navigable fields.
* @param {?ASTNode} node The AST node to check whether it is valid.
* @return {boolean} True if the node should be visited, false otherwise.
* @override
*/
validNode_(node) {
let isValid = false;
const type = node && node.getType();
if (node) {
const location = /** @type {Field} */ (node.getLocation());
if (type === ASTNode.types.FIELD && location &&
location.isTabNavigable() && location.isClickable()) {
isValid = true;
}
}
return isValid;
}
return isValid;
};
}
exports.TabNavigateCursor = TabNavigateCursor;

Some files were not shown because too many files have changed in this diff Show More