mirror of
https://github.com/google/blockly.git
synced 2026-03-16 18:20:11 +01:00
Merge branch 'develop' into master_into_develop
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
*_compressed*.js
|
||||
blockly_uncompressed.js
|
||||
gulpfile.js
|
||||
/msg/*
|
||||
/build/*
|
||||
/dist/*
|
||||
/core/utils/global.js
|
||||
/tests/blocks/*
|
||||
/tests/themes/*
|
||||
/tests/compile/*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
47
blocks/blocks.js
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
99
closure/goog/goog.js
Normal 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.
|
||||
4157
core/block.js
4157
core/block.js
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
3434
core/block_svg.js
3434
core/block_svg.js
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
1718
core/bubble.js
1718
core/bubble.js
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
731
core/comment.js
731
core/comment.js
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
87
core/config.js
Normal 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;
|
||||
1227
core/connection.js
1227
core/connection.js
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
938
core/css.js
938
core/css.js
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
2252
core/field.js
2252
core/field.js
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
1050
core/field_colour.js
1050
core/field_colour.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 '
'. 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, ' ');
|
||||
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(/ /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 '
'. 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, ' ');
|
||||
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(/ /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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
2095
core/flyout_base.js
2095
core/flyout_base.js
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
1880
core/gesture.js
1880
core/gesture.js
File diff suppressed because it is too large
Load Diff
357
core/grid.js
357
core/grid.js
@@ -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;
|
||||
|
||||
335
core/icon.js
335
core/icon.js
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
537
core/input.js
537
core/input.js
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user