release: Merge branch 'develop' into rc/v10.2.0

This commit is contained in:
Rachel Fenichel
2023-09-25 11:30:06 -07:00
508 changed files with 5427 additions and 18123 deletions

View File

@@ -1,5 +1,5 @@
name: Bug Report
description: Create a report to help us improve
name: Report a bug 🐛
description: Report bugs in Blockly, so we can fix them.
labels: 'issue: bug, issue: triage'
body:
- type: markdown

View File

@@ -1,7 +1,7 @@
contact_links:
- name: Blockly Forum
- name: Ask a question ❓
url: https://groups.google.com/forum/#!forum/blockly
about: The Blockly developer forum, where you can ask and answer questions.
- name: Plugins and examples
about: Go to the Blockly developer forum, where you can ask and answer questions.
- name: Report issues with plugins and examples 🧩
url: https://github.com/google/blockly-samples/issues/new/choose
about: File bugs or feature requests about plugins and samples in our blockly-samples repository.

View File

@@ -1,5 +1,5 @@
name: Documentation
description: Report an issue with our documentation
name: Report a documentation problem 📖
description: Could our documentation be better? Tell us how.
labels: 'issue: docs, issue: triage'
body:
- type: markdown

View File

@@ -1,5 +1,5 @@
name: Feature request
description: Suggest an idea for this project
name: Make a feature request
description: Suggest an idea to make Blockly better.
labels: 'issue: feature request, issue: triage'
body:
- type: markdown

View File

@@ -7,10 +7,7 @@
<!-- TODO: Verify the following, checking each box with an 'x' between the brackets: [x] -->
- [ ] I branched from develop
- [ ] My pull request is against develop
- [ ] My code follows the [style guide](https://developers.google.com/blockly/guides/modify/web/style-guide)
- [ ] I ran `npm run format` and `npm run lint`
- [ ] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change)
## The details
### Resolves
@@ -22,14 +19,6 @@ Fixes
<!-- TODO: Describe what this Pull Request does. Include screenshots if applicable. -->
#### Behavior Before Change
<!--TODO: Image, gif or explanation of behavior before this pull request. -->
#### Behavior After Change
<!--TODO: Image, gif or explanation of behavior after this pull request. -->
### Reason for Changes
<!--TODO: Explain why these changes should be made. Include screenshots if applicable. -->

View File

@@ -15,7 +15,7 @@ jobs:
steps:
# Checks-out the repository under $GITHUB_WORKSPACE.
# When running manually this checks out the master branch.
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Prepare demo files
# Install all dependencies, then copy all the files needed for demos.

View File

@@ -24,7 +24,7 @@ jobs:
# https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
persist-credentials: false
@@ -45,10 +45,6 @@ jobs:
if: runner.os == 'Linux'
run: source ./tests/scripts/setup_linux_env.sh
- name: MacOS Test Setup
if: runner.os == 'macOS'
run: source ./tests/scripts/setup_osx_env.sh
- name: Run Build
run: npm run build

View File

@@ -23,7 +23,7 @@ jobs:
# https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
persist-credentials: false
@@ -44,10 +44,6 @@ jobs:
if: runner.os == 'Linux'
run: source ./tests/scripts/setup_linux_env.sh
- name: MacOS Test Setup
if: runner.os == 'macOS'
run: source ./tests/scripts/setup_osx_env.sh
- name: Run
run: npm run test
@@ -58,7 +54,7 @@ jobs:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v3
@@ -75,7 +71,7 @@ jobs:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v3

View File

@@ -0,0 +1,37 @@
on:
pull_request_target:
types:
- opened
name: Welcome new contributors
jobs:
welcome:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: >
Welcome! It looks like this is your first pull request in Blockly,
so here are a couple of tips:
- You can find tips about contributing to Blockly and how to
validate your changes on our
[developer site](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change).
- All contributors must sign the Google Contributor License
Agreement (CLA). If the google-cla bot leaves a comment on this
PR, make sure you follow the instructions.
- We use [conventional commits](https://www.conventionalcommits.org/)
to make versioning the package easier. Make sure your commit
message is in the [proper format](https://developers.google.com/blockly/guides/contribute/get-started/commits)
or [learn how to fix it](https://developers.google.com/blockly/guides/contribute/get-started/commits#fixing_non-conventional_commits).
- If any of the other checks on this PR fail, you can click on
them to learn why. It might be that your change caused a test
failure, or that you need to double-check the
[style guide](https://developers.google.com/blockly/guides/contribute/core/style_guide).
Thank you for opening this PR! A member of the Blockly team will review it soon.

View File

@@ -42,7 +42,7 @@ Want to make Blockly better? We welcome contributions to Blockly in the form of
## Releases
We release by pushing the latest code to the master branch, followed by updating the npm package, our [docs](https://developers.google.com/blockly), and [demo pages](https://google.github.io/blockly-samples/). We typically release a new version of Blockly once a quarter (every 3 months). If there are breaking bugs, such as a crash when performing a standard action or a rendering issue that makes Blockly unusable, we will cherry-pick fixes to master between releases to fix them. The [releases page](https://github.com/google/blockly/releases) has a list of all releases.
We release by pushing the latest code to the master branch, followed by updating the npm package, our [docs](https://developers.google.com/blockly), and [demo pages](https://google.github.io/blockly-samples/). If there are breaking bugs, such as a crash when performing a standard action or a rendering issue that makes Blockly unusable, we will cherry-pick fixes to master between releases to fix them. The [releases page](https://github.com/google/blockly/releases) has a list of all releases.
We use [semantic versioning](https://semver.org/). Releases that have breaking changes or are otherwise not backwards compatible will have a new major version. Patch versions are reserved for bug-fix patches between scheduled releases.
@@ -72,15 +72,7 @@ Unreleased APIs may change radically. Anything that is in `develop` but not `mas
## Issues and Milestones
We typically triage all bugs within 2 working days, which includes adding any appropriate labels and assigning it to a milestone. Please keep in mind, we are a small team so even feature requests that everyone agrees on may not be prioritized.
### Milestones
**Upcoming release** - The upcoming release milestone is for all bugs we plan on fixing before the next release. This typically has the form of `year_quarter_release` (such as `2019_q2_release`). Some bugs will be added to this release when they are triaged, others may be added closer to a release.
**Bug Bash Backlog** - These are bugs that we're still prioritizing. They haven't been added to a specific release yet, but we'll consider them for each release depending on relative priority and available time.
**Icebox** - These are bugs that we do not intend to spend time on. They are either too much work or minor enough that we don't expect them to ever take priority. We are still happy to accept pull requests for these bugs.
We typically triage all bugs within 1 week, which includes adding any appropriate labels and assigning it to a milestone. Please keep in mind, we are a small team so even feature requests that everyone agrees on may not be prioritized.
## Good to Know

View File

@@ -130,6 +130,8 @@ BlocklyStorage.handleRequest_ = function() {
BlocklyStorage.alert(BlocklyStorage.HASH_ERROR.replace('%1',
window.location.hash));
} else {
// Remove poison line to prevent raw content from being served.
data = data.replace(/^\{\[\(\< UNTRUSTED CONTENT \>\)\]\}\n/, '');
BlocklyStorage.loadXml_(data, BlocklyStorage.httpRequest_.workspace);
}
}

View File

@@ -15,15 +15,15 @@ See the License for the specific language governing permissions and
limitations under the License.
"""
"""Store and retrieve XML with App Engine.
"""Store and retrieve Blockly XML/JSON with App Engine.
"""
__author__ = "q.neutron@gmail.com (Quynh Neutron)"
import cgi
import hashlib
from random import randint
from google.cloud import ndb
from random import randint
from urllib.parse import unquote
class Xml(ndb.Model):
@@ -32,6 +32,7 @@ class Xml(ndb.Model):
xml_content = ndb.TextProperty()
last_accessed = ndb.DateTimeProperty(auto_now=True)
def keyGen():
# Generate a random string of length KEY_LEN.
KEY_LEN = 6
@@ -40,8 +41,23 @@ def keyGen():
return "".join([CHARS[randint(0, max_index)] for x in range(KEY_LEN)])
# Parse POST data (e.g. a=1&b=2) into a dictionary (e.g. {"a": 1, "b": 2}).
# Very minimal parser. Does not combine repeated names (a=1&a=2), ignores
# valueless names (a&b), does not support isindex or multipart/form-data.
def parse_post(environ):
fp = environ["wsgi.input"]
data = fp.read().decode()
parts = data.split("&")
dict = {}
for part in parts:
tuple = part.split("=", 1)
if len(tuple) == 2:
dict[tuple[0]] = unquote(tuple[1])
return dict
def xmlToKey(xml_content):
# Store XML and return a generated key.
# Store XML/JSON and return a generated key.
xml_hash = int(hashlib.sha1(xml_content.encode("utf-8")).hexdigest(), 16)
xml_hash = int(xml_hash % (2 ** 64) - (2 ** 63))
client = ndb.Client()
@@ -65,7 +81,7 @@ def xmlToKey(xml_content):
def keyToXml(key_provided):
# Retrieve stored XML based on the provided key.
# Retrieve stored XML/JSON based on the provided key.
# Normalize the string.
key_provided = key_provided.lower().strip()
# Check datastore for a match.
@@ -80,20 +96,30 @@ def keyToXml(key_provided):
with client.context():
result.put()
xml = result.xml_content
# Add a poison line to prevent raw content from being served.
xml = "{[(< UNTRUSTED CONTENT >)]}\n" + xml
return xml
def app(environ, start_response):
forms = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ)
if "xml" in forms:
out = xmlToKey(forms["xml"].value)
elif "key" in forms:
out = keyToXml(forms["key"].value)
else:
out = ""
headers = [
("Content-Type", "text/plain")
]
if environ["REQUEST_METHOD"] != "POST":
start_response("405 Method Not Allowed", headers)
return ["Storage only accepts POST".encode("utf-8")]
if ("CONTENT_TYPE" in environ and
environ["CONTENT_TYPE"] != "application/x-www-form-urlencoded"):
start_response("405 Method Not Allowed", headers)
return ["Storage only accepts application/x-www-form-urlencoded".encode("utf-8")]
forms = parse_post(environ)
if "xml" in forms:
out = xmlToKey(forms["xml"])
elif "key" in forms:
out = keyToXml(forms["key"])
else:
out = ""
start_response("200 OK", headers)
return [out.encode("utf-8")]

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks');
// Former goog.module ID: Blockly.libraryBlocks
import * as colour from './colour.js';
import * as lists from './lists.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.colour');
// Former goog.module ID: Blockly.libraryBlocks.colour
import {
createBlockDefinitionsFromJsonArray,

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.lists');
// Former goog.module ID: Blockly.libraryBlocks.lists
import * as fieldRegistry from '../core/field_registry.js';
import * as xmlUtils from '../core/utils/xml.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.logic');
// Former goog.module ID: Blockly.libraryBlocks.logic
import * as Events from '../core/events/events.js';
import * as Extensions from '../core/extensions.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.loops');
// Former goog.module ID: Blockly.libraryBlocks.loops
import type {Abstract as AbstractEvent} from '../core/events/events_abstract.js';
import type {Block} from '../core/block.js';
@@ -16,8 +15,6 @@ import type {
} from '../core/contextmenu_registry.js';
import * as Events from '../core/events/events.js';
import * as Extensions from '../core/extensions.js';
import * as Variables from '../core/variables.js';
import * as xmlUtils from '../core/utils/xml.js';
import {Msg} from '../core/msg.js';
import {
createBlockDefinitionsFromJsonArray,
@@ -273,15 +270,15 @@ const CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN = {
const variable = varField.getVariable()!;
const varName = variable.name;
if (!this.isCollapsed() && varName !== null) {
const xmlField = Variables.generateVariableFieldDom(variable);
const xmlBlock = xmlUtils.createElement('block');
xmlBlock.setAttribute('type', 'variables_get');
xmlBlock.appendChild(xmlField);
const getVarBlockState = {
type: 'variables_get',
fields: {VAR: varField.saveState(true)},
};
options.push({
enabled: true,
text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', varName),
callback: ContextMenu.callbackFactory(this, xmlBlock),
callback: ContextMenu.callbackFactory(this, getVarBlockState),
});
}
},

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.math');
// Former goog.module ID: Blockly.libraryBlocks.math
import * as Extensions from '../core/extensions.js';
import type {FieldDropdown} from '../core/field_dropdown.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.procedures');
// Former goog.module ID: Blockly.libraryBlocks.procedures
import * as ContextMenu from '../core/contextmenu.js';
import * as Events from '../core/events/events.js';
@@ -455,34 +454,30 @@ const PROCEDURE_DEF_COMMON = {
}
// Add option to create caller.
const name = this.getFieldValue('NAME');
const xmlMutation = xmlUtils.createElement('mutation');
xmlMutation.setAttribute('name', name);
for (let i = 0; i < this.arguments_.length; i++) {
const xmlArg = xmlUtils.createElement('arg');
xmlArg.setAttribute('name', this.arguments_[i]);
xmlMutation.appendChild(xmlArg);
}
const xmlBlock = xmlUtils.createElement('block');
xmlBlock.setAttribute('type', (this as AnyDuringMigration).callType_);
xmlBlock.appendChild(xmlMutation);
const callProcedureBlockState = {
type: (this as AnyDuringMigration).callType_,
extraState: {name: name, params: this.arguments_},
};
options.push({
enabled: true,
text: Msg['PROCEDURES_CREATE_DO'].replace('%1', name),
callback: ContextMenu.callbackFactory(this, xmlBlock),
callback: ContextMenu.callbackFactory(this, callProcedureBlockState),
});
// Add options to create getters for each parameter.
if (!this.isCollapsed()) {
for (let i = 0; i < this.argumentVarModels_.length; i++) {
const argVar = this.argumentVarModels_[i];
const argXmlField = Variables.generateVariableFieldDom(argVar);
const argXmlBlock = xmlUtils.createElement('block');
argXmlBlock.setAttribute('type', 'variables_get');
argXmlBlock.appendChild(argXmlField);
const getVarBlockState = {
type: 'variables_get',
fields: {
VAR: {name: argVar.name, id: argVar.getId(), type: argVar.type},
},
};
options.push({
enabled: true,
text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', argVar.name),
callback: ContextMenu.callbackFactory(this, argXmlBlock),
callback: ContextMenu.callbackFactory(this, getVarBlockState),
});
}
}

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.texts');
// Former goog.module ID: Blockly.libraryBlocks.texts
import * as Extensions from '../core/extensions.js';
import * as fieldRegistry from '../core/field_registry.js';

View File

@@ -4,13 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.variables');
// Former goog.module ID: Blockly.libraryBlocks.variables
import * as ContextMenu from '../core/contextmenu.js';
import * as Extensions from '../core/extensions.js';
import * as Variables from '../core/variables.js';
import * as xmlUtils from '../core/utils/xml.js';
import type {Block} from '../core/block.js';
import type {
ContextMenuOption,
@@ -103,18 +101,16 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
contextMenuMsg = Msg['VARIABLES_SET_CREATE_GET'];
}
const name = this.getField('VAR')!.getText();
const xmlField = xmlUtils.createElement('field');
xmlField.setAttribute('name', 'VAR');
xmlField.appendChild(xmlUtils.createTextNode(name));
const xmlBlock = xmlUtils.createElement('block');
xmlBlock.setAttribute('type', oppositeType);
xmlBlock.appendChild(xmlField);
const varField = this.getField('VAR')!;
const newVarBlockState = {
type: oppositeType,
fields: {VAR: varField.saveState(true)},
};
options.push({
enabled: this.workspace.remainingCapacity() > 0,
text: contextMenuMsg.replace('%1', name),
callback: ContextMenu.callbackFactory(this, xmlBlock),
text: contextMenuMsg.replace('%1', varField.getText()),
callback: ContextMenu.callbackFactory(this, newVarBlockState),
});
// Getter blocks have the option to rename or delete that variable.
} else {

View File

@@ -4,13 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.variablesDynamic');
// Former goog.module ID: Blockly.libraryBlocks.variablesDynamic
import * as ContextMenu from '../core/contextmenu.js';
import * as Extensions from '../core/extensions.js';
import * as Variables from '../core/variables.js';
import * as xml from '../core/utils/xml.js';
import {Abstract as AbstractEvent} from '../core/events/events_abstract.js';
import type {Block} from '../core/block.js';
import type {
@@ -96,9 +94,6 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
if (!this.isInFlyout) {
let oppositeType;
let contextMenuMsg;
const id = this.getFieldValue('VAR');
const variableModel = this.workspace.getVariableById(id);
const varType = variableModel!.type;
if (this.type === 'variables_get_dynamic') {
oppositeType = 'variables_set_dynamic';
contextMenuMsg = Msg['VARIABLES_GET_CREATE_SET'];
@@ -107,19 +102,16 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
contextMenuMsg = Msg['VARIABLES_SET_CREATE_GET'];
}
const name = this.getField('VAR')!.getText();
const xmlField = xml.createElement('field');
xmlField.setAttribute('name', 'VAR');
xmlField.setAttribute('variabletype', varType);
xmlField.appendChild(xml.createTextNode(name));
const xmlBlock = xml.createElement('block');
xmlBlock.setAttribute('type', oppositeType);
xmlBlock.appendChild(xmlField);
const varField = this.getField('VAR')!;
const newVarBlockState = {
type: oppositeType,
fields: {VAR: varField.saveState(true)},
};
options.push({
enabled: this.workspace.remainingCapacity() > 0,
text: contextMenuMsg.replace('%1', name),
callback: ContextMenu.callbackFactory(this, xmlBlock),
text: contextMenuMsg.replace('%1', varField.getText()),
callback: ContextMenu.callbackFactory(this, newVarBlockState),
});
} else {
if (

File diff suppressed because it is too large Load Diff

View File

@@ -1,112 +0,0 @@
/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview A minimal implementation of base.js.
*
* This file is used in place of base.js (Closure library bootstrap
* code) when building Blockly using the Closure Compiler. Refer to
* base.js for more information about items defined here.
*
* @provideGoog
*/
/** @define {boolean} Overridden to true by the compiler. */
var COMPILED = false;
/** @const */
var goog = goog || {};
/**
* Reference to the global object. This is provided as 'root' by the
* UMD wrapper, but prefer globalThis if it is defined.
*
* https://www.ecma-international.org/ecma-262/9.0/index.html#sec-global-object
*
* @const
* @type {!Global}
* @suppress {undefinedVars}
*/
goog.global = globalThis || root;
/** @type {Object<string, (string|number|boolean)>|undefined} */
goog.global.CLOSURE_DEFINES;
/**
* Defines a named value.
* When compiled the default can be overridden using the compiler options or the
* value set in the CLOSURE_DEFINES object. Returns the defined value so that it
* can be used safely in modules. Note that the value type MUST be either
* boolean, number, or string.
*
* @param {string} name
* @param {T} defaultValue
* @return {T}
* @template T
*/
goog.define = function(name, defaultValue) {
return defaultValue;
};
/** @define {boolean} */
goog.DEBUG = goog.define('goog.DEBUG', false);
/** @define {boolean} */
goog.DISALLOW_TEST_ONLY_CODE =
goog.define('goog.DISALLOW_TEST_ONLY_CODE', COMPILED && !goog.DEBUG);
/**
* @param {string} name
*/
goog.provide = function(name) {};
/**
* @param {string} name
* @return {void}
*/
goog.module = function(name) {};
/**
* @param {string} name
* @return {?}
* @suppress {missingProvide}
*/
goog.module.get = function(name) {};
/** @suppress {missingProvide} */
goog.module.declareLegacyNamespace = function() {};
/**
* Marks that the current file should only be used for testing, and never for
* live code in production.
*
* In the case of unit tests, the message may optionally be an exact namespace
* for the test (e.g. 'goog.stringTest'). The linter will then ignore the extra
* provide (if not explicitly defined in the code).
*
* @param {string=} opt_message Optional message to add to the error that's
* raised when used in production code.
*/
goog.setTestOnly = function(opt_message) {
if (goog.DISALLOW_TEST_ONLY_CODE) {
opt_message = opt_message || '';
throw new Error(
'Importing test-only code into non-debug environment' +
(opt_message ? ': ' + opt_message : '.'));
}
};
/**
* @param {string} namespace
* @return {?}
*/
goog.require = function(namespace) {};
/**
* @param {string} namespace
* @return {?}
*/
goog.requireType = function(namespace) {};

View File

@@ -1,102 +0,0 @@
// 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 = globalThis;
// 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 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 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 = function(namespace) {
if (window.goog && window.goog.declareModuleId) {
window.goog.declareModuleId.call(this, namespace);
}
};
// 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.

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Block');
// Former goog.module ID: Blockly.Block
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';
@@ -48,6 +47,7 @@ import {Size} from './utils/size.js';
import type {VariableModel} from './variable_model.js';
import type {Workspace} from './workspace.js';
import {DummyInput} from './inputs/dummy_input.js';
import {EndRowInput} from './inputs/end_row_input.js';
import {ValueInput} from './inputs/value_input.js';
import {StatementInput} from './inputs/statement_input.js';
import {IconType} from './icons/icon_types.js';
@@ -119,8 +119,14 @@ export class Block implements IASTNodeLocation, IDeletable {
* An optional serialization method for defining how to serialize the
* block's extra state (eg mutation state) to something JSON compatible.
* This must be coupled with defining `loadExtraState`.
*
* @param doFullSerialization Whether or not to serialize the full state of
* the extra state (rather than possibly saving a reference to some
* state). This is used during copy-paste. See the
* {@link https://developers.devsite.google.com/blockly/guides/create-custom-blocks/extensions#full_serialization_and_backing_data | block serialization docs}
* for more information.
*/
saveExtraState?: () => AnyDuringMigration;
saveExtraState?: (doFullSerialization?: boolean) => AnyDuringMigration;
/**
* An optional serialization method for defining how to deserialize the
@@ -933,6 +939,19 @@ export class Block implements IASTNodeLocation, IDeletable {
return this.disposed;
}
/**
* @returns True if this block is a value block with a single editable field.
* @internal
*/
isSimpleReporter(): boolean {
if (!this.outputConnection) return false;
for (const input of this.inputList) {
if (input.connection || input.fieldRow.length > 1) return false;
}
return true;
}
/**
* Find the connection on this block that corresponds to the given connection
* on the other block.
@@ -1339,6 +1358,12 @@ export class Block implements IASTNodeLocation, IDeletable {
return true;
}
}
for (let i = 0; i < this.inputList.length; i++) {
if (this.inputList[i] instanceof EndRowInput) {
// A row-end input is present. Inline value inputs.
return true;
}
}
return false;
}
@@ -1560,6 +1585,17 @@ export class Block implements IASTNodeLocation, IDeletable {
return this.appendInput(new DummyInput(name, this));
}
/**
* Appends an input that ends the row.
*
* @param name Optional language-neutral identifier which may used to find
* this input again. Should be unique to this block.
* @returns The input object created.
*/
appendEndRowInput(name = ''): Input {
return this.appendInput(new EndRowInput(name, this));
}
/**
* Appends the given input row.
*
@@ -1628,15 +1664,19 @@ export class Block implements IASTNodeLocation, IDeletable {
this.interpolate_(
json['message' + i],
json['args' + i] || [],
json['lastDummyAlign' + i],
// Backwards compatibility: lastDummyAlign aliases implicitAlign.
json['implicitAlign' + i] || json['lastDummyAlign' + i],
warningPrefix,
);
i++;
}
if (json['inputsInline'] !== undefined) {
eventUtils.disable();
this.setInputsInline(json['inputsInline']);
eventUtils.enable();
}
// Set output and previous/next connections.
if (json['output'] !== undefined) {
this.setOutput(true, json['output']);
@@ -1765,19 +1805,19 @@ export class Block implements IASTNodeLocation, IDeletable {
* @param message Text contains interpolation tokens (%1, %2, ...) that match
* with fields or inputs defined in the args array.
* @param args Array of arguments to be interpolated.
* @param lastDummyAlign If a dummy input is added at the end, how should it
* be aligned?
* @param implicitAlign If an implicit input is added at the end or in place
* of newline tokens, how should it be aligned?
* @param warningPrefix Warning prefix string identifying block.
*/
private interpolate_(
message: string,
args: AnyDuringMigration[],
lastDummyAlign: string | undefined,
implicitAlign: string | undefined,
warningPrefix: string,
) {
const tokens = parsing.tokenizeInterpolation(message);
this.validateTokens_(tokens, args.length);
const elements = this.interpolateArguments_(tokens, args, lastDummyAlign);
const elements = this.interpolateArguments_(tokens, args, implicitAlign);
// An array of [field, fieldName] tuples.
const fieldStack = [];
@@ -1855,19 +1895,20 @@ export class Block implements IASTNodeLocation, IDeletable {
/**
* Inserts args in place of numerical tokens. String args are converted to
* JSON that defines a label field. If necessary an extra dummy input is added
* to the end of the elements.
* JSON that defines a label field. Newline characters are converted to
* end-row inputs, and if necessary an extra dummy input is added to the end
* of the elements.
*
* @param tokens The tokens to interpolate
* @param args The arguments to insert.
* @param lastDummyAlign The alignment the added dummy input should have, if
* we are required to add one.
* @param implicitAlign The alignment to use for any implicitly added end-row
* or dummy inputs, if necessary.
* @returns The JSON definitions of field and inputs to add to the block.
*/
private interpolateArguments_(
tokens: Array<string | number>,
args: Array<AnyDuringMigration | string>,
lastDummyAlign: string | undefined,
implicitAlign: string | undefined,
): AnyDuringMigration[] {
const elements = [];
for (let i = 0; i < tokens.length; i++) {
@@ -1877,11 +1918,20 @@ export class Block implements IASTNodeLocation, IDeletable {
}
// Args can be strings, which is why this isn't elseif.
if (typeof element === 'string') {
// AnyDuringMigration because: Type '{ text: string; type: string; } |
// null' is not assignable to type 'string | number'.
element = this.stringToFieldJson_(element) as AnyDuringMigration;
if (!element) {
continue;
if (element === '\n') {
// Convert newline tokens to end-row inputs.
const newlineInput = {'type': 'input_end_row'};
if (implicitAlign) {
(newlineInput as AnyDuringMigration)['align'] = implicitAlign;
}
element = newlineInput as AnyDuringMigration;
} else {
// AnyDuringMigration because: Type '{ text: string; type: string; }
// | null' is not assignable to type 'string | number'.
element = this.stringToFieldJson_(element) as AnyDuringMigration;
if (!element) {
continue;
}
}
}
elements.push(element);
@@ -1895,8 +1945,8 @@ export class Block implements IASTNodeLocation, IDeletable {
)
) {
const dummyInput = {'type': 'input_dummy'};
if (lastDummyAlign) {
(dummyInput as AnyDuringMigration)['align'] = lastDummyAlign;
if (implicitAlign) {
(dummyInput as AnyDuringMigration)['align'] = implicitAlign;
}
elements.push(dummyInput);
}
@@ -1960,6 +2010,9 @@ export class Block implements IASTNodeLocation, IDeletable {
case 'input_dummy':
input = this.appendDummyInput(element['name']);
break;
case 'input_end_row':
input = this.appendEndRowInput(element['name']);
break;
default: {
input = this.appendInputFromRegistry(element['type'], element['name']);
break;
@@ -1998,6 +2051,7 @@ export class Block implements IASTNodeLocation, IDeletable {
str === 'input_value' ||
str === 'input_statement' ||
str === 'input_dummy' ||
str === 'input_end_row' ||
registry.hasItem(registry.Type.INPUT, str)
);
}

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.blockAnimations');
// Former goog.module ID: Blockly.blockAnimations
import type {BlockSvg} from './block_svg.js';
import * as dom from './utils/dom.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.BlockDragger');
// Former goog.module ID: Blockly.BlockDragger
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_drag.js';
@@ -105,13 +104,10 @@ export class BlockDragger implements IBlockDragger {
}
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();
}
// The z-order of blocks depends on their order in the SVG, so move the
// block being dragged to the front so that it will appear atop other blocks
// in the workspace.
this.draggingBlock_.bringToFront(true);
// 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.

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.BlockSvg');
// Former goog.module ID: Blockly.BlockSvg
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_selected.js';
@@ -37,7 +36,7 @@ import {FieldLabel} from './field_label.js';
import type {Input} from './inputs/input.js';
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {CopyData, ICopyable} from './interfaces/i_copyable.js';
import type {ICopyable} from './interfaces/i_copyable.js';
import type {IDraggable} from './interfaces/i_draggable.js';
import {IIcon} from './interfaces/i_icon.js';
import * as internalConstants from './internal_constants.js';
@@ -62,6 +61,7 @@ import type {WorkspaceSvg} from './workspace_svg.js';
import * as renderManagement from './render_management.js';
import * as deprecation from './utils/deprecation.js';
import {IconType} from './icons/icon_types.js';
import {BlockCopyData, BlockPaster} from './clipboard/block_paster.js';
/**
* Class for a block's SVG representation.
@@ -69,7 +69,11 @@ import {IconType} from './icons/icon_types.js';
*/
export class BlockSvg
extends Block
implements IASTNodeLocationSvg, IBoundedElement, ICopyable, IDraggable
implements
IASTNodeLocationSvg,
IBoundedElement,
ICopyable<BlockCopyData>,
IDraggable
{
/**
* Constant for identifying rows that are to be rendered inline.
@@ -323,7 +327,14 @@ export class BlockSvg
} else if (oldParent) {
// If we are losing a parent, we want to move our DOM element to the
// root of the workspace.
this.workspace.getCanvas().appendChild(svgRoot);
const draggingBlock = this.workspace
.getCanvas()
.querySelector('.blocklyDragging');
if (draggingBlock) {
this.workspace.getCanvas().insertBefore(svgRoot, draggingBlock);
} else {
this.workspace.getCanvas().appendChild(svgRoot);
}
this.translate(oldXY.x, oldXY.y);
}
@@ -823,18 +834,17 @@ export class BlockSvg
* Encode a block for copying.
*
* @returns Copy metadata, or null if the block is an insertion marker.
* @internal
*/
toCopyData(): CopyData | null {
toCopyData(): BlockCopyData | null {
if (this.isInsertionMarker_) {
return null;
}
return {
saveInfo: blocks.save(this, {
paster: BlockPaster.TYPE,
blockState: blocks.save(this, {
addCoordinates: true,
addNextBlocks: false,
}) as blocks.State,
source: this.workspace,
typeCounts: common.getBlockTypeCounts(this, true),
};
}
@@ -896,11 +906,10 @@ export class BlockSvg
* Set this block's warning text.
*
* @param text The text, or null to delete.
* @param opt_id An optional ID for the warning text to be able to maintain
* @param id An optional ID for the warning text to be able to maintain
* multiple warnings.
*/
override setWarningText(text: string | null, opt_id?: string) {
const id = opt_id || '';
override setWarningText(text: string | null, id: string = '') {
if (!id) {
// Kill all previous pending processes, this edit supersedes them all.
for (const timeout of this.warningTextDb.values()) {
@@ -931,8 +940,9 @@ export class BlockSvg
}
const icon = this.getIcon(WarningIcon.TYPE) as WarningIcon | undefined;
if (typeof text === 'string') {
if (text) {
// Bubble up to add a warning on top-most collapsed block.
// TODO(#6020): This warning is never removed.
let parent = this.getSurroundParent();
let collapsedParent = null;
while (parent) {
@@ -958,6 +968,9 @@ export class BlockSvg
if (!id) {
this.removeIcon(WarningIcon.TYPE);
} else {
// Remove just this warning id's message.
icon.addMessage('', id);
// Then remove the entire icon if there is no longer any text.
if (!icon.getText()) this.removeIcon(WarningIcon.TYPE);
}
}
@@ -1139,9 +1152,11 @@ export class BlockSvg
* order that they are in the DOM. By placing this block first within the
* block group's <g>, it will render on top of any other blocks.
*
* @param blockOnly: True to only move this block to the front without
* adjusting its parents.
* @internal
*/
bringToFront() {
bringToFront(blockOnly = false) {
/* eslint-disable-next-line @typescript-eslint/no-this-alias */
let block: this | null = this;
do {
@@ -1152,6 +1167,7 @@ export class BlockSvg
if (childNodes[childNodes.length - 1] !== root) {
parent!.appendChild(root);
}
if (blockOnly) break;
block = block.getParent();
} while (block);
}
@@ -1499,13 +1515,14 @@ export class BlockSvg
* or an insertion marker.
*
* @param sourceConnection The connection on the moving block's stack.
* @param targetConnection The connection that should stay stationary as this
* block is positioned.
* @param originalOffsetToTarget The connection original offset to the target connection
* @param originalOffsetInBlock The connection original offset in its block
* @internal
*/
positionNearConnection(
sourceConnection: RenderedConnection,
targetConnection: RenderedConnection,
originalOffsetToTarget: {x: number; y: number},
originalOffsetInBlock: Coordinate,
) {
// We only need to position the new block if it's before the existing one,
// otherwise its position is set by the previous block.
@@ -1513,8 +1530,12 @@ export class BlockSvg
sourceConnection.type === ConnectionType.NEXT_STATEMENT ||
sourceConnection.type === ConnectionType.INPUT_VALUE
) {
const dx = targetConnection.x - sourceConnection.x;
const dy = targetConnection.y - sourceConnection.y;
// First move the block to match the orginal target connection position
let dx = originalOffsetToTarget.x;
let dy = originalOffsetToTarget.y;
// Then adjust its position according to the connection resize
dx += originalOffsetInBlock.x - sourceConnection.getOffsetInBlock().x;
dy += originalOffsetInBlock.y - sourceConnection.getOffsetInBlock().y;
this.moveBy(dx, dy);
}

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly');
// Former goog.module ID: Blockly
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_create.js';
@@ -140,7 +139,7 @@ import {ICollapsibleToolboxItem} from './interfaces/i_collapsible_toolbox_item.j
import {IComponent} from './interfaces/i_component.js';
import {IConnectionChecker} from './interfaces/i_connection_checker.js';
import {IContextMenu} from './interfaces/i_contextmenu.js';
import {ICopyable} from './interfaces/i_copyable.js';
import {ICopyable, isCopyable} from './interfaces/i_copyable.js';
import {IDeletable} from './interfaces/i_deletable.js';
import {IDeleteArea} from './interfaces/i_delete_area.js';
import {IDragTarget} from './interfaces/i_drag_target.js';
@@ -152,6 +151,7 @@ import {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
import {IMetricsManager} from './interfaces/i_metrics_manager.js';
import {IMovable} from './interfaces/i_movable.js';
import {IObservable, isObservable} from './interfaces/i_observable.js';
import {IPaster, isPaster} from './interfaces/i_paster.js';
import {IPositionable} from './interfaces/i_positionable.js';
import {IRegistrable} from './interfaces/i_registrable.js';
import {ISelectable} from './interfaces/i_selectable.js';
@@ -591,7 +591,7 @@ export {IComponent};
export {IConnectionChecker};
export {IContextMenu};
export {icons};
export {ICopyable};
export {ICopyable, isCopyable};
export {IDeletable};
export {IDeleteArea};
export {IDragTarget};
@@ -606,6 +606,7 @@ export {Input};
export {inputs};
export {InsertionMarkerManager};
export {IObservable, isObservable};
export {IPaster, isPaster};
export {IPositionable};
export {IRegistrable};
export {ISelectable};

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.BlocklyOptions');
// Former goog.module ID: Blockly.BlocklyOptions
import type {Theme, ITheme} from './theme.js';
import type {WorkspaceSvg} from './workspace_svg.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.blocks');
// Former goog.module ID: Blockly.blocks
/**
* A block definition. For now this very loose, but it can potentially

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.browserEvents');
// Former goog.module ID: Blockly.browserEvents
import * as Touch from './touch.js';
import * as userAgent from './utils/useragent.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.BubbleDragger');
// Former goog.module ID: Blockly.BubbleDragger
import {ComponentManager} from './component_manager.js';
import type {CommentMove} from './events/events_comment_move.js';

View File

@@ -65,6 +65,9 @@ export class MiniWorkspaceBubble extends Bubble {
);
workspaceOptions.parentWorkspace = this.workspace;
this.miniWorkspace = this.newWorkspaceSvg(new Options(workspaceOptions));
// TODO (#7422): Change this to `internalIsMiniWorkspace` or something. Not
// all mini workspaces are necessarily mutators.
this.miniWorkspace.internalIsMutator = true;
const background = this.miniWorkspace.createDom('blocklyMutatorBackground');
this.svgDialog.appendChild(background);
if (options.languageTree) {
@@ -218,12 +221,10 @@ export class MiniWorkspaceBubble extends Bubble {
* Calculates the size of the mini workspace for use in resizing the bubble.
*/
private calculateWorkspaceSize(): Size {
// TODO (#7104): Clean this up to be more readable and unified for RTL
// vs LTR.
const canvas = this.miniWorkspace.getCanvas();
const workspaceSize = canvas.getBBox();
let width = workspaceSize.width + workspaceSize.x;
const workspaceSize = this.miniWorkspace.getCanvas().getBBox();
let width = workspaceSize.width + MiniWorkspaceBubble.MARGIN;
let height = workspaceSize.height + MiniWorkspaceBubble.MARGIN;
const flyout = this.miniWorkspace.getFlyout();
if (flyout) {
const flyoutScrollMetrics = flyout
@@ -233,10 +234,6 @@ export class MiniWorkspaceBubble extends Bubble {
height = Math.max(height, flyoutScrollMetrics.height + 20);
width += flyout.getWidth();
}
if (this.miniWorkspace.RTL) {
width = -workspaceSize.x;
}
width += MiniWorkspaceBubble.MARGIN;
return new Size(width, height);
}

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.bumpObjects');
// Former goog.module ID: Blockly.bumpObjects
import type {BlockSvg} from './block_svg.js';
import type {Abstract} from './events/events_abstract.js';

View File

@@ -4,81 +4,151 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.clipboard');
// Former goog.module ID: Blockly.clipboard
import type {CopyData, ICopyable} from './interfaces/i_copyable.js';
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
import {BlockPaster} from './clipboard/block_paster.js';
import * as globalRegistry from './registry.js';
import {WorkspaceSvg} from './workspace_svg.js';
import * as registry from './clipboard/registry.js';
import {Coordinate} from './utils/coordinate.js';
import * as deprecation from './utils/deprecation.js';
/** Metadata about the object that is currently on the clipboard. */
let copyData: CopyData | null = null;
let stashedCopyData: ICopyData | null = null;
let stashedWorkspace: WorkspaceSvg | null = null;
/**
* Copy a block or workspace comment onto the local clipboard.
* Copy a copyable element onto the local clipboard.
*
* @param toCopy Block or Workspace Comment to be copied.
* @param toCopy The copyable element to be copied.
* @deprecated v11. Use `myCopyable.toCopyData()` instead. To be removed v12.
* @internal
*/
export function copy(toCopy: ICopyable) {
TEST_ONLY.copyInternal(toCopy);
export function copy<T extends ICopyData>(toCopy: ICopyable<T>): T | null {
deprecation.warn(
'Blockly.clipboard.copy',
'v11',
'v12',
'myCopyable.toCopyData()',
);
return TEST_ONLY.copyInternal(toCopy);
}
/**
* Private version of copy for stubbing in tests.
*/
function copyInternal(toCopy: ICopyable) {
copyData = toCopy.toCopyData();
function copyInternal<T extends ICopyData>(toCopy: ICopyable<T>): T | null {
const data = toCopy.toCopyData();
stashedCopyData = data;
stashedWorkspace = (toCopy as any).workspace ?? null;
return data;
}
/**
* Paste a block or workspace comment on to the main workspace.
* Paste a pasteable element into the workspace.
*
* @param copyData The data to paste into the workspace.
* @param workspace The workspace to paste the data into.
* @param coordinate The location to paste the thing at.
* @returns The pasted thing if the paste was successful, null otherwise.
* @internal
*/
export function paste(): ICopyable | null {
if (!copyData) {
return null;
export function paste<T extends ICopyData>(
copyData: T,
workspace: WorkspaceSvg,
coordinate?: Coordinate,
): ICopyable<T> | null;
/**
* Pastes the last copied ICopyable into the workspace.
*
* @returns the pasted thing if the paste was successful, null otherwise.
*/
export function paste(): ICopyable<ICopyData> | null;
/**
* Pastes the given data into the workspace, or the last copied ICopyable if
* no data is passed.
*
* @param copyData The data to paste into the workspace.
* @param workspace The workspace to paste the data into.
* @param coordinate The location to paste the thing at.
* @returns The pasted thing if the paste was successful, null otherwise.
*/
export function paste<T extends ICopyData>(
copyData?: T,
workspace?: WorkspaceSvg,
coordinate?: Coordinate,
): ICopyable<ICopyData> | null {
if (!copyData || !workspace) {
if (!stashedCopyData || !stashedWorkspace) return null;
return pasteFromData(stashedCopyData, stashedWorkspace);
}
// Pasting always pastes to the main workspace, even if the copy
// started in a flyout workspace.
let workspace = copyData.source;
if (workspace.isFlyout) {
workspace = workspace.targetWorkspace!;
}
if (
copyData.typeCounts &&
workspace.isCapacityAvailable(copyData.typeCounts)
) {
return workspace.paste(copyData.saveInfo);
}
return null;
return pasteFromData(copyData, workspace, coordinate);
}
/**
* Duplicate this block and its children, or a workspace comment.
* Paste a pasteable element into the workspace.
*
* @param toDuplicate Block or Workspace Comment to be duplicated.
* @returns The block or workspace comment that was duplicated, or null if the
* duplication failed.
* @param copyData The data to paste into the workspace.
* @param workspace The workspace to paste the data into.
* @param coordinate The location to paste the thing at.
* @returns The pasted thing if the paste was successful, null otherwise.
*/
function pasteFromData<T extends ICopyData>(
copyData: T,
workspace: WorkspaceSvg,
coordinate?: Coordinate,
): ICopyable<T> | null {
workspace = workspace.getRootWorkspace() ?? workspace;
return (globalRegistry
.getObject(globalRegistry.Type.PASTER, copyData.paster, false)
?.paste(copyData, workspace, coordinate) ?? null) as ICopyable<T> | null;
}
/**
* Duplicate this copy-paste-able element.
*
* @param toDuplicate The element to be duplicated.
* @returns The element that was duplicated, or null if the duplication failed.
* @deprecated v11. Use
* `Blockly.clipboard.paste(myCopyable.toCopyData(), myWorkspace)` instead.
* To be removed v12.
* @internal
*/
export function duplicate(toDuplicate: ICopyable): ICopyable | null {
export function duplicate<
U extends ICopyData,
T extends ICopyable<U> & IHasWorkspace,
>(toDuplicate: T): T | null {
deprecation.warn(
'Blockly.clipboard.duplicate',
'v11',
'v12',
'Blockly.clipboard.paste(myCopyable.toCopyData(), myWorkspace)',
);
return TEST_ONLY.duplicateInternal(toDuplicate);
}
/**
* Private version of duplicate for stubbing in tests.
*/
function duplicateInternal(toDuplicate: ICopyable): ICopyable | null {
const oldCopyData = copyData;
copy(toDuplicate);
const pastedThing =
toDuplicate.toCopyData()?.source?.paste(copyData!.saveInfo) ?? null;
copyData = oldCopyData;
return pastedThing;
function duplicateInternal<
U extends ICopyData,
T extends ICopyable<U> & IHasWorkspace,
>(toDuplicate: T): T | null {
const data = toDuplicate.toCopyData();
if (!data) return null;
return paste(data, toDuplicate.workspace) as T;
}
interface IHasWorkspace {
workspace: WorkspaceSvg;
}
export const TEST_ONLY = {
duplicateInternal,
copyInternal,
};
export {BlockPaster, registry};

View File

@@ -0,0 +1,120 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {BlockSvg} from '../block_svg.js';
import * as registry from './registry.js';
import {ICopyData} from '../interfaces/i_copyable.js';
import {IPaster} from '../interfaces/i_paster.js';
import {State, append} from '../serialization/blocks.js';
import {Coordinate} from '../utils/coordinate.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import * as eventUtils from '../events/utils.js';
import {config} from '../config.js';
export class BlockPaster implements IPaster<BlockCopyData, BlockSvg> {
static TYPE = 'block';
paste(
copyData: BlockCopyData,
workspace: WorkspaceSvg,
coordinate?: Coordinate,
): BlockSvg | null {
if (!workspace.isCapacityAvailable(copyData.typeCounts!)) return null;
if (coordinate) {
copyData.blockState['x'] = coordinate.x;
copyData.blockState['y'] = coordinate.y;
}
eventUtils.disable();
let block;
try {
block = append(copyData.blockState, workspace) as BlockSvg;
moveBlockToNotConflict(block);
} finally {
eventUtils.enable();
}
if (!block) return block;
if (eventUtils.isEnabled() && !block.isShadow()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
}
block.select();
return block;
}
}
/**
* Moves the given block to a location where it does not: (1) overlap exactly
* with any other blocks, or (2) look like it is connected to any other blocks.
*
* Exported for testing.
*
* @param block The block to move to an unambiguous location.
* @internal
*/
export function moveBlockToNotConflict(block: BlockSvg) {
const workspace = block.workspace;
const snapRadius = config.snapRadius;
const coord = block.getRelativeToSurfaceXY();
const offset = new Coordinate(0, 0);
// getRelativeToSurfaceXY is really expensive, so we want to cache this.
const otherCoords = workspace
.getAllBlocks(false)
.filter((otherBlock) => otherBlock.id != block.id)
.map((b) => b.getRelativeToSurfaceXY());
while (
blockOverlapsOtherExactly(Coordinate.sum(coord, offset), otherCoords) ||
blockIsInSnapRadius(block, offset, snapRadius)
) {
if (workspace.RTL) {
offset.translate(-snapRadius, snapRadius * 2);
} else {
offset.translate(snapRadius, snapRadius * 2);
}
}
block!.moveTo(Coordinate.sum(coord, offset));
}
/**
* @returns true if the given block coordinates are less than a delta of 1 from
* any of the other coordinates.
*/
function blockOverlapsOtherExactly(
coord: Coordinate,
otherCoords: Coordinate[],
): boolean {
return otherCoords.some(
(otherCoord) =>
Math.abs(otherCoord.x - coord.x) <= 1 &&
Math.abs(otherCoord.y - coord.y) <= 1,
);
}
/**
* @returns true if the given block (when offset by the given amount) is close
* enough to any other connections (within the snap radius) that it looks
* like they could connect.
*/
function blockIsInSnapRadius(
block: BlockSvg,
offset: Coordinate,
snapRadius: number,
): boolean {
return block
.getConnections_(false)
.some((connection) => !!connection.closest(snapRadius, offset).connection);
}
export interface BlockCopyData extends ICopyData {
blockState: State;
typeCounts: {[key: string]: number};
}
registry.register(BlockPaster.TYPE, new BlockPaster());

View File

@@ -0,0 +1,31 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {ICopyable, ICopyData} from '../interfaces/i_copyable.js';
import type {IPaster} from '../interfaces/i_paster.js';
import * as registry from '../registry.js';
/**
* Registers the given paster so that it cna be used for pasting.
*
* @param type The type of the paster to register, e.g. 'block', 'comment', etc.
* @param paster The paster to register.
*/
export function register<U extends ICopyData, T extends ICopyable<U>>(
type: string,
paster: IPaster<U, T>,
) {
registry.register(registry.Type.PASTER, type, paster);
}
/**
* Unregisters the paster associated with the given type.
*
* @param type The type of the paster to unregister.
*/
export function unregister(type: string) {
registry.unregister(registry.Type.PASTER, type);
}

View File

@@ -0,0 +1,45 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {IPaster} from '../interfaces/i_paster.js';
import {ICopyData} from '../interfaces/i_copyable.js';
import {Coordinate} from '../utils/coordinate.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import {WorkspaceCommentSvg} from '../workspace_comment_svg.js';
import * as registry from './registry.js';
export class WorkspaceCommentPaster
implements IPaster<WorkspaceCommentCopyData, WorkspaceCommentSvg>
{
static TYPE = 'workspace-comment';
paste(
copyData: WorkspaceCommentCopyData,
workspace: WorkspaceSvg,
coordinate?: Coordinate,
): WorkspaceCommentSvg {
const state = copyData.commentState;
if (coordinate) {
state.setAttribute('x', `${coordinate.x}`);
state.setAttribute('y', `${coordinate.y}`);
} else {
const x = parseInt(state.getAttribute('x') ?? '0') + 50;
const y = parseInt(state.getAttribute('y') ?? '0') + 50;
state.setAttribute('x', `${x}`);
state.setAttribute('y', `${y}`);
}
return WorkspaceCommentSvg.fromXmlRendered(
copyData.commentState,
workspace,
);
}
}
export interface WorkspaceCommentCopyData extends ICopyData {
commentState: Element;
}
registry.register(WorkspaceCommentPaster.TYPE, new WorkspaceCommentPaster());

View File

@@ -4,14 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.common');
// Former goog.module ID: Blockly.common
/* eslint-disable-next-line no-unused-vars */
import type {Block} from './block.js';
import {ISelectable} from './blockly.js';
import {BlockDefinition, Blocks} from './blocks.js';
import type {Connection} from './connection.js';
import type {ICopyable} from './interfaces/i_copyable.js';
import type {Workspace} from './workspace.js';
import type {WorkspaceSvg} from './workspace_svg.js';
@@ -88,12 +87,12 @@ export function setMainWorkspace(workspace: Workspace) {
/**
* Currently selected copyable object.
*/
let selected: ICopyable | null = null;
let selected: ISelectable | null = null;
/**
* Returns the currently selected copyable object.
*/
export function getSelected(): ICopyable | null {
export function getSelected(): ISelectable | null {
return selected;
}
@@ -105,7 +104,7 @@ export function getSelected(): ICopyable | null {
* @param newSelection The newly selected block.
* @internal
*/
export function setSelected(newSelection: ICopyable | null) {
export function setSelected(newSelection: ISelectable | null) {
selected = newSelection;
}

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ComponentManager');
// Former goog.module ID: Blockly.ComponentManager
import type {IAutoHideable} from './interfaces/i_autohideable.js';
import type {IComponent} from './interfaces/i_component.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.config');
// Former goog.module ID: Blockly.config
/**
* All the values that we expect developers to be able to change

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Connection');
// Former goog.module ID: Blockly.Connection
import type {Block} from './block.js';
import {ConnectionType} from './connection_type.js';

View File

@@ -10,8 +10,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ConnectionChecker');
// Former goog.module ID: Blockly.ConnectionChecker
import * as common from './common.js';
import {Connection} from './connection.js';

View File

@@ -11,8 +11,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ConnectionDB');
// Former goog.module ID: Blockly.ConnectionDB
import {ConnectionType} from './connection_type.js';
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ConnectionType');
// Former goog.module ID: Blockly.ConnectionType
/**
* Enum for the type of a connection or input.

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.constants');
// Former goog.module ID: Blockly.constants
/**
* The language-neutral ID given to the collapsed input.

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ContextMenu');
// Former goog.module ID: Blockly.ContextMenu
import type {Block} from './block.js';
import type {BlockSvg} from './block_svg.js';
@@ -24,6 +23,7 @@ import {Msg} from './msg.js';
import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js';
import {Rect} from './utils/rect.js';
import * as serializationBlocks from './serialization/blocks.js';
import * as svgMath from './utils/svg_math.js';
import * as WidgetDiv from './widgetdiv.js';
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
@@ -224,18 +224,28 @@ export function dispose() {
/**
* Create a callback function that creates and configures a block,
* then places the new block next to the original.
* then places the new block next to the original and returns it.
*
* @param block Original block.
* @param xml XML representation of new block.
* @param state XML or JSON object representation of the new block.
* @returns Function that creates a block.
*/
export function callbackFactory(block: Block, xml: Element): () => void {
export function callbackFactory(
block: Block,
state: Element | serializationBlocks.State,
): () => BlockSvg {
return () => {
eventUtils.disable();
let newBlock;
let newBlock: BlockSvg;
try {
newBlock = Xml.domToBlockInternal(xml, block.workspace!) as BlockSvg;
if (state instanceof Element) {
newBlock = Xml.domToBlockInternal(state, block.workspace!) as BlockSvg;
} else {
newBlock = serializationBlocks.appendInternal(
state,
block.workspace,
) as BlockSvg;
}
// Move the new block next to the old block.
const xy = block.getRelativeToSurfaceXY();
if (block.RTL) {
@@ -252,6 +262,7 @@ export function callbackFactory(block: Block, xml: Element): () => void {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(newBlock));
}
newBlock.select();
return newBlock;
};
}
@@ -297,7 +308,9 @@ export function commentDuplicateOption(
text: Msg['DUPLICATE_COMMENT'],
enabled: true,
callback: function () {
clipboard.duplicate(comment);
const data = comment.toCopyData();
if (!data) return;
clipboard.paste(data, comment.workspace);
},
};
return duplicateOption;

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ContextMenuItems');
// Former goog.module ID: Blockly.ContextMenuItems
import type {BlockSvg} from './block_svg.js';
import * as clipboard from './clipboard.js';
@@ -331,9 +330,10 @@ export function registerDuplicate() {
return 'hidden';
},
callback(scope: Scope) {
if (scope.block) {
clipboard.duplicate(scope.block);
}
if (!scope.block) return;
const data = scope.block.toCopyData();
if (!data) return;
clipboard.paste(data, scope.block.workspace);
},
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
id: 'blockDuplicate',

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ContextMenuRegistry');
// Former goog.module ID: Blockly.ContextMenuRegistry
import type {BlockSvg} from './block_svg.js';
import type {WorkspaceSvg} from './workspace_svg.js';
@@ -137,7 +136,7 @@ export namespace ContextMenuRegistry {
export interface RegistryItem {
callback: (p1: Scope) => void;
scopeType: ScopeType;
displayText: ((p1: Scope) => string) | string;
displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement;
preconditionFn: (p1: Scope) => string;
weight: number;
id: string;
@@ -147,7 +146,7 @@ export namespace ContextMenuRegistry {
* A menu item as presented to contextmenu.js.
*/
export interface ContextMenuOption {
text: string;
text: string | HTMLElement;
enabled: boolean;
callback: (p1: Scope) => void;
scope: Scope;

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Css');
// Former goog.module ID: Blockly.Css
/** Has CSS already been injected? */
let injected = false;

View File

@@ -10,8 +10,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.DeleteArea');
// Former goog.module ID: Blockly.DeleteArea
import {BlockSvg} from './block_svg.js';
import {DragTarget} from './drag_target.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.dialog');
// Former goog.module ID: Blockly.dialog
let alertImplementation = function (
message: string,

View File

@@ -10,8 +10,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.DragTarget');
// Former goog.module ID: Blockly.DragTarget
import type {IDragTarget} from './interfaces/i_drag_target.js';
import type {IDraggable} from './interfaces/i_draggable.js';

View File

@@ -10,8 +10,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.dropDownDiv');
// Former goog.module ID: Blockly.dropDownDiv
import type {BlockSvg} from './block_svg.js';
import * as common from './common.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events');
// Former goog.module ID: Blockly.Events
import {Abstract, AbstractEventJson} from './events_abstract.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';

View File

@@ -10,8 +10,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.Abstract');
// Former goog.module ID: Blockly.Events.Abstract
import * as common from '../common.js';
import type {Workspace} from '../workspace.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockBase');
// Former goog.module ID: Blockly.Events.BlockBase
import type {Block} from '../block.js';
import type {Workspace} from '../workspace.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockChange');
// Former goog.module ID: Blockly.Events.BlockChange
import type {Block} from '../block.js';
import type {BlockSvg} from '../block_svg.js';
@@ -205,7 +204,7 @@ export class BlockChange extends BlockBase {
*/
static getExtraBlockState_(block: BlockSvg): string {
if (block.saveExtraState) {
const state = block.saveExtraState();
const state = block.saveExtraState(true);
return state ? JSON.stringify(state) : '';
} else if (block.mutationToDom) {
const state = block.mutationToDom();

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockCreate');
// Former goog.module ID: Blockly.Events.BlockCreate
import type {Block} from '../block.js';
import * as registry from '../registry.js';
@@ -138,6 +137,7 @@ export class BlockCreate extends BlockBase {
'the constructor, or call fromJson',
);
}
if (allShadowBlocks(workspace, this.ids)) return;
if (forward) {
blocks.append(this.json, workspace);
} else {
@@ -154,6 +154,26 @@ export class BlockCreate extends BlockBase {
}
}
}
/**
* Returns true if all blocks in the list are shadow blocks. If so, that means
* the top-level block being created is a shadow block. This only happens when a
* block that was covering up a shadow block is removed. We don't need to create
* an additional block in that case because the original block still has its
* shadow block.
*
* @param workspace Workspace to check for blocks
* @param ids A list of block ids that were created in this event
* @returns True if all block ids in the list are shadow blocks
*/
const allShadowBlocks = function (
workspace: Workspace,
ids: string[],
): boolean {
const shadows = ids
.map((id) => workspace.getBlockById(id))
.filter((block) => block && block.isShadow());
return shadows.length === ids.length;
};
export interface BlockCreateJson extends BlockBaseJson {
xml: string;

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockDelete');
// Former goog.module ID: Blockly.Events.BlockDelete
import type {Block} from '../block.js';
import * as registry from '../registry.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockDrag');
// Former goog.module ID: Blockly.Events.BlockDrag
import type {Block} from '../block.js';
import * as registry from '../registry.js';

View File

@@ -10,8 +10,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockFieldIntermediateChange');
// Former goog.module ID: Blockly.Events.BlockFieldIntermediateChange
import type {Block} from '../block.js';
import * as registry from '../registry.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockMove');
// Former goog.module ID: Blockly.Events.BlockMove
import type {Block} from '../block.js';
import {ConnectionType} from '../connection_type.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BubbleOpen');
// Former goog.module ID: Blockly.Events.BubbleOpen
import type {AbstractEventJson} from './events_abstract.js';
import type {BlockSvg} from '../block_svg.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.Click');
// Former goog.module ID: Blockly.Events.Click
import type {Block} from '../block.js';
import * as registry from '../registry.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentBase');
// Former goog.module ID: Blockly.Events.CommentBase
import * as utilsXml from '../utils/xml.js';
import type {WorkspaceComment} from '../workspace_comment.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentChange');
// Former goog.module ID: Blockly.Events.CommentChange
import * as registry from '../registry.js';
import type {WorkspaceComment} from '../workspace_comment.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentCreate');
// Former goog.module ID: Blockly.Events.CommentCreate
import * as registry from '../registry.js';
import type {WorkspaceComment} from '../workspace_comment.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentDelete');
// Former goog.module ID: Blockly.Events.CommentDelete
import * as registry from '../registry.js';
import type {WorkspaceComment} from '../workspace_comment.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentMove');
// Former goog.module ID: Blockly.Events.CommentMove
import * as registry from '../registry.js';
import {Coordinate} from '../utils/coordinate.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.MarkerMove');
// Former goog.module ID: Blockly.Events.MarkerMove
import type {Block} from '../block.js';
import {ASTNode} from '../keyboard_nav/ast_node.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.Selected');
// Former goog.module ID: Blockly.Events.Selected
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ThemeChange');
// Former goog.module ID: Blockly.Events.ThemeChange
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ToolboxItemSelect');
// Former goog.module ID: Blockly.Events.ToolboxItemSelect
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.TrashcanOpen');
// Former goog.module ID: Blockly.Events.TrashcanOpen
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';

View File

@@ -10,8 +10,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.UiBase');
// Former goog.module ID: Blockly.Events.UiBase
import {Abstract as AbstractEvent} from './events_abstract.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarBase');
// Former goog.module ID: Blockly.Events.VarBase
import type {VariableModel} from '../variable_model.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarCreate');
// Former goog.module ID: Blockly.Events.VarCreate
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarDelete');
// Former goog.module ID: Blockly.Events.VarDelete
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarRename');
// Former goog.module ID: Blockly.Events.VarRename
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ViewportChange');
// Former goog.module ID: Blockly.Events.ViewportChange
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.utils');
// Former goog.module ID: Blockly.Events.utils
import type {Block} from '../block.js';
import * as common from '../common.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.FinishedLoading');
// Former goog.module ID: Blockly.Events.FinishedLoading
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Extensions');
// Former goog.module ID: Blockly.Extensions
import type {Block} from './block.js';
import type {BlockSvg} from './block_svg.js';
@@ -389,16 +388,6 @@ export function runAfterPageLoad(fn: () => void) {
* Builds an extension function that will map a dropdown value to a tooltip
* string.
*
* This method includes multiple checks to ensure tooltips, dropdown options,
* and message references are aligned. This aims to catch errors as early as
* possible, without requiring developers to manually test tooltips under each
* option. After the page is loaded, each tooltip text string will be checked
* for matching message keys in the internationalized string table. Deferring
* this until the page is loaded decouples loading dependencies. Later, upon
* loading the first block of any given type, the extension will validate every
* dropdown option has a matching tooltip in the lookupTable. Errors are
* reported as warnings in the console, and are never fatal.
*
* @param dropdownName The name of the field whose value is the key to the
* lookup table.
* @param lookupTable The table of field values to tooltip text.
@@ -407,27 +396,12 @@ export function runAfterPageLoad(fn: () => void) {
export function buildTooltipForDropdown(
dropdownName: string,
lookupTable: {[key: string]: string},
): Function {
): (this: Block) => void {
// List of block types already validated, to minimize duplicate warnings.
const blockTypesChecked: AnyDuringMigration[] = [];
const blockTypesChecked: string[] = [];
// Check the tooltip string messages for invalid references.
// Wait for load, in case Blockly.Msg is not yet populated.
// runAfterPageLoad() does not run in a Node.js environment due to lack
// of document object, in which case skip the validation.
if (typeof document === 'object') {
// Relies on document.readyState
runAfterPageLoad(function () {
for (const key in lookupTable) {
// Will print warnings if reference is missing.
parsing.checkMessageReferences(lookupTable[key]);
}
});
}
/** The actual extension. */
function extensionFn(this: Block) {
if (this.type && blockTypesChecked.indexOf(this.type) === -1) {
return function (this: Block) {
if (blockTypesChecked.indexOf(this.type) === -1) {
checkDropdownOptionsInTable(this, dropdownName, lookupTable);
blockTypesChecked.push(this.type);
}
@@ -435,28 +409,10 @@ export function buildTooltipForDropdown(
this.setTooltip(
function (this: Block) {
const value = String(this.getFieldValue(dropdownName));
let tooltip = lookupTable[value];
if (tooltip === null) {
if (blockTypesChecked.indexOf(this.type) === -1) {
// Warn for missing values on generated tooltips.
let warning =
'No tooltip mapping for value ' +
value +
' of field ' +
dropdownName;
if (this.type !== null) {
warning += ' of block type ' + this.type;
}
console.warn(warning + '.');
}
} else {
tooltip = parsing.replaceMessageReferences(tooltip);
}
return tooltip;
return parsing.replaceMessageReferences(lookupTable[value]);
}.bind(this),
);
}
return extensionFn;
};
}
/**
@@ -472,22 +428,18 @@ function checkDropdownOptionsInTable(
dropdownName: string,
lookupTable: {[key: string]: string},
) {
// Validate all dropdown options have values.
const dropdown = block.getField(dropdownName);
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
if (lookupTable[optionKey] === null) {
console.warn(
'No tooltip mapping for value ' +
optionKey +
' of field ' +
dropdownName +
' of block type ' +
block.type,
);
}
if (!(dropdown instanceof FieldDropdown) || dropdown.isOptionListDynamic()) {
return;
}
const options = dropdown.getOptions();
for (const [, key] of options) {
if (lookupTable[key] === undefined) {
console.warn(
`No tooltip mapping for value ${key} of field ` +
`${dropdownName} of block type ${block.type}.`,
);
}
}
}
@@ -505,21 +457,8 @@ function checkDropdownOptionsInTable(
export function buildTooltipWithFieldText(
msgTemplate: string,
fieldName: string,
): Function {
// Check the tooltip string messages for invalid references.
// Wait for load, in case Blockly.Msg is not yet populated.
// runAfterPageLoad() does not run in a Node.js environment due to lack
// of document object, in which case skip the validation.
if (typeof document === 'object') {
// Relies on document.readyState
runAfterPageLoad(function () {
// Will print warnings if reference is missing.
parsing.checkMessageReferences(msgTemplate);
});
}
/** The actual extension. */
function extensionFn(this: Block) {
): (this: Block) => void {
return function (this: Block) {
this.setTooltip(
function (this: Block) {
const field = this.getField(fieldName);
@@ -528,8 +467,7 @@ export function buildTooltipWithFieldText(
.replace('%1', field ? field.getText() : '');
}.bind(this),
);
}
return extensionFn;
};
}
/**

View File

@@ -11,8 +11,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Field');
// Former goog.module ID: Blockly.Field
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';
@@ -79,9 +78,7 @@ export abstract class Field<T = any>
* the prototype.
*
* Example:
* ```typescript
* FieldImage.prototype.DEFAULT_VALUE = null;
* ```
* `FieldImage.prototype.DEFAULT_VALUE = null;`
*/
DEFAULT_VALUE: T | null = null;
@@ -333,6 +330,18 @@ export abstract class Field<T = any>
*/
initModel() {}
/**
* Defines whether this field should take up the full block or not.
*
* Be cautious when overriding this function. It may not work as you expect /
* intend because the behavior was kind of hacked in. If you are thinking
* about overriding this function, post on the forum with your intended
* behavior to see if there's another approach.
*/
protected isFullBlockField(): boolean {
return !this.borderRect_;
}
/**
* Create a field border rect element. Not to be overridden by subclasses.
* Instead modify the result of the function inside initView, or create a
@@ -423,6 +432,9 @@ export abstract class Field<T = any>
* @param _doFullSerialization If true, this signals to the field that if it
* normally just saves a reference to some state (eg variable fields) it
* should instead serialize the full state of the thing being referenced.
* See the
* {@link https://developers.devsite.google.com/blockly/guides/create-custom-blocks/fields/customizing-fields/creating#full_serialization_and_backing_data | field serialization docs}
* for more information.
* @returns JSON serializable state.
* @internal
*/
@@ -797,7 +809,7 @@ export abstract class Field<T = any>
const xOffset =
margin !== undefined
? margin
: this.borderRect_
: !this.isFullBlockField()
? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING
: 0;
let totalWidth = xOffset * 2;
@@ -813,7 +825,7 @@ export abstract class Field<T = any>
);
totalWidth += contentWidth;
}
if (this.borderRect_) {
if (!this.isFullBlockField()) {
totalHeight = Math.max(totalHeight, constants!.FIELD_BORDER_RECT_HEIGHT);
}
@@ -922,7 +934,7 @@ export abstract class Field<T = any>
throw new UnattachedFieldError();
}
if (!this.borderRect_) {
if (this.isFullBlockField()) {
// Browsers are inconsistent in what they return for a bounding box.
// - Webkit / Blink: fill-box / object bounding box
// - Gecko: stroke-box
@@ -940,8 +952,8 @@ export abstract class Field<T = any>
xy.y -= 0.5 * scale;
}
} else {
const bBox = this.borderRect_.getBoundingClientRect();
xy = style.getPageOffset(this.borderRect_);
const bBox = this.borderRect_!.getBoundingClientRect();
xy = style.getPageOffset(this.borderRect_!);
scaledWidth = bBox.width;
scaledHeight = bBox.height;
}

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldAngle');
// Former goog.module ID: Blockly.FieldAngle
import {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
@@ -220,8 +219,8 @@ export class FieldAngle extends FieldInput<number> {
'version': '1.1',
'height': FieldAngle.HALF * 2 + 'px',
'width': FieldAngle.HALF * 2 + 'px',
'style': 'touch-action: none',
});
svg.style.touchAction = 'none';
const circle = dom.createSvgElement(
Svg.CIRCLE,
{

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldCheckbox');
// Former goog.module ID: Blockly.FieldCheckbox
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldColour');
// Former goog.module ID: Blockly.FieldColour
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';
@@ -20,7 +19,12 @@ import * as browserEvents from './browser_events.js';
import * as Css from './css.js';
import * as dom from './utils/dom.js';
import * as dropDownDiv from './dropdowndiv.js';
import {Field, FieldConfig, FieldValidator} from './field.js';
import {
Field,
FieldConfig,
FieldValidator,
UnattachedFieldError,
} from './field.js';
import * as fieldRegistry from './field_registry.js';
import * as aria from './utils/aria.js';
import * as colour from './utils/colour.js';
@@ -169,29 +173,118 @@ export class FieldColour extends Field<string> {
this.getConstants()!.FIELD_COLOUR_DEFAULT_WIDTH,
this.getConstants()!.FIELD_COLOUR_DEFAULT_HEIGHT,
);
if (!this.getConstants()!.FIELD_COLOUR_FULL_BLOCK) {
this.createBorderRect_();
this.getBorderRect().style['fillOpacity'] = '1';
} else if (this.sourceBlock_ instanceof BlockSvg) {
this.clickTarget_ = this.sourceBlock_.getSvgRoot();
this.createBorderRect_();
this.getBorderRect().style['fillOpacity'] = '1';
this.getBorderRect().setAttribute('stroke', '#fff');
if (this.isFullBlockField()) {
this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();
}
}
protected override isFullBlockField(): boolean {
const block = this.getSourceBlock();
if (!block) throw new UnattachedFieldError();
const constants = this.getConstants();
return block.isSimpleReporter() && !!constants?.FIELD_COLOUR_FULL_BLOCK;
}
/**
* Updates text field to match the colour/style of the block.
*/
override applyColour() {
if (!this.getConstants()!.FIELD_COLOUR_FULL_BLOCK) {
if (this.borderRect_) {
this.borderRect_.style.fill = this.getValue() as string;
}
} else if (this.sourceBlock_ instanceof BlockSvg) {
this.sourceBlock_.pathObject.svgPath.setAttribute(
'fill',
this.getValue() as string,
);
this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff');
const block = this.getSourceBlock() as BlockSvg | null;
if (!block) throw new UnattachedFieldError();
if (!this.fieldGroup_) return;
const borderRect = this.borderRect_;
if (!borderRect) {
throw new Error('The border rect has not been initialized');
}
if (!this.isFullBlockField()) {
borderRect.style.display = 'block';
borderRect.style.fill = this.getValue() as string;
} else {
borderRect.style.display = 'none';
// In general, do *not* let fields control the color of blocks. Having the
// field control the color is unexpected, and could have performance
// impacts.
block.pathObject.svgPath.setAttribute('fill', this.getValue() as string);
block.pathObject.svgPath.setAttribute('stroke', '#fff');
}
}
/**
* Returns the height and width of the field.
*
* This should *in general* be the only place render_ gets called from.
*
* @returns Height and width.
*/
override getSize(): Size {
if (this.getConstants()?.FIELD_COLOUR_FULL_BLOCK) {
// In general, do *not* let fields control the color of blocks. Having the
// field control the color is unexpected, and could have performance
// impacts.
// Full block fields have more control of the block than they should
// (i.e. updating fill colour). Whenever we get the size, the field may
// no longer be a full-block field, so we need to rerender.
this.render_();
this.isDirty_ = false;
}
return super.getSize();
}
/**
* Updates the colour of the block to reflect whether this is a full
* block field or not.
*/
protected override render_() {
super.render_();
const block = this.getSourceBlock() as BlockSvg | null;
if (!block) throw new UnattachedFieldError();
// In general, do *not* let fields control the color of blocks. Having the
// field control the color is unexpected, and could have performance
// impacts.
// Whenever we render, the field may no longer be a full-block-field so
// we need to update the colour.
if (this.getConstants()!.FIELD_COLOUR_FULL_BLOCK) block.applyColour();
}
/**
* Updates the size of the field based on whether it is a full block field
* or not.
*
* @param margin margin to use when positioning the field.
*/
protected updateSize_(margin?: number) {
const constants = this.getConstants();
const xOffset =
margin !== undefined
? margin
: !this.isFullBlockField()
? constants!.FIELD_BORDER_RECT_X_PADDING
: 0;
let totalWidth = xOffset * 2;
let contentWidth = 0;
if (!this.isFullBlockField()) {
contentWidth = constants!.FIELD_COLOUR_DEFAULT_WIDTH;
totalWidth += contentWidth;
}
let totalHeight = constants!.FIELD_TEXT_HEIGHT;
if (!this.isFullBlockField()) {
totalHeight = Math.max(totalHeight, constants!.FIELD_BORDER_RECT_HEIGHT);
}
this.size_.height = totalHeight;
this.size_.width = totalWidth;
this.positionTextElement_(xOffset, contentWidth);
this.positionBorderRect_();
}
/**
@@ -207,26 +300,6 @@ export class FieldColour extends Field<string> {
return colour.parse(newValue);
}
/**
* Update the value of this colour field, and update the displayed colour.
*
* @param newValue The value to be saved. The default validator guarantees
* that this is a colour in '#rrggbb' format.
*/
protected override doValueUpdate_(newValue: string) {
this.value_ = newValue;
if (this.borderRect_) {
this.borderRect_.style.fill = newValue;
} else if (
this.sourceBlock_ &&
this.sourceBlock_.rendered &&
this.sourceBlock_ instanceof BlockSvg
) {
this.sourceBlock_.pathObject.svgPath.setAttribute('fill', newValue);
this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff');
}
}
/**
* Get the text for this field. Used when the block is collapsed.
*

View File

@@ -11,8 +11,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldDropdown');
// Former goog.module ID: Blockly.FieldDropdown
import type {BlockSvg} from './block_svg.js';
import * as dropDownDiv from './dropdowndiv.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldImage');
// Former goog.module ID: Blockly.FieldImage
import {Field, FieldConfig} from './field.js';
import * as fieldRegistry from './field_registry.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldInput');
// Former goog.module ID: Blockly.FieldInput
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';
@@ -35,6 +34,7 @@ import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import * as renderManagement from './render_management.js';
import {Size} from './utils/size.js';
/**
* Supported types for FieldInput subclasses.
@@ -89,7 +89,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
* Whether the field should consider the whole parent block to be its click
* target.
*/
fullBlockClickTarget_: boolean | null = false;
fullBlockClickTarget_: boolean = false;
/** The workspace that this field belongs to. */
protected workspace_: WorkspaceSvg | null = null;
@@ -143,37 +143,22 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
override initView() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
if (this.getConstants()!.FULL_BLOCK_FIELDS) {
// Step one: figure out if this is the only field on this block.
// Rendering is quite different in that case.
let nFields = 0;
let nConnections = 0;
// Count the number of fields, excluding text fields
for (let i = 0, input; (input = block.inputList[i]); i++) {
for (let j = 0; input.fieldRow[j]; j++) {
nFields++;
}
if (input.connection) {
nConnections++;
}
}
// The special case is when this is the only non-label field on the block
// and it has an output but no inputs.
this.fullBlockClickTarget_ =
nFields <= 1 && block.outputConnection && !nConnections;
} else {
this.fullBlockClickTarget_ = false;
}
if (!block) throw new UnattachedFieldError();
super.initView();
if (this.fullBlockClickTarget_) {
if (this.isFullBlockField()) {
this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();
} else {
this.createBorderRect_();
}
this.createTextElement_();
}
protected override isFullBlockField(): boolean {
const block = this.getSourceBlock();
if (!block) throw new UnattachedFieldError();
// Side effect for backwards compatibility.
this.fullBlockClickTarget_ =
!!this.getConstants()?.FULL_BLOCK_FIELDS && block.isSimpleReporter();
return this.fullBlockClickTarget_;
}
/**
@@ -224,23 +209,54 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
* Updates text field to match the colour/style of the block.
*/
override applyColour() {
if (!this.sourceBlock_ || !this.getConstants()!.FULL_BLOCK_FIELDS) return;
const block = this.getSourceBlock() as BlockSvg | null;
if (!block) throw new UnattachedFieldError();
const source = this.sourceBlock_ as BlockSvg;
if (!this.getConstants()!.FULL_BLOCK_FIELDS) return;
if (!this.fieldGroup_) return;
if (this.borderRect_) {
this.borderRect_.setAttribute('stroke', source.style.colourTertiary);
if (!this.isFullBlockField() && this.borderRect_) {
this.borderRect_!.style.display = 'block';
this.borderRect_.setAttribute('stroke', block.style.colourTertiary);
} else {
source.pathObject.svgPath.setAttribute(
this.borderRect_!.style.display = 'none';
// In general, do *not* let fields control the color of blocks. Having the
// field control the color is unexpected, and could have performance
// impacts.
block.pathObject.svgPath.setAttribute(
'fill',
this.getConstants()!.FIELD_BORDER_RECT_COLOUR,
);
}
}
/**
* Returns the height and width of the field.
*
* This should *in general* be the only place render_ gets called from.
*
* @returns Height and width.
*/
override getSize(): Size {
if (this.getConstants()?.FULL_BLOCK_FIELDS) {
// In general, do *not* let fields control the color of blocks. Having the
// field control the color is unexpected, and could have performance
// impacts.
// Full block fields have more control of the block than they should
// (i.e. updating fill colour). Whenever we get the size, the field may
// no longer be a full-block field, so we need to rerender.
this.render_();
this.isDirty_ = false;
}
return super.getSize();
}
/**
* Updates the colour of the htmlInput given the current validity of the
* field's value.
*
* Also updates the colour of the block to reflect whether this is a full
* block field or not.
*/
protected override render_() {
super.render_();
@@ -257,6 +273,15 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
aria.setState(htmlInput, aria.State.INVALID, false);
}
}
const block = this.getSourceBlock() as BlockSvg | null;
if (!block) throw new UnattachedFieldError();
// In general, do *not* let fields control the color of blocks. Having the
// field control the color is unexpected, and could have performance
// impacts.
// Whenever we render, the field may no longer be a full-block-field so
// we need to update the colour.
if (this.getConstants()!.FULL_BLOCK_FIELDS) block.applyColour();
}
/**
@@ -317,6 +342,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
if (text !== null) {
this.setValue(this.getValueFromEditorText_(text));
}
this.onFinishEditing_(this.value_);
},
);
}
@@ -375,7 +401,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
htmlInput.style.fontSize = fontSize;
let borderRadius = FieldInput.BORDERRADIUS * scale + 'px';
if (this.fullBlockClickTarget_) {
if (this.isFullBlockField()) {
const bBox = this.getScaledBBox();
// Override border radius.
@@ -463,8 +489,6 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
* @param _value The new value of the field.
*/
onFinishEditing_(_value: AnyDuringMigration) {}
// NOP by default.
// TODO(#2496): Support people passing a func into the field.
/**
* Bind handlers for user input on the text input field's editor.

View File

@@ -10,8 +10,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldLabel');
// Former goog.module ID: Blockly.FieldLabel
import * as dom from './utils/dom.js';
import {Field, FieldConfig} from './field.js';

View File

@@ -11,8 +11,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldLabelSerializable');
// Former goog.module ID: Blockly.FieldLabelSerializable
import {
FieldLabel,

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldMultilineInput');
// Former goog.module ID: Blockly.FieldMultilineInput
import * as Css from './css.js';
import {Field, UnattachedFieldError} from './field.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldNumber');
// Former goog.module ID: Blockly.FieldNumber
import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';

View File

@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.fieldRegistry');
// Former goog.module ID: Blockly.fieldRegistry
import type {Field, FieldProto} from './field.js';
import * as registry from './registry.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldTextInput');
// Former goog.module ID: Blockly.FieldTextInput
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';

View File

@@ -9,8 +9,7 @@
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldVariable');
// Former goog.module ID: Blockly.FieldVariable
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';

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