mirror of
https://github.com/google/blockly.git
synced 2026-06-16 16:15:14 +02:00
@@ -1,10 +1,7 @@
|
||||
*_compressed*.js
|
||||
blockly_uncompressed.js
|
||||
gulpfile.js
|
||||
/msg/*
|
||||
/build/*
|
||||
/dist/*
|
||||
/core/utils/global.js
|
||||
/tests/blocks/*
|
||||
/tests/themes/*
|
||||
/tests/compile/*
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "7.20211209.0"
|
||||
".": "8.0.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Changelog
|
||||
|
||||
## [8.0.0](https://github.com/google/blockly/compare/blockly-v7.20211209.0...blockly-v8.0.0) (2022-03-31)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* change paste to return the pasted thing to support keyboard nav (#5996)
|
||||
* **blocks:** ...and rename Blockly.blocks.all (blocks/all.js) to Blockly.libraryBlocks (blocks/blocks.js
|
||||
* * refactor(blocks): Make loopTypes a Set
|
||||
* allows previously internal constants to be configurable (#5897)
|
||||
* * refactor(blocks): Make loopTypes a Set
|
||||
* remove unused constants from internalConstants (#5889)
|
||||
|
||||
### Features
|
||||
|
||||
* add mocha failure messages to console output ([#5984](https://github.com/google/blockly/issues/5984)) ([7d250fa](https://github.com/google/blockly/commit/7d250fa9cfb30f95e7af523720b66c8b001df15c))
|
||||
* Allow developers to set a custom tooltip rendering function. ([#5956](https://github.com/google/blockly/issues/5956)) ([6841ccc](https://github.com/google/blockly/commit/6841ccc99fdbcc5f6d5a63bb36cb3b6ebd2be246))
|
||||
* **blocks:** Export block definitions ([#5908](https://github.com/google/blockly/issues/5908)) ([ffb8907](https://github.com/google/blockly/commit/ffb8907db8d0f11609c1fe14b2a450d3e639a871))
|
||||
* make mocha fail if it encounters 0 tests ([#5981](https://github.com/google/blockly/issues/5981)) ([0b2bf3a](https://github.com/google/blockly/commit/0b2bf3ae9d0c777f4d13d47692f5ae224dff1ec8))
|
||||
* **tests:** Add a test to validate `scripts/migration/renamings.js` ([#5980](https://github.com/google/blockly/issues/5980)) ([3c723f0](https://github.com/google/blockly/commit/3c723f0199b1f3b5eaac58f064b02d52b60d0ddb))
|
||||
* **tests:** Use official semver.org RegExp ([#5990](https://github.com/google/blockly/issues/5990)) ([afc4088](https://github.com/google/blockly/commit/afc4088ce278f97585f9ff5e65a921f7c4c65531))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Adds check for changedTouches ([#5869](https://github.com/google/blockly/issues/5869)) ([3f4f505](https://github.com/google/blockly/commit/3f4f5057919fdb4a329e9d2b15378c5c5831ae3b))
|
||||
* advanced playground and playground to work when hosted ([#6021](https://github.com/google/blockly/issues/6021)) ([364bf14](https://github.com/google/blockly/commit/364bf14ce6932f426591e3f53c1d066771ddcb8e))
|
||||
* always rename caller to legal name ([#6014](https://github.com/google/blockly/issues/6014)) ([c430800](https://github.com/google/blockly/commit/c4308007bc4b58d51adf1fda7b51ffa9f1d3f093))
|
||||
* **blocks:** correct the callType_ of procedures_defreturn ([#5974](https://github.com/google/blockly/issues/5974)) ([b34db5b](https://github.com/google/blockly/commit/b34db5bd01f7b532ebabc80264ca9fc804a76c75))
|
||||
* **build:** Correctly handle deep export paths in UMD wrapper ([#5945](https://github.com/google/blockly/issues/5945)) ([71ab146](https://github.com/google/blockly/commit/71ab146bc21aef9bdd6b2385c1df5f51e3ff5b58))
|
||||
* bumping a block after duplicate breaking undo ([#5844](https://github.com/google/blockly/issues/5844)) ([5204569](https://github.com/google/blockly/commit/5204569cff58c1ead7c15165a1351fa6a2ba2ad3))
|
||||
* change getCandidate_ and showInsertionMarker_ to be more dynamic ([#5722](https://github.com/google/blockly/issues/5722)) ([68d8113](https://github.com/google/blockly/commit/68d81132b851d20884ee9da41719fa62cdfce0ee))
|
||||
* change paste to return the pasted thing to support keyboard nav ([#5996](https://github.com/google/blockly/issues/5996)) ([20f1475](https://github.com/google/blockly/commit/20f1475afc1abf4b5e600219c2981150fc621ba5))
|
||||
* Change the truthy tests of width and height in WorkspaceSvg.setCachedParentSvgSize to actual comparisons with null so that zero value can be saved into the cache ([#5997](https://github.com/google/blockly/issues/5997)) ([fec44d9](https://github.com/google/blockly/commit/fec44d917e4b8475beba28e4769a50982425e887))
|
||||
* comments not being restored when dragging ([#6011](https://github.com/google/blockly/issues/6011)) ([85ce3b8](https://github.com/google/blockly/commit/85ce3b82c6c32e8a2a1608c6d604262ea0e5c38d))
|
||||
* convert the common renderer to an ES6 class ([#5978](https://github.com/google/blockly/issues/5978)) ([c1004be](https://github.com/google/blockly/commit/c1004be1f24debe1df1566e6067cf2f6769d51aa))
|
||||
* convert the Workspace class to an ES6 class ([#5977](https://github.com/google/blockly/issues/5977)) ([e2eaebe](https://github.com/google/blockly/commit/e2eaebec47b08a83eb36d0d04cefa254d1c5d666))
|
||||
* custom block context menus ([#5976](https://github.com/google/blockly/issues/5976)) ([8058df2](https://github.com/google/blockly/commit/8058df2a71dcecdc1190ae1d6f5dcccfafc980e8))
|
||||
* Don't throw if drag surface is empty. ([#5695](https://github.com/google/blockly/issues/5695)) ([769a25f](https://github.com/google/blockly/commit/769a25f4badffd2409ce19535344c98f5d8b01c9))
|
||||
* export Blockly.Names.NameType and Blockly.Input.Align correctly ([#6030](https://github.com/google/blockly/issues/6030)) ([2c15d00](https://github.com/google/blockly/commit/2c15d002ababcba7f34c526c05f231735e3e0169))
|
||||
* Export loopTypes from Blockly.blocks.loops ([#5900](https://github.com/google/blockly/issues/5900)) ([4f74210](https://github.com/google/blockly/commit/4f74210e74ef0b06216ab0f288268192674d69c9))
|
||||
* Export loopTypes from Blockly.blocks.loops ([#5900](https://github.com/google/blockly/issues/5900)) ([74ef1cb](https://github.com/google/blockly/commit/74ef1cbf521f7c6447ea9672ddbfe861d2292e5f))
|
||||
* Fix bug where workspace comments could not be created. ([#6024](https://github.com/google/blockly/issues/6024)) ([2cf8eb8](https://github.com/google/blockly/commit/2cf8eb87dcb029ba152b63b01ee7e4df431d1bb6))
|
||||
* Fix downloading screenshots on the playground. ([#6025](https://github.com/google/blockly/issues/6025)) ([ca6e590](https://github.com/google/blockly/commit/ca6e590101d511a8d98a5c5438af32ff6749e020))
|
||||
* fix keycodes type ([#5805](https://github.com/google/blockly/issues/5805)) ([0a96543](https://github.com/google/blockly/commit/0a96543a1179636e4efeb3c654c075952aca0c9f))
|
||||
* Fixed the label closure on demo/blockfactory ([#5833](https://github.com/google/blockly/issues/5833)) ([e8ea2e9](https://github.com/google/blockly/commit/e8ea2e9902fb9f642459e7341c3d59e19f359fca))
|
||||
* **generators:** Fix an operator precedence issue in the math_number_property generators to remove extra parentheses ([#5685](https://github.com/google/blockly/issues/5685)) ([a31003f](https://github.com/google/blockly/commit/a31003fab964e529152389029ec3126a3802851b))
|
||||
* incorrect module for event data in renamings database ([#6012](https://github.com/google/blockly/issues/6012)) ([e502eaa](https://github.com/google/blockly/commit/e502eaa6e1c88b2bb34e9a87917a15098b81cfa3))
|
||||
* Move [@alias](https://github.com/alias) onto classes instead of constructors ([#6003](https://github.com/google/blockly/issues/6003)) ([1647a32](https://github.com/google/blockly/commit/1647a3299ac48b5924f987015d8f3c47593922af))
|
||||
* move test helpers from samples into core ([#5969](https://github.com/google/blockly/issues/5969)) ([2edd228](https://github.com/google/blockly/commit/2edd22811752f05e16c68d593e5d1b809e24ed25))
|
||||
* move the dropdown div to a namespace instead of a class with only static properties ([#5979](https://github.com/google/blockly/issues/5979)) ([543cb8e](https://github.com/google/blockly/commit/543cb8e1b1c1a7fca5a1629f42f71c9b18e1a255))
|
||||
* msg imports in type definitions ([#5858](https://github.com/google/blockly/issues/5858)) ([07a75de](https://github.com/google/blockly/commit/07a75dee8de13b6c5a02959325a0155d413d6712))
|
||||
* opening/closing the mutators ([#6000](https://github.com/google/blockly/issues/6000)) ([243fc52](https://github.com/google/blockly/commit/243fc52a96e1089aad89ff6b642c6605d8f71afd))
|
||||
* playground access to Blockly ([9e1cda8](https://github.com/google/blockly/commit/9e1cda8f45cea1707c5a228d5ce79b4cd81566f8))
|
||||
* playground test blocks, text area listeners, and show/hide buttons ([#6015](https://github.com/google/blockly/issues/6015)) ([7abf3de](https://github.com/google/blockly/commit/7abf3de910a35e1a6086a3243570627a41e73339))
|
||||
* procedure param edits breaking undo ([#5845](https://github.com/google/blockly/issues/5845)) ([8a71f87](https://github.com/google/blockly/commit/8a71f879504503f4aec1140fe653d93602c664df))
|
||||
* re-expose HSV_VALUE and HSV_SATURATION as settable properties on Blockly ([#5821](https://github.com/google/blockly/issues/5821)) ([0e5f3ce](https://github.com/google/blockly/commit/0e5f3ce6074fbbb2923e9a62bffefeae0a813be8))
|
||||
* re-expose HSV_VALUE and HSV_SATURATION as settable properties on Blockly ([#5821](https://github.com/google/blockly/issues/5821)) ([6fc3316](https://github.com/google/blockly/commit/6fc3316309534270106050f0e1fecb7a09b8e62c))
|
||||
* revert "Delete events should animate when played ([#5919](https://github.com/google/blockly/issues/5919))" ([#6031](https://github.com/google/blockly/issues/6031)) ([c4a25eb](https://github.com/google/blockly/commit/c4a25eb3c432b0e8a9a18aae42839d163a177c48))
|
||||
* revert converting test helpers to es modules ([#5982](https://github.com/google/blockly/issues/5982)) ([01d4597](https://github.com/google/blockly/commit/01d45972d4df8b5e4afa4a19d93defb8961fea57))
|
||||
* setting null for a font style on a theme ([#5831](https://github.com/google/blockly/issues/5831)) ([835fb02](https://github.com/google/blockly/commit/835fb02343df0a4b9dab7704a4b3d8be8e9a497c))
|
||||
* **tests:** Enable --debug for test:compile:advanced; fix some errors ([#5959](https://github.com/google/blockly/issues/5959)) ([88334be](https://github.com/google/blockly/commit/88334bea80aa26c08705f16aba5e79dd708158f9))
|
||||
* **tests:** Enable `--debug` for `test:compile:advanced`; fix some errors (and demote the rest to warnings) ([#5983](https://github.com/google/blockly/issues/5983)) ([e11b583](https://github.com/google/blockly/commit/e11b5834e5e4e8fe991be32afb08eafa7c8adffd))
|
||||
* TypeScript exporting of the serialization functions ([#5890](https://github.com/google/blockly/issues/5890)) ([5d7c890](https://github.com/google/blockly/commit/5d7c890243ba7d0501514ba48778715097ce5a3b))
|
||||
* undo/redo for auto disabling if-return blocks ([#6018](https://github.com/google/blockly/issues/6018)) ([c7a359a](https://github.com/google/blockly/commit/c7a359a8424287f139752573a27a8a6eb97cb7b3))
|
||||
* update the playground to load compressed when hosted ([#5835](https://github.com/google/blockly/issues/5835)) ([2adf326](https://github.com/google/blockly/commit/2adf326c230589800880faa9599eca2ecc94d283))
|
||||
* Update typings for q1 2022 release ([#6051](https://github.com/google/blockly/issues/6051)) ([69f3d4a](https://github.com/google/blockly/commit/69f3d4ae89ce16a558443dd0a772e35b62c096d3))
|
||||
* Use correct namespace for svgMath functions ([#5813](https://github.com/google/blockly/issues/5813)) ([b8cc983](https://github.com/google/blockly/commit/b8cc983324338b2cbd536425c93ff3e7d512751e))
|
||||
* Use correct namespace for svgMath functions ([#5813](https://github.com/google/blockly/issues/5813)) ([025bab6](https://github.com/google/blockly/commit/025bab656669f99ebdb8b95bea39ebae296f1495))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* allows previously internal constants to be configurable ([#5897](https://github.com/google/blockly/issues/5897)) ([4b5733e](https://github.com/google/blockly/commit/4b5733e7c85f2e196719550a3cfdcbcbd61739df))
|
||||
* **blocks:** Rename Blockly.blocks.* modules to Blockly.libraryBlocks.* ([#5953](https://github.com/google/blockly/issues/5953)) ([5078dcb](https://github.com/google/blockly/commit/5078dcbc6d4d48422313732e87191b29569b5eed))
|
||||
* remove unused constants from internalConstants ([#5889](https://github.com/google/blockly/issues/5889)) ([f0b1077](https://github.com/google/blockly/commit/f0b10776eb0657a5446adcfc62ad13f419c14271))
|
||||
|
||||
+669
-664
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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');
|
||||
@@ -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;
|
||||
+13
-3
@@ -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);
|
||||
|
||||
+22
-13
@@ -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);
|
||||
|
||||
+13
-3
@@ -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);
|
||||
|
||||
+24
-11
@@ -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);
|
||||
|
||||
+12
-3
@@ -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);
|
||||
|
||||
+41
-27
@@ -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);
|
||||
|
||||
+22
-13
@@ -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);
|
||||
|
||||
+14
-3
@@ -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);
|
||||
|
||||
+146
-141
@@ -7,60 +7,61 @@
|
||||
} else if (typeof exports === 'object') { // Node.js
|
||||
module.exports = factory(require("./blockly_compressed.js"));
|
||||
} else { // Browser
|
||||
root.Blockly.Blocks = factory(root.Blockly);
|
||||
var factoryExports = factory(root.Blockly);
|
||||
root.Blockly.libraryBlocks = factoryExports;
|
||||
}
|
||||
}(this, function(Blockly) {
|
||||
const $=Blockly.internal_;
|
||||
var module$exports$Blockly$blocks$variablesDynamic={};
|
||||
(0,$.module$exports$Blockly$common.defineBlocksWithJsonArray)([{type:"variables_get_dynamic",message0:"%1",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_VARIABLES_DEFAULT_NAME}"}],output:null,style:"variable_dynamic_blocks",helpUrl:"%{BKY_VARIABLES_GET_HELPURL}",tooltip:"%{BKY_VARIABLES_GET_TOOLTIP}",extensions:["contextMenu_variableDynamicSetterGetter"]},{type:"variables_set_dynamic",message0:"%{BKY_VARIABLES_SET}",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_VARIABLES_DEFAULT_NAME}"},
|
||||
{type:"input_value",name:"VALUE"}],previousStatement:null,nextStatement:null,style:"variable_dynamic_blocks",tooltip:"%{BKY_VARIABLES_SET_TOOLTIP}",helpUrl:"%{BKY_VARIABLES_SET_HELPURL}",extensions:["contextMenu_variableDynamicSetterGetter"]}]);
|
||||
var module$contents$Blockly$blocks$variablesDynamic_CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN={customContextMenu:function(a){if(!this.isInFlyout){var b=this.getFieldValue("VAR");var c=this.workspace.getVariableById(b).type;if("variables_get_dynamic"===this.type){b="variables_set_dynamic";var d=$.module$exports$Blockly$Msg.Msg.VARIABLES_GET_CREATE_SET}else b="variables_get_dynamic",d=$.module$exports$Blockly$Msg.Msg.VARIABLES_SET_CREATE_GET;var e={enabled:0<this.workspace.remainingCapacity()},
|
||||
}(this, function(__parent__) {
|
||||
var $=__parent__.__namespace__;
|
||||
var module$exports$Blockly$libraryBlocks$variablesDynamic={};
|
||||
module$exports$Blockly$libraryBlocks$variablesDynamic.blocks=(0,$.module$exports$Blockly$common.createBlockDefinitionsFromJsonArray)([{type:"variables_get_dynamic",message0:"%1",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_VARIABLES_DEFAULT_NAME}"}],output:null,style:"variable_dynamic_blocks",helpUrl:"%{BKY_VARIABLES_GET_HELPURL}",tooltip:"%{BKY_VARIABLES_GET_TOOLTIP}",extensions:["contextMenu_variableDynamicSetterGetter"]},{type:"variables_set_dynamic",message0:"%{BKY_VARIABLES_SET}",
|
||||
args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_VARIABLES_DEFAULT_NAME}"},{type:"input_value",name:"VALUE"}],previousStatement:null,nextStatement:null,style:"variable_dynamic_blocks",tooltip:"%{BKY_VARIABLES_SET_TOOLTIP}",helpUrl:"%{BKY_VARIABLES_SET_HELPURL}",extensions:["contextMenu_variableDynamicSetterGetter"]}]);
|
||||
var module$contents$Blockly$libraryBlocks$variablesDynamic_CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN={customContextMenu:function(a){if(!this.isInFlyout){var b=this.getFieldValue("VAR");var c=this.workspace.getVariableById(b).type;if("variables_get_dynamic"===this.type){b="variables_set_dynamic";var d=$.module$exports$Blockly$Msg.Msg.VARIABLES_GET_CREATE_SET}else b="variables_get_dynamic",d=$.module$exports$Blockly$Msg.Msg.VARIABLES_SET_CREATE_GET;var e={enabled:0<this.workspace.remainingCapacity()},
|
||||
f=this.getField("VAR").getText();e.text=d.replace("%1",f);d=(0,$.module$exports$Blockly$utils$xml.createElement)("field");d.setAttribute("name","VAR");d.setAttribute("variabletype",c);d.appendChild((0,$.module$exports$Blockly$utils$xml.createTextNode)(f));c=(0,$.module$exports$Blockly$utils$xml.createElement)("block");c.setAttribute("type",b);c.appendChild(d);e.callback=(0,$.module$exports$Blockly$ContextMenu.callbackFactory)(this,c);a.push(e)}else if("variables_get_dynamic"===this.type||"variables_get_reporter_dynamic"===
|
||||
this.type)b={text:$.module$exports$Blockly$Msg.Msg.RENAME_VARIABLE,enabled:!0,callback:module$contents$Blockly$blocks$variablesDynamic_renameOptionCallbackFactory(this)},e=this.getField("VAR").getText(),e={text:$.module$exports$Blockly$Msg.Msg.DELETE_VARIABLE.replace("%1",e),enabled:!0,callback:module$contents$Blockly$blocks$variablesDynamic_deleteOptionCallbackFactory(this)},a.unshift(b),a.unshift(e)},onchange:function(a){a=this.getFieldValue("VAR");a=(0,$.module$exports$Blockly$Variables.getVariable)(this.workspace,
|
||||
a);"variables_get_dynamic"===this.type?this.outputConnection.setCheck(a.type):this.getInput("VALUE").connection.setCheck(a.type)}},module$contents$Blockly$blocks$variablesDynamic_renameOptionCallbackFactory=function(a){return function(){var b=a.workspace,c=a.getField("VAR").getVariable();(0,$.module$exports$Blockly$Variables.renameVariable)(b,c)}},module$contents$Blockly$blocks$variablesDynamic_deleteOptionCallbackFactory=function(a){return function(){var b=a.workspace,c=a.getField("VAR").getVariable();
|
||||
b.deleteVariableById(c.getId());b.refreshToolboxSelection()}};(0,$.module$exports$Blockly$Extensions.registerMixin)("contextMenu_variableDynamicSetterGetter",module$contents$Blockly$blocks$variablesDynamic_CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN);var module$exports$Blockly$blocks$variables={};
|
||||
(0,$.module$exports$Blockly$common.defineBlocksWithJsonArray)([{type:"variables_get",message0:"%1",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_VARIABLES_DEFAULT_NAME}"}],output:null,style:"variable_blocks",helpUrl:"%{BKY_VARIABLES_GET_HELPURL}",tooltip:"%{BKY_VARIABLES_GET_TOOLTIP}",extensions:["contextMenu_variableSetterGetter"]},{type:"variables_set",message0:"%{BKY_VARIABLES_SET}",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_VARIABLES_DEFAULT_NAME}"},{type:"input_value",
|
||||
name:"VALUE"}],previousStatement:null,nextStatement:null,style:"variable_blocks",tooltip:"%{BKY_VARIABLES_SET_TOOLTIP}",helpUrl:"%{BKY_VARIABLES_SET_HELPURL}",extensions:["contextMenu_variableSetterGetter"]}]);
|
||||
var module$contents$Blockly$blocks$variables_CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN={customContextMenu:function(a){if(!this.isInFlyout){if("variables_get"===this.type){var b="variables_set";var c=$.module$exports$Blockly$Msg.Msg.VARIABLES_GET_CREATE_SET}else b="variables_get",c=$.module$exports$Blockly$Msg.Msg.VARIABLES_SET_CREATE_GET;var d={enabled:0<this.workspace.remainingCapacity()},e=this.getField("VAR").getText();d.text=c.replace("%1",e);c=(0,$.module$exports$Blockly$utils$xml.createElement)("field");
|
||||
c.setAttribute("name","VAR");c.appendChild((0,$.module$exports$Blockly$utils$xml.createTextNode)(e));e=(0,$.module$exports$Blockly$utils$xml.createElement)("block");e.setAttribute("type",b);e.appendChild(c);d.callback=(0,$.module$exports$Blockly$ContextMenu.callbackFactory)(this,e);a.push(d)}else if("variables_get"===this.type||"variables_get_reporter"===this.type)b={text:$.module$exports$Blockly$Msg.Msg.RENAME_VARIABLE,enabled:!0,callback:module$contents$Blockly$blocks$variables_renameOptionCallbackFactory(this)},
|
||||
d=this.getField("VAR").getText(),d={text:$.module$exports$Blockly$Msg.Msg.DELETE_VARIABLE.replace("%1",d),enabled:!0,callback:module$contents$Blockly$blocks$variables_deleteOptionCallbackFactory(this)},a.unshift(b),a.unshift(d)}},module$contents$Blockly$blocks$variables_renameOptionCallbackFactory=function(a){return function(){var b=a.workspace,c=a.getField("VAR").getVariable();(0,$.module$exports$Blockly$Variables.renameVariable)(b,c)}},module$contents$Blockly$blocks$variables_deleteOptionCallbackFactory=
|
||||
function(a){return function(){var b=a.workspace,c=a.getField("VAR").getVariable();b.deleteVariableById(c.getId());b.refreshToolboxSelection()}};(0,$.module$exports$Blockly$Extensions.registerMixin)("contextMenu_variableSetterGetter",module$contents$Blockly$blocks$variables_CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN);var module$exports$Blockly$blocks$texts={};
|
||||
(0,$.module$exports$Blockly$common.defineBlocksWithJsonArray)([{type:"text",message0:"%1",args0:[{type:"field_input",name:"TEXT",text:""}],output:"String",style:"text_blocks",helpUrl:"%{BKY_TEXT_TEXT_HELPURL}",tooltip:"%{BKY_TEXT_TEXT_TOOLTIP}",extensions:["text_quotes","parent_tooltip_when_inline"]},{type:"text_multiline",message0:"%1 %2",args0:[{type:"field_image",src:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAARCAYAAADpPU2iAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAdhgAAHYYBXaITgQAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMS42/U4J6AAAAP1JREFUOE+Vks0KQUEYhjmRIja4ABtZ2dm5A3t3Ia6AUm7CylYuQRaUhZSlLZJiQbFAyRnPN33y01HOW08z8873zpwzM4F3GWOCruvGIE4/rLaV+Nq1hVGMBqzhqlxgCys4wJA65xnogMHsQ5lujnYHTejBBCK2mE4abjCgMGhNxHgDFWjDSG07kdfVa2pZMf4ZyMAdWmpZMfYOsLiDMYMjlMB+K613QISRhTnITnsYg5yUd0DETmEoMlkFOeIT/A58iyK5E18BuTBfgYXfwNJv4P9/oEBerLylOnRhygmGdPpTTBZAPkde61lbQe4moWUvYUZYLfUNftIY4zwA5X2Z9AYnQrEAAAAASUVORK5CYII=",width:12,
|
||||
this.type)b={text:$.module$exports$Blockly$Msg.Msg.RENAME_VARIABLE,enabled:!0,callback:module$contents$Blockly$libraryBlocks$variablesDynamic_renameOptionCallbackFactory(this)},e=this.getField("VAR").getText(),e={text:$.module$exports$Blockly$Msg.Msg.DELETE_VARIABLE.replace("%1",e),enabled:!0,callback:module$contents$Blockly$libraryBlocks$variablesDynamic_deleteOptionCallbackFactory(this)},a.unshift(b),a.unshift(e)},onchange:function(a){a=this.getFieldValue("VAR");a=(0,$.module$exports$Blockly$Variables.getVariable)(this.workspace,
|
||||
a);"variables_get_dynamic"===this.type?this.outputConnection.setCheck(a.type):this.getInput("VALUE").connection.setCheck(a.type)}},module$contents$Blockly$libraryBlocks$variablesDynamic_renameOptionCallbackFactory=function(a){return function(){var b=a.workspace,c=a.getField("VAR").getVariable();(0,$.module$exports$Blockly$Variables.renameVariable)(b,c)}},module$contents$Blockly$libraryBlocks$variablesDynamic_deleteOptionCallbackFactory=function(a){return function(){var b=a.workspace,c=a.getField("VAR").getVariable();
|
||||
b.deleteVariableById(c.getId());b.refreshToolboxSelection()}};(0,$.module$exports$Blockly$Extensions.registerMixin)("contextMenu_variableDynamicSetterGetter",module$contents$Blockly$libraryBlocks$variablesDynamic_CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN);(0,$.module$exports$Blockly$common.defineBlocks)(module$exports$Blockly$libraryBlocks$variablesDynamic.blocks);var module$exports$Blockly$libraryBlocks$variables={};
|
||||
module$exports$Blockly$libraryBlocks$variables.blocks=(0,$.module$exports$Blockly$common.createBlockDefinitionsFromJsonArray)([{type:"variables_get",message0:"%1",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_VARIABLES_DEFAULT_NAME}"}],output:null,style:"variable_blocks",helpUrl:"%{BKY_VARIABLES_GET_HELPURL}",tooltip:"%{BKY_VARIABLES_GET_TOOLTIP}",extensions:["contextMenu_variableSetterGetter"]},{type:"variables_set",message0:"%{BKY_VARIABLES_SET}",args0:[{type:"field_variable",name:"VAR",
|
||||
variable:"%{BKY_VARIABLES_DEFAULT_NAME}"},{type:"input_value",name:"VALUE"}],previousStatement:null,nextStatement:null,style:"variable_blocks",tooltip:"%{BKY_VARIABLES_SET_TOOLTIP}",helpUrl:"%{BKY_VARIABLES_SET_HELPURL}",extensions:["contextMenu_variableSetterGetter"]}]);
|
||||
var module$contents$Blockly$libraryBlocks$variables_CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN={customContextMenu:function(a){if(!this.isInFlyout){if("variables_get"===this.type){var b="variables_set";var c=$.module$exports$Blockly$Msg.Msg.VARIABLES_GET_CREATE_SET}else b="variables_get",c=$.module$exports$Blockly$Msg.Msg.VARIABLES_SET_CREATE_GET;var d={enabled:0<this.workspace.remainingCapacity()},e=this.getField("VAR").getText();d.text=c.replace("%1",e);c=(0,$.module$exports$Blockly$utils$xml.createElement)("field");
|
||||
c.setAttribute("name","VAR");c.appendChild((0,$.module$exports$Blockly$utils$xml.createTextNode)(e));e=(0,$.module$exports$Blockly$utils$xml.createElement)("block");e.setAttribute("type",b);e.appendChild(c);d.callback=(0,$.module$exports$Blockly$ContextMenu.callbackFactory)(this,e);a.push(d)}else if("variables_get"===this.type||"variables_get_reporter"===this.type)b={text:$.module$exports$Blockly$Msg.Msg.RENAME_VARIABLE,enabled:!0,callback:module$contents$Blockly$libraryBlocks$variables_renameOptionCallbackFactory(this)},
|
||||
d=this.getField("VAR").getText(),d={text:$.module$exports$Blockly$Msg.Msg.DELETE_VARIABLE.replace("%1",d),enabled:!0,callback:module$contents$Blockly$libraryBlocks$variables_deleteOptionCallbackFactory(this)},a.unshift(b),a.unshift(d)}},module$contents$Blockly$libraryBlocks$variables_renameOptionCallbackFactory=function(a){return function(){var b=a.workspace,c=a.getField("VAR").getVariable();(0,$.module$exports$Blockly$Variables.renameVariable)(b,c)}},module$contents$Blockly$libraryBlocks$variables_deleteOptionCallbackFactory=
|
||||
function(a){return function(){var b=a.workspace,c=a.getField("VAR").getVariable();b.deleteVariableById(c.getId());b.refreshToolboxSelection()}};(0,$.module$exports$Blockly$Extensions.registerMixin)("contextMenu_variableSetterGetter",module$contents$Blockly$libraryBlocks$variables_CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN);(0,$.module$exports$Blockly$common.defineBlocks)(module$exports$Blockly$libraryBlocks$variables.blocks);var module$exports$Blockly$libraryBlocks$texts={};
|
||||
module$exports$Blockly$libraryBlocks$texts.blocks=(0,$.module$exports$Blockly$common.createBlockDefinitionsFromJsonArray)([{type:"text",message0:"%1",args0:[{type:"field_input",name:"TEXT",text:""}],output:"String",style:"text_blocks",helpUrl:"%{BKY_TEXT_TEXT_HELPURL}",tooltip:"%{BKY_TEXT_TEXT_TOOLTIP}",extensions:["text_quotes","parent_tooltip_when_inline"]},{type:"text_multiline",message0:"%1 %2",args0:[{type:"field_image",src:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAARCAYAAADpPU2iAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAdhgAAHYYBXaITgQAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMS42/U4J6AAAAP1JREFUOE+Vks0KQUEYhjmRIja4ABtZ2dm5A3t3Ia6AUm7CylYuQRaUhZSlLZJiQbFAyRnPN33y01HOW08z8873zpwzM4F3GWOCruvGIE4/rLaV+Nq1hVGMBqzhqlxgCys4wJA65xnogMHsQ5lujnYHTejBBCK2mE4abjCgMGhNxHgDFWjDSG07kdfVa2pZMf4ZyMAdWmpZMfYOsLiDMYMjlMB+K613QISRhTnITnsYg5yUd0DETmEoMlkFOeIT/A58iyK5E18BuTBfgYXfwNJv4P9/oEBerLylOnRhygmGdPpTTBZAPkde61lbQe4moWUvYUZYLfUNftIY4zwA5X2Z9AYnQrEAAAAASUVORK5CYII=",width:12,
|
||||
height:17,alt:"\u00b6"},{type:"field_multilinetext",name:"TEXT",text:""}],output:"String",style:"text_blocks",helpUrl:"%{BKY_TEXT_TEXT_HELPURL}",tooltip:"%{BKY_TEXT_TEXT_TOOLTIP}",extensions:["parent_tooltip_when_inline"]},{type:"text_join",message0:"",output:"String",style:"text_blocks",helpUrl:"%{BKY_TEXT_JOIN_HELPURL}",tooltip:"%{BKY_TEXT_JOIN_TOOLTIP}",mutator:"text_join_mutator"},{type:"text_create_join_container",message0:"%{BKY_TEXT_CREATE_JOIN_TITLE_JOIN} %1 %2",args0:[{type:"input_dummy"},
|
||||
{type:"input_statement",name:"STACK"}],style:"text_blocks",tooltip:"%{BKY_TEXT_CREATE_JOIN_TOOLTIP}",enableContextMenu:!1},{type:"text_create_join_item",message0:"%{BKY_TEXT_CREATE_JOIN_ITEM_TITLE_ITEM}",previousStatement:null,nextStatement:null,style:"text_blocks",tooltip:"%{BKY_TEXT_CREATE_JOIN_ITEM_TOOLTIP}",enableContextMenu:!1},{type:"text_append",message0:"%{BKY_TEXT_APPEND_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_TEXT_APPEND_VARIABLE}"},{type:"input_value",name:"TEXT"}],
|
||||
previousStatement:null,nextStatement:null,style:"text_blocks",extensions:["text_append_tooltip"]},{type:"text_length",message0:"%{BKY_TEXT_LENGTH_TITLE}",args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Number",style:"text_blocks",tooltip:"%{BKY_TEXT_LENGTH_TOOLTIP}",helpUrl:"%{BKY_TEXT_LENGTH_HELPURL}"},{type:"text_isEmpty",message0:"%{BKY_TEXT_ISEMPTY_TITLE}",args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Boolean",style:"text_blocks",tooltip:"%{BKY_TEXT_ISEMPTY_TOOLTIP}",
|
||||
helpUrl:"%{BKY_TEXT_ISEMPTY_HELPURL}"},{type:"text_indexOf",message0:"%{BKY_TEXT_INDEXOF_TITLE}",args0:[{type:"input_value",name:"VALUE",check:"String"},{type:"field_dropdown",name:"END",options:[["%{BKY_TEXT_INDEXOF_OPERATOR_FIRST}","FIRST"],["%{BKY_TEXT_INDEXOF_OPERATOR_LAST}","LAST"]]},{type:"input_value",name:"FIND",check:"String"}],output:"Number",style:"text_blocks",helpUrl:"%{BKY_TEXT_INDEXOF_HELPURL}",inputsInline:!0,extensions:["text_indexOf_tooltip"]},{type:"text_charAt",message0:"%{BKY_TEXT_CHARAT_TITLE}",
|
||||
args0:[{type:"input_value",name:"VALUE",check:"String"},{type:"field_dropdown",name:"WHERE",options:[["%{BKY_TEXT_CHARAT_FROM_START}","FROM_START"],["%{BKY_TEXT_CHARAT_FROM_END}","FROM_END"],["%{BKY_TEXT_CHARAT_FIRST}","FIRST"],["%{BKY_TEXT_CHARAT_LAST}","LAST"],["%{BKY_TEXT_CHARAT_RANDOM}","RANDOM"]]}],output:"String",style:"text_blocks",helpUrl:"%{BKY_TEXT_CHARAT_HELPURL}",inputsInline:!0,mutator:"text_charAt_mutator"}]);
|
||||
$.module$exports$Blockly$blocks.Blocks.text_getSubstring={init:function(){this.WHERE_OPTIONS_1=[[$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_START_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_START_FROM_END,"FROM_END"],[$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_START_FIRST,"FIRST"]];this.WHERE_OPTIONS_2=[[$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_END_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_END_FROM_END,
|
||||
module$exports$Blockly$libraryBlocks$texts.blocks.text_getSubstring={init:function(){this.WHERE_OPTIONS_1=[[$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_START_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_START_FROM_END,"FROM_END"],[$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_START_FIRST,"FIRST"]];this.WHERE_OPTIONS_2=[[$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_END_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_END_FROM_END,
|
||||
"FROM_END"],[$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_END_LAST,"LAST"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_HELPURL);this.setStyle("text_blocks");this.appendValueInput("STRING").setCheck("String").appendField($.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_INPUT_IN_TEXT);this.appendDummyInput("AT1");this.appendDummyInput("AT2");$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_TAIL&&this.appendDummyInput("TAIL").appendField($.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_TAIL);
|
||||
this.setInputsInline(!0);this.setOutput(!0,"String");this.updateAt_(1,!0);this.updateAt_(2,!0);this.setTooltip($.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_TOOLTIP)},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation"),b=this.getInput("AT1").type===$.module$exports$Blockly$ConnectionType.ConnectionType.INPUT_VALUE;a.setAttribute("at1",b);b=this.getInput("AT2").type===$.module$exports$Blockly$ConnectionType.ConnectionType.INPUT_VALUE;a.setAttribute("at2",
|
||||
b);return a},domToMutation:function(a){var b="true"===a.getAttribute("at1");a="true"===a.getAttribute("at2");this.updateAt_(1,b);this.updateAt_(2,a)},updateAt_:function(a,b){this.removeInput("AT"+a);this.removeInput("ORDINAL"+a,!0);b?(this.appendValueInput("AT"+a).setCheck("Number"),$.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX&&this.appendDummyInput("ORDINAL"+a).appendField($.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX)):this.appendDummyInput("AT"+a);2===a&&$.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_TAIL&&
|
||||
(this.removeInput("TAIL",!0),this.appendDummyInput("TAIL").appendField($.module$exports$Blockly$Msg.Msg.TEXT_GET_SUBSTRING_TAIL));var c=new $.module$exports$Blockly$FieldDropdown.FieldDropdown(this["WHERE_OPTIONS_"+a],function(d){var e="FROM_START"===d||"FROM_END"===d;if(e!==b){var f=this.getSourceBlock();f.updateAt_(a,e);f.setFieldValue(d,"WHERE"+a);return null}});this.getInput("AT"+a).appendField(c,"WHERE"+a);1===a&&(this.moveInputBefore("AT1","AT2"),this.getInput("ORDINAL1")&&this.moveInputBefore("ORDINAL1",
|
||||
"AT2"))}};
|
||||
$.module$exports$Blockly$blocks.Blocks.text_changeCase={init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.TEXT_CHANGECASE_OPERATOR_UPPERCASE,"UPPERCASE"],[$.module$exports$Blockly$Msg.Msg.TEXT_CHANGECASE_OPERATOR_LOWERCASE,"LOWERCASE"],[$.module$exports$Blockly$Msg.Msg.TEXT_CHANGECASE_OPERATOR_TITLECASE,"TITLECASE"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.TEXT_CHANGECASE_HELPURL);this.setStyle("text_blocks");this.appendValueInput("TEXT").setCheck("String").appendField(new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a),"CASE");
|
||||
module$exports$Blockly$libraryBlocks$texts.blocks.text_changeCase={init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.TEXT_CHANGECASE_OPERATOR_UPPERCASE,"UPPERCASE"],[$.module$exports$Blockly$Msg.Msg.TEXT_CHANGECASE_OPERATOR_LOWERCASE,"LOWERCASE"],[$.module$exports$Blockly$Msg.Msg.TEXT_CHANGECASE_OPERATOR_TITLECASE,"TITLECASE"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.TEXT_CHANGECASE_HELPURL);this.setStyle("text_blocks");this.appendValueInput("TEXT").setCheck("String").appendField(new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a),"CASE");
|
||||
this.setOutput(!0,"String");this.setTooltip($.module$exports$Blockly$Msg.Msg.TEXT_CHANGECASE_TOOLTIP)}};
|
||||
$.module$exports$Blockly$blocks.Blocks.text_trim={init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.TEXT_TRIM_OPERATOR_BOTH,"BOTH"],[$.module$exports$Blockly$Msg.Msg.TEXT_TRIM_OPERATOR_LEFT,"LEFT"],[$.module$exports$Blockly$Msg.Msg.TEXT_TRIM_OPERATOR_RIGHT,"RIGHT"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.TEXT_TRIM_HELPURL);this.setStyle("text_blocks");this.appendValueInput("TEXT").setCheck("String").appendField(new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a),"MODE");this.setOutput(!0,
|
||||
"String");this.setTooltip($.module$exports$Blockly$Msg.Msg.TEXT_TRIM_TOOLTIP)}};$.module$exports$Blockly$blocks.Blocks.text_print={init:function(){this.jsonInit({message0:$.module$exports$Blockly$Msg.Msg.TEXT_PRINT_TITLE,args0:[{type:"input_value",name:"TEXT"}],previousStatement:null,nextStatement:null,style:"text_blocks",tooltip:$.module$exports$Blockly$Msg.Msg.TEXT_PRINT_TOOLTIP,helpUrl:$.module$exports$Blockly$Msg.Msg.TEXT_PRINT_HELPURL})}};
|
||||
var module$contents$Blockly$blocks$texts_TEXT_PROMPT_COMMON={updateType_:function(a){this.outputConnection.setCheck("NUMBER"===a?"Number":"String")},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("type",this.getFieldValue("TYPE"));return a},domToMutation:function(a){this.updateType_(a.getAttribute("type"))}};
|
||||
$.module$exports$Blockly$blocks.Blocks.text_prompt_ext=Object.assign({},module$contents$Blockly$blocks$texts_TEXT_PROMPT_COMMON,{init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TYPE_TEXT,"TEXT"],[$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TYPE_NUMBER,"NUMBER"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_HELPURL);this.setStyle("text_blocks");var b=this;a=new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a,function(c){b.updateType_(c)});this.appendValueInput("TEXT").appendField(a,
|
||||
"TYPE");this.setOutput(!0,"String");this.setTooltip(function(){return"TEXT"===b.getFieldValue("TYPE")?$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TOOLTIP_TEXT:$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TOOLTIP_NUMBER})}});
|
||||
$.module$exports$Blockly$blocks.Blocks.text_prompt=Object.assign({},module$contents$Blockly$blocks$texts_TEXT_PROMPT_COMMON,{init:function(){this.mixin(module$contents$Blockly$blocks$texts_QUOTE_IMAGE_MIXIN);var a=[[$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TYPE_TEXT,"TEXT"],[$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TYPE_NUMBER,"NUMBER"]],b=this;this.setHelpUrl($.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_HELPURL);this.setStyle("text_blocks");a=new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a,
|
||||
module$exports$Blockly$libraryBlocks$texts.blocks.text_trim={init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.TEXT_TRIM_OPERATOR_BOTH,"BOTH"],[$.module$exports$Blockly$Msg.Msg.TEXT_TRIM_OPERATOR_LEFT,"LEFT"],[$.module$exports$Blockly$Msg.Msg.TEXT_TRIM_OPERATOR_RIGHT,"RIGHT"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.TEXT_TRIM_HELPURL);this.setStyle("text_blocks");this.appendValueInput("TEXT").setCheck("String").appendField(new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a),"MODE");
|
||||
this.setOutput(!0,"String");this.setTooltip($.module$exports$Blockly$Msg.Msg.TEXT_TRIM_TOOLTIP)}};module$exports$Blockly$libraryBlocks$texts.blocks.text_print={init:function(){this.jsonInit({message0:$.module$exports$Blockly$Msg.Msg.TEXT_PRINT_TITLE,args0:[{type:"input_value",name:"TEXT"}],previousStatement:null,nextStatement:null,style:"text_blocks",tooltip:$.module$exports$Blockly$Msg.Msg.TEXT_PRINT_TOOLTIP,helpUrl:$.module$exports$Blockly$Msg.Msg.TEXT_PRINT_HELPURL})}};
|
||||
var module$contents$Blockly$libraryBlocks$texts_TEXT_PROMPT_COMMON={updateType_:function(a){this.outputConnection.setCheck("NUMBER"===a?"Number":"String")},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("type",this.getFieldValue("TYPE"));return a},domToMutation:function(a){this.updateType_(a.getAttribute("type"))}};
|
||||
module$exports$Blockly$libraryBlocks$texts.blocks.text_prompt_ext=Object.assign({},module$contents$Blockly$libraryBlocks$texts_TEXT_PROMPT_COMMON,{init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TYPE_TEXT,"TEXT"],[$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TYPE_NUMBER,"NUMBER"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_HELPURL);this.setStyle("text_blocks");var b=this;a=new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a,function(c){b.updateType_(c)});
|
||||
this.appendValueInput("TEXT").appendField(a,"TYPE");this.setOutput(!0,"String");this.setTooltip(function(){return"TEXT"===b.getFieldValue("TYPE")?$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TOOLTIP_TEXT:$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TOOLTIP_NUMBER})}});
|
||||
module$exports$Blockly$libraryBlocks$texts.blocks.text_prompt=Object.assign({},module$contents$Blockly$libraryBlocks$texts_TEXT_PROMPT_COMMON,{init:function(){this.mixin(module$contents$Blockly$libraryBlocks$texts_QUOTE_IMAGE_MIXIN);var a=[[$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TYPE_TEXT,"TEXT"],[$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TYPE_NUMBER,"NUMBER"]],b=this;this.setHelpUrl($.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_HELPURL);this.setStyle("text_blocks");a=new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a,
|
||||
function(c){b.updateType_(c)});this.appendDummyInput().appendField(a,"TYPE").appendField(this.newQuote_(!0)).appendField(new $.module$exports$Blockly$FieldTextInput.FieldTextInput(""),"TEXT").appendField(this.newQuote_(!1));this.setOutput(!0,"String");this.setTooltip(function(){return"TEXT"===b.getFieldValue("TYPE")?$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TOOLTIP_TEXT:$.module$exports$Blockly$Msg.Msg.TEXT_PROMPT_TOOLTIP_NUMBER})}});
|
||||
$.module$exports$Blockly$blocks.Blocks.text_count={init:function(){this.jsonInit({message0:$.module$exports$Blockly$Msg.Msg.TEXT_COUNT_MESSAGE0,args0:[{type:"input_value",name:"SUB",check:"String"},{type:"input_value",name:"TEXT",check:"String"}],output:"Number",inputsInline:!0,style:"text_blocks",tooltip:$.module$exports$Blockly$Msg.Msg.TEXT_COUNT_TOOLTIP,helpUrl:$.module$exports$Blockly$Msg.Msg.TEXT_COUNT_HELPURL})}};
|
||||
$.module$exports$Blockly$blocks.Blocks.text_replace={init:function(){this.jsonInit({message0:$.module$exports$Blockly$Msg.Msg.TEXT_REPLACE_MESSAGE0,args0:[{type:"input_value",name:"FROM",check:"String"},{type:"input_value",name:"TO",check:"String"},{type:"input_value",name:"TEXT",check:"String"}],output:"String",inputsInline:!0,style:"text_blocks",tooltip:$.module$exports$Blockly$Msg.Msg.TEXT_REPLACE_TOOLTIP,helpUrl:$.module$exports$Blockly$Msg.Msg.TEXT_REPLACE_HELPURL})}};
|
||||
$.module$exports$Blockly$blocks.Blocks.text_reverse={init:function(){this.jsonInit({message0:$.module$exports$Blockly$Msg.Msg.TEXT_REVERSE_MESSAGE0,args0:[{type:"input_value",name:"TEXT",check:"String"}],output:"String",inputsInline:!0,style:"text_blocks",tooltip:$.module$exports$Blockly$Msg.Msg.TEXT_REVERSE_TOOLTIP,helpUrl:$.module$exports$Blockly$Msg.Msg.TEXT_REVERSE_HELPURL})}};
|
||||
var module$contents$Blockly$blocks$texts_QUOTE_IMAGE_MIXIN={QUOTE_IMAGE_LEFT_DATAURI:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAn0lEQVQI1z3OMa5BURSF4f/cQhAKjUQhuQmFNwGJEUi0RKN5rU7FHKhpjEH3TEMtkdBSCY1EIv8r7nFX9e29V7EBAOvu7RPjwmWGH/VuF8CyN9/OAdvqIXYLvtRaNjx9mMTDyo+NjAN1HNcl9ZQ5oQMM3dgDUqDo1l8DzvwmtZN7mnD+PkmLa+4mhrxVA9fRowBWmVBhFy5gYEjKMfz9AylsaRRgGzvZAAAAAElFTkSuQmCC",QUOTE_IMAGE_RIGHT_DATAURI:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAqUlEQVQI1z3KvUpCcRiA8ef9E4JNHhI0aFEacm1o0BsI0Slx8wa8gLauoDnoBhq7DcfWhggONDmJJgqCPA7neJ7p934EOOKOnM8Q7PDElo/4x4lFb2DmuUjcUzS3URnGib9qaPNbuXvBO3sGPHJDRG6fGVdMSeWDP2q99FQdFrz26Gu5Tq7dFMzUvbXy8KXeAj57cOklgA+u1B5AoslLtGIHQMaCVnwDnADZIFIrXsoXrgAAAABJRU5ErkJggg==",
|
||||
module$exports$Blockly$libraryBlocks$texts.blocks.text_count={init:function(){this.jsonInit({message0:$.module$exports$Blockly$Msg.Msg.TEXT_COUNT_MESSAGE0,args0:[{type:"input_value",name:"SUB",check:"String"},{type:"input_value",name:"TEXT",check:"String"}],output:"Number",inputsInline:!0,style:"text_blocks",tooltip:$.module$exports$Blockly$Msg.Msg.TEXT_COUNT_TOOLTIP,helpUrl:$.module$exports$Blockly$Msg.Msg.TEXT_COUNT_HELPURL})}};
|
||||
module$exports$Blockly$libraryBlocks$texts.blocks.text_replace={init:function(){this.jsonInit({message0:$.module$exports$Blockly$Msg.Msg.TEXT_REPLACE_MESSAGE0,args0:[{type:"input_value",name:"FROM",check:"String"},{type:"input_value",name:"TO",check:"String"},{type:"input_value",name:"TEXT",check:"String"}],output:"String",inputsInline:!0,style:"text_blocks",tooltip:$.module$exports$Blockly$Msg.Msg.TEXT_REPLACE_TOOLTIP,helpUrl:$.module$exports$Blockly$Msg.Msg.TEXT_REPLACE_HELPURL})}};
|
||||
module$exports$Blockly$libraryBlocks$texts.blocks.text_reverse={init:function(){this.jsonInit({message0:$.module$exports$Blockly$Msg.Msg.TEXT_REVERSE_MESSAGE0,args0:[{type:"input_value",name:"TEXT",check:"String"}],output:"String",inputsInline:!0,style:"text_blocks",tooltip:$.module$exports$Blockly$Msg.Msg.TEXT_REVERSE_TOOLTIP,helpUrl:$.module$exports$Blockly$Msg.Msg.TEXT_REVERSE_HELPURL})}};
|
||||
var module$contents$Blockly$libraryBlocks$texts_QUOTE_IMAGE_MIXIN={QUOTE_IMAGE_LEFT_DATAURI:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAn0lEQVQI1z3OMa5BURSF4f/cQhAKjUQhuQmFNwGJEUi0RKN5rU7FHKhpjEH3TEMtkdBSCY1EIv8r7nFX9e29V7EBAOvu7RPjwmWGH/VuF8CyN9/OAdvqIXYLvtRaNjx9mMTDyo+NjAN1HNcl9ZQ5oQMM3dgDUqDo1l8DzvwmtZN7mnD+PkmLa+4mhrxVA9fRowBWmVBhFy5gYEjKMfz9AylsaRRgGzvZAAAAAElFTkSuQmCC",QUOTE_IMAGE_RIGHT_DATAURI:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAqUlEQVQI1z3KvUpCcRiA8ef9E4JNHhI0aFEacm1o0BsI0Slx8wa8gLauoDnoBhq7DcfWhggONDmJJgqCPA7neJ7p934EOOKOnM8Q7PDElo/4x4lFb2DmuUjcUzS3URnGib9qaPNbuXvBO3sGPHJDRG6fGVdMSeWDP2q99FQdFrz26Gu5Tq7dFMzUvbXy8KXeAj57cOklgA+u1B5AoslLtGIHQMaCVnwDnADZIFIrXsoXrgAAAABJRU5ErkJggg==",
|
||||
QUOTE_IMAGE_WIDTH:12,QUOTE_IMAGE_HEIGHT:12,quoteField_:function(a){for(var b=0,c;c=this.inputList[b];b++)for(var d=0,e;e=c.fieldRow[d];d++)if(a===e.name){c.insertFieldAt(d,this.newQuote_(!0));c.insertFieldAt(d+2,this.newQuote_(!1));return}console.warn('field named "'+a+'" not found in '+this.toDevString())},newQuote_:function(a){a=this.RTL?!a:a;return new $.module$exports$Blockly$FieldImage.FieldImage(a?this.QUOTE_IMAGE_LEFT_DATAURI:this.QUOTE_IMAGE_RIGHT_DATAURI,this.QUOTE_IMAGE_WIDTH,this.QUOTE_IMAGE_HEIGHT,
|
||||
a?"\u201c":"\u201d")}},module$contents$Blockly$blocks$texts_TEXT_QUOTES_EXTENSION=function(){this.mixin(module$contents$Blockly$blocks$texts_QUOTE_IMAGE_MIXIN);this.quoteField_("TEXT")},module$contents$Blockly$blocks$texts_TEXT_JOIN_MUTATOR_MIXIN={mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("items",this.itemCount_);return a},domToMutation:function(a){this.itemCount_=parseInt(a.getAttribute("items"),10);this.updateShape_()},saveExtraState:function(){return{itemCount:this.itemCount_}},
|
||||
loadExtraState:function(a){this.itemCount_=a.itemCount;this.updateShape_()},decompose:function(a){var b=a.newBlock("text_create_join_container");b.initSvg();for(var c=b.getInput("STACK").connection,d=0;d<this.itemCount_;d++){var e=a.newBlock("text_create_join_item");e.initSvg();c.connect(e.previousConnection);c=e.nextConnection}return b},compose:function(a){var b=a.getInputTargetBlock("STACK");for(a=[];b&&!b.isInsertionMarker();)a.push(b.valueConnection_),b=b.nextConnection&&b.nextConnection.targetBlock();
|
||||
for(b=0;b<this.itemCount_;b++){var c=this.getInput("ADD"+b).connection.targetConnection;c&&-1===a.indexOf(c)&&c.disconnect()}this.itemCount_=a.length;this.updateShape_();for(b=0;b<this.itemCount_;b++)$.module$exports$Blockly$Mutator.Mutator.reconnect(a[b],this,"ADD"+b)},saveConnections:function(a){a=a.getInputTargetBlock("STACK");for(var b=0;a;){var c=this.getInput("ADD"+b);a.valueConnection_=c&&c.connection.targetConnection;a=a.nextConnection&&a.nextConnection.targetBlock();b++}},updateShape_:function(){this.itemCount_&&
|
||||
this.getInput("EMPTY")?this.removeInput("EMPTY"):this.itemCount_||this.getInput("EMPTY")||this.appendDummyInput("EMPTY").appendField(this.newQuote_(!0)).appendField(this.newQuote_(!1));for(var a=0;a<this.itemCount_;a++)if(!this.getInput("ADD"+a)){var b=this.appendValueInput("ADD"+a).setAlign($.module$exports$Blockly$Input.Align.RIGHT);0===a&&b.appendField($.module$exports$Blockly$Msg.Msg.TEXT_JOIN_TITLE_CREATEWITH)}for(a=this.itemCount_;this.getInput("ADD"+a);a++)this.removeInput("ADD"+a)}},module$contents$Blockly$blocks$texts_TEXT_JOIN_EXTENSION=
|
||||
function(){this.mixin(module$contents$Blockly$blocks$texts_QUOTE_IMAGE_MIXIN);this.itemCount_=2;this.updateShape_();this.setMutator(new $.module$exports$Blockly$Mutator.Mutator(["text_create_join_item"]))};(0,$.module$exports$Blockly$Extensions.register)("text_append_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipWithFieldText)("%{BKY_TEXT_APPEND_TOOLTIP}","VAR"));
|
||||
var module$contents$Blockly$blocks$texts_TEXT_INDEXOF_TOOLTIP_EXTENSION=function(){var a=this;this.setTooltip(function(){return $.module$exports$Blockly$Msg.Msg.TEXT_INDEXOF_TOOLTIP.replace("%1",a.workspace.options.oneBasedIndex?"0":"-1")})},module$contents$Blockly$blocks$texts_TEXT_CHARAT_MUTATOR_MIXIN={mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("at",!!this.isAt_);return a},domToMutation:function(a){a="false"!==a.getAttribute("at");
|
||||
this.updateAt_(a)},updateAt_:function(a){this.removeInput("AT",!0);this.removeInput("ORDINAL",!0);a&&(this.appendValueInput("AT").setCheck("Number"),$.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX&&this.appendDummyInput("ORDINAL").appendField($.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX));$.module$exports$Blockly$Msg.Msg.TEXT_CHARAT_TAIL&&(this.removeInput("TAIL",!0),this.appendDummyInput("TAIL").appendField($.module$exports$Blockly$Msg.Msg.TEXT_CHARAT_TAIL));this.isAt_=a}},module$contents$Blockly$blocks$texts_TEXT_CHARAT_EXTENSION=
|
||||
a?"\u201c":"\u201d")}},module$contents$Blockly$libraryBlocks$texts_TEXT_QUOTES_EXTENSION=function(){this.mixin(module$contents$Blockly$libraryBlocks$texts_QUOTE_IMAGE_MIXIN);this.quoteField_("TEXT")},module$contents$Blockly$libraryBlocks$texts_TEXT_JOIN_MUTATOR_MIXIN={mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("items",this.itemCount_);return a},domToMutation:function(a){this.itemCount_=parseInt(a.getAttribute("items"),10);this.updateShape_()},
|
||||
saveExtraState:function(){return{itemCount:this.itemCount_}},loadExtraState:function(a){this.itemCount_=a.itemCount;this.updateShape_()},decompose:function(a){var b=a.newBlock("text_create_join_container");b.initSvg();for(var c=b.getInput("STACK").connection,d=0;d<this.itemCount_;d++){var e=a.newBlock("text_create_join_item");e.initSvg();c.connect(e.previousConnection);c=e.nextConnection}return b},compose:function(a){var b=a.getInputTargetBlock("STACK");for(a=[];b&&!b.isInsertionMarker();)a.push(b.valueConnection_),
|
||||
b=b.nextConnection&&b.nextConnection.targetBlock();for(b=0;b<this.itemCount_;b++){var c=this.getInput("ADD"+b).connection.targetConnection;c&&-1===a.indexOf(c)&&c.disconnect()}this.itemCount_=a.length;this.updateShape_();for(b=0;b<this.itemCount_;b++)$.module$exports$Blockly$Mutator.Mutator.reconnect(a[b],this,"ADD"+b)},saveConnections:function(a){a=a.getInputTargetBlock("STACK");for(var b=0;a;){var c=this.getInput("ADD"+b);a.valueConnection_=c&&c.connection.targetConnection;a=a.nextConnection&&a.nextConnection.targetBlock();
|
||||
b++}},updateShape_:function(){this.itemCount_&&this.getInput("EMPTY")?this.removeInput("EMPTY"):this.itemCount_||this.getInput("EMPTY")||this.appendDummyInput("EMPTY").appendField(this.newQuote_(!0)).appendField(this.newQuote_(!1));for(var a=0;a<this.itemCount_;a++)if(!this.getInput("ADD"+a)){var b=this.appendValueInput("ADD"+a).setAlign($.module$exports$Blockly$Input.Align.RIGHT);0===a&&b.appendField($.module$exports$Blockly$Msg.Msg.TEXT_JOIN_TITLE_CREATEWITH)}for(a=this.itemCount_;this.getInput("ADD"+
|
||||
a);a++)this.removeInput("ADD"+a)}},module$contents$Blockly$libraryBlocks$texts_TEXT_JOIN_EXTENSION=function(){this.mixin(module$contents$Blockly$libraryBlocks$texts_QUOTE_IMAGE_MIXIN);this.itemCount_=2;this.updateShape_();this.setMutator(new $.module$exports$Blockly$Mutator.Mutator(["text_create_join_item"]))};(0,$.module$exports$Blockly$Extensions.register)("text_append_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipWithFieldText)("%{BKY_TEXT_APPEND_TOOLTIP}","VAR"));
|
||||
var module$contents$Blockly$libraryBlocks$texts_TEXT_INDEXOF_TOOLTIP_EXTENSION=function(){var a=this;this.setTooltip(function(){return $.module$exports$Blockly$Msg.Msg.TEXT_INDEXOF_TOOLTIP.replace("%1",a.workspace.options.oneBasedIndex?"0":"-1")})},module$contents$Blockly$libraryBlocks$texts_TEXT_CHARAT_MUTATOR_MIXIN={mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("at",!!this.isAt_);return a},domToMutation:function(a){a="false"!==a.getAttribute("at");
|
||||
this.updateAt_(a)},updateAt_:function(a){this.removeInput("AT",!0);this.removeInput("ORDINAL",!0);a&&(this.appendValueInput("AT").setCheck("Number"),$.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX&&this.appendDummyInput("ORDINAL").appendField($.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX));$.module$exports$Blockly$Msg.Msg.TEXT_CHARAT_TAIL&&(this.removeInput("TAIL",!0),this.appendDummyInput("TAIL").appendField($.module$exports$Blockly$Msg.Msg.TEXT_CHARAT_TAIL));this.isAt_=a}},module$contents$Blockly$libraryBlocks$texts_TEXT_CHARAT_EXTENSION=
|
||||
function(){this.getField("WHERE").setValidator(function(b){b="FROM_START"===b||"FROM_END"===b;b!==this.isAt_&&this.getSourceBlock().updateAt_(b)});this.updateAt_(!0);var a=this;this.setTooltip(function(){var b=a.getFieldValue("WHERE"),c=$.module$exports$Blockly$Msg.Msg.TEXT_CHARAT_TOOLTIP;("FROM_START"===b||"FROM_END"===b)&&(b="FROM_START"===b?$.module$exports$Blockly$Msg.Msg.LISTS_INDEX_FROM_START_TOOLTIP:$.module$exports$Blockly$Msg.Msg.LISTS_INDEX_FROM_END_TOOLTIP)&&(c+=" "+b.replace("%1",a.workspace.options.oneBasedIndex?
|
||||
"#1":"#0"));return c})};(0,$.module$exports$Blockly$Extensions.register)("text_indexOf_tooltip",module$contents$Blockly$blocks$texts_TEXT_INDEXOF_TOOLTIP_EXTENSION);(0,$.module$exports$Blockly$Extensions.register)("text_quotes",module$contents$Blockly$blocks$texts_TEXT_QUOTES_EXTENSION);(0,$.module$exports$Blockly$Extensions.registerMutator)("text_join_mutator",module$contents$Blockly$blocks$texts_TEXT_JOIN_MUTATOR_MIXIN,module$contents$Blockly$blocks$texts_TEXT_JOIN_EXTENSION);
|
||||
(0,$.module$exports$Blockly$Extensions.registerMutator)("text_charAt_mutator",module$contents$Blockly$blocks$texts_TEXT_CHARAT_MUTATOR_MIXIN,module$contents$Blockly$blocks$texts_TEXT_CHARAT_EXTENSION);var module$exports$Blockly$blocks$procedures={},module$contents$Blockly$blocks$procedures_PROCEDURE_DEF_COMMON={setStatements_:function(a){this.hasStatements_!==a&&(a?(this.appendStatementInput("STACK").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_DO),this.getInput("RETURN")&&this.moveInputBefore("STACK","RETURN")):this.removeInput("STACK",!0),this.hasStatements_=a)},updateParams_:function(){var a="";this.arguments_.length&&(a=$.module$exports$Blockly$Msg.Msg.PROCEDURES_BEFORE_PARAMS+
|
||||
"#1":"#0"));return c})};(0,$.module$exports$Blockly$Extensions.register)("text_indexOf_tooltip",module$contents$Blockly$libraryBlocks$texts_TEXT_INDEXOF_TOOLTIP_EXTENSION);(0,$.module$exports$Blockly$Extensions.register)("text_quotes",module$contents$Blockly$libraryBlocks$texts_TEXT_QUOTES_EXTENSION);(0,$.module$exports$Blockly$Extensions.registerMutator)("text_join_mutator",module$contents$Blockly$libraryBlocks$texts_TEXT_JOIN_MUTATOR_MIXIN,module$contents$Blockly$libraryBlocks$texts_TEXT_JOIN_EXTENSION);
|
||||
(0,$.module$exports$Blockly$Extensions.registerMutator)("text_charAt_mutator",module$contents$Blockly$libraryBlocks$texts_TEXT_CHARAT_MUTATOR_MIXIN,module$contents$Blockly$libraryBlocks$texts_TEXT_CHARAT_EXTENSION);(0,$.module$exports$Blockly$common.defineBlocks)(module$exports$Blockly$libraryBlocks$texts.blocks);var module$exports$Blockly$libraryBlocks$procedures={blocks:{}},module$contents$Blockly$libraryBlocks$procedures_PROCEDURE_DEF_COMMON={setStatements_:function(a){this.hasStatements_!==a&&(a?(this.appendStatementInput("STACK").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_DO),this.getInput("RETURN")&&this.moveInputBefore("STACK","RETURN")):this.removeInput("STACK",!0),this.hasStatements_=a)},updateParams_:function(){var a="";this.arguments_.length&&(a=$.module$exports$Blockly$Msg.Msg.PROCEDURES_BEFORE_PARAMS+
|
||||
" "+this.arguments_.join(", "));(0,$.module$exports$Blockly$Events.disable)();try{this.setFieldValue(a,"PARAMS")}finally{(0,$.module$exports$Blockly$Events.enable)()}},mutationToDom:function(a){var b=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a&&b.setAttribute("name",this.getFieldValue("NAME"));for(var c=0;c<this.argumentVarModels_.length;c++){var d=(0,$.module$exports$Blockly$utils$xml.createElement)("arg"),e=this.argumentVarModels_[c];d.setAttribute("name",e.name);d.setAttribute("varid",
|
||||
e.getId());a&&this.paramIds_&&d.setAttribute("paramId",this.paramIds_[c]);b.appendChild(d)}this.hasStatements_||b.setAttribute("statements","false");return b},domToMutation:function(a){this.arguments_=[];this.argumentVarModels_=[];for(var b=0,c;c=a.childNodes[b];b++)if("arg"===c.nodeName.toLowerCase()){var d=c.getAttribute("name");c=c.getAttribute("varid")||c.getAttribute("varId");this.arguments_.push(d);c=(0,$.module$exports$Blockly$Variables.getOrCreateVariablePackage)(this.workspace,c,d,"");null!==
|
||||
c?this.argumentVarModels_.push(c):console.log("Failed to create a variable with name "+d+", ignoring.")}this.updateParams_();(0,$.module$exports$Blockly$Procedures.mutateCallers)(this);this.setStatements_("false"!==a.getAttribute("statements"))},saveExtraState:function(){if(!this.argumentVarModels_.length&&this.hasStatements_)return null;var a=Object.create(null);if(this.argumentVarModels_.length){a.params=[];for(var b=0;b<this.argumentVarModels_.length;b++)a.params.push({name:this.argumentVarModels_[b].name,
|
||||
@@ -72,105 +73,106 @@ $.module$exports$Blockly$Mutator.Mutator.reconnect(this.statementConnection_,thi
|
||||
for(var d=!1,e=0;e<this.argumentVarModels_.length;e++)this.argumentVarModels_[e].getId()===a&&(this.arguments_[e]=b.name,this.argumentVarModels_[e]=b,d=!0);d&&(this.displayRenamedVar_(c,b.name),(0,$.module$exports$Blockly$Procedures.mutateCallers)(this))}},updateVarName:function(a){for(var b=a.name,c=!1,d,e=0;e<this.argumentVarModels_.length;e++)this.argumentVarModels_[e].getId()===a.getId()&&(d=this.arguments_[e],this.arguments_[e]=b,c=!0);c&&(this.displayRenamedVar_(d,b),(0,$.module$exports$Blockly$Procedures.mutateCallers)(this))},
|
||||
displayRenamedVar_:function(a,b){this.updateParams_();if(this.mutator&&this.mutator.isVisible())for(var c=this.mutator.workspace_.getAllBlocks(!1),d=0,e;e=c[d];d++)"procedures_mutatorarg"===e.type&&$.module$exports$Blockly$Names.Names.equals(a,e.getFieldValue("NAME"))&&e.setFieldValue(b,"NAME")},customContextMenu:function(a){if(!this.isInFlyout){var b={enabled:!0},c=this.getFieldValue("NAME");b.text=$.module$exports$Blockly$Msg.Msg.PROCEDURES_CREATE_DO.replace("%1",c);var d=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");
|
||||
d.setAttribute("name",c);for(c=0;c<this.arguments_.length;c++){var e=(0,$.module$exports$Blockly$utils$xml.createElement)("arg");e.setAttribute("name",this.arguments_[c]);d.appendChild(e)}c=(0,$.module$exports$Blockly$utils$xml.createElement)("block");c.setAttribute("type",this.callType_);c.appendChild(d);b.callback=(0,$.module$exports$Blockly$ContextMenu.callbackFactory)(this,c);a.push(b);if(!this.isCollapsed())for(b=0;b<this.argumentVarModels_.length;b++)d={enabled:!0},c=this.argumentVarModels_[b],
|
||||
d.text=$.module$exports$Blockly$Msg.Msg.VARIABLES_SET_CREATE_GET.replace("%1",c.name),c=(0,$.module$exports$Blockly$Variables.generateVariableFieldDom)(c),e=(0,$.module$exports$Blockly$utils$xml.createElement)("block"),e.setAttribute("type","variables_get"),e.appendChild(c),d.callback=(0,$.module$exports$Blockly$ContextMenu.callbackFactory)(this,e),a.push(d)}},callType_:"procedures_callnoreturn"};
|
||||
$.module$exports$Blockly$blocks.Blocks.procedures_defnoreturn=Object.assign({},module$contents$Blockly$blocks$procedures_PROCEDURE_DEF_COMMON,{init:function(){var a=(0,$.module$exports$Blockly$Procedures.findLegalName)("",this);a=new $.module$exports$Blockly$FieldTextInput.FieldTextInput(a,$.module$exports$Blockly$Procedures.rename);a.setSpellcheck(!1);this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_TITLE).appendField(a,"NAME").appendField("","PARAMS");
|
||||
this.setMutator(new $.module$exports$Blockly$Mutator.Mutator(["procedures_mutatorarg"]));(this.workspace.options.comments||this.workspace.options.parentWorkspace&&this.workspace.options.parentWorkspace.options.comments)&&$.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_COMMENT&&this.setCommentText($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_COMMENT);this.setStyle("procedure_blocks");this.setTooltip($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_TOOLTIP);this.setHelpUrl($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_HELPURL);
|
||||
this.arguments_=[];this.argumentVarModels_=[];this.setStatements_(!0);this.statementConnection_=null},getProcedureDef:function(){return[this.getFieldValue("NAME"),this.arguments_,!1]}});
|
||||
$.module$exports$Blockly$blocks.Blocks.procedures_defreturn=Object.assign({},module$contents$Blockly$blocks$procedures_PROCEDURE_DEF_COMMON,{init:function(){var a=(0,$.module$exports$Blockly$Procedures.findLegalName)("",this);a=new $.module$exports$Blockly$FieldTextInput.FieldTextInput(a,$.module$exports$Blockly$Procedures.rename);a.setSpellcheck(!1);this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_TITLE).appendField(a,"NAME").appendField("","PARAMS");this.appendValueInput("RETURN").setAlign($.module$exports$Blockly$Input.Align.RIGHT).appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_RETURN);
|
||||
this.setMutator(new $.module$exports$Blockly$Mutator.Mutator(["procedures_mutatorarg"]));(this.workspace.options.comments||this.workspace.options.parentWorkspace&&this.workspace.options.parentWorkspace.options.comments)&&$.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_COMMENT&&this.setCommentText($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_COMMENT);this.setStyle("procedure_blocks");this.setTooltip($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_TOOLTIP);this.setHelpUrl($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_HELPURL);
|
||||
this.arguments_=[];this.argumentVarModels_=[];this.setStatements_(!0);this.statementConnection_=null},getProcedureDef:function(){return[this.getFieldValue("NAME"),this.arguments_,!0]}});
|
||||
$.module$exports$Blockly$blocks.Blocks.procedures_mutatorcontainer={init:function(){this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_MUTATORCONTAINER_TITLE);this.appendStatementInput("STACK");this.appendDummyInput("STATEMENT_INPUT").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_ALLOW_STATEMENTS).appendField(new $.module$exports$Blockly$FieldCheckbox.FieldCheckbox("TRUE"),"STATEMENTS");this.setStyle("procedure_blocks");this.setTooltip($.module$exports$Blockly$Msg.Msg.PROCEDURES_MUTATORCONTAINER_TOOLTIP);
|
||||
d.text=$.module$exports$Blockly$Msg.Msg.VARIABLES_SET_CREATE_GET.replace("%1",c.name),c=(0,$.module$exports$Blockly$Variables.generateVariableFieldDom)(c),e=(0,$.module$exports$Blockly$utils$xml.createElement)("block"),e.setAttribute("type","variables_get"),e.appendChild(c),d.callback=(0,$.module$exports$Blockly$ContextMenu.callbackFactory)(this,e),a.push(d)}}};
|
||||
module$exports$Blockly$libraryBlocks$procedures.blocks.procedures_defnoreturn=Object.assign({},module$contents$Blockly$libraryBlocks$procedures_PROCEDURE_DEF_COMMON,{init:function(){var a=(0,$.module$exports$Blockly$Procedures.findLegalName)("",this);a=new $.module$exports$Blockly$FieldTextInput.FieldTextInput(a,$.module$exports$Blockly$Procedures.rename);a.setSpellcheck(!1);this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_TITLE).appendField(a,"NAME").appendField("",
|
||||
"PARAMS");this.setMutator(new $.module$exports$Blockly$Mutator.Mutator(["procedures_mutatorarg"]));(this.workspace.options.comments||this.workspace.options.parentWorkspace&&this.workspace.options.parentWorkspace.options.comments)&&$.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_COMMENT&&this.setCommentText($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_COMMENT);this.setStyle("procedure_blocks");this.setTooltip($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_TOOLTIP);this.setHelpUrl($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFNORETURN_HELPURL);
|
||||
this.arguments_=[];this.argumentVarModels_=[];this.setStatements_(!0);this.statementConnection_=null},getProcedureDef:function(){return[this.getFieldValue("NAME"),this.arguments_,!1]},callType_:"procedures_callnoreturn"});
|
||||
module$exports$Blockly$libraryBlocks$procedures.blocks.procedures_defreturn=Object.assign({},module$contents$Blockly$libraryBlocks$procedures_PROCEDURE_DEF_COMMON,{init:function(){var a=(0,$.module$exports$Blockly$Procedures.findLegalName)("",this);a=new $.module$exports$Blockly$FieldTextInput.FieldTextInput(a,$.module$exports$Blockly$Procedures.rename);a.setSpellcheck(!1);this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_TITLE).appendField(a,"NAME").appendField("",
|
||||
"PARAMS");this.appendValueInput("RETURN").setAlign($.module$exports$Blockly$Input.Align.RIGHT).appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_RETURN);this.setMutator(new $.module$exports$Blockly$Mutator.Mutator(["procedures_mutatorarg"]));(this.workspace.options.comments||this.workspace.options.parentWorkspace&&this.workspace.options.parentWorkspace.options.comments)&&$.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_COMMENT&&this.setCommentText($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_COMMENT);
|
||||
this.setStyle("procedure_blocks");this.setTooltip($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_TOOLTIP);this.setHelpUrl($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_HELPURL);this.arguments_=[];this.argumentVarModels_=[];this.setStatements_(!0);this.statementConnection_=null},getProcedureDef:function(){return[this.getFieldValue("NAME"),this.arguments_,!0]},callType_:"procedures_callreturn"});
|
||||
module$exports$Blockly$libraryBlocks$procedures.blocks.procedures_mutatorcontainer={init:function(){this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_MUTATORCONTAINER_TITLE);this.appendStatementInput("STACK");this.appendDummyInput("STATEMENT_INPUT").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_ALLOW_STATEMENTS).appendField(new $.module$exports$Blockly$FieldCheckbox.FieldCheckbox("TRUE"),"STATEMENTS");this.setStyle("procedure_blocks");this.setTooltip($.module$exports$Blockly$Msg.Msg.PROCEDURES_MUTATORCONTAINER_TOOLTIP);
|
||||
this.contextMenu=!1}};
|
||||
$.module$exports$Blockly$blocks.Blocks.procedures_mutatorarg={init:function(){var a=new $.module$exports$Blockly$FieldTextInput.FieldTextInput($.module$exports$Blockly$Procedures.DEFAULT_ARG,this.validator_);a.oldShowEditorFn_=a.showEditor_;a.showEditor_=function(){this.createdVariables_=[];this.oldShowEditorFn_()};this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_MUTATORARG_TITLE).appendField(a,"NAME");this.setPreviousStatement(!0);this.setNextStatement(!0);this.setStyle("procedure_blocks");
|
||||
this.setTooltip($.module$exports$Blockly$Msg.Msg.PROCEDURES_MUTATORARG_TOOLTIP);this.contextMenu=!1;a.onFinishEditing_=this.deleteIntermediateVars_;a.createdVariables_=[];a.onFinishEditing_("x")},validator_:function(a){var b=this.getSourceBlock(),c=$.module$exports$Blockly$Mutator.Mutator.findParentWs(b.workspace);a=a.replace(/[\s\xa0]+/g," ").replace(/^ | $/g,"");if(!a)return null;for(var d=(b.workspace.targetWorkspace||b.workspace).getAllBlocks(!1),e=a.toLowerCase(),f=0;f<d.length;f++)if(d[f].id!==
|
||||
this.getSourceBlock().id){var g=d[f].getFieldValue("NAME");if(g&&g.toLowerCase()===e)return null}if(b.isInFlyout)return a;(b=c.getVariable(a,""))&&b.name!==a&&c.renameVariableById(b.getId(),a);b||(b=c.createVariable(a,""))&&this.createdVariables_&&this.createdVariables_.push(b);return a},deleteIntermediateVars_:function(a){var b=$.module$exports$Blockly$Mutator.Mutator.findParentWs(this.getSourceBlock().workspace);if(b)for(var c=0;c<this.createdVariables_.length;c++){var d=this.createdVariables_[c];
|
||||
d.name!==a&&b.deleteVariableById(d.getId())}}};
|
||||
var module$contents$Blockly$blocks$procedures_PROCEDURE_CALL_COMMON={getProcedureCall:function(){return this.getFieldValue("NAME")},renameProcedure:function(a,b){$.module$exports$Blockly$Names.Names.equals(a,this.getProcedureCall())&&(this.setFieldValue(b,"NAME"),this.setTooltip((this.outputConnection?$.module$exports$Blockly$Msg.Msg.PROCEDURES_CALLRETURN_TOOLTIP:$.module$exports$Blockly$Msg.Msg.PROCEDURES_CALLNORETURN_TOOLTIP).replace("%1",b)))},setProcedureParameters_:function(a,b){var c=(0,$.module$exports$Blockly$Procedures.getDefinition)(this.getProcedureCall(),
|
||||
this.workspace),d=c&&c.mutator&&c.mutator.isVisible();d||(this.quarkConnections_={},this.quarkIds_=null);if(b)if(a.join("\n")===this.arguments_.join("\n"))this.quarkIds_=b;else{if(b.length!==a.length)throw RangeError("paramNames and paramIds must be the same length.");this.setCollapsed(!1);this.quarkIds_||(this.quarkConnections_={},this.quarkIds_=[]);c=this.rendered;this.rendered=!1;for(var e=0;e<this.arguments_.length;e++){var f=this.getInput("ARG"+e);f&&(f=f.connection.targetConnection,this.quarkConnections_[this.quarkIds_[e]]=
|
||||
f,d&&f&&-1===b.indexOf(this.quarkIds_[e])&&(f.disconnect(),f.getSourceBlock().bumpNeighbours()))}this.arguments_=[].concat(a);this.argumentVarModels_=[];for(a=0;a<this.arguments_.length;a++)d=(0,$.module$exports$Blockly$Variables.getOrCreateVariablePackage)(this.workspace,null,this.arguments_[a],""),this.argumentVarModels_.push(d);this.updateShape_();if(this.quarkIds_=b)for(b=0;b<this.arguments_.length;b++)a=this.quarkIds_[b],a in this.quarkConnections_&&($.module$exports$Blockly$Mutator.Mutator.reconnect(this.quarkConnections_[a],
|
||||
this,"ARG"+b)||delete this.quarkConnections_[a]);(this.rendered=c)&&this.render()}},updateShape_:function(){for(var a=0;a<this.arguments_.length;a++){var b=this.getField("ARGNAME"+a);if(b){(0,$.module$exports$Blockly$Events.disable)();try{b.setValue(this.arguments_[a])}finally{(0,$.module$exports$Blockly$Events.enable)()}}else b=new $.module$exports$Blockly$FieldLabel.FieldLabel(this.arguments_[a]),this.appendValueInput("ARG"+a).setAlign($.module$exports$Blockly$Input.Align.RIGHT).appendField(b,"ARGNAME"+
|
||||
a).init()}for(a=this.arguments_.length;this.getInput("ARG"+a);a++)this.removeInput("ARG"+a);if(a=this.getInput("TOPROW"))this.arguments_.length?this.getField("WITH")||(a.appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_CALL_BEFORE_PARAMS,"WITH"),a.init()):this.getField("WITH")&&a.removeField("WITH")},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("name",this.getProcedureCall());for(var b=0;b<this.arguments_.length;b++){var c=(0,$.module$exports$Blockly$utils$xml.createElement)("arg");
|
||||
c.setAttribute("name",this.arguments_[b]);a.appendChild(c)}return a},domToMutation:function(a){var b=a.getAttribute("name");this.renameProcedure(this.getProcedureCall(),b);b=[];for(var c=[],d=0,e;e=a.childNodes[d];d++)"arg"===e.nodeName.toLowerCase()&&(b.push(e.getAttribute("name")),c.push(e.getAttribute("paramId")));this.setProcedureParameters_(b,c)},saveExtraState:function(){var a=Object.create(null);a.name=this.getProcedureCall();this.arguments_.length&&(a.params=this.arguments_);return a},loadExtraState:function(a){this.renameProcedure(this.getProcedureCall(),
|
||||
a.name);if(a=a.params){var b=[];b.length=a.length;b.fill(null);this.setProcedureParameters_(a,b)}},getVars:function(){return this.arguments_},getVarModels:function(){return this.argumentVarModels_},onchange:function(a){if(this.workspace&&!this.workspace.isFlyout&&a.recordUndo)if(a.type===$.module$exports$Blockly$Events.BLOCK_CREATE&&-1!==a.ids.indexOf(this.id)){var b=this.getProcedureCall();b=(0,$.module$exports$Blockly$Procedures.getDefinition)(b,this.workspace);!b||b.type===this.defType_&&JSON.stringify(b.getVars())===
|
||||
JSON.stringify(this.arguments_)||(b=null);if(!b){(0,$.module$exports$Blockly$Events.setGroup)(a.group);a=(0,$.module$exports$Blockly$utils$xml.createElement)("xml");b=(0,$.module$exports$Blockly$utils$xml.createElement)("block");b.setAttribute("type",this.defType_);var c=this.getRelativeToSurfaceXY(),d=c.y+2*$.module$exports$Blockly$internalConstants.SNAP_RADIUS;b.setAttribute("x",c.x+$.module$exports$Blockly$internalConstants.SNAP_RADIUS*(this.RTL?-1:1));b.setAttribute("y",d);c=this.mutationToDom();
|
||||
b.appendChild(c);c=(0,$.module$exports$Blockly$utils$xml.createElement)("field");c.setAttribute("name","NAME");d=this.getProcedureCall();d||(d=(0,$.module$exports$Blockly$Procedures.findLegalName)("",this),this.renameProcedure("",d));c.appendChild((0,$.module$exports$Blockly$utils$xml.createTextNode)(d));b.appendChild(c);a.appendChild(b);(0,$.module$exports$Blockly$Xml.domToWorkspace)(a,this.workspace);(0,$.module$exports$Blockly$Events.setGroup)(!1)}}else a.type===$.module$exports$Blockly$Events.BLOCK_DELETE?
|
||||
(b=this.getProcedureCall(),(0,$.module$exports$Blockly$Procedures.getDefinition)(b,this.workspace)||((0,$.module$exports$Blockly$Events.setGroup)(a.group),this.dispose(!0),(0,$.module$exports$Blockly$Events.setGroup)(!1))):a.type===$.module$exports$Blockly$Events.CHANGE&&"disabled"===a.element&&(b=this.getProcedureCall(),(b=(0,$.module$exports$Blockly$Procedures.getDefinition)(b,this.workspace))&&b.id===a.blockId&&((b=(0,$.module$exports$Blockly$Events.getGroup)())&&console.log("Saw an existing group while responding to a definition change"),
|
||||
(0,$.module$exports$Blockly$Events.setGroup)(a.group),a.newValue?(this.previousEnabledState_=this.isEnabled(),this.setEnabled(!1)):this.setEnabled(this.previousEnabledState_),(0,$.module$exports$Blockly$Events.setGroup)(b)))},customContextMenu:function(a){if(this.workspace.isMovable()){var b={enabled:!0};b.text=$.module$exports$Blockly$Msg.Msg.PROCEDURES_HIGHLIGHT_DEF;var c=this.getProcedureCall(),d=this.workspace;b.callback=function(){var e=(0,$.module$exports$Blockly$Procedures.getDefinition)(c,
|
||||
d);e&&(d.centerOnBlock(e.id),e.select())};a.push(b)}}};
|
||||
$.module$exports$Blockly$blocks.Blocks.procedures_callnoreturn=Object.assign({},module$contents$Blockly$blocks$procedures_PROCEDURE_CALL_COMMON,{init:function(){this.appendDummyInput("TOPROW").appendField("","NAME");this.setPreviousStatement(!0);this.setNextStatement(!0);this.setStyle("procedure_blocks");this.setHelpUrl($.module$exports$Blockly$Msg.Msg.PROCEDURES_CALLNORETURN_HELPURL);this.arguments_=[];this.argumentVarModels_=[];this.quarkConnections_={};this.quarkIds_=null;this.previousEnabledState_=
|
||||
!0},defType_:"procedures_defnoreturn"});
|
||||
$.module$exports$Blockly$blocks.Blocks.procedures_callreturn=Object.assign({},module$contents$Blockly$blocks$procedures_PROCEDURE_CALL_COMMON,{init:function(){this.appendDummyInput("TOPROW").appendField("","NAME");this.setOutput(!0);this.setStyle("procedure_blocks");this.setHelpUrl($.module$exports$Blockly$Msg.Msg.PROCEDURES_CALLRETURN_HELPURL);this.arguments_=[];this.argumentVarModels_=[];this.quarkConnections_={};this.quarkIds_=null;this.previousEnabledState_=!0},defType_:"procedures_defreturn"});
|
||||
$.module$exports$Blockly$blocks.Blocks.procedures_ifreturn={init:function(){this.appendValueInput("CONDITION").setCheck("Boolean").appendField($.module$exports$Blockly$Msg.Msg.CONTROLS_IF_MSG_IF);this.appendValueInput("VALUE").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_RETURN);this.setInputsInline(!0);this.setPreviousStatement(!0);this.setNextStatement(!0);this.setStyle("procedure_blocks");this.setTooltip($.module$exports$Blockly$Msg.Msg.PROCEDURES_IFRETURN_TOOLTIP);this.setHelpUrl($.module$exports$Blockly$Msg.Msg.PROCEDURES_IFRETURN_HELPURL);
|
||||
this.hasReturnValue_=!0},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("value",Number(this.hasReturnValue_));return a},domToMutation:function(a){this.hasReturnValue_="1"===a.getAttribute("value");this.hasReturnValue_||(this.removeInput("VALUE"),this.appendDummyInput("VALUE").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_RETURN))},onchange:function(a){if(!this.workspace.isDragging||!this.workspace.isDragging()){a=
|
||||
!1;var b=this;do{if(-1!==this.FUNCTION_TYPES.indexOf(b.type)){a=!0;break}b=b.getSurroundParent()}while(b);a?("procedures_defnoreturn"===b.type&&this.hasReturnValue_?(this.removeInput("VALUE"),this.appendDummyInput("VALUE").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_RETURN),this.hasReturnValue_=!1):"procedures_defreturn"!==b.type||this.hasReturnValue_||(this.removeInput("VALUE"),this.appendValueInput("VALUE").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_RETURN),
|
||||
this.hasReturnValue_=!0),this.setWarningText(null),this.isInFlyout||this.setEnabled(!0)):(this.setWarningText($.module$exports$Blockly$Msg.Msg.PROCEDURES_IFRETURN_WARNING),this.isInFlyout||this.getInheritedDisabled()||this.setEnabled(!1))}},FUNCTION_TYPES:["procedures_defnoreturn","procedures_defreturn"]};var module$exports$Blockly$blocks$math={};
|
||||
(0,$.module$exports$Blockly$common.defineBlocksWithJsonArray)([{type:"math_number",message0:"%1",args0:[{type:"field_number",name:"NUM",value:0}],output:"Number",helpUrl:"%{BKY_MATH_NUMBER_HELPURL}",style:"math_blocks",tooltip:"%{BKY_MATH_NUMBER_TOOLTIP}",extensions:["parent_tooltip_when_inline"]},{type:"math_arithmetic",message0:"%1 %2 %3",args0:[{type:"input_value",name:"A",check:"Number"},{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ADDITION_SYMBOL}","ADD"],["%{BKY_MATH_SUBTRACTION_SYMBOL}",
|
||||
"MINUS"],["%{BKY_MATH_MULTIPLICATION_SYMBOL}","MULTIPLY"],["%{BKY_MATH_DIVISION_SYMBOL}","DIVIDE"],["%{BKY_MATH_POWER_SYMBOL}","POWER"]]},{type:"input_value",name:"B",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ARITHMETIC_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_single",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_SINGLE_OP_ROOT}","ROOT"],["%{BKY_MATH_SINGLE_OP_ABSOLUTE}","ABS"],["-","NEG"],["ln","LN"],["log10",
|
||||
"LOG10"],["e^","EXP"],["10^","POW10"]]},{type:"input_value",name:"NUM",check:"Number"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_SINGLE_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_trig",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_TRIG_SIN}","SIN"],["%{BKY_MATH_TRIG_COS}","COS"],["%{BKY_MATH_TRIG_TAN}","TAN"],["%{BKY_MATH_TRIG_ASIN}","ASIN"],["%{BKY_MATH_TRIG_ACOS}","ACOS"],["%{BKY_MATH_TRIG_ATAN}","ATAN"]]},{type:"input_value",name:"NUM",
|
||||
check:"Number"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_TRIG_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_constant",message0:"%1",args0:[{type:"field_dropdown",name:"CONSTANT",options:[["\u03c0","PI"],["e","E"],["\u03c6","GOLDEN_RATIO"],["sqrt(2)","SQRT2"],["sqrt(\u00bd)","SQRT1_2"],["\u221e","INFINITY"]]}],output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_CONSTANT_TOOLTIP}",helpUrl:"%{BKY_MATH_CONSTANT_HELPURL}"},{type:"math_number_property",message0:"%1 %2",args0:[{type:"input_value",
|
||||
name:"NUMBER_TO_CHECK",check:"Number"},{type:"field_dropdown",name:"PROPERTY",options:[["%{BKY_MATH_IS_EVEN}","EVEN"],["%{BKY_MATH_IS_ODD}","ODD"],["%{BKY_MATH_IS_PRIME}","PRIME"],["%{BKY_MATH_IS_WHOLE}","WHOLE"],["%{BKY_MATH_IS_POSITIVE}","POSITIVE"],["%{BKY_MATH_IS_NEGATIVE}","NEGATIVE"],["%{BKY_MATH_IS_DIVISIBLE_BY}","DIVISIBLE_BY"]]}],inputsInline:!0,output:"Boolean",style:"math_blocks",tooltip:"%{BKY_MATH_IS_TOOLTIP}",mutator:"math_is_divisibleby_mutator"},{type:"math_change",message0:"%{BKY_MATH_CHANGE_TITLE}",
|
||||
args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_MATH_CHANGE_TITLE_ITEM}"},{type:"input_value",name:"DELTA",check:"Number"}],previousStatement:null,nextStatement:null,style:"variable_blocks",helpUrl:"%{BKY_MATH_CHANGE_HELPURL}",extensions:["math_change_tooltip"]},{type:"math_round",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ROUND_OPERATOR_ROUND}","ROUND"],["%{BKY_MATH_ROUND_OPERATOR_ROUNDUP}","ROUNDUP"],["%{BKY_MATH_ROUND_OPERATOR_ROUNDDOWN}","ROUNDDOWN"]]},
|
||||
{type:"input_value",name:"NUM",check:"Number"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ROUND_HELPURL}",tooltip:"%{BKY_MATH_ROUND_TOOLTIP}"},{type:"math_on_list",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ONLIST_OPERATOR_SUM}","SUM"],["%{BKY_MATH_ONLIST_OPERATOR_MIN}","MIN"],["%{BKY_MATH_ONLIST_OPERATOR_MAX}","MAX"],["%{BKY_MATH_ONLIST_OPERATOR_AVERAGE}","AVERAGE"],["%{BKY_MATH_ONLIST_OPERATOR_MEDIAN}","MEDIAN"],["%{BKY_MATH_ONLIST_OPERATOR_MODE}",
|
||||
"MODE"],["%{BKY_MATH_ONLIST_OPERATOR_STD_DEV}","STD_DEV"],["%{BKY_MATH_ONLIST_OPERATOR_RANDOM}","RANDOM"]]},{type:"input_value",name:"LIST",check:"Array"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ONLIST_HELPURL}",mutator:"math_modes_of_list_mutator",extensions:["math_op_tooltip"]},{type:"math_modulo",message0:"%{BKY_MATH_MODULO_TITLE}",args0:[{type:"input_value",name:"DIVIDEND",check:"Number"},{type:"input_value",name:"DIVISOR",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",
|
||||
tooltip:"%{BKY_MATH_MODULO_TOOLTIP}",helpUrl:"%{BKY_MATH_MODULO_HELPURL}"},{type:"math_constrain",message0:"%{BKY_MATH_CONSTRAIN_TITLE}",args0:[{type:"input_value",name:"VALUE",check:"Number"},{type:"input_value",name:"LOW",check:"Number"},{type:"input_value",name:"HIGH",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_CONSTRAIN_TOOLTIP}",helpUrl:"%{BKY_MATH_CONSTRAIN_HELPURL}"},{type:"math_random_int",message0:"%{BKY_MATH_RANDOM_INT_TITLE}",args0:[{type:"input_value",
|
||||
name:"FROM",check:"Number"},{type:"input_value",name:"TO",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_RANDOM_INT_TOOLTIP}",helpUrl:"%{BKY_MATH_RANDOM_INT_HELPURL}"},{type:"math_random_float",message0:"%{BKY_MATH_RANDOM_FLOAT_TITLE_RANDOM}",output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_RANDOM_FLOAT_TOOLTIP}",helpUrl:"%{BKY_MATH_RANDOM_FLOAT_HELPURL}"},{type:"math_atan2",message0:"%{BKY_MATH_ATAN2_TITLE}",args0:[{type:"input_value",name:"X",check:"Number"},
|
||||
{type:"input_value",name:"Y",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_ATAN2_TOOLTIP}",helpUrl:"%{BKY_MATH_ATAN2_HELPURL}"}]);
|
||||
var module$contents$Blockly$blocks$math_TOOLTIPS_BY_OP={ADD:"%{BKY_MATH_ARITHMETIC_TOOLTIP_ADD}",MINUS:"%{BKY_MATH_ARITHMETIC_TOOLTIP_MINUS}",MULTIPLY:"%{BKY_MATH_ARITHMETIC_TOOLTIP_MULTIPLY}",DIVIDE:"%{BKY_MATH_ARITHMETIC_TOOLTIP_DIVIDE}",POWER:"%{BKY_MATH_ARITHMETIC_TOOLTIP_POWER}",ROOT:"%{BKY_MATH_SINGLE_TOOLTIP_ROOT}",ABS:"%{BKY_MATH_SINGLE_TOOLTIP_ABS}",NEG:"%{BKY_MATH_SINGLE_TOOLTIP_NEG}",LN:"%{BKY_MATH_SINGLE_TOOLTIP_LN}",LOG10:"%{BKY_MATH_SINGLE_TOOLTIP_LOG10}",EXP:"%{BKY_MATH_SINGLE_TOOLTIP_EXP}",
|
||||
module$exports$Blockly$libraryBlocks$procedures.blocks.procedures_mutatorarg={init:function(){var a=new $.module$exports$Blockly$FieldTextInput.FieldTextInput($.module$exports$Blockly$Procedures.DEFAULT_ARG,this.validator_);a.oldShowEditorFn_=a.showEditor_;a.showEditor_=function(){this.createdVariables_=[];this.oldShowEditorFn_()};this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_MUTATORARG_TITLE).appendField(a,"NAME");this.setPreviousStatement(!0);this.setNextStatement(!0);
|
||||
this.setStyle("procedure_blocks");this.setTooltip($.module$exports$Blockly$Msg.Msg.PROCEDURES_MUTATORARG_TOOLTIP);this.contextMenu=!1;a.onFinishEditing_=this.deleteIntermediateVars_;a.createdVariables_=[];a.onFinishEditing_("x")},validator_:function(a){var b=this.getSourceBlock(),c=$.module$exports$Blockly$Mutator.Mutator.findParentWs(b.workspace);a=a.replace(/[\s\xa0]+/g," ").replace(/^ | $/g,"");if(!a)return null;for(var d=(b.workspace.targetWorkspace||b.workspace).getAllBlocks(!1),e=a.toLowerCase(),
|
||||
f=0;f<d.length;f++)if(d[f].id!==this.getSourceBlock().id){var g=d[f].getFieldValue("NAME");if(g&&g.toLowerCase()===e)return null}if(b.isInFlyout)return a;(b=c.getVariable(a,""))&&b.name!==a&&c.renameVariableById(b.getId(),a);b||(b=c.createVariable(a,""))&&this.createdVariables_&&this.createdVariables_.push(b);return a},deleteIntermediateVars_:function(a){var b=$.module$exports$Blockly$Mutator.Mutator.findParentWs(this.getSourceBlock().workspace);if(b)for(var c=0;c<this.createdVariables_.length;c++){var d=
|
||||
this.createdVariables_[c];d.name!==a&&b.deleteVariableById(d.getId())}}};
|
||||
var module$contents$Blockly$libraryBlocks$procedures_PROCEDURE_CALL_COMMON={getProcedureCall:function(){return this.getFieldValue("NAME")},renameProcedure:function(a,b){$.module$exports$Blockly$Names.Names.equals(a,this.getProcedureCall())&&(this.setFieldValue(b,"NAME"),this.setTooltip((this.outputConnection?$.module$exports$Blockly$Msg.Msg.PROCEDURES_CALLRETURN_TOOLTIP:$.module$exports$Blockly$Msg.Msg.PROCEDURES_CALLNORETURN_TOOLTIP).replace("%1",b)))},setProcedureParameters_:function(a,b){var c=
|
||||
(0,$.module$exports$Blockly$Procedures.getDefinition)(this.getProcedureCall(),this.workspace),d=c&&c.mutator&&c.mutator.isVisible();d||(this.quarkConnections_={},this.quarkIds_=null);if(b)if(a.join("\n")===this.arguments_.join("\n"))this.quarkIds_=b;else{if(b.length!==a.length)throw RangeError("paramNames and paramIds must be the same length.");this.setCollapsed(!1);this.quarkIds_||(this.quarkConnections_={},this.quarkIds_=[]);c=this.rendered;this.rendered=!1;for(var e=0;e<this.arguments_.length;e++){var f=
|
||||
this.getInput("ARG"+e);f&&(f=f.connection.targetConnection,this.quarkConnections_[this.quarkIds_[e]]=f,d&&f&&-1===b.indexOf(this.quarkIds_[e])&&(f.disconnect(),f.getSourceBlock().bumpNeighbours()))}this.arguments_=[].concat(a);this.argumentVarModels_=[];for(a=0;a<this.arguments_.length;a++)d=(0,$.module$exports$Blockly$Variables.getOrCreateVariablePackage)(this.workspace,null,this.arguments_[a],""),this.argumentVarModels_.push(d);this.updateShape_();if(this.quarkIds_=b)for(b=0;b<this.arguments_.length;b++)a=
|
||||
this.quarkIds_[b],a in this.quarkConnections_&&($.module$exports$Blockly$Mutator.Mutator.reconnect(this.quarkConnections_[a],this,"ARG"+b)||delete this.quarkConnections_[a]);(this.rendered=c)&&this.render()}},updateShape_:function(){for(var a=0;a<this.arguments_.length;a++){var b=this.getField("ARGNAME"+a);if(b){(0,$.module$exports$Blockly$Events.disable)();try{b.setValue(this.arguments_[a])}finally{(0,$.module$exports$Blockly$Events.enable)()}}else b=new $.module$exports$Blockly$FieldLabel.FieldLabel(this.arguments_[a]),
|
||||
this.appendValueInput("ARG"+a).setAlign($.module$exports$Blockly$Input.Align.RIGHT).appendField(b,"ARGNAME"+a).init()}for(a=this.arguments_.length;this.getInput("ARG"+a);a++)this.removeInput("ARG"+a);if(a=this.getInput("TOPROW"))this.arguments_.length?this.getField("WITH")||(a.appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_CALL_BEFORE_PARAMS,"WITH"),a.init()):this.getField("WITH")&&a.removeField("WITH")},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");
|
||||
a.setAttribute("name",this.getProcedureCall());for(var b=0;b<this.arguments_.length;b++){var c=(0,$.module$exports$Blockly$utils$xml.createElement)("arg");c.setAttribute("name",this.arguments_[b]);a.appendChild(c)}return a},domToMutation:function(a){var b=a.getAttribute("name");this.renameProcedure(this.getProcedureCall(),b);b=[];for(var c=[],d=0,e;e=a.childNodes[d];d++)"arg"===e.nodeName.toLowerCase()&&(b.push(e.getAttribute("name")),c.push(e.getAttribute("paramId")));this.setProcedureParameters_(b,
|
||||
c)},saveExtraState:function(){var a=Object.create(null);a.name=this.getProcedureCall();this.arguments_.length&&(a.params=this.arguments_);return a},loadExtraState:function(a){this.renameProcedure(this.getProcedureCall(),a.name);if(a=a.params){var b=[];b.length=a.length;b.fill(null);this.setProcedureParameters_(a,b)}},getVars:function(){return this.arguments_},getVarModels:function(){return this.argumentVarModels_},onchange:function(a){if(this.workspace&&!this.workspace.isFlyout&&a.recordUndo)if(a.type===
|
||||
$.module$exports$Blockly$Events.BLOCK_CREATE&&-1!==a.ids.indexOf(this.id)){var b=this.getProcedureCall();b=(0,$.module$exports$Blockly$Procedures.getDefinition)(b,this.workspace);!b||b.type===this.defType_&&JSON.stringify(b.getVars())===JSON.stringify(this.arguments_)||(b=null);if(!b){(0,$.module$exports$Blockly$Events.setGroup)(a.group);a=(0,$.module$exports$Blockly$utils$xml.createElement)("xml");b=(0,$.module$exports$Blockly$utils$xml.createElement)("block");b.setAttribute("type",this.defType_);
|
||||
var c=this.getRelativeToSurfaceXY(),d=c.y+2*$.module$exports$Blockly$config.config.snapRadius;b.setAttribute("x",c.x+$.module$exports$Blockly$config.config.snapRadius*(this.RTL?-1:1));b.setAttribute("y",d);c=this.mutationToDom();b.appendChild(c);c=(0,$.module$exports$Blockly$utils$xml.createElement)("field");c.setAttribute("name","NAME");d=this.getProcedureCall();var e=(0,$.module$exports$Blockly$Procedures.findLegalName)(d,this);d!==e&&this.renameProcedure(d,e);c.appendChild((0,$.module$exports$Blockly$utils$xml.createTextNode)(d));
|
||||
b.appendChild(c);a.appendChild(b);(0,$.module$exports$Blockly$Xml.domToWorkspace)(a,this.workspace);(0,$.module$exports$Blockly$Events.setGroup)(!1)}}else a.type===$.module$exports$Blockly$Events.BLOCK_DELETE?(b=this.getProcedureCall(),(0,$.module$exports$Blockly$Procedures.getDefinition)(b,this.workspace)||((0,$.module$exports$Blockly$Events.setGroup)(a.group),this.dispose(!0),(0,$.module$exports$Blockly$Events.setGroup)(!1))):a.type===$.module$exports$Blockly$Events.CHANGE&&"disabled"===a.element&&
|
||||
(b=this.getProcedureCall(),(b=(0,$.module$exports$Blockly$Procedures.getDefinition)(b,this.workspace))&&b.id===a.blockId&&((b=(0,$.module$exports$Blockly$Events.getGroup)())&&console.log("Saw an existing group while responding to a definition change"),(0,$.module$exports$Blockly$Events.setGroup)(a.group),a.newValue?(this.previousEnabledState_=this.isEnabled(),this.setEnabled(!1)):this.setEnabled(this.previousEnabledState_),(0,$.module$exports$Blockly$Events.setGroup)(b)))},customContextMenu:function(a){if(this.workspace.isMovable()){var b=
|
||||
{enabled:!0};b.text=$.module$exports$Blockly$Msg.Msg.PROCEDURES_HIGHLIGHT_DEF;var c=this.getProcedureCall(),d=this.workspace;b.callback=function(){var e=(0,$.module$exports$Blockly$Procedures.getDefinition)(c,d);e&&(d.centerOnBlock(e.id),e.select())};a.push(b)}}};
|
||||
module$exports$Blockly$libraryBlocks$procedures.blocks.procedures_callnoreturn=Object.assign({},module$contents$Blockly$libraryBlocks$procedures_PROCEDURE_CALL_COMMON,{init:function(){this.appendDummyInput("TOPROW").appendField("","NAME");this.setPreviousStatement(!0);this.setNextStatement(!0);this.setStyle("procedure_blocks");this.setHelpUrl($.module$exports$Blockly$Msg.Msg.PROCEDURES_CALLNORETURN_HELPURL);this.arguments_=[];this.argumentVarModels_=[];this.quarkConnections_={};this.quarkIds_=null;
|
||||
this.previousEnabledState_=!0},defType_:"procedures_defnoreturn"});
|
||||
module$exports$Blockly$libraryBlocks$procedures.blocks.procedures_callreturn=Object.assign({},module$contents$Blockly$libraryBlocks$procedures_PROCEDURE_CALL_COMMON,{init:function(){this.appendDummyInput("TOPROW").appendField("","NAME");this.setOutput(!0);this.setStyle("procedure_blocks");this.setHelpUrl($.module$exports$Blockly$Msg.Msg.PROCEDURES_CALLRETURN_HELPURL);this.arguments_=[];this.argumentVarModels_=[];this.quarkConnections_={};this.quarkIds_=null;this.previousEnabledState_=!0},defType_:"procedures_defreturn"});
|
||||
module$exports$Blockly$libraryBlocks$procedures.blocks.procedures_ifreturn={init:function(){this.appendValueInput("CONDITION").setCheck("Boolean").appendField($.module$exports$Blockly$Msg.Msg.CONTROLS_IF_MSG_IF);this.appendValueInput("VALUE").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_RETURN);this.setInputsInline(!0);this.setPreviousStatement(!0);this.setNextStatement(!0);this.setStyle("procedure_blocks");this.setTooltip($.module$exports$Blockly$Msg.Msg.PROCEDURES_IFRETURN_TOOLTIP);
|
||||
this.setHelpUrl($.module$exports$Blockly$Msg.Msg.PROCEDURES_IFRETURN_HELPURL);this.hasReturnValue_=!0},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("value",Number(this.hasReturnValue_));return a},domToMutation:function(a){this.hasReturnValue_="1"===a.getAttribute("value");this.hasReturnValue_||(this.removeInput("VALUE"),this.appendDummyInput("VALUE").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_RETURN))},onchange:function(a){if(!(this.workspace.isDragging&&
|
||||
this.workspace.isDragging()||a.type!==$.module$exports$Blockly$Events.BLOCK_MOVE)){var b=!1,c=this;do{if(-1!==this.FUNCTION_TYPES.indexOf(c.type)){b=!0;break}c=c.getSurroundParent()}while(c);b?("procedures_defnoreturn"===c.type&&this.hasReturnValue_?(this.removeInput("VALUE"),this.appendDummyInput("VALUE").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_RETURN),this.hasReturnValue_=!1):"procedures_defreturn"!==c.type||this.hasReturnValue_||(this.removeInput("VALUE"),this.appendValueInput("VALUE").appendField($.module$exports$Blockly$Msg.Msg.PROCEDURES_DEFRETURN_RETURN),
|
||||
this.hasReturnValue_=!0),this.setWarningText(null)):this.setWarningText($.module$exports$Blockly$Msg.Msg.PROCEDURES_IFRETURN_WARNING);this.isInFlyout||(c=(0,$.module$exports$Blockly$Events.getGroup)(),(0,$.module$exports$Blockly$Events.setGroup)(a.group),this.setEnabled(b),(0,$.module$exports$Blockly$Events.setGroup)(c))}},FUNCTION_TYPES:["procedures_defnoreturn","procedures_defreturn"]};(0,$.module$exports$Blockly$common.defineBlocks)(module$exports$Blockly$libraryBlocks$procedures.blocks);var module$exports$Blockly$libraryBlocks$math={};
|
||||
module$exports$Blockly$libraryBlocks$math.blocks=(0,$.module$exports$Blockly$common.createBlockDefinitionsFromJsonArray)([{type:"math_number",message0:"%1",args0:[{type:"field_number",name:"NUM",value:0}],output:"Number",helpUrl:"%{BKY_MATH_NUMBER_HELPURL}",style:"math_blocks",tooltip:"%{BKY_MATH_NUMBER_TOOLTIP}",extensions:["parent_tooltip_when_inline"]},{type:"math_arithmetic",message0:"%1 %2 %3",args0:[{type:"input_value",name:"A",check:"Number"},{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ADDITION_SYMBOL}",
|
||||
"ADD"],["%{BKY_MATH_SUBTRACTION_SYMBOL}","MINUS"],["%{BKY_MATH_MULTIPLICATION_SYMBOL}","MULTIPLY"],["%{BKY_MATH_DIVISION_SYMBOL}","DIVIDE"],["%{BKY_MATH_POWER_SYMBOL}","POWER"]]},{type:"input_value",name:"B",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ARITHMETIC_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_single",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_SINGLE_OP_ROOT}","ROOT"],["%{BKY_MATH_SINGLE_OP_ABSOLUTE}",
|
||||
"ABS"],["-","NEG"],["ln","LN"],["log10","LOG10"],["e^","EXP"],["10^","POW10"]]},{type:"input_value",name:"NUM",check:"Number"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_SINGLE_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_trig",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_TRIG_SIN}","SIN"],["%{BKY_MATH_TRIG_COS}","COS"],["%{BKY_MATH_TRIG_TAN}","TAN"],["%{BKY_MATH_TRIG_ASIN}","ASIN"],["%{BKY_MATH_TRIG_ACOS}","ACOS"],["%{BKY_MATH_TRIG_ATAN}",
|
||||
"ATAN"]]},{type:"input_value",name:"NUM",check:"Number"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_TRIG_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_constant",message0:"%1",args0:[{type:"field_dropdown",name:"CONSTANT",options:[["\u03c0","PI"],["e","E"],["\u03c6","GOLDEN_RATIO"],["sqrt(2)","SQRT2"],["sqrt(\u00bd)","SQRT1_2"],["\u221e","INFINITY"]]}],output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_CONSTANT_TOOLTIP}",helpUrl:"%{BKY_MATH_CONSTANT_HELPURL}"},{type:"math_number_property",
|
||||
message0:"%1 %2",args0:[{type:"input_value",name:"NUMBER_TO_CHECK",check:"Number"},{type:"field_dropdown",name:"PROPERTY",options:[["%{BKY_MATH_IS_EVEN}","EVEN"],["%{BKY_MATH_IS_ODD}","ODD"],["%{BKY_MATH_IS_PRIME}","PRIME"],["%{BKY_MATH_IS_WHOLE}","WHOLE"],["%{BKY_MATH_IS_POSITIVE}","POSITIVE"],["%{BKY_MATH_IS_NEGATIVE}","NEGATIVE"],["%{BKY_MATH_IS_DIVISIBLE_BY}","DIVISIBLE_BY"]]}],inputsInline:!0,output:"Boolean",style:"math_blocks",tooltip:"%{BKY_MATH_IS_TOOLTIP}",mutator:"math_is_divisibleby_mutator"},
|
||||
{type:"math_change",message0:"%{BKY_MATH_CHANGE_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_MATH_CHANGE_TITLE_ITEM}"},{type:"input_value",name:"DELTA",check:"Number"}],previousStatement:null,nextStatement:null,style:"variable_blocks",helpUrl:"%{BKY_MATH_CHANGE_HELPURL}",extensions:["math_change_tooltip"]},{type:"math_round",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ROUND_OPERATOR_ROUND}","ROUND"],["%{BKY_MATH_ROUND_OPERATOR_ROUNDUP}","ROUNDUP"],
|
||||
["%{BKY_MATH_ROUND_OPERATOR_ROUNDDOWN}","ROUNDDOWN"]]},{type:"input_value",name:"NUM",check:"Number"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ROUND_HELPURL}",tooltip:"%{BKY_MATH_ROUND_TOOLTIP}"},{type:"math_on_list",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ONLIST_OPERATOR_SUM}","SUM"],["%{BKY_MATH_ONLIST_OPERATOR_MIN}","MIN"],["%{BKY_MATH_ONLIST_OPERATOR_MAX}","MAX"],["%{BKY_MATH_ONLIST_OPERATOR_AVERAGE}","AVERAGE"],["%{BKY_MATH_ONLIST_OPERATOR_MEDIAN}",
|
||||
"MEDIAN"],["%{BKY_MATH_ONLIST_OPERATOR_MODE}","MODE"],["%{BKY_MATH_ONLIST_OPERATOR_STD_DEV}","STD_DEV"],["%{BKY_MATH_ONLIST_OPERATOR_RANDOM}","RANDOM"]]},{type:"input_value",name:"LIST",check:"Array"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ONLIST_HELPURL}",mutator:"math_modes_of_list_mutator",extensions:["math_op_tooltip"]},{type:"math_modulo",message0:"%{BKY_MATH_MODULO_TITLE}",args0:[{type:"input_value",name:"DIVIDEND",check:"Number"},{type:"input_value",name:"DIVISOR",check:"Number"}],
|
||||
inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_MODULO_TOOLTIP}",helpUrl:"%{BKY_MATH_MODULO_HELPURL}"},{type:"math_constrain",message0:"%{BKY_MATH_CONSTRAIN_TITLE}",args0:[{type:"input_value",name:"VALUE",check:"Number"},{type:"input_value",name:"LOW",check:"Number"},{type:"input_value",name:"HIGH",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_CONSTRAIN_TOOLTIP}",helpUrl:"%{BKY_MATH_CONSTRAIN_HELPURL}"},{type:"math_random_int",message0:"%{BKY_MATH_RANDOM_INT_TITLE}",
|
||||
args0:[{type:"input_value",name:"FROM",check:"Number"},{type:"input_value",name:"TO",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_RANDOM_INT_TOOLTIP}",helpUrl:"%{BKY_MATH_RANDOM_INT_HELPURL}"},{type:"math_random_float",message0:"%{BKY_MATH_RANDOM_FLOAT_TITLE_RANDOM}",output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_RANDOM_FLOAT_TOOLTIP}",helpUrl:"%{BKY_MATH_RANDOM_FLOAT_HELPURL}"},{type:"math_atan2",message0:"%{BKY_MATH_ATAN2_TITLE}",args0:[{type:"input_value",
|
||||
name:"X",check:"Number"},{type:"input_value",name:"Y",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_ATAN2_TOOLTIP}",helpUrl:"%{BKY_MATH_ATAN2_HELPURL}"}]);
|
||||
var module$contents$Blockly$libraryBlocks$math_TOOLTIPS_BY_OP={ADD:"%{BKY_MATH_ARITHMETIC_TOOLTIP_ADD}",MINUS:"%{BKY_MATH_ARITHMETIC_TOOLTIP_MINUS}",MULTIPLY:"%{BKY_MATH_ARITHMETIC_TOOLTIP_MULTIPLY}",DIVIDE:"%{BKY_MATH_ARITHMETIC_TOOLTIP_DIVIDE}",POWER:"%{BKY_MATH_ARITHMETIC_TOOLTIP_POWER}",ROOT:"%{BKY_MATH_SINGLE_TOOLTIP_ROOT}",ABS:"%{BKY_MATH_SINGLE_TOOLTIP_ABS}",NEG:"%{BKY_MATH_SINGLE_TOOLTIP_NEG}",LN:"%{BKY_MATH_SINGLE_TOOLTIP_LN}",LOG10:"%{BKY_MATH_SINGLE_TOOLTIP_LOG10}",EXP:"%{BKY_MATH_SINGLE_TOOLTIP_EXP}",
|
||||
POW10:"%{BKY_MATH_SINGLE_TOOLTIP_POW10}",SIN:"%{BKY_MATH_TRIG_TOOLTIP_SIN}",COS:"%{BKY_MATH_TRIG_TOOLTIP_COS}",TAN:"%{BKY_MATH_TRIG_TOOLTIP_TAN}",ASIN:"%{BKY_MATH_TRIG_TOOLTIP_ASIN}",ACOS:"%{BKY_MATH_TRIG_TOOLTIP_ACOS}",ATAN:"%{BKY_MATH_TRIG_TOOLTIP_ATAN}",SUM:"%{BKY_MATH_ONLIST_TOOLTIP_SUM}",MIN:"%{BKY_MATH_ONLIST_TOOLTIP_MIN}",MAX:"%{BKY_MATH_ONLIST_TOOLTIP_MAX}",AVERAGE:"%{BKY_MATH_ONLIST_TOOLTIP_AVERAGE}",MEDIAN:"%{BKY_MATH_ONLIST_TOOLTIP_MEDIAN}",MODE:"%{BKY_MATH_ONLIST_TOOLTIP_MODE}",STD_DEV:"%{BKY_MATH_ONLIST_TOOLTIP_STD_DEV}",
|
||||
RANDOM:"%{BKY_MATH_ONLIST_TOOLTIP_RANDOM}"};(0,$.module$exports$Blockly$Extensions.register)("math_op_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipForDropdown)("OP",module$contents$Blockly$blocks$math_TOOLTIPS_BY_OP));
|
||||
var module$contents$Blockly$blocks$math_IS_DIVISIBLEBY_MUTATOR_MIXIN={mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation"),b="DIVISIBLE_BY"===this.getFieldValue("PROPERTY");a.setAttribute("divisor_input",b);return a},domToMutation:function(a){a="true"===a.getAttribute("divisor_input");this.updateShape_(a)},updateShape_:function(a){var b=this.getInput("DIVISOR");a?b||this.appendValueInput("DIVISOR").setCheck("Number"):b&&this.removeInput("DIVISOR")}},module$contents$Blockly$blocks$math_IS_DIVISIBLE_MUTATOR_EXTENSION=
|
||||
function(){this.getField("PROPERTY").setValidator(function(a){a="DIVISIBLE_BY"===a;this.getSourceBlock().updateShape_(a)})};(0,$.module$exports$Blockly$Extensions.registerMutator)("math_is_divisibleby_mutator",module$contents$Blockly$blocks$math_IS_DIVISIBLEBY_MUTATOR_MIXIN,module$contents$Blockly$blocks$math_IS_DIVISIBLE_MUTATOR_EXTENSION);
|
||||
RANDOM:"%{BKY_MATH_ONLIST_TOOLTIP_RANDOM}"};(0,$.module$exports$Blockly$Extensions.register)("math_op_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipForDropdown)("OP",module$contents$Blockly$libraryBlocks$math_TOOLTIPS_BY_OP));
|
||||
var module$contents$Blockly$libraryBlocks$math_IS_DIVISIBLEBY_MUTATOR_MIXIN={mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation"),b="DIVISIBLE_BY"===this.getFieldValue("PROPERTY");a.setAttribute("divisor_input",b);return a},domToMutation:function(a){a="true"===a.getAttribute("divisor_input");this.updateShape_(a)},updateShape_:function(a){var b=this.getInput("DIVISOR");a?b||this.appendValueInput("DIVISOR").setCheck("Number"):b&&this.removeInput("DIVISOR")}},
|
||||
module$contents$Blockly$libraryBlocks$math_IS_DIVISIBLE_MUTATOR_EXTENSION=function(){this.getField("PROPERTY").setValidator(function(a){a="DIVISIBLE_BY"===a;this.getSourceBlock().updateShape_(a)})};(0,$.module$exports$Blockly$Extensions.registerMutator)("math_is_divisibleby_mutator",module$contents$Blockly$libraryBlocks$math_IS_DIVISIBLEBY_MUTATOR_MIXIN,module$contents$Blockly$libraryBlocks$math_IS_DIVISIBLE_MUTATOR_EXTENSION);
|
||||
(0,$.module$exports$Blockly$Extensions.register)("math_change_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipWithFieldText)("%{BKY_MATH_CHANGE_TOOLTIP}","VAR"));
|
||||
var module$contents$Blockly$blocks$math_LIST_MODES_MUTATOR_MIXIN={updateType_:function(a){"MODE"===a?this.outputConnection.setCheck("Array"):this.outputConnection.setCheck("Number")},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("op",this.getFieldValue("OP"));return a},domToMutation:function(a){this.updateType_(a.getAttribute("op"))}},module$contents$Blockly$blocks$math_LIST_MODES_MUTATOR_EXTENSION=function(){this.getField("OP").setValidator(function(a){this.updateType_(a)}.bind(this))};
|
||||
(0,$.module$exports$Blockly$Extensions.registerMutator)("math_modes_of_list_mutator",module$contents$Blockly$blocks$math_LIST_MODES_MUTATOR_MIXIN,module$contents$Blockly$blocks$math_LIST_MODES_MUTATOR_EXTENSION);var module$exports$Blockly$blocks$loops={};
|
||||
(0,$.module$exports$Blockly$common.defineBlocksWithJsonArray)([{type:"controls_repeat_ext",message0:"%{BKY_CONTROLS_REPEAT_TITLE}",args0:[{type:"input_value",name:"TIMES",check:"Number"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",tooltip:"%{BKY_CONTROLS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_CONTROLS_REPEAT_HELPURL}"},{type:"controls_repeat",message0:"%{BKY_CONTROLS_REPEAT_TITLE}",args0:[{type:"field_number",
|
||||
name:"TIMES",value:10,min:0,precision:1}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",tooltip:"%{BKY_CONTROLS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_CONTROLS_REPEAT_HELPURL}"},{type:"controls_whileUntil",message0:"%1 %2",args0:[{type:"field_dropdown",name:"MODE",options:[["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}","WHILE"],["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}","UNTIL"]]},{type:"input_value",
|
||||
name:"BOOL",check:"Boolean"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_WHILEUNTIL_HELPURL}",extensions:["controls_whileUntil_tooltip"]},{type:"controls_for",message0:"%{BKY_CONTROLS_FOR_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:null},{type:"input_value",name:"FROM",check:"Number",align:"RIGHT"},{type:"input_value",name:"TO",check:"Number",align:"RIGHT"},
|
||||
{type:"input_value",name:"BY",check:"Number",align:"RIGHT"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],inputsInline:!0,previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FOR_HELPURL}",extensions:["contextMenu_newGetVariableBlock","controls_for_tooltip"]},{type:"controls_forEach",message0:"%{BKY_CONTROLS_FOREACH_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:null},{type:"input_value",name:"LIST",check:"Array"}],
|
||||
message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FOREACH_HELPURL}",extensions:["contextMenu_newGetVariableBlock","controls_forEach_tooltip"]},{type:"controls_flow_statements",message0:"%1",args0:[{type:"field_dropdown",name:"FLOW",options:[["%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}","BREAK"],["%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}","CONTINUE"]]}],previousStatement:null,
|
||||
style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}",suppressPrefixSuffix:!0,extensions:["controls_flow_tooltip","controls_flow_in_loop_check"]}]);var module$contents$Blockly$blocks$loops_WHILE_UNTIL_TOOLTIPS={WHILE:"%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_WHILE}",UNTIL:"%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL}"};(0,$.module$exports$Blockly$Extensions.register)("controls_whileUntil_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipForDropdown)("MODE",module$contents$Blockly$blocks$loops_WHILE_UNTIL_TOOLTIPS));
|
||||
var module$contents$Blockly$blocks$loops_BREAK_CONTINUE_TOOLTIPS={BREAK:"%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK}",CONTINUE:"%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE}"};(0,$.module$exports$Blockly$Extensions.register)("controls_flow_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipForDropdown)("FLOW",module$contents$Blockly$blocks$loops_BREAK_CONTINUE_TOOLTIPS));
|
||||
var module$contents$Blockly$blocks$loops_CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN={customContextMenu:function(a){if(!this.isInFlyout){var b=this.getField("VAR").getVariable(),c=b.name;if(!this.isCollapsed()&&null!==c){var d={enabled:!0};d.text=$.module$exports$Blockly$Msg.Msg.VARIABLES_SET_CREATE_GET.replace("%1",c);b=(0,$.module$exports$Blockly$Variables.generateVariableFieldDom)(b);c=(0,$.module$exports$Blockly$utils$xml.createElement)("block");c.setAttribute("type","variables_get");c.appendChild(b);
|
||||
d.callback=(0,$.module$exports$Blockly$ContextMenu.callbackFactory)(this,c);a.push(d)}}}};(0,$.module$exports$Blockly$Extensions.registerMixin)("contextMenu_newGetVariableBlock",module$contents$Blockly$blocks$loops_CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN);(0,$.module$exports$Blockly$Extensions.register)("controls_for_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipWithFieldText)("%{BKY_CONTROLS_FOR_TOOLTIP}","VAR"));
|
||||
(0,$.module$exports$Blockly$Extensions.register)("controls_forEach_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipWithFieldText)("%{BKY_CONTROLS_FOREACH_TOOLTIP}","VAR"));module$exports$Blockly$blocks$loops.loopTypes=["controls_repeat","controls_repeat_ext","controls_forEach","controls_for","controls_whileUntil"];
|
||||
var module$contents$Blockly$blocks$loops_CONTROL_FLOW_IN_LOOP_CHECK_MIXIN={getSurroundLoop:function(){var a=this;do{if(module$exports$Blockly$blocks$loops.loopTypes.includes(a.type))return a;a=a.getSurroundParent()}while(a);return null},onchange:function(a){if(this.workspace.isDragging&&!this.workspace.isDragging()&&a.type===$.module$exports$Blockly$Events.BLOCK_MOVE){var b=this.getSurroundLoop(this);this.setWarningText(b?null:$.module$exports$Blockly$Msg.Msg.CONTROLS_FLOW_STATEMENTS_WARNING);if(!this.isInFlyout){var c=
|
||||
(0,$.module$exports$Blockly$Events.getGroup)();(0,$.module$exports$Blockly$Events.setGroup)(a.group);this.setEnabled(b);(0,$.module$exports$Blockly$Events.setGroup)(c)}}}};(0,$.module$exports$Blockly$Extensions.registerMixin)("controls_flow_in_loop_check",module$contents$Blockly$blocks$loops_CONTROL_FLOW_IN_LOOP_CHECK_MIXIN);var module$exports$Blockly$blocks$logic={};
|
||||
(0,$.module$exports$Blockly$common.defineBlocksWithJsonArray)([{type:"logic_boolean",message0:"%1",args0:[{type:"field_dropdown",name:"BOOL",options:[["%{BKY_LOGIC_BOOLEAN_TRUE}","TRUE"],["%{BKY_LOGIC_BOOLEAN_FALSE}","FALSE"]]}],output:"Boolean",style:"logic_blocks",tooltip:"%{BKY_LOGIC_BOOLEAN_TOOLTIP}",helpUrl:"%{BKY_LOGIC_BOOLEAN_HELPURL}"},{type:"controls_if",message0:"%{BKY_CONTROLS_IF_MSG_IF} %1",args0:[{type:"input_value",name:"IF0",check:"Boolean"}],message1:"%{BKY_CONTROLS_IF_MSG_THEN} %1",
|
||||
args1:[{type:"input_statement",name:"DO0"}],previousStatement:null,nextStatement:null,style:"logic_blocks",helpUrl:"%{BKY_CONTROLS_IF_HELPURL}",suppressPrefixSuffix:!0,mutator:"controls_if_mutator",extensions:["controls_if_tooltip"]},{type:"controls_ifelse",message0:"%{BKY_CONTROLS_IF_MSG_IF} %1",args0:[{type:"input_value",name:"IF0",check:"Boolean"}],message1:"%{BKY_CONTROLS_IF_MSG_THEN} %1",args1:[{type:"input_statement",name:"DO0"}],message2:"%{BKY_CONTROLS_IF_MSG_ELSE} %1",args2:[{type:"input_statement",
|
||||
name:"ELSE"}],previousStatement:null,nextStatement:null,style:"logic_blocks",tooltip:"%{BKYCONTROLS_IF_TOOLTIP_2}",helpUrl:"%{BKY_CONTROLS_IF_HELPURL}",suppressPrefixSuffix:!0,extensions:["controls_if_tooltip"]},{type:"logic_compare",message0:"%1 %2 %3",args0:[{type:"input_value",name:"A"},{type:"field_dropdown",name:"OP",options:[["=","EQ"],["\u2260","NEQ"],["\u200f<","LT"],["\u200f\u2264","LTE"],["\u200f>","GT"],["\u200f\u2265","GTE"]]},{type:"input_value",name:"B"}],inputsInline:!0,output:"Boolean",
|
||||
style:"logic_blocks",helpUrl:"%{BKY_LOGIC_COMPARE_HELPURL}",extensions:["logic_compare","logic_op_tooltip"]},{type:"logic_operation",message0:"%1 %2 %3",args0:[{type:"input_value",name:"A",check:"Boolean"},{type:"field_dropdown",name:"OP",options:[["%{BKY_LOGIC_OPERATION_AND}","AND"],["%{BKY_LOGIC_OPERATION_OR}","OR"]]},{type:"input_value",name:"B",check:"Boolean"}],inputsInline:!0,output:"Boolean",style:"logic_blocks",helpUrl:"%{BKY_LOGIC_OPERATION_HELPURL}",extensions:["logic_op_tooltip"]},{type:"logic_negate",
|
||||
message0:"%{BKY_LOGIC_NEGATE_TITLE}",args0:[{type:"input_value",name:"BOOL",check:"Boolean"}],output:"Boolean",style:"logic_blocks",tooltip:"%{BKY_LOGIC_NEGATE_TOOLTIP}",helpUrl:"%{BKY_LOGIC_NEGATE_HELPURL}"},{type:"logic_null",message0:"%{BKY_LOGIC_NULL}",output:null,style:"logic_blocks",tooltip:"%{BKY_LOGIC_NULL_TOOLTIP}",helpUrl:"%{BKY_LOGIC_NULL_HELPURL}"},{type:"logic_ternary",message0:"%{BKY_LOGIC_TERNARY_CONDITION} %1",args0:[{type:"input_value",name:"IF",check:"Boolean"}],message1:"%{BKY_LOGIC_TERNARY_IF_TRUE} %1",
|
||||
args1:[{type:"input_value",name:"THEN"}],message2:"%{BKY_LOGIC_TERNARY_IF_FALSE} %1",args2:[{type:"input_value",name:"ELSE"}],output:null,style:"logic_blocks",tooltip:"%{BKY_LOGIC_TERNARY_TOOLTIP}",helpUrl:"%{BKY_LOGIC_TERNARY_HELPURL}",extensions:["logic_ternary"]},{type:"controls_if_if",message0:"%{BKY_CONTROLS_IF_IF_TITLE_IF}",nextStatement:null,enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_IF_TOOLTIP}"},{type:"controls_if_elseif",message0:"%{BKY_CONTROLS_IF_ELSEIF_TITLE_ELSEIF}",
|
||||
previousStatement:null,nextStatement:null,enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_ELSEIF_TOOLTIP}"},{type:"controls_if_else",message0:"%{BKY_CONTROLS_IF_ELSE_TITLE_ELSE}",previousStatement:null,enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_ELSE_TOOLTIP}"}]);
|
||||
var module$contents$Blockly$blocks$logic_TOOLTIPS_BY_OP={EQ:"%{BKY_LOGIC_COMPARE_TOOLTIP_EQ}",NEQ:"%{BKY_LOGIC_COMPARE_TOOLTIP_NEQ}",LT:"%{BKY_LOGIC_COMPARE_TOOLTIP_LT}",LTE:"%{BKY_LOGIC_COMPARE_TOOLTIP_LTE}",GT:"%{BKY_LOGIC_COMPARE_TOOLTIP_GT}",GTE:"%{BKY_LOGIC_COMPARE_TOOLTIP_GTE}",AND:"%{BKY_LOGIC_OPERATION_TOOLTIP_AND}",OR:"%{BKY_LOGIC_OPERATION_TOOLTIP_OR}"};
|
||||
(0,$.module$exports$Blockly$Extensions.register)("logic_op_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipForDropdown)("OP",module$contents$Blockly$blocks$logic_TOOLTIPS_BY_OP));
|
||||
var module$contents$Blockly$blocks$logic_CONTROLS_IF_MUTATOR_MIXIN={elseifCount_:0,elseCount_:0,mutationToDom:function(){if(!this.elseifCount_&&!this.elseCount_)return null;var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");this.elseifCount_&&a.setAttribute("elseif",this.elseifCount_);this.elseCount_&&a.setAttribute("else",1);return a},domToMutation:function(a){this.elseifCount_=parseInt(a.getAttribute("elseif"),10)||0;this.elseCount_=parseInt(a.getAttribute("else"),10)||0;this.rebuildShape_()},
|
||||
saveExtraState:function(){if(!this.elseifCount_&&!this.elseCount_)return null;var a=Object.create(null);this.elseifCount_&&(a.elseIfCount=this.elseifCount_);this.elseCount_&&(a.hasElse=!0);return a},loadExtraState:function(a){this.elseifCount_=a.elseIfCount||0;this.elseCount_=a.hasElse?1:0;this.updateShape_()},decompose:function(a){var b=a.newBlock("controls_if_if");b.initSvg();for(var c=b.nextConnection,d=1;d<=this.elseifCount_;d++){var e=a.newBlock("controls_if_elseif");e.initSvg();c.connect(e.previousConnection);
|
||||
c=e.nextConnection}this.elseCount_&&(a=a.newBlock("controls_if_else"),a.initSvg(),c.connect(a.previousConnection));return b},compose:function(a){a=a.nextConnection.targetBlock();this.elseCount_=this.elseifCount_=0;for(var b=[null],c=[null],d=null;a&&!a.isInsertionMarker();){switch(a.type){case "controls_if_elseif":this.elseifCount_++;b.push(a.valueConnection_);c.push(a.statementConnection_);break;case "controls_if_else":this.elseCount_++;d=a.statementConnection_;break;default:throw TypeError("Unknown block type: "+
|
||||
a.type);}a=a.nextConnection&&a.nextConnection.targetBlock()}this.updateShape_();this.reconnectChildBlocks_(b,c,d)},saveConnections:function(a){a=a.nextConnection.targetBlock();for(var b=1;a;){switch(a.type){case "controls_if_elseif":var c=this.getInput("IF"+b),d=this.getInput("DO"+b);a.valueConnection_=c&&c.connection.targetConnection;a.statementConnection_=d&&d.connection.targetConnection;b++;break;case "controls_if_else":c=this.getInput("ELSE");a.statementConnection_=c&&c.connection.targetConnection;
|
||||
break;default:throw TypeError("Unknown block type: "+a.type);}a=a.nextConnection&&a.nextConnection.targetBlock()}},rebuildShape_:function(){var a=[null],b=[null],c=null;this.getInput("ELSE")&&(c=this.getInput("ELSE").connection.targetConnection);for(var d=1;this.getInput("IF"+d);d++){var e=this.getInput("IF"+d),f=this.getInput("DO"+d);a.push(e.connection.targetConnection);b.push(f.connection.targetConnection)}this.updateShape_();this.reconnectChildBlocks_(a,b,c)},updateShape_:function(){this.getInput("ELSE")&&
|
||||
this.removeInput("ELSE");for(var a=1;this.getInput("IF"+a);a++)this.removeInput("IF"+a),this.removeInput("DO"+a);for(a=1;a<=this.elseifCount_;a++)this.appendValueInput("IF"+a).setCheck("Boolean").appendField($.module$exports$Blockly$Msg.Msg.CONTROLS_IF_MSG_ELSEIF),this.appendStatementInput("DO"+a).appendField($.module$exports$Blockly$Msg.Msg.CONTROLS_IF_MSG_THEN);this.elseCount_&&this.appendStatementInput("ELSE").appendField($.module$exports$Blockly$Msg.Msg.CONTROLS_IF_MSG_ELSE)},reconnectChildBlocks_:function(a,
|
||||
b,c){for(var d=1;d<=this.elseifCount_;d++)$.module$exports$Blockly$Mutator.Mutator.reconnect(a[d],this,"IF"+d),$.module$exports$Blockly$Mutator.Mutator.reconnect(b[d],this,"DO"+d);$.module$exports$Blockly$Mutator.Mutator.reconnect(c,this,"ELSE")}};(0,$.module$exports$Blockly$Extensions.registerMutator)("controls_if_mutator",module$contents$Blockly$blocks$logic_CONTROLS_IF_MUTATOR_MIXIN,null,["controls_if_elseif","controls_if_else"]);
|
||||
var module$contents$Blockly$blocks$logic_CONTROLS_IF_TOOLTIP_EXTENSION=function(){this.setTooltip(function(){if(this.elseifCount_||this.elseCount_){if(!this.elseifCount_&&this.elseCount_)return $.module$exports$Blockly$Msg.Msg.CONTROLS_IF_TOOLTIP_2;if(this.elseifCount_&&!this.elseCount_)return $.module$exports$Blockly$Msg.Msg.CONTROLS_IF_TOOLTIP_3;if(this.elseifCount_&&this.elseCount_)return $.module$exports$Blockly$Msg.Msg.CONTROLS_IF_TOOLTIP_4}else return $.module$exports$Blockly$Msg.Msg.CONTROLS_IF_TOOLTIP_1;
|
||||
return""}.bind(this))};(0,$.module$exports$Blockly$Extensions.register)("controls_if_tooltip",module$contents$Blockly$blocks$logic_CONTROLS_IF_TOOLTIP_EXTENSION);
|
||||
var module$contents$Blockly$blocks$logic_LOGIC_COMPARE_ONCHANGE_MIXIN={onchange:function(a){this.prevBlocks_||(this.prevBlocks_=[null,null]);var b=this.getInputTargetBlock("A"),c=this.getInputTargetBlock("B");b&&c&&!this.workspace.connectionChecker.doTypeChecks(b.outputConnection,c.outputConnection)&&((0,$.module$exports$Blockly$Events.setGroup)(a.group),a=this.prevBlocks_[0],a!==b&&(b.unplug(),!a||a.isDisposed()||a.isShadow()||this.getInput("A").connection.connect(a.outputConnection)),b=this.prevBlocks_[1],
|
||||
b!==c&&(c.unplug(),!b||b.isDisposed()||b.isShadow()||this.getInput("B").connection.connect(b.outputConnection)),this.bumpNeighbours(),(0,$.module$exports$Blockly$Events.setGroup)(!1));this.prevBlocks_[0]=this.getInputTargetBlock("A");this.prevBlocks_[1]=this.getInputTargetBlock("B")}},module$contents$Blockly$blocks$logic_LOGIC_COMPARE_EXTENSION=function(){this.mixin(module$contents$Blockly$blocks$logic_LOGIC_COMPARE_ONCHANGE_MIXIN)};
|
||||
(0,$.module$exports$Blockly$Extensions.register)("logic_compare",module$contents$Blockly$blocks$logic_LOGIC_COMPARE_EXTENSION);
|
||||
var module$contents$Blockly$blocks$logic_LOGIC_TERNARY_ONCHANGE_MIXIN={prevParentConnection_:null,onchange:function(a){var b=this.getInputTargetBlock("THEN"),c=this.getInputTargetBlock("ELSE"),d=this.outputConnection.targetConnection;if((b||c)&&d)for(var e=0;2>e;e++){var f=1===e?b:c;f&&!f.workspace.connectionChecker.doTypeChecks(f.outputConnection,d)&&((0,$.module$exports$Blockly$Events.setGroup)(a.group),d===this.prevParentConnection_?(this.unplug(),d.getSourceBlock().bumpNeighbours()):(f.unplug(),
|
||||
f.bumpNeighbours()),(0,$.module$exports$Blockly$Events.setGroup)(!1))}this.prevParentConnection_=d}};(0,$.module$exports$Blockly$Extensions.registerMixin)("logic_ternary",module$contents$Blockly$blocks$logic_LOGIC_TERNARY_ONCHANGE_MIXIN);var module$exports$Blockly$blocks$lists={};
|
||||
(0,$.module$exports$Blockly$common.defineBlocksWithJsonArray)([{type:"lists_create_empty",message0:"%{BKY_LISTS_CREATE_EMPTY_TITLE}",output:"Array",style:"list_blocks",tooltip:"%{BKY_LISTS_CREATE_EMPTY_TOOLTIP}",helpUrl:"%{BKY_LISTS_CREATE_EMPTY_HELPURL}"},{type:"lists_repeat",message0:"%{BKY_LISTS_REPEAT_TITLE}",args0:[{type:"input_value",name:"ITEM"},{type:"input_value",name:"NUM",check:"Number"}],output:"Array",style:"list_blocks",tooltip:"%{BKY_LISTS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_LISTS_REPEAT_HELPURL}"},
|
||||
{type:"lists_reverse",message0:"%{BKY_LISTS_REVERSE_MESSAGE0}",args0:[{type:"input_value",name:"LIST",check:"Array"}],output:"Array",inputsInline:!0,style:"list_blocks",tooltip:"%{BKY_LISTS_REVERSE_TOOLTIP}",helpUrl:"%{BKY_LISTS_REVERSE_HELPURL}"},{type:"lists_isEmpty",message0:"%{BKY_LISTS_ISEMPTY_TITLE}",args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Boolean",style:"list_blocks",tooltip:"%{BKY_LISTS_ISEMPTY_TOOLTIP}",helpUrl:"%{BKY_LISTS_ISEMPTY_HELPURL}"},{type:"lists_length",
|
||||
message0:"%{BKY_LISTS_LENGTH_TITLE}",args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Number",style:"list_blocks",tooltip:"%{BKY_LISTS_LENGTH_TOOLTIP}",helpUrl:"%{BKY_LISTS_LENGTH_HELPURL}"}]);
|
||||
$.module$exports$Blockly$blocks.Blocks.lists_create_with={init:function(){this.setHelpUrl($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_HELPURL);this.setStyle("list_blocks");this.itemCount_=3;this.updateShape_();this.setOutput(!0,"Array");this.setMutator(new $.module$exports$Blockly$Mutator.Mutator(["lists_create_with_item"]));this.setTooltip($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_TOOLTIP)},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");
|
||||
var module$contents$Blockly$libraryBlocks$math_LIST_MODES_MUTATOR_MIXIN={updateType_:function(a){"MODE"===a?this.outputConnection.setCheck("Array"):this.outputConnection.setCheck("Number")},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("op",this.getFieldValue("OP"));return a},domToMutation:function(a){this.updateType_(a.getAttribute("op"))}},module$contents$Blockly$libraryBlocks$math_LIST_MODES_MUTATOR_EXTENSION=function(){this.getField("OP").setValidator(function(a){this.updateType_(a)}.bind(this))};
|
||||
(0,$.module$exports$Blockly$Extensions.registerMutator)("math_modes_of_list_mutator",module$contents$Blockly$libraryBlocks$math_LIST_MODES_MUTATOR_MIXIN,module$contents$Blockly$libraryBlocks$math_LIST_MODES_MUTATOR_EXTENSION);(0,$.module$exports$Blockly$common.defineBlocks)(module$exports$Blockly$libraryBlocks$math.blocks);var module$exports$Blockly$libraryBlocks$loops={};
|
||||
module$exports$Blockly$libraryBlocks$loops.blocks=(0,$.module$exports$Blockly$common.createBlockDefinitionsFromJsonArray)([{type:"controls_repeat_ext",message0:"%{BKY_CONTROLS_REPEAT_TITLE}",args0:[{type:"input_value",name:"TIMES",check:"Number"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",tooltip:"%{BKY_CONTROLS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_CONTROLS_REPEAT_HELPURL}"},{type:"controls_repeat",
|
||||
message0:"%{BKY_CONTROLS_REPEAT_TITLE}",args0:[{type:"field_number",name:"TIMES",value:10,min:0,precision:1}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",tooltip:"%{BKY_CONTROLS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_CONTROLS_REPEAT_HELPURL}"},{type:"controls_whileUntil",message0:"%1 %2",args0:[{type:"field_dropdown",name:"MODE",options:[["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}","WHILE"],["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}",
|
||||
"UNTIL"]]},{type:"input_value",name:"BOOL",check:"Boolean"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_WHILEUNTIL_HELPURL}",extensions:["controls_whileUntil_tooltip"]},{type:"controls_for",message0:"%{BKY_CONTROLS_FOR_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:null},{type:"input_value",name:"FROM",check:"Number",align:"RIGHT"},{type:"input_value",name:"TO",
|
||||
check:"Number",align:"RIGHT"},{type:"input_value",name:"BY",check:"Number",align:"RIGHT"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],inputsInline:!0,previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FOR_HELPURL}",extensions:["contextMenu_newGetVariableBlock","controls_for_tooltip"]},{type:"controls_forEach",message0:"%{BKY_CONTROLS_FOREACH_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:null},{type:"input_value",
|
||||
name:"LIST",check:"Array"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FOREACH_HELPURL}",extensions:["contextMenu_newGetVariableBlock","controls_forEach_tooltip"]},{type:"controls_flow_statements",message0:"%1",args0:[{type:"field_dropdown",name:"FLOW",options:[["%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}","BREAK"],["%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}",
|
||||
"CONTINUE"]]}],previousStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}",suppressPrefixSuffix:!0,extensions:["controls_flow_tooltip","controls_flow_in_loop_check"]}]);var module$contents$Blockly$libraryBlocks$loops_WHILE_UNTIL_TOOLTIPS={WHILE:"%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_WHILE}",UNTIL:"%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL}"};
|
||||
(0,$.module$exports$Blockly$Extensions.register)("controls_whileUntil_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipForDropdown)("MODE",module$contents$Blockly$libraryBlocks$loops_WHILE_UNTIL_TOOLTIPS));var module$contents$Blockly$libraryBlocks$loops_BREAK_CONTINUE_TOOLTIPS={BREAK:"%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK}",CONTINUE:"%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE}"};
|
||||
(0,$.module$exports$Blockly$Extensions.register)("controls_flow_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipForDropdown)("FLOW",module$contents$Blockly$libraryBlocks$loops_BREAK_CONTINUE_TOOLTIPS));
|
||||
var module$contents$Blockly$libraryBlocks$loops_CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN={customContextMenu:function(a){if(!this.isInFlyout){var b=this.getField("VAR").getVariable(),c=b.name;if(!this.isCollapsed()&&null!==c){var d={enabled:!0};d.text=$.module$exports$Blockly$Msg.Msg.VARIABLES_SET_CREATE_GET.replace("%1",c);b=(0,$.module$exports$Blockly$Variables.generateVariableFieldDom)(b);c=(0,$.module$exports$Blockly$utils$xml.createElement)("block");c.setAttribute("type","variables_get");
|
||||
c.appendChild(b);d.callback=(0,$.module$exports$Blockly$ContextMenu.callbackFactory)(this,c);a.push(d)}}}};(0,$.module$exports$Blockly$Extensions.registerMixin)("contextMenu_newGetVariableBlock",module$contents$Blockly$libraryBlocks$loops_CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN);(0,$.module$exports$Blockly$Extensions.register)("controls_for_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipWithFieldText)("%{BKY_CONTROLS_FOR_TOOLTIP}","VAR"));
|
||||
(0,$.module$exports$Blockly$Extensions.register)("controls_forEach_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipWithFieldText)("%{BKY_CONTROLS_FOREACH_TOOLTIP}","VAR"));module$exports$Blockly$libraryBlocks$loops.loopTypes=new Set(["controls_repeat","controls_repeat_ext","controls_forEach","controls_for","controls_whileUntil"]);
|
||||
var module$contents$Blockly$libraryBlocks$loops_CONTROL_FLOW_IN_LOOP_CHECK_MIXIN={getSurroundLoop:function(){var a=this;do{if(module$exports$Blockly$libraryBlocks$loops.loopTypes.has(a.type))return a;a=a.getSurroundParent()}while(a);return null},onchange:function(a){if(this.workspace.isDragging&&!this.workspace.isDragging()&&a.type===$.module$exports$Blockly$Events.BLOCK_MOVE){var b=this.getSurroundLoop(this);this.setWarningText(b?null:$.module$exports$Blockly$Msg.Msg.CONTROLS_FLOW_STATEMENTS_WARNING);
|
||||
if(!this.isInFlyout){var c=(0,$.module$exports$Blockly$Events.getGroup)();(0,$.module$exports$Blockly$Events.setGroup)(a.group);this.setEnabled(b);(0,$.module$exports$Blockly$Events.setGroup)(c)}}}};(0,$.module$exports$Blockly$Extensions.registerMixin)("controls_flow_in_loop_check",module$contents$Blockly$libraryBlocks$loops_CONTROL_FLOW_IN_LOOP_CHECK_MIXIN);(0,$.module$exports$Blockly$common.defineBlocks)(module$exports$Blockly$libraryBlocks$loops.blocks);var module$exports$Blockly$libraryBlocks$logic={};
|
||||
module$exports$Blockly$libraryBlocks$logic.blocks=(0,$.module$exports$Blockly$common.createBlockDefinitionsFromJsonArray)([{type:"logic_boolean",message0:"%1",args0:[{type:"field_dropdown",name:"BOOL",options:[["%{BKY_LOGIC_BOOLEAN_TRUE}","TRUE"],["%{BKY_LOGIC_BOOLEAN_FALSE}","FALSE"]]}],output:"Boolean",style:"logic_blocks",tooltip:"%{BKY_LOGIC_BOOLEAN_TOOLTIP}",helpUrl:"%{BKY_LOGIC_BOOLEAN_HELPURL}"},{type:"controls_if",message0:"%{BKY_CONTROLS_IF_MSG_IF} %1",args0:[{type:"input_value",name:"IF0",
|
||||
check:"Boolean"}],message1:"%{BKY_CONTROLS_IF_MSG_THEN} %1",args1:[{type:"input_statement",name:"DO0"}],previousStatement:null,nextStatement:null,style:"logic_blocks",helpUrl:"%{BKY_CONTROLS_IF_HELPURL}",suppressPrefixSuffix:!0,mutator:"controls_if_mutator",extensions:["controls_if_tooltip"]},{type:"controls_ifelse",message0:"%{BKY_CONTROLS_IF_MSG_IF} %1",args0:[{type:"input_value",name:"IF0",check:"Boolean"}],message1:"%{BKY_CONTROLS_IF_MSG_THEN} %1",args1:[{type:"input_statement",name:"DO0"}],message2:"%{BKY_CONTROLS_IF_MSG_ELSE} %1",
|
||||
args2:[{type:"input_statement",name:"ELSE"}],previousStatement:null,nextStatement:null,style:"logic_blocks",tooltip:"%{BKYCONTROLS_IF_TOOLTIP_2}",helpUrl:"%{BKY_CONTROLS_IF_HELPURL}",suppressPrefixSuffix:!0,extensions:["controls_if_tooltip"]},{type:"logic_compare",message0:"%1 %2 %3",args0:[{type:"input_value",name:"A"},{type:"field_dropdown",name:"OP",options:[["=","EQ"],["\u2260","NEQ"],["\u200f<","LT"],["\u200f\u2264","LTE"],["\u200f>","GT"],["\u200f\u2265","GTE"]]},{type:"input_value",name:"B"}],
|
||||
inputsInline:!0,output:"Boolean",style:"logic_blocks",helpUrl:"%{BKY_LOGIC_COMPARE_HELPURL}",extensions:["logic_compare","logic_op_tooltip"]},{type:"logic_operation",message0:"%1 %2 %3",args0:[{type:"input_value",name:"A",check:"Boolean"},{type:"field_dropdown",name:"OP",options:[["%{BKY_LOGIC_OPERATION_AND}","AND"],["%{BKY_LOGIC_OPERATION_OR}","OR"]]},{type:"input_value",name:"B",check:"Boolean"}],inputsInline:!0,output:"Boolean",style:"logic_blocks",helpUrl:"%{BKY_LOGIC_OPERATION_HELPURL}",extensions:["logic_op_tooltip"]},
|
||||
{type:"logic_negate",message0:"%{BKY_LOGIC_NEGATE_TITLE}",args0:[{type:"input_value",name:"BOOL",check:"Boolean"}],output:"Boolean",style:"logic_blocks",tooltip:"%{BKY_LOGIC_NEGATE_TOOLTIP}",helpUrl:"%{BKY_LOGIC_NEGATE_HELPURL}"},{type:"logic_null",message0:"%{BKY_LOGIC_NULL}",output:null,style:"logic_blocks",tooltip:"%{BKY_LOGIC_NULL_TOOLTIP}",helpUrl:"%{BKY_LOGIC_NULL_HELPURL}"},{type:"logic_ternary",message0:"%{BKY_LOGIC_TERNARY_CONDITION} %1",args0:[{type:"input_value",name:"IF",check:"Boolean"}],
|
||||
message1:"%{BKY_LOGIC_TERNARY_IF_TRUE} %1",args1:[{type:"input_value",name:"THEN"}],message2:"%{BKY_LOGIC_TERNARY_IF_FALSE} %1",args2:[{type:"input_value",name:"ELSE"}],output:null,style:"logic_blocks",tooltip:"%{BKY_LOGIC_TERNARY_TOOLTIP}",helpUrl:"%{BKY_LOGIC_TERNARY_HELPURL}",extensions:["logic_ternary"]},{type:"controls_if_if",message0:"%{BKY_CONTROLS_IF_IF_TITLE_IF}",nextStatement:null,enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_IF_TOOLTIP}"},{type:"controls_if_elseif",
|
||||
message0:"%{BKY_CONTROLS_IF_ELSEIF_TITLE_ELSEIF}",previousStatement:null,nextStatement:null,enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_ELSEIF_TOOLTIP}"},{type:"controls_if_else",message0:"%{BKY_CONTROLS_IF_ELSE_TITLE_ELSE}",previousStatement:null,enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_ELSE_TOOLTIP}"}]);
|
||||
var module$contents$Blockly$libraryBlocks$logic_TOOLTIPS_BY_OP={EQ:"%{BKY_LOGIC_COMPARE_TOOLTIP_EQ}",NEQ:"%{BKY_LOGIC_COMPARE_TOOLTIP_NEQ}",LT:"%{BKY_LOGIC_COMPARE_TOOLTIP_LT}",LTE:"%{BKY_LOGIC_COMPARE_TOOLTIP_LTE}",GT:"%{BKY_LOGIC_COMPARE_TOOLTIP_GT}",GTE:"%{BKY_LOGIC_COMPARE_TOOLTIP_GTE}",AND:"%{BKY_LOGIC_OPERATION_TOOLTIP_AND}",OR:"%{BKY_LOGIC_OPERATION_TOOLTIP_OR}"};
|
||||
(0,$.module$exports$Blockly$Extensions.register)("logic_op_tooltip",(0,$.module$exports$Blockly$Extensions.buildTooltipForDropdown)("OP",module$contents$Blockly$libraryBlocks$logic_TOOLTIPS_BY_OP));
|
||||
var module$contents$Blockly$libraryBlocks$logic_CONTROLS_IF_MUTATOR_MIXIN={elseifCount_:0,elseCount_:0,mutationToDom:function(){if(!this.elseifCount_&&!this.elseCount_)return null;var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");this.elseifCount_&&a.setAttribute("elseif",this.elseifCount_);this.elseCount_&&a.setAttribute("else",1);return a},domToMutation:function(a){this.elseifCount_=parseInt(a.getAttribute("elseif"),10)||0;this.elseCount_=parseInt(a.getAttribute("else"),10)||
|
||||
0;this.rebuildShape_()},saveExtraState:function(){if(!this.elseifCount_&&!this.elseCount_)return null;var a=Object.create(null);this.elseifCount_&&(a.elseIfCount=this.elseifCount_);this.elseCount_&&(a.hasElse=!0);return a},loadExtraState:function(a){this.elseifCount_=a.elseIfCount||0;this.elseCount_=a.hasElse?1:0;this.updateShape_()},decompose:function(a){var b=a.newBlock("controls_if_if");b.initSvg();for(var c=b.nextConnection,d=1;d<=this.elseifCount_;d++){var e=a.newBlock("controls_if_elseif");
|
||||
e.initSvg();c.connect(e.previousConnection);c=e.nextConnection}this.elseCount_&&(a=a.newBlock("controls_if_else"),a.initSvg(),c.connect(a.previousConnection));return b},compose:function(a){a=a.nextConnection.targetBlock();this.elseCount_=this.elseifCount_=0;for(var b=[null],c=[null],d=null;a&&!a.isInsertionMarker();){switch(a.type){case "controls_if_elseif":this.elseifCount_++;b.push(a.valueConnection_);c.push(a.statementConnection_);break;case "controls_if_else":this.elseCount_++;d=a.statementConnection_;
|
||||
break;default:throw TypeError("Unknown block type: "+a.type);}a=a.nextConnection&&a.nextConnection.targetBlock()}this.updateShape_();this.reconnectChildBlocks_(b,c,d)},saveConnections:function(a){a=a.nextConnection.targetBlock();for(var b=1;a;){switch(a.type){case "controls_if_elseif":var c=this.getInput("IF"+b),d=this.getInput("DO"+b);a.valueConnection_=c&&c.connection.targetConnection;a.statementConnection_=d&&d.connection.targetConnection;b++;break;case "controls_if_else":c=this.getInput("ELSE");
|
||||
a.statementConnection_=c&&c.connection.targetConnection;break;default:throw TypeError("Unknown block type: "+a.type);}a=a.nextConnection&&a.nextConnection.targetBlock()}},rebuildShape_:function(){var a=[null],b=[null],c=null;this.getInput("ELSE")&&(c=this.getInput("ELSE").connection.targetConnection);for(var d=1;this.getInput("IF"+d);d++){var e=this.getInput("IF"+d),f=this.getInput("DO"+d);a.push(e.connection.targetConnection);b.push(f.connection.targetConnection)}this.updateShape_();this.reconnectChildBlocks_(a,
|
||||
b,c)},updateShape_:function(){this.getInput("ELSE")&&this.removeInput("ELSE");for(var a=1;this.getInput("IF"+a);a++)this.removeInput("IF"+a),this.removeInput("DO"+a);for(a=1;a<=this.elseifCount_;a++)this.appendValueInput("IF"+a).setCheck("Boolean").appendField($.module$exports$Blockly$Msg.Msg.CONTROLS_IF_MSG_ELSEIF),this.appendStatementInput("DO"+a).appendField($.module$exports$Blockly$Msg.Msg.CONTROLS_IF_MSG_THEN);this.elseCount_&&this.appendStatementInput("ELSE").appendField($.module$exports$Blockly$Msg.Msg.CONTROLS_IF_MSG_ELSE)},
|
||||
reconnectChildBlocks_:function(a,b,c){for(var d=1;d<=this.elseifCount_;d++)$.module$exports$Blockly$Mutator.Mutator.reconnect(a[d],this,"IF"+d),$.module$exports$Blockly$Mutator.Mutator.reconnect(b[d],this,"DO"+d);$.module$exports$Blockly$Mutator.Mutator.reconnect(c,this,"ELSE")}};(0,$.module$exports$Blockly$Extensions.registerMutator)("controls_if_mutator",module$contents$Blockly$libraryBlocks$logic_CONTROLS_IF_MUTATOR_MIXIN,null,["controls_if_elseif","controls_if_else"]);
|
||||
var module$contents$Blockly$libraryBlocks$logic_CONTROLS_IF_TOOLTIP_EXTENSION=function(){this.setTooltip(function(){if(this.elseifCount_||this.elseCount_){if(!this.elseifCount_&&this.elseCount_)return $.module$exports$Blockly$Msg.Msg.CONTROLS_IF_TOOLTIP_2;if(this.elseifCount_&&!this.elseCount_)return $.module$exports$Blockly$Msg.Msg.CONTROLS_IF_TOOLTIP_3;if(this.elseifCount_&&this.elseCount_)return $.module$exports$Blockly$Msg.Msg.CONTROLS_IF_TOOLTIP_4}else return $.module$exports$Blockly$Msg.Msg.CONTROLS_IF_TOOLTIP_1;
|
||||
return""}.bind(this))};(0,$.module$exports$Blockly$Extensions.register)("controls_if_tooltip",module$contents$Blockly$libraryBlocks$logic_CONTROLS_IF_TOOLTIP_EXTENSION);
|
||||
var module$contents$Blockly$libraryBlocks$logic_LOGIC_COMPARE_ONCHANGE_MIXIN={onchange:function(a){this.prevBlocks_||(this.prevBlocks_=[null,null]);var b=this.getInputTargetBlock("A"),c=this.getInputTargetBlock("B");b&&c&&!this.workspace.connectionChecker.doTypeChecks(b.outputConnection,c.outputConnection)&&((0,$.module$exports$Blockly$Events.setGroup)(a.group),a=this.prevBlocks_[0],a!==b&&(b.unplug(),!a||a.isDisposed()||a.isShadow()||this.getInput("A").connection.connect(a.outputConnection)),b=this.prevBlocks_[1],
|
||||
b!==c&&(c.unplug(),!b||b.isDisposed()||b.isShadow()||this.getInput("B").connection.connect(b.outputConnection)),this.bumpNeighbours(),(0,$.module$exports$Blockly$Events.setGroup)(!1));this.prevBlocks_[0]=this.getInputTargetBlock("A");this.prevBlocks_[1]=this.getInputTargetBlock("B")}},module$contents$Blockly$libraryBlocks$logic_LOGIC_COMPARE_EXTENSION=function(){this.mixin(module$contents$Blockly$libraryBlocks$logic_LOGIC_COMPARE_ONCHANGE_MIXIN)};
|
||||
(0,$.module$exports$Blockly$Extensions.register)("logic_compare",module$contents$Blockly$libraryBlocks$logic_LOGIC_COMPARE_EXTENSION);
|
||||
var module$contents$Blockly$libraryBlocks$logic_LOGIC_TERNARY_ONCHANGE_MIXIN={prevParentConnection_:null,onchange:function(a){var b=this.getInputTargetBlock("THEN"),c=this.getInputTargetBlock("ELSE"),d=this.outputConnection.targetConnection;if((b||c)&&d)for(var e=0;2>e;e++){var f=1===e?b:c;f&&!f.workspace.connectionChecker.doTypeChecks(f.outputConnection,d)&&((0,$.module$exports$Blockly$Events.setGroup)(a.group),d===this.prevParentConnection_?(this.unplug(),d.getSourceBlock().bumpNeighbours()):(f.unplug(),
|
||||
f.bumpNeighbours()),(0,$.module$exports$Blockly$Events.setGroup)(!1))}this.prevParentConnection_=d}};(0,$.module$exports$Blockly$Extensions.registerMixin)("logic_ternary",module$contents$Blockly$libraryBlocks$logic_LOGIC_TERNARY_ONCHANGE_MIXIN);(0,$.module$exports$Blockly$common.defineBlocks)(module$exports$Blockly$libraryBlocks$logic.blocks);var module$exports$Blockly$libraryBlocks$lists={};
|
||||
module$exports$Blockly$libraryBlocks$lists.blocks=(0,$.module$exports$Blockly$common.createBlockDefinitionsFromJsonArray)([{type:"lists_create_empty",message0:"%{BKY_LISTS_CREATE_EMPTY_TITLE}",output:"Array",style:"list_blocks",tooltip:"%{BKY_LISTS_CREATE_EMPTY_TOOLTIP}",helpUrl:"%{BKY_LISTS_CREATE_EMPTY_HELPURL}"},{type:"lists_repeat",message0:"%{BKY_LISTS_REPEAT_TITLE}",args0:[{type:"input_value",name:"ITEM"},{type:"input_value",name:"NUM",check:"Number"}],output:"Array",style:"list_blocks",tooltip:"%{BKY_LISTS_REPEAT_TOOLTIP}",
|
||||
helpUrl:"%{BKY_LISTS_REPEAT_HELPURL}"},{type:"lists_reverse",message0:"%{BKY_LISTS_REVERSE_MESSAGE0}",args0:[{type:"input_value",name:"LIST",check:"Array"}],output:"Array",inputsInline:!0,style:"list_blocks",tooltip:"%{BKY_LISTS_REVERSE_TOOLTIP}",helpUrl:"%{BKY_LISTS_REVERSE_HELPURL}"},{type:"lists_isEmpty",message0:"%{BKY_LISTS_ISEMPTY_TITLE}",args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Boolean",style:"list_blocks",tooltip:"%{BKY_LISTS_ISEMPTY_TOOLTIP}",helpUrl:"%{BKY_LISTS_ISEMPTY_HELPURL}"},
|
||||
{type:"lists_length",message0:"%{BKY_LISTS_LENGTH_TITLE}",args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Number",style:"list_blocks",tooltip:"%{BKY_LISTS_LENGTH_TOOLTIP}",helpUrl:"%{BKY_LISTS_LENGTH_HELPURL}"}]);
|
||||
module$exports$Blockly$libraryBlocks$lists.blocks.lists_create_with={init:function(){this.setHelpUrl($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_HELPURL);this.setStyle("list_blocks");this.itemCount_=3;this.updateShape_();this.setOutput(!0,"Array");this.setMutator(new $.module$exports$Blockly$Mutator.Mutator(["lists_create_with_item"]));this.setTooltip($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_TOOLTIP)},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");
|
||||
a.setAttribute("items",this.itemCount_);return a},domToMutation:function(a){this.itemCount_=parseInt(a.getAttribute("items"),10);this.updateShape_()},saveExtraState:function(){return{itemCount:this.itemCount_}},loadExtraState:function(a){this.itemCount_=a.itemCount;this.updateShape_()},decompose:function(a){var b=a.newBlock("lists_create_with_container");b.initSvg();for(var c=b.getInput("STACK").connection,d=0;d<this.itemCount_;d++){var e=a.newBlock("lists_create_with_item");e.initSvg();c.connect(e.previousConnection);
|
||||
c=e.nextConnection}return b},compose:function(a){var b=a.getInputTargetBlock("STACK");for(a=[];b&&!b.isInsertionMarker();)a.push(b.valueConnection_),b=b.nextConnection&&b.nextConnection.targetBlock();for(b=0;b<this.itemCount_;b++){var c=this.getInput("ADD"+b).connection.targetConnection;c&&-1===a.indexOf(c)&&c.disconnect()}this.itemCount_=a.length;this.updateShape_();for(b=0;b<this.itemCount_;b++)$.module$exports$Blockly$Mutator.Mutator.reconnect(a[b],this,"ADD"+b)},saveConnections:function(a){a=
|
||||
a.getInputTargetBlock("STACK");for(var b=0;a;){var c=this.getInput("ADD"+b);a.valueConnection_=c&&c.connection.targetConnection;a=a.nextConnection&&a.nextConnection.targetBlock();b++}},updateShape_:function(){this.itemCount_&&this.getInput("EMPTY")?this.removeInput("EMPTY"):this.itemCount_||this.getInput("EMPTY")||this.appendDummyInput("EMPTY").appendField($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_EMPTY_TITLE);for(var a=0;a<this.itemCount_;a++)if(!this.getInput("ADD"+a)){var b=this.appendValueInput("ADD"+
|
||||
a).setAlign($.module$exports$Blockly$Input.Align.RIGHT);0===a&&b.appendField($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_INPUT_WITH)}for(a=this.itemCount_;this.getInput("ADD"+a);a++)this.removeInput("ADD"+a)}};
|
||||
$.module$exports$Blockly$blocks.Blocks.lists_create_with_container={init:function(){this.setStyle("list_blocks");this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_CONTAINER_TITLE_ADD);this.appendStatementInput("STACK");this.setTooltip($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_CONTAINER_TOOLTIP);this.contextMenu=!1}};
|
||||
$.module$exports$Blockly$blocks.Blocks.lists_create_with_item={init:function(){this.setStyle("list_blocks");this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_ITEM_TITLE);this.setPreviousStatement(!0);this.setNextStatement(!0);this.setTooltip($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_ITEM_TOOLTIP);this.contextMenu=!1}};
|
||||
$.module$exports$Blockly$blocks.Blocks.lists_indexOf={init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.LISTS_INDEX_OF_FIRST,"FIRST"],[$.module$exports$Blockly$Msg.Msg.LISTS_INDEX_OF_LAST,"LAST"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.LISTS_INDEX_OF_HELPURL);this.setStyle("list_blocks");this.setOutput(!0,"Number");this.appendValueInput("VALUE").setCheck("Array").appendField($.module$exports$Blockly$Msg.Msg.LISTS_INDEX_OF_INPUT_IN_LIST);this.appendValueInput("FIND").appendField(new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a),
|
||||
module$exports$Blockly$libraryBlocks$lists.blocks.lists_create_with_container={init:function(){this.setStyle("list_blocks");this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_CONTAINER_TITLE_ADD);this.appendStatementInput("STACK");this.setTooltip($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_CONTAINER_TOOLTIP);this.contextMenu=!1}};
|
||||
module$exports$Blockly$libraryBlocks$lists.blocks.lists_create_with_item={init:function(){this.setStyle("list_blocks");this.appendDummyInput().appendField($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_ITEM_TITLE);this.setPreviousStatement(!0);this.setNextStatement(!0);this.setTooltip($.module$exports$Blockly$Msg.Msg.LISTS_CREATE_WITH_ITEM_TOOLTIP);this.contextMenu=!1}};
|
||||
module$exports$Blockly$libraryBlocks$lists.blocks.lists_indexOf={init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.LISTS_INDEX_OF_FIRST,"FIRST"],[$.module$exports$Blockly$Msg.Msg.LISTS_INDEX_OF_LAST,"LAST"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.LISTS_INDEX_OF_HELPURL);this.setStyle("list_blocks");this.setOutput(!0,"Number");this.appendValueInput("VALUE").setCheck("Array").appendField($.module$exports$Blockly$Msg.Msg.LISTS_INDEX_OF_INPUT_IN_LIST);this.appendValueInput("FIND").appendField(new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a),
|
||||
"END");this.setInputsInline(!0);var b=this;this.setTooltip(function(){return $.module$exports$Blockly$Msg.Msg.LISTS_INDEX_OF_TOOLTIP.replace("%1",b.workspace.options.oneBasedIndex?"0":"-1")})}};
|
||||
$.module$exports$Blockly$blocks.Blocks.lists_getIndex={init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_GET,"GET"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_GET_REMOVE,"GET_REMOVE"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_REMOVE,"REMOVE"]];this.WHERE_OPTIONS=[[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FROM_END,"FROM_END"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FIRST,
|
||||
module$exports$Blockly$libraryBlocks$lists.blocks.lists_getIndex={init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_GET,"GET"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_GET_REMOVE,"GET_REMOVE"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_REMOVE,"REMOVE"]];this.WHERE_OPTIONS=[[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FROM_END,"FROM_END"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FIRST,
|
||||
"FIRST"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_LAST,"LAST"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_RANDOM,"RANDOM"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_HELPURL);this.setStyle("list_blocks");a=new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a,function(c){c="REMOVE"===c;this.getSourceBlock().updateStatement_(c)});this.appendValueInput("VALUE").setCheck("Array").appendField($.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_INPUT_IN_LIST);this.appendDummyInput().appendField(a,
|
||||
"MODE").appendField("","SPACE");this.appendDummyInput("AT");$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_TAIL&&this.appendDummyInput("TAIL").appendField($.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_TAIL);this.setInputsInline(!0);this.setOutput(!0);this.updateAt_(!0);var b=this;this.setTooltip(function(){var c=b.getFieldValue("MODE"),d=b.getFieldValue("WHERE"),e="";switch(c+" "+d){case "GET FROM_START":case "GET FROM_END":e=$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FROM;
|
||||
break;case "GET FIRST":e=$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FIRST;break;case "GET LAST":e=$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_TOOLTIP_GET_LAST;break;case "GET RANDOM":e=$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_TOOLTIP_GET_RANDOM;break;case "GET_REMOVE FROM_START":case "GET_REMOVE FROM_END":e=$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM;break;case "GET_REMOVE FIRST":e=$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST;
|
||||
@@ -179,28 +181,31 @@ break;case "REMOVE RANDOM":e=$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_TO
|
||||
var b=this.getInput("AT").type===$.module$exports$Blockly$ConnectionType.ConnectionType.INPUT_VALUE;a.setAttribute("at",b);return a},domToMutation:function(a){var b="true"===a.getAttribute("statement");this.updateStatement_(b);a="false"!==a.getAttribute("at");this.updateAt_(a)},updateStatement_:function(a){a!==!this.outputConnection&&(this.unplug(!0,!0),a?(this.setOutput(!1),this.setPreviousStatement(!0),this.setNextStatement(!0)):(this.setPreviousStatement(!1),this.setNextStatement(!1),this.setOutput(!0)))},
|
||||
updateAt_:function(a){this.removeInput("AT");this.removeInput("ORDINAL",!0);a?(this.appendValueInput("AT").setCheck("Number"),$.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX&&this.appendDummyInput("ORDINAL").appendField($.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX)):this.appendDummyInput("AT");var b=new $.module$exports$Blockly$FieldDropdown.FieldDropdown(this.WHERE_OPTIONS,function(c){var d="FROM_START"===c||"FROM_END"===c;if(d!==a){var e=this.getSourceBlock();e.updateAt_(d);e.setFieldValue(c,
|
||||
"WHERE");return null}});this.getInput("AT").appendField(b,"WHERE");$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_TAIL&&this.moveInputBefore("TAIL",null)}};
|
||||
$.module$exports$Blockly$blocks.Blocks.lists_setIndex={init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_SET,"SET"],[$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_INSERT,"INSERT"]];this.WHERE_OPTIONS=[[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FROM_END,"FROM_END"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FIRST,"FIRST"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_LAST,"LAST"],
|
||||
[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_RANDOM,"RANDOM"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_HELPURL);this.setStyle("list_blocks");this.appendValueInput("LIST").setCheck("Array").appendField($.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_INPUT_IN_LIST);this.appendDummyInput().appendField(new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a),"MODE").appendField("","SPACE");this.appendDummyInput("AT");this.appendValueInput("TO").appendField($.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_INPUT_TO);
|
||||
module$exports$Blockly$libraryBlocks$lists.blocks.lists_setIndex={init:function(){var a=[[$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_SET,"SET"],[$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_INSERT,"INSERT"]];this.WHERE_OPTIONS=[[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FROM_END,"FROM_END"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_FIRST,"FIRST"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_LAST,
|
||||
"LAST"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_INDEX_RANDOM,"RANDOM"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_HELPURL);this.setStyle("list_blocks");this.appendValueInput("LIST").setCheck("Array").appendField($.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_INPUT_IN_LIST);this.appendDummyInput().appendField(new $.module$exports$Blockly$FieldDropdown.FieldDropdown(a),"MODE").appendField("","SPACE");this.appendDummyInput("AT");this.appendValueInput("TO").appendField($.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_INPUT_TO);
|
||||
this.setInputsInline(!0);this.setPreviousStatement(!0);this.setNextStatement(!0);this.setTooltip($.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_TOOLTIP);this.updateAt_(!0);var b=this;this.setTooltip(function(){var c=b.getFieldValue("MODE"),d=b.getFieldValue("WHERE"),e="";switch(c+" "+d){case "SET FROM_START":case "SET FROM_END":e=$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FROM;break;case "SET FIRST":e=$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FIRST;break;case "SET LAST":e=
|
||||
$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_TOOLTIP_SET_LAST;break;case "SET RANDOM":e=$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_TOOLTIP_SET_RANDOM;break;case "INSERT FROM_START":case "INSERT FROM_END":e=$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FROM;break;case "INSERT FIRST":e=$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST;break;case "INSERT LAST":e=$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_LAST;break;case "INSERT RANDOM":e=
|
||||
$.module$exports$Blockly$Msg.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM}if("FROM_START"===d||"FROM_END"===d)e+=" "+$.module$exports$Blockly$Msg.Msg.LISTS_INDEX_FROM_START_TOOLTIP.replace("%1",b.workspace.options.oneBasedIndex?"#1":"#0");return e})},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation"),b=this.getInput("AT").type===$.module$exports$Blockly$ConnectionType.ConnectionType.INPUT_VALUE;a.setAttribute("at",b);return a},domToMutation:function(a){a=
|
||||
"false"!==a.getAttribute("at");this.updateAt_(a)},updateAt_:function(a){this.removeInput("AT");this.removeInput("ORDINAL",!0);a?(this.appendValueInput("AT").setCheck("Number"),$.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX&&this.appendDummyInput("ORDINAL").appendField($.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX)):this.appendDummyInput("AT");var b=new $.module$exports$Blockly$FieldDropdown.FieldDropdown(this.WHERE_OPTIONS,function(c){var d="FROM_START"===c||"FROM_END"===c;if(d!==
|
||||
a){var e=this.getSourceBlock();e.updateAt_(d);e.setFieldValue(c,"WHERE");return null}});this.moveInputBefore("AT","TO");this.getInput("ORDINAL")&&this.moveInputBefore("ORDINAL","TO");this.getInput("AT").appendField(b,"WHERE")}};
|
||||
$.module$exports$Blockly$blocks.Blocks.lists_getSublist={init:function(){this.WHERE_OPTIONS_1=[[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_START_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_START_FROM_END,"FROM_END"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_START_FIRST,"FIRST"]];this.WHERE_OPTIONS_2=[[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_END_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_END_FROM_END,"FROM_END"],
|
||||
[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_END_LAST,"LAST"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_HELPURL);this.setStyle("list_blocks");this.appendValueInput("LIST").setCheck("Array").appendField($.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_INPUT_IN_LIST);this.appendDummyInput("AT1");this.appendDummyInput("AT2");$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_TAIL&&this.appendDummyInput("TAIL").appendField($.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_TAIL);
|
||||
module$exports$Blockly$libraryBlocks$lists.blocks.lists_getSublist={init:function(){this.WHERE_OPTIONS_1=[[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_START_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_START_FROM_END,"FROM_END"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_START_FIRST,"FIRST"]];this.WHERE_OPTIONS_2=[[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_END_FROM_START,"FROM_START"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_END_FROM_END,
|
||||
"FROM_END"],[$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_END_LAST,"LAST"]];this.setHelpUrl($.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_HELPURL);this.setStyle("list_blocks");this.appendValueInput("LIST").setCheck("Array").appendField($.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_INPUT_IN_LIST);this.appendDummyInput("AT1");this.appendDummyInput("AT2");$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_TAIL&&this.appendDummyInput("TAIL").appendField($.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_TAIL);
|
||||
this.setInputsInline(!0);this.setOutput(!0,"Array");this.updateAt_(1,!0);this.updateAt_(2,!0);this.setTooltip($.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_TOOLTIP)},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation"),b=this.getInput("AT1").type===$.module$exports$Blockly$ConnectionType.ConnectionType.INPUT_VALUE;a.setAttribute("at1",b);b=this.getInput("AT2").type===$.module$exports$Blockly$ConnectionType.ConnectionType.INPUT_VALUE;a.setAttribute("at2",
|
||||
b);return a},domToMutation:function(a){var b="true"===a.getAttribute("at1");a="true"===a.getAttribute("at2");this.updateAt_(1,b);this.updateAt_(2,a)},updateAt_:function(a,b){this.removeInput("AT"+a);this.removeInput("ORDINAL"+a,!0);b?(this.appendValueInput("AT"+a).setCheck("Number"),$.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX&&this.appendDummyInput("ORDINAL"+a).appendField($.module$exports$Blockly$Msg.Msg.ORDINAL_NUMBER_SUFFIX)):this.appendDummyInput("AT"+a);var c=new $.module$exports$Blockly$FieldDropdown.FieldDropdown(this["WHERE_OPTIONS_"+
|
||||
a],function(d){var e="FROM_START"===d||"FROM_END"===d;if(e!==b){var f=this.getSourceBlock();f.updateAt_(a,e);f.setFieldValue(d,"WHERE"+a);return null}});this.getInput("AT"+a).appendField(c,"WHERE"+a);1===a&&(this.moveInputBefore("AT1","AT2"),this.getInput("ORDINAL1")&&this.moveInputBefore("ORDINAL1","AT2"));$.module$exports$Blockly$Msg.Msg.LISTS_GET_SUBLIST_TAIL&&this.moveInputBefore("TAIL",null)}};
|
||||
$.module$exports$Blockly$blocks.Blocks.lists_sort={init:function(){this.jsonInit({message0:$.module$exports$Blockly$Msg.Msg.LISTS_SORT_TITLE,args0:[{type:"field_dropdown",name:"TYPE",options:[[$.module$exports$Blockly$Msg.Msg.LISTS_SORT_TYPE_NUMERIC,"NUMERIC"],[$.module$exports$Blockly$Msg.Msg.LISTS_SORT_TYPE_TEXT,"TEXT"],[$.module$exports$Blockly$Msg.Msg.LISTS_SORT_TYPE_IGNORECASE,"IGNORE_CASE"]]},{type:"field_dropdown",name:"DIRECTION",options:[[$.module$exports$Blockly$Msg.Msg.LISTS_SORT_ORDER_ASCENDING,
|
||||
module$exports$Blockly$libraryBlocks$lists.blocks.lists_sort={init:function(){this.jsonInit({message0:$.module$exports$Blockly$Msg.Msg.LISTS_SORT_TITLE,args0:[{type:"field_dropdown",name:"TYPE",options:[[$.module$exports$Blockly$Msg.Msg.LISTS_SORT_TYPE_NUMERIC,"NUMERIC"],[$.module$exports$Blockly$Msg.Msg.LISTS_SORT_TYPE_TEXT,"TEXT"],[$.module$exports$Blockly$Msg.Msg.LISTS_SORT_TYPE_IGNORECASE,"IGNORE_CASE"]]},{type:"field_dropdown",name:"DIRECTION",options:[[$.module$exports$Blockly$Msg.Msg.LISTS_SORT_ORDER_ASCENDING,
|
||||
"1"],[$.module$exports$Blockly$Msg.Msg.LISTS_SORT_ORDER_DESCENDING,"-1"]]},{type:"input_value",name:"LIST",check:"Array"}],output:"Array",style:"list_blocks",tooltip:$.module$exports$Blockly$Msg.Msg.LISTS_SORT_TOOLTIP,helpUrl:$.module$exports$Blockly$Msg.Msg.LISTS_SORT_HELPURL})}};
|
||||
$.module$exports$Blockly$blocks.Blocks.lists_split={init:function(){var a=this,b=new $.module$exports$Blockly$FieldDropdown.FieldDropdown([[$.module$exports$Blockly$Msg.Msg.LISTS_SPLIT_LIST_FROM_TEXT,"SPLIT"],[$.module$exports$Blockly$Msg.Msg.LISTS_SPLIT_TEXT_FROM_LIST,"JOIN"]],function(c){a.updateType_(c)});this.setHelpUrl($.module$exports$Blockly$Msg.Msg.LISTS_SPLIT_HELPURL);this.setStyle("list_blocks");this.appendValueInput("INPUT").setCheck("String").appendField(b,"MODE");this.appendValueInput("DELIM").setCheck("String").appendField($.module$exports$Blockly$Msg.Msg.LISTS_SPLIT_WITH_DELIMITER);
|
||||
module$exports$Blockly$libraryBlocks$lists.blocks.lists_split={init:function(){var a=this,b=new $.module$exports$Blockly$FieldDropdown.FieldDropdown([[$.module$exports$Blockly$Msg.Msg.LISTS_SPLIT_LIST_FROM_TEXT,"SPLIT"],[$.module$exports$Blockly$Msg.Msg.LISTS_SPLIT_TEXT_FROM_LIST,"JOIN"]],function(c){a.updateType_(c)});this.setHelpUrl($.module$exports$Blockly$Msg.Msg.LISTS_SPLIT_HELPURL);this.setStyle("list_blocks");this.appendValueInput("INPUT").setCheck("String").appendField(b,"MODE");this.appendValueInput("DELIM").setCheck("String").appendField($.module$exports$Blockly$Msg.Msg.LISTS_SPLIT_WITH_DELIMITER);
|
||||
this.setInputsInline(!0);this.setOutput(!0,"Array");this.setTooltip(function(){var c=a.getFieldValue("MODE");if("SPLIT"===c)return $.module$exports$Blockly$Msg.Msg.LISTS_SPLIT_TOOLTIP_SPLIT;if("JOIN"===c)return $.module$exports$Blockly$Msg.Msg.LISTS_SPLIT_TOOLTIP_JOIN;throw Error("Unknown mode: "+c);})},updateType_:function(a){if(this.getFieldValue("MODE")!==a){var b=this.getInput("INPUT").connection;b.setShadowDom(null);var c=b.targetBlock();c&&(b.disconnect(),c.isShadow()?c.dispose():this.bumpNeighbours())}"SPLIT"===
|
||||
a?(this.outputConnection.setCheck("Array"),this.getInput("INPUT").setCheck("String")):(this.outputConnection.setCheck("String"),this.getInput("INPUT").setCheck("Array"))},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("mode",this.getFieldValue("MODE"));return a},domToMutation:function(a){this.updateType_(a.getAttribute("mode"))}};var module$exports$Blockly$blocks$colour={};
|
||||
(0,$.module$exports$Blockly$common.defineBlocksWithJsonArray)([{type:"colour_picker",message0:"%1",args0:[{type:"field_colour",name:"COLOUR",colour:"#ff0000"}],output:"Colour",helpUrl:"%{BKY_COLOUR_PICKER_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_PICKER_TOOLTIP}",extensions:["parent_tooltip_when_inline"]},{type:"colour_random",message0:"%{BKY_COLOUR_RANDOM_TITLE}",output:"Colour",helpUrl:"%{BKY_COLOUR_RANDOM_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_RANDOM_TOOLTIP}"},{type:"colour_rgb",
|
||||
message0:"%{BKY_COLOUR_RGB_TITLE} %{BKY_COLOUR_RGB_RED} %1 %{BKY_COLOUR_RGB_GREEN} %2 %{BKY_COLOUR_RGB_BLUE} %3",args0:[{type:"input_value",name:"RED",check:"Number",align:"RIGHT"},{type:"input_value",name:"GREEN",check:"Number",align:"RIGHT"},{type:"input_value",name:"BLUE",check:"Number",align:"RIGHT"}],output:"Colour",helpUrl:"%{BKY_COLOUR_RGB_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_RGB_TOOLTIP}"},{type:"colour_blend",message0:"%{BKY_COLOUR_BLEND_TITLE} %{BKY_COLOUR_BLEND_COLOUR1} %1 %{BKY_COLOUR_BLEND_COLOUR2} %2 %{BKY_COLOUR_BLEND_RATIO} %3",
|
||||
args0:[{type:"input_value",name:"COLOUR1",check:"Colour",align:"RIGHT"},{type:"input_value",name:"COLOUR2",check:"Colour",align:"RIGHT"},{type:"input_value",name:"RATIO",check:"Number",align:"RIGHT"}],output:"Colour",helpUrl:"%{BKY_COLOUR_BLEND_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_BLEND_TOOLTIP}"}]);var module$exports$Blockly$blocks$all={};
|
||||
|
||||
return $.Blockly.Blocks;
|
||||
a?(this.outputConnection.setCheck("Array"),this.getInput("INPUT").setCheck("String")):(this.outputConnection.setCheck("String"),this.getInput("INPUT").setCheck("Array"))},mutationToDom:function(){var a=(0,$.module$exports$Blockly$utils$xml.createElement)("mutation");a.setAttribute("mode",this.getFieldValue("MODE"));return a},domToMutation:function(a){this.updateType_(a.getAttribute("mode"))}};(0,$.module$exports$Blockly$common.defineBlocks)(module$exports$Blockly$libraryBlocks$lists.blocks);var module$exports$Blockly$libraryBlocks$colour={};
|
||||
module$exports$Blockly$libraryBlocks$colour.blocks=(0,$.module$exports$Blockly$common.createBlockDefinitionsFromJsonArray)([{type:"colour_picker",message0:"%1",args0:[{type:"field_colour",name:"COLOUR",colour:"#ff0000"}],output:"Colour",helpUrl:"%{BKY_COLOUR_PICKER_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_PICKER_TOOLTIP}",extensions:["parent_tooltip_when_inline"]},{type:"colour_random",message0:"%{BKY_COLOUR_RANDOM_TITLE}",output:"Colour",helpUrl:"%{BKY_COLOUR_RANDOM_HELPURL}",style:"colour_blocks",
|
||||
tooltip:"%{BKY_COLOUR_RANDOM_TOOLTIP}"},{type:"colour_rgb",message0:"%{BKY_COLOUR_RGB_TITLE} %{BKY_COLOUR_RGB_RED} %1 %{BKY_COLOUR_RGB_GREEN} %2 %{BKY_COLOUR_RGB_BLUE} %3",args0:[{type:"input_value",name:"RED",check:"Number",align:"RIGHT"},{type:"input_value",name:"GREEN",check:"Number",align:"RIGHT"},{type:"input_value",name:"BLUE",check:"Number",align:"RIGHT"}],output:"Colour",helpUrl:"%{BKY_COLOUR_RGB_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_RGB_TOOLTIP}"},{type:"colour_blend",message0:"%{BKY_COLOUR_BLEND_TITLE} %{BKY_COLOUR_BLEND_COLOUR1} %1 %{BKY_COLOUR_BLEND_COLOUR2} %2 %{BKY_COLOUR_BLEND_RATIO} %3",
|
||||
args0:[{type:"input_value",name:"COLOUR1",check:"Colour",align:"RIGHT"},{type:"input_value",name:"COLOUR2",check:"Colour",align:"RIGHT"},{type:"input_value",name:"RATIO",check:"Number",align:"RIGHT"}],output:"Colour",helpUrl:"%{BKY_COLOUR_BLEND_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_BLEND_TOOLTIP}"}]);(0,$.module$exports$Blockly$common.defineBlocks)(module$exports$Blockly$libraryBlocks$colour.blocks);$.Blockly.libraryBlocks={};$.Blockly.libraryBlocks.colour=module$exports$Blockly$libraryBlocks$colour;$.Blockly.libraryBlocks.lists=module$exports$Blockly$libraryBlocks$lists;$.Blockly.libraryBlocks.logic=module$exports$Blockly$libraryBlocks$logic;$.Blockly.libraryBlocks.loops=module$exports$Blockly$libraryBlocks$loops;$.Blockly.libraryBlocks.math=module$exports$Blockly$libraryBlocks$math;$.Blockly.libraryBlocks.procedures=module$exports$Blockly$libraryBlocks$procedures;
|
||||
$.Blockly.libraryBlocks.texts=module$exports$Blockly$libraryBlocks$texts;$.Blockly.libraryBlocks.variables=module$exports$Blockly$libraryBlocks$variables;$.Blockly.libraryBlocks.variablesDynamic=module$exports$Blockly$libraryBlocks$variablesDynamic;
|
||||
var module$contents$Blockly$libraryBlocks_blocks=Object.assign({},module$exports$Blockly$libraryBlocks$colour.blocks,module$exports$Blockly$libraryBlocks$lists.blocks,module$exports$Blockly$libraryBlocks$logic.blocks,module$exports$Blockly$libraryBlocks$loops.blocks,module$exports$Blockly$libraryBlocks$math.blocks,module$exports$Blockly$libraryBlocks$procedures.blocks,module$exports$Blockly$libraryBlocks$variables.blocks,module$exports$Blockly$libraryBlocks$variablesDynamic.blocks);
|
||||
$.Blockly.libraryBlocks.blocks=module$contents$Blockly$libraryBlocks_blocks;
|
||||
$.Blockly.libraryBlocks.__namespace__=$;
|
||||
return $.Blockly.libraryBlocks;
|
||||
}));
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+14
-37
@@ -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
|
||||
|
||||
@@ -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.
|
||||
+2106
-2051
File diff suppressed because it is too large
Load Diff
+217
-216
@@ -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;
|
||||
|
||||
+378
-374
@@ -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;
|
||||
|
||||
+1774
-1660
File diff suppressed because it is too large
Load Diff
+8
-22
@@ -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;
|
||||
|
||||
+9
-2
@@ -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;
|
||||
|
||||
+22
-5
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
+881
-837
File diff suppressed because it is too large
Load Diff
+226
-219
@@ -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');
|
||||
|
||||
+9
-9
@@ -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;
|
||||
|
||||
+376
-355
@@ -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;
|
||||
|
||||
+48
-19
@@ -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;
|
||||
|
||||
+184
-177
@@ -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>} */
|
||||
|
||||
@@ -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;
|
||||
+619
-608
File diff suppressed because it is too large
Load Diff
+251
-253
@@ -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);
|
||||
|
||||
+254
-249
@@ -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;
|
||||
|
||||
+9
-6
@@ -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;
|
||||
|
||||
+469
-469
@@ -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;
|
||||
|
||||
+45
-42
@@ -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;
|
||||
|
||||
+57
-56
@@ -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;
|
||||
|
||||
+153
-163
@@ -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;
|
||||
|
||||
+137
-131
@@ -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);
|
||||
|
||||
|
||||
+154
-147
@@ -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);
|
||||
|
||||
|
||||
+49
-45
@@ -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);
|
||||
|
||||
+110
-108
@@ -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);
|
||||
|
||||
|
||||
+48
-47
@@ -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);
|
||||
|
||||
+29
-14
@@ -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);
|
||||
|
||||
+4
-3
@@ -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);
|
||||
|
||||
+1158
-1094
File diff suppressed because it is too large
Load Diff
+463
-443
@@ -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);
|
||||
|
||||
+204
-192
@@ -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;
|
||||
|
||||
+532
-518
File diff suppressed because it is too large
Load Diff
+643
-624
File diff suppressed because it is too large
Load Diff
+240
-231
@@ -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;
|
||||
|
||||
+103
-92
@@ -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);
|
||||
|
||||
|
||||
+384
-370
@@ -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);
|
||||
|
||||
+288
-269
@@ -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;
|
||||
|
||||
+553
-521
File diff suppressed because it is too large
Load Diff
+480
-442
@@ -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);
|
||||
|
||||
|
||||
+1074
-1021
File diff suppressed because it is too large
Load Diff
+294
-277
@@ -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;
|
||||
|
||||
+316
-314
@@ -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;
|
||||
|
||||
+345
-339
@@ -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);
|
||||
|
||||
|
||||
+468
-455
@@ -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;
|
||||
|
||||
+948
-932
File diff suppressed because it is too large
Load Diff
+180
-177
@@ -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;
|
||||
|
||||
+172
-163
@@ -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;
|
||||
|
||||
+7
-5
@@ -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();
|
||||
|
||||
+273
-264
@@ -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;
|
||||
|
||||
+692
-661
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user