release: v9.3.0

Merge pull request #6934 from google/rc/v9.3.0
This commit is contained in:
Beka Westberg
2023-03-29 10:56:46 -07:00
committed by GitHub
411 changed files with 7740 additions and 14301 deletions

222
.eslintrc.js Normal file
View File

@@ -0,0 +1,222 @@
const rules = {
'curly': ['error'],
'eol-last': ['error'],
'keyword-spacing': ['error'],
'linebreak-style': ['error', 'unix'],
'max-len': [
'error',
{
'code': 100,
'tabWidth': 4,
'ignoreStrings': true,
'ignoreRegExpLiterals': true,
'ignoreUrls': true,
},
],
'no-trailing-spaces': ['error', {'skipBlankLines': true}],
'no-unused-vars': [
'warn',
{
'args': 'after-used',
// Ignore vars starting with an underscore.
'varsIgnorePattern': '^_',
// Ignore arguments starting with an underscore.
'argsIgnorePattern': '^_',
},
],
// Blockly uses for exporting symbols. no-self-assign added in eslint 5.
'no-self-assign': ['off'],
// Blockly uses single quotes except for JSON blobs, which must use double
// quotes.
'quotes': ['off'],
'semi': ['error', 'always'],
// Blockly doesn't have space before function paren when defining functions.
'space-before-function-paren': ['error', 'never'],
// Blockly doesn't have space before function paren when calling functions.
'func-call-spacing': ['error', 'never'],
'space-infix-ops': ['error'],
// Blockly uses 'use strict' in files.
'strict': ['off'],
// Closure style allows redeclarations.
'no-redeclare': ['off'],
'valid-jsdoc': ['error'],
'no-console': ['off'],
'no-multi-spaces': ['error', {'ignoreEOLComments': true}],
'operator-linebreak': ['error', 'after'],
'spaced-comment': [
'error',
'always',
{
'block': {
'balanced': true,
},
'exceptions': ['*'],
},
],
// Blockly uses prefixes for optional arguments and test-only functions.
'camelcase': [
'error',
{
'properties': 'never',
'allow': ['^opt_', '^_opt_', '^testOnly_'],
},
],
// Use clang-format for indentation by running `npm run format`.
'indent': ['off'],
// Blockly uses capital letters for some non-constructor namespaces.
// Keep them for legacy reasons.
'new-cap': ['off'],
// Mostly use default rules for brace style, but allow single-line blocks.
'brace-style': ['error', '1tbs', {'allowSingleLine': true}],
// Blockly uses objects as maps, but uses Object.create(null) to
// instantiate them.
'guard-for-in': ['off'],
'prefer-spread': ['off'],
'comma-dangle': [
'error',
{
'arrays': 'always-multiline',
'objects': 'always-multiline',
'imports': 'always-multiline',
'exports': 'always-multiline',
'functions': 'ignore',
},
],
};
/**
* Build shared settings for TS linting and add in the config differences.
* @return {Object} The override TS linting for given files and a given
* tsconfig.
*/
function buildTSOverride({files, tsconfig}) {
return {
'files': files,
'plugins': [
'@typescript-eslint/eslint-plugin',
'jsdoc',
],
'settings': {
'jsdoc': {
'mode': 'typescript',
},
},
'parser': '@typescript-eslint/parser',
'parserOptions': {
'project': tsconfig,
'tsconfigRootDir': '.',
'ecmaVersion': 2020,
'sourceType': 'module',
},
'extends': [
'plugin:@typescript-eslint/recommended',
'plugin:jsdoc/recommended',
],
'rules': {
// TS rules
// Blockly uses namespaces to do declaration merging in some cases.
'@typescript-eslint/no-namespace': ['off'],
// Use the updated TypeScript-specific rule.
'no-invalid-this': ['off'],
'@typescript-eslint/no-invalid-this': ['error'],
// Needs decision. 601 problems.
'@typescript-eslint/no-non-null-assertion': ['off'],
// Use TS-specific rule.
'no-unused-vars': ['off'],
'@typescript-eslint/no-unused-vars': [
'warn',
{
'argsIgnorePattern': '^_',
'varsIgnorePattern': '^_',
},
],
'func-call-spacing': ['off'],
'@typescript-eslint/func-call-spacing': ['warn'],
// Temporarily disable. 23 problems.
'@typescript-eslint/no-explicit-any': ['off'],
// Temporarily disable. 128 problems.
'require-jsdoc': ['off'],
// Temporarily disable. 55 problems.
'@typescript-eslint/ban-types': ['off'],
// Temporarily disable. 33 problems.
'@typescript-eslint/no-empty-function': ['off'],
// Temporarily disable. 3 problems.
'@typescript-eslint/no-empty-interface': ['off'],
// TsDoc rules (using JsDoc plugin)
// Disable built-in jsdoc verifier.
'valid-jsdoc': ['off'],
// Don't require types in params and returns docs.
'jsdoc/require-param-type': ['off'],
'jsdoc/require-returns-type': ['off'],
// params and returns docs are optional.
'jsdoc/require-param-description': ['off'],
'jsdoc/require-returns': ['off'],
// Disable for now (breaks on `this` which is not really a param).
'jsdoc/require-param': ['off'],
// Don't auto-add missing jsdoc. Only required on exported items.
'jsdoc/require-jsdoc': [
'warn',
{
'enableFixer': false,
'publicOnly': true,
},
],
// Disable because of false alarms with Closure-supported tags.
// Re-enable after Closure is removed.
'jsdoc/check-tag-names': ['off'],
// Re-enable after Closure is removed. There shouldn't even be
// types in the TsDoc.
// These are "types" because of Closure's @suppress {warningName}
'jsdoc/no-undefined-types': ['off'],
'jsdoc/valid-types': ['off'],
// Disabled due to not handling `this`. If re-enabled,
// checkDestructured option
// should be left as false.
'jsdoc/check-param-names': ['off', {'checkDestructured': false}],
// Allow any text in the license tag. Other checks are not relevant.
'jsdoc/check-values': ['off'],
'jsdoc/newline-after-description': ['error'],
},
};
}
// NOTE: When this output is put directly in `module.exports`, the formatter
// does not align with the linter.
const eslintJSON = {
'rules': rules,
'env': {
'es2020': true,
'browser': true,
},
'globals': {
'goog': true,
'exports': true,
},
'extends': [
'eslint:recommended',
'google',
],
// TypeScript-specific config. Uses above rules plus these.
'overrides': [
buildTSOverride({
files: ['./core/**/*.ts', './core/**/*.tsx'],
tsconfig: './tsconfig.json',
}),
buildTSOverride({
files: [
'./tests/typescript/**/*.ts',
'./tests/typescript/**/*.tsx',
],
tsconfig: './tests/typescript/tsconfig.json',
}),
{
'files': ['./.eslintrc.js'],
'env': {
'node': true,
},
},
],
};
module.exports = eslintJSON;

View File

@@ -1,165 +0,0 @@
{
"rules": {
"curly": ["error"],
"eol-last": ["error"],
"keyword-spacing": ["error"],
"linebreak-style": ["error", "unix"],
"max-len": [
"error",
{
"code": 100,
"tabWidth": 4,
"ignoreStrings": true,
"ignoreRegExpLiterals": true,
"ignoreUrls": true
}
],
"no-trailing-spaces": ["error", { "skipBlankLines": true }],
"no-unused-vars": [
"warn",
{
"args": "after-used",
// Ignore vars starting with an underscore.
"varsIgnorePattern": "^_",
// Ignore arguments starting with an underscore.
"argsIgnorePattern": "^_"
}
],
// Blockly uses for exporting symbols. no-self-assign added in eslint 5.
"no-self-assign": ["off"],
// Blockly uses single quotes except for JSON blobs, which must use double quotes.
"quotes": ["off"],
"semi": ["error", "always"],
// Blockly doesn't have space before function paren when defining functions.
"space-before-function-paren": ["error", "never"],
// Blockly doesn't have space before function paren when calling functions.
"func-call-spacing": ["error", "never"],
"space-infix-ops": ["error"],
// Blockly uses 'use strict' in files.
"strict": ["off"],
// Closure style allows redeclarations.
"no-redeclare": ["off"],
"valid-jsdoc": ["error"],
"no-console": ["off"],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
"operator-linebreak": ["error", "after"],
"spaced-comment": ["error", "always", {
"block": {
"balanced": true
},
"exceptions": ["*"]
}],
// Blockly uses prefixes for optional arguments and test-only functions.
"camelcase": ["error", {
"properties": "never",
"allow": ["^opt_", "^_opt_", "^testOnly_"]
}],
// Use clang-format for indentation by running `npm run format`.
"indent": ["off"],
// Blockly uses capital letters for some non-constructor namespaces.
// Keep them for legacy reasons.
"new-cap": ["off"],
// Mostly use default rules for brace style, but allow single-line blocks.
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
// Blockly uses objects as maps, but uses Object.create(null) to
// instantiate them.
"guard-for-in": ["off"],
"prefer-spread": ["off"],
"comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "ignore"
}]
},
"env": {
"es2020": true,
"browser": true
},
"globals": {
"goog": true,
"exports": true
},
"extends": [
"eslint:recommended", "google"
],
// TypeScript-specific config. Uses above rules plus these.
"overrides": [{
"files": ["**/*.ts", "**/*.tsx"],
"plugins": [
"@typescript-eslint/eslint-plugin",
"jsdoc"
],
"settings": {
"jsdoc": {
"mode": "typescript"
}
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"tsconfigRootDir": ".",
"ecmaVersion": 2020,
"sourceType": "module"
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:jsdoc/recommended"
],
"rules": {
// TS rules
// Blockly uses namespaces to do declaration merging in some cases.
"@typescript-eslint/no-namespace": ["off"],
// Use the updated TypeScript-specific rule.
"no-invalid-this": ["off"],
"@typescript-eslint/no-invalid-this": ["error"],
// Needs decision. 601 problems.
"@typescript-eslint/no-non-null-assertion": ["off"],
// Use TS-specific rule.
"no-unused-vars": ["off"],
"@typescript-eslint/no-unused-vars": ["warn", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}],
"func-call-spacing": ["off"],
"@typescript-eslint/func-call-spacing": ["warn"],
// Temporarily disable. 23 problems.
"@typescript-eslint/no-explicit-any": ["off"],
// Temporarily disable. 128 problems.
"require-jsdoc": ["off"],
// Temporarily disable. 55 problems.
"@typescript-eslint/ban-types": ["off"],
// Temporarily disable. 33 problems.
"@typescript-eslint/no-empty-function": ["off"],
// Temporarily disable. 3 problems.
"@typescript-eslint/no-empty-interface": ["off"],
// TsDoc rules (using JsDoc plugin)
// Disable built-in jsdoc verifier.
"valid-jsdoc": ["off"],
// Don't require types in params and returns docs.
"jsdoc/require-param-type": ["off"],
"jsdoc/require-returns-type": ["off"],
// params and returns docs are optional.
"jsdoc/require-param-description": ["off"],
"jsdoc/require-returns": ["off"],
// Disable for now (breaks on `this` which is not really a param).
"jsdoc/require-param": ["off"],
// Don't auto-add missing jsdoc. Only required on exported items.
"jsdoc/require-jsdoc": ["warn", {"enableFixer": false, "publicOnly": true}],
// Disable because of false alarms with Closure-supported tags.
// Re-enable after Closure is removed.
"jsdoc/check-tag-names": ["off"],
// Re-enable after Closure is removed. There shouldn't even be types in the TsDoc.
// These are "types" because of Closure's @suppress {warningName}
"jsdoc/no-undefined-types": ["off"],
"jsdoc/valid-types": ["off"],
// Disabled due to not handling `this`. If re-enabled, checkDestructured option
// should be left as false.
"jsdoc/check-param-names": ["off", {"checkDestructured": false}],
// Allow any text in the license tag. Other checks are not relevant.
"jsdoc/check-values": ["off"]
}
}]
}

View File

@@ -1,65 +0,0 @@
---
name: Bug Report
about: Create a report to help us improve
labels: 'type: bug, issue: triage'
assignees: ''
---
<!--
- Thanks for opening an issue for us! Before you open an issue,
- please check if a similar issue exists or has been closed before.
-
- If you're asking a question about how to use Blockly in your application,
- please ask questions on the mailing list, instead of filing issues:
- https://groups.google.com/forum/#!forum/blockly
-->
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
<!-- Explain what someone needs to do in order to see what's described above -->
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Stack Traces**
<!-- Please open up the console. If you see any Blockly-related errors,
- paste them between the quotes below.
-
- Ignore any instances of...
- "Uncaught (in promise) DOMException: The play() request was interrupted by a call to pause()."
-->
```
Replace with error stack trace.
```
**Additional context**
<!-- Add any other context about the problem here. -->

58
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Bug Report
description: Create a report to help us improve
labels: 'issue: bug, issue: triage'
body:
- type: markdown
attributes:
value: >
Thank you for taking the time to fill out a bug report!
If you have a question about how to use Blockly in your application,
please ask on the [forum](https://groups.google.com/forum/#!forum/blockly) instead of filing an issue.
- type: checkboxes
id: duplicates
attributes:
label: Check for duplicates
options:
- label: I have searched for similar issues before opening a new one.
- type: textarea
id: description
attributes:
label: Description
description: Please provide a clear and concise description of the bug.
placeholder: What happened? What did you expect to happen?
validations:
required: true
- type: textarea
id: repro
attributes:
label: Reproduction steps
description: What steps should we take to reproduce the issue?
value: |
1.
2.
3.
- type: textarea
id: stack-trace
attributes:
label: Stack trace
description: If you saw an error message or stack trace, please include it here.
placeholder: The text in this section will be formatted automatically; no need to include backticks.
render: shell
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Screenshots can help us see the behavior you're describing. Please add a screenshot or gif, especially if you are describing a rendering or visual bug.
placeholder: Paste or drag-and-drop an image to upload it.
- type: dropdown
id: browsers
attributes:
label: Browsers
description: Please select all browsers you've observed this behavior on. If the bug isn't browser-specific, you can leave this blank.
multiple: true
options:
- Chrome desktop
- Safari desktop
- Firefox desktop
- Android mobile
- iOS mobile

View File

@@ -1,44 +0,0 @@
---
name: Documentation
about: Report an issue with our documentation
labels: 'type: documentation, issue: triage'
assignees: ''
---
<!--
- Thanks for helping us improve our developer site documentation!
- Use this template to describe issues with the content at
- developers.google.com/blockly/guides
-->
**Where**
<!-- A link to the page with the documentation you want us to update.
- More specific is better. If no page exists, describe what the page
- should be, and where.
-->
**What**
<!-- What kind of content is it?
- Check a box with an 'x' between the brackets: [x]
-->
- [ ] Text
- [ ] Image or Gif
- [ ] Other
**Old content**
<!-- What the documentation currently says -->
**Suggested content**
<!-- Your suggestion for improved documentation -->
**Additional context**
<!-- Add any other context about the problem here.
- If this is related to a specific pull request, link to it.
-->

View File

@@ -0,0 +1,39 @@
name: Documentation
description: Report an issue with our documentation
labels: 'issue: docs, issue: triage'
body:
- type: markdown
attributes:
value: >
Thanks for helping us improve our developer site documentation!
Use this template to describe issues with the content on our
[developer site](https://developers.google.com/blockly/guides).
- type: input
id: link
attributes:
label: Location
description: >
A link to the page with the documentation you want us to be updated.
If no page exists, describe what the page should be, and where.
- type: checkboxes
id: type
attributes:
label: Type
description: What kind of content is it?
options:
- label: Text
- label: Image or Gif
- label: Other
- type: textarea
id: content
attributes:
label: Suggested content
description: Your suggestion for improved documentation. If it's helpful, also include the old content for comparison.
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem. If this is related to a specific pull request, link to it.

View File

@@ -1,23 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
labels: 'type: feature request, issue: triage'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

View File

@@ -0,0 +1,40 @@
name: Feature request
description: Suggest an idea for this project
labels: 'issue: feature request, issue: triage'
body:
- type: markdown
attributes:
value: >
Thank you for taking the time to fill out a feature request!
If you have a question about how to use Blockly in your application,
please ask on the [forum](https://groups.google.com/forum/#!forum/blockly) instead of filing an issue.
- type: checkboxes
id: duplicates
attributes:
label: Check for duplicates
options:
- label: I have searched for similar issues before opening a new one.
- type: textarea
id: problem
attributes:
label: Problem
description: Is your feature request related to a problem? Please describe.
placeholder: I'm always frustrated when...
- type: textarea
id: request
attributes:
label: Request
description: Describe your feature request and how it solves your problem.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Describe any alternative solutions or features you've considered.
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.

View File

@@ -42,7 +42,7 @@ jobs:
path: _deploy/
- name: Deploy to App Engine
uses: google-github-actions/deploy-appengine@v1.0.0
uses: google-github-actions/deploy-appengine@v1.2.2
# For parameters see:
# https://github.com/google-github-actions/deploy-appengine#inputs
with:

View File

@@ -10,6 +10,7 @@ permissions:
jobs:
build:
timeout-minutes: 10
runs-on: ${{ matrix.os }}
strategy:
@@ -17,8 +18,9 @@ jobs:
# TODO (#2114): re-enable osx build.
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
node-version: [14.x, 16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
node-version: [14.x, 16.x, 18.x]
# See supported Node.js release schedule at
# https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
@@ -53,6 +55,7 @@ jobs:
CI: true
lint:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -67,3 +70,18 @@ jobs:
- name: Lint
run: npm run lint
clang-formatter:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: DoozyX/clang-format-lint-action@v0.15
with:
source: 'core'
extensions: 'js,ts'
# This should be as close as possible to the version that the npm
# package supports. This can be found by running:
# npx clang-format --version.
clangFormatVersion: 15

View File

@@ -1,28 +0,0 @@
name: Check clang format
# N.B.: Runs with a read-only repo token. Safe(ish) to check out the
# submitted branch.
on: [pull_request]
permissions:
contents: read
jobs:
clang-formatter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: DoozyX/clang-format-lint-action@v0.15
with:
source: 'core'
extensions: 'js,ts'
# This should be as close as possible to the version that the npm
# package supports. This can be found by running:
# npx clang-format --version.
clangFormatVersion: 15
# The Report clang format workflow (report_clang_format.yml) will
# run (if required) after this one to post a comment to the PR.
# (Note that the version of that workflow run will be the one on
# the master (default) branch, not the PR target branch.)

View File

@@ -36,7 +36,7 @@ jobs:
run: source ./tests/scripts/update_metadata.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@2b011faafdcbc9ceb11414d64d0573f37c774b04
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54
with:
commit-message: Update build artifact sizes in check_metadata.sh
delete-branch: true

View File

@@ -83,4 +83,4 @@ We typically triage all bugs within 2 working days, which includes adding any ap
## Good to Know
* Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com)
* We support IE11 and test it using [BrowserStack](https://browserstack.com)
* We test browsers using [BrowserStack](https://browserstack.com)

View File

@@ -44,7 +44,7 @@ BlocklyStorage.restoreBlocks = function(opt_workspace) {
var url = window.location.href.split('#')[0];
if ('localStorage' in window && window.localStorage[url]) {
var workspace = opt_workspace || Blockly.getMainWorkspace();
var xml = Blockly.Xml.textToDom(window.localStorage[url]);
var xml = Blockly.utils.xml.textToDom(window.localStorage[url]);
Blockly.Xml.domToWorkspace(xml, workspace);
}
};
@@ -168,7 +168,7 @@ BlocklyStorage.monitorChanges_ = function(workspace) {
*/
BlocklyStorage.loadXml_ = function(xml, workspace) {
try {
xml = Blockly.Xml.textToDom(xml);
xml = Blockly.utils.xml.textToDom(xml);
} catch (e) {
BlocklyStorage.alert(BlocklyStorage.XML_ERROR + '\nXML: ' + xml);
return;

View File

@@ -7,24 +7,19 @@
/**
* @fileoverview Colour blocks for Blockly.
*/
'use strict';
goog.module('Blockly.libraryBlocks.colour');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.colour');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const BlockDefinition = Object;
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldColour');
import type {BlockDefinition} from '../core/blocks.js';
import {createBlockDefinitionsFromJsonArray, defineBlocks} from '../core/common.js';
import '../core/field_colour.js';
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = createBlockDefinitionsFromJsonArray([
export const blocks = createBlockDefinitionsFromJsonArray([
// Block for colour picker.
{
'type': 'colour_picker',
@@ -115,7 +110,6 @@ const blocks = createBlockDefinitionsFromJsonArray([
'tooltip': '%{BKY_COLOUR_BLEND_TOOLTIP}',
},
]);
exports.blocks = blocks;
// Register provided blocks.
defineBlocks(blocks);

View File

@@ -12,8 +12,8 @@
goog.module('Blockly.libraryBlocks.lists');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const xmlUtils = goog.require('Blockly.utils.xml');
const Xml = goog.require('Blockly.Xml');
const {Align} = goog.require('Blockly.Input');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
@@ -22,7 +22,6 @@ const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const BlockDefinition = Object;
const {ConnectionType} = goog.require('Blockly.ConnectionType');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
const {Msg} = goog.require('Blockly.Msg');
const {Mutator} = goog.require('Blockly.Mutator');
/* eslint-disable-next-line no-unused-vars */
@@ -315,8 +314,11 @@ blocks['lists_indexOf'] = {
this.setOutput(true, 'Number');
this.appendValueInput('VALUE').setCheck('Array').appendField(
Msg['LISTS_INDEX_OF_INPUT_IN_LIST']);
this.appendValueInput('FIND').appendField(
new FieldDropdown(OPERATORS), 'END');
const operatorsDropdown = fieldRegistry.fromJson({
type: 'field_dropdown',
options: OPERATORS,
});
this.appendValueInput('FIND').appendField(operatorsDropdown, 'END');
this.setInputsInline(true);
// Assign 'this' to a variable for use in the tooltip closure below.
const thisBlock = this;
@@ -347,8 +349,11 @@ blocks['lists_getIndex'] = {
];
this.setHelpUrl(Msg['LISTS_GET_INDEX_HELPURL']);
this.setStyle('list_blocks');
const modeMenu = new FieldDropdown(
MODE,
const modeMenu = fieldRegistry.fromJson({
type: 'field_dropdown',
options: MODE,
});
modeMenu.setValidator(
/**
* @param {*} value The input value.
* @this {FieldDropdown}
@@ -480,7 +485,7 @@ blocks['lists_getIndex'] = {
this.updateStatement_(true);
} else if (typeof state === 'string') {
// backward compatible for json serialised mutations
this.domToMutation(Xml.textToDom(state));
this.domToMutation(xmlUtils.textToDom(state));
}
},
@@ -526,8 +531,11 @@ blocks['lists_getIndex'] = {
} else {
this.appendDummyInput('AT');
}
const menu = new FieldDropdown(
this.WHERE_OPTIONS,
const menu = fieldRegistry.fromJson({
type: 'field_dropdown',
options: this.WHERE_OPTIONS,
});
menu.setValidator(
/**
* @param {*} value The input value.
* @this {FieldDropdown}
@@ -576,8 +584,12 @@ blocks['lists_setIndex'] = {
this.setStyle('list_blocks');
this.appendValueInput('LIST').setCheck('Array').appendField(
Msg['LISTS_SET_INDEX_INPUT_IN_LIST']);
const operationDropdown = fieldRegistry.fromJson({
type: 'field_dropdown',
options: MODE,
});
this.appendDummyInput()
.appendField(new FieldDropdown(MODE), 'MODE')
.appendField(operationDropdown, 'MODE')
.appendField('', 'SPACE');
this.appendDummyInput('AT');
this.appendValueInput('TO').appendField(Msg['LISTS_SET_INDEX_INPUT_TO']);
@@ -689,8 +701,11 @@ blocks['lists_setIndex'] = {
} else {
this.appendDummyInput('AT');
}
const menu = new FieldDropdown(
this.WHERE_OPTIONS,
const menu = fieldRegistry.fromJson({
type: 'field_dropdown',
options: this.WHERE_OPTIONS,
});
menu.setValidator(
/**
* @param {*} value The input value.
* @this {FieldDropdown}
@@ -817,8 +832,11 @@ blocks['lists_getSublist'] = {
} else {
this.appendDummyInput('AT' + n);
}
const menu = new FieldDropdown(
this['WHERE_OPTIONS_' + n],
const menu = fieldRegistry.fromJson({
type: 'field_dropdown',
options: this['WHERE_OPTIONS_' + n],
});
menu.setValidator(
/**
* @param {*} value The input value.
* @this {FieldDropdown}
@@ -899,14 +917,16 @@ blocks['lists_split'] = {
init: function() {
// Assign 'this' to a variable for use in the closures below.
const thisBlock = this;
const dropdown = new FieldDropdown(
[
[Msg['LISTS_SPLIT_LIST_FROM_TEXT'], 'SPLIT'],
[Msg['LISTS_SPLIT_TEXT_FROM_LIST'], 'JOIN'],
],
function(newMode) {
thisBlock.updateType_(newMode);
});
const dropdown = fieldRegistry.fromJson({
type: 'field_dropdown',
options: [
[Msg['LISTS_SPLIT_LIST_FROM_TEXT'], 'SPLIT'],
[Msg['LISTS_SPLIT_TEXT_FROM_LIST'], 'JOIN'],
],
});
dropdown.setValidator(function(newMode) {
thisBlock.updateType_(newMode);
});
this.setHelpUrl(Msg['LISTS_SPLIT_HELPURL']);
this.setStyle('list_blocks');
this.appendValueInput('INPUT').setCheck('String').appendField(

View File

@@ -6,37 +6,28 @@
/**
* @fileoverview Math blocks for Blockly.
* @suppress {checkTypes}
*/
'use strict';
goog.module('Blockly.libraryBlocks.math');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.math');
const Extensions = goog.require('Blockly.Extensions');
// N.B.: Blockly.FieldDropdown needed for type AND side-effects.
/* eslint-disable-next-line no-unused-vars */
const FieldDropdown = goog.require('Blockly.FieldDropdown');
const xmlUtils = goog.require('Blockly.utils.xml');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const BlockDefinition = Object;
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldLabel');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldNumber');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldVariable');
import * as Extensions from '../core/extensions.js';
import type {Field} from '../core/field.js';
import type {FieldDropdown} from '../core/field_dropdown.js';
import * as xmlUtils from '../core/utils/xml.js';
import type {Block} from '../core/block.js';
import type {BlockDefinition} from '../core/blocks.js';
import {createBlockDefinitionsFromJsonArray, defineBlocks} from '../core/common.js';
import '../core/field_dropdown.js';
import '../core/field_label.js';
import '../core/field_number.js';
import '../core/field_variable.js';
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = createBlockDefinitionsFromJsonArray([
export const blocks = createBlockDefinitionsFromJsonArray([
// Block for numeric value.
{
'type': 'math_number',
@@ -391,11 +382,11 @@ const blocks = createBlockDefinitionsFromJsonArray([
'helpUrl': '%{BKY_MATH_ATAN2_HELPURL}',
},
]);
exports.blocks = blocks;
/**
* Mapping of math block OP value to tooltip message for blocks
* math_arithmetic, math_simple, math_trig, and math_on_lists.
*
* @see {Extensions#buildTooltipForDropdown}
* @package
* @readonly
@@ -440,10 +431,15 @@ Extensions.register(
'math_op_tooltip',
Extensions.buildTooltipForDropdown('OP', TOOLTIPS_BY_OP));
/** Type of a block that has IS_DIVISBLEBY_MUTATOR_MIXIN */
type DivisiblebyBlock = Block&DivisiblebyMixin;
interface DivisiblebyMixin extends DivisiblebyMixinType {};
type DivisiblebyMixinType = typeof IS_DIVISIBLEBY_MUTATOR_MIXIN;
/**
* Mixin for mutator functions in the 'math_is_divisibleby_mutator'
* extension.
*
* @mixin
* @augments Block
* @package
@@ -452,22 +448,22 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
/**
* Create XML to represent whether the 'divisorInput' should be present.
* Backwards compatible serialization implementation.
* @return {!Element} XML storage element.
* @this {Block}
*
* @returns XML storage element.
*/
mutationToDom: function() {
mutationToDom: function(this: DivisiblebyBlock): Element {
const container = xmlUtils.createElement('mutation');
const divisorInput = (this.getFieldValue('PROPERTY') === 'DIVISIBLE_BY');
container.setAttribute('divisor_input', divisorInput);
container.setAttribute('divisor_input', String(divisorInput));
return container;
},
/**
* Parse XML to restore the 'divisorInput'.
* Backwards compatible serialization implementation.
* @param {!Element} xmlElement XML storage element.
* @this {Block}
*
* @param xmlElement XML storage element.
*/
domToMutation: function(xmlElement) {
domToMutation: function(this: DivisiblebyBlock, xmlElement: Element) {
const divisorInput = (xmlElement.getAttribute('divisor_input') === 'true');
this.updateShape_(divisorInput);
},
@@ -479,11 +475,10 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
/**
* Modify this block to have (or not have) an input for 'is divisible by'.
* @param {boolean} divisorInput True if this block has a divisor input.
* @private
* @this {Block}
*
* @param divisorInput True if this block has a divisor input.
*/
updateShape_: function(divisorInput) {
updateShape_: function(this: DivisiblebyBlock, divisorInput: boolean) {
// Add or remove a Value Input.
const inputExists = this.getInput('DIVISOR');
if (divisorInput) {
@@ -500,20 +495,15 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
* 'math_is_divisibleby_mutator' extension to the 'math_property' block that
* can update the block shape (add/remove divisor input) based on whether
* property is "divisible by".
* @this {Block}
* @package
*/
const IS_DIVISIBLE_MUTATOR_EXTENSION = function() {
this.getField('PROPERTY')
.setValidator(
/**
* @this {FieldDropdown}
* @param {*} option The selected dropdown option.
*/
function(option) {
const divisorInput = (option === 'DIVISIBLE_BY');
this.getSourceBlock().updateShape_(divisorInput);
});
const IS_DIVISIBLE_MUTATOR_EXTENSION = function(this: DivisiblebyBlock) {
this.getField('PROPERTY')!.setValidator(
/** @param option The selected dropdown option. */
function(this: FieldDropdown, option: string) {
const divisorInput = (option === 'DIVISIBLE_BY');
(this.getSourceBlock() as DivisiblebyBlock).updateShape_(divisorInput);
return undefined; // FieldValidators can't be void. Use option as-is.
});
};
Extensions.registerMutator(
@@ -525,35 +515,35 @@ Extensions.register(
'math_change_tooltip',
Extensions.buildTooltipWithFieldText('%{BKY_MATH_CHANGE_TOOLTIP}', 'VAR'));
/** Type of a block that has LIST_MODES_MUTATOR_MIXIN */
type ListModesBlock = Block&ListModesMixin;
interface ListModesMixin extends ListModesMixinType {};
type ListModesMixinType = typeof LIST_MODES_MUTATOR_MIXIN;
/**
* Mixin with mutator methods to support alternate output based if the
* 'math_on_list' block uses the 'MODE' operation.
* @mixin
* @augments Block
* @package
* @readonly
*/
const LIST_MODES_MUTATOR_MIXIN = {
/**
* Modify this block to have the correct output type.
* @param {string} newOp Either 'MODE' or some op than returns a number.
* @private
* @this {Block}
*
* @param newOp Either 'MODE' or some op than returns a number.
*/
updateType_: function(newOp) {
updateType_: function(this: ListModesBlock, newOp: string) {
if (newOp === 'MODE') {
this.outputConnection.setCheck('Array');
this.outputConnection!.setCheck('Array');
} else {
this.outputConnection.setCheck('Number');
this.outputConnection!.setCheck('Number');
}
},
/**
* Create XML to represent the output type.
* Backwards compatible serialization implementation.
* @return {!Element} XML storage element.
* @this {Block}
*
* @returns XML storage element.
*/
mutationToDom: function() {
mutationToDom: function(this: ListModesBlock): Element {
const container = xmlUtils.createElement('mutation');
container.setAttribute('op', this.getFieldValue('OP'));
return container;
@@ -561,11 +551,13 @@ const LIST_MODES_MUTATOR_MIXIN = {
/**
* Parse XML to restore the output type.
* Backwards compatible serialization implementation.
* @param {!Element} xmlElement XML storage element.
* @this {Block}
*
* @param xmlElement XML storage element.
*/
domToMutation: function(xmlElement) {
this.updateType_(xmlElement.getAttribute('op'));
domToMutation: function(this: ListModesBlock, xmlElement: Element) {
const op = xmlElement.getAttribute('op');
if (op === null) throw new TypeError('xmlElement had no op attribute');
this.updateType_(op);
},
// This block does not need JSO serialization hooks (saveExtraState and
@@ -577,13 +569,13 @@ const LIST_MODES_MUTATOR_MIXIN = {
/**
* Extension to 'math_on_list' blocks that allows support of
* modes operation (outputs a list of numbers).
* @this {Block}
* @package
*/
const LIST_MODES_MUTATOR_EXTENSION = function() {
this.getField('OP').setValidator(function(newOp) {
this.updateType_(newOp);
}.bind(this));
const LIST_MODES_MUTATOR_EXTENSION = function(this: ListModesBlock) {
this.getField('OP')!.setValidator(
function(this: ListModesBlock, newOp: string) {
this.updateType_(newOp);
return undefined;
}.bind(this));
};
Extensions.registerMutator(

View File

@@ -19,6 +19,7 @@ const Events = goog.require('Blockly.Events');
const Procedures = goog.require('Blockly.Procedures');
const Variables = goog.require('Blockly.Variables');
const Xml = goog.require('Blockly.Xml');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const xmlUtils = goog.require('Blockly.utils.xml');
const {Align} = goog.require('Blockly.Input');
/* eslint-disable-next-line no-unused-vars */
@@ -28,10 +29,6 @@ const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const BlockDefinition = Object;
const {config} = goog.require('Blockly.config');
/* eslint-disable-next-line no-unused-vars */
const {FieldCheckbox} = goog.require('Blockly.FieldCheckbox');
const {FieldLabel} = goog.require('Blockly.FieldLabel');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
const {Msg} = goog.require('Blockly.Msg');
const {Mutator} = goog.require('Blockly.Mutator');
const {Names} = goog.require('Blockly.Names');
@@ -454,7 +451,11 @@ blocks['procedures_defnoreturn'] = {
*/
init: function() {
const initName = Procedures.findLegalName('', this);
const nameField = new FieldTextInput(initName, Procedures.rename);
const nameField = fieldRegistry.fromJson({
type: 'field_input',
text: initName,
});
nameField.setValidator(Procedures.rename);
nameField.setSpellcheck(false);
this.appendDummyInput()
.appendField(Msg['PROCEDURES_DEFNORETURN_TITLE'])
@@ -497,7 +498,11 @@ blocks['procedures_defreturn'] = {
*/
init: function() {
const initName = Procedures.findLegalName('', this);
const nameField = new FieldTextInput(initName, Procedures.rename);
const nameField = fieldRegistry.fromJson({
type: 'field_input',
text: initName,
});
nameField.setValidator(Procedures.rename);
nameField.setSpellcheck(false);
this.appendDummyInput()
.appendField(Msg['PROCEDURES_DEFRETURN_TITLE'])
@@ -546,7 +551,12 @@ blocks['procedures_mutatorcontainer'] = {
this.appendStatementInput('STACK');
this.appendDummyInput('STATEMENT_INPUT')
.appendField(Msg['PROCEDURES_ALLOW_STATEMENTS'])
.appendField(new FieldCheckbox('TRUE'), 'STATEMENTS');
.appendField(
fieldRegistry.fromJson({
type: 'field_checkbox',
checked: true,
}),
'STATEMENTS');
this.setStyle('procedure_blocks');
this.setTooltip(Msg['PROCEDURES_MUTATORCONTAINER_TOOLTIP']);
this.contextMenu = false;
@@ -559,7 +569,11 @@ blocks['procedures_mutatorarg'] = {
* @this {Block}
*/
init: function() {
const field = new FieldTextInput(Procedures.DEFAULT_ARG, this.validator_);
const field = fieldRegistry.fromJson({
type: 'field_input',
text: Procedures.DEFAULT_ARG,
});
field.setValidator(this.validator_);
// Hack: override showEditor to do just a little bit more work.
// We don't have a good place to hook into the start of a text edit.
field.oldShowEditorFn_ = field.showEditor_;
@@ -810,7 +824,10 @@ const PROCEDURE_CALL_COMMON = {
}
} else {
// Add new input.
const newField = new FieldLabel(this.arguments_[i]);
const newField = fieldRegistry.fromJson({
type: 'field_label',
text: this.arguments_[i],
});
const input = this.appendValueInput('ARG' + i)
.setAlign(Align.RIGHT)
.appendField(newField, 'ARGNAME' + i);

View File

@@ -14,6 +14,7 @@ goog.module('Blockly.libraryBlocks.texts');
const Extensions = goog.require('Blockly.Extensions');
const {Msg} = goog.require('Blockly.Msg');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
/* eslint-disable-next-line no-unused-vars */
const xmlUtils = goog.require('Blockly.utils.xml');
const {Align} = goog.require('Blockly.Input');
@@ -24,9 +25,6 @@ const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const BlockDefinition = Object;
const {ConnectionType} = goog.require('Blockly.ConnectionType');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
const {FieldImage} = goog.require('Blockly.FieldImage');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
const {Mutator} = goog.require('Blockly.Mutator');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
@@ -338,8 +336,11 @@ blocks['text_getSubstring'] = {
this.removeInput('TAIL', true);
this.appendDummyInput('TAIL').appendField(Msg['TEXT_GET_SUBSTRING_TAIL']);
}
const menu = new FieldDropdown(
this['WHERE_OPTIONS_' + n],
const menu = fieldRegistry.fromJson({
type: 'field_dropdown',
options: this['WHERE_OPTIONS_' + n],
});
menu.setValidator(
/**
* @param {*} value The input value.
* @this {FieldDropdown}
@@ -385,7 +386,11 @@ blocks['text_changeCase'] = {
this.setHelpUrl(Msg['TEXT_CHANGECASE_HELPURL']);
this.setStyle('text_blocks');
this.appendValueInput('TEXT').setCheck('String').appendField(
new FieldDropdown(OPERATORS), 'CASE');
fieldRegistry.fromJson({
type: 'field_dropdown',
options: OPERATORS,
}),
'CASE');
this.setOutput(true, 'String');
this.setTooltip(Msg['TEXT_CHANGECASE_TOOLTIP']);
},
@@ -405,7 +410,11 @@ blocks['text_trim'] = {
this.setHelpUrl(Msg['TEXT_TRIM_HELPURL']);
this.setStyle('text_blocks');
this.appendValueInput('TEXT').setCheck('String').appendField(
new FieldDropdown(OPERATORS), 'MODE');
fieldRegistry.fromJson({
type: 'field_dropdown',
options: OPERATORS,
}),
'MODE');
this.setOutput(true, 'String');
this.setTooltip(Msg['TEXT_TRIM_TOOLTIP']);
},
@@ -486,7 +495,11 @@ blocks['text_prompt_ext'] = {
this.setStyle('text_blocks');
// Assign 'this' to a variable for use in the closures below.
const thisBlock = this;
const dropdown = new FieldDropdown(TYPES, function(newOp) {
const dropdown = fieldRegistry.fromJson({
type: 'field_dropdown',
options: TYPES,
});
dropdown.setValidator(function(newOp) {
thisBlock.updateType_(newOp);
});
this.appendValueInput('TEXT').appendField(dropdown, 'TYPE');
@@ -522,13 +535,22 @@ blocks['text_prompt'] = {
const thisBlock = this;
this.setHelpUrl(Msg['TEXT_PROMPT_HELPURL']);
this.setStyle('text_blocks');
const dropdown = new FieldDropdown(TYPES, function(newOp) {
const dropdown = fieldRegistry.fromJson({
type: 'field_dropdown',
options: TYPES,
});
dropdown.setValidator(function(newOp) {
thisBlock.updateType_(newOp);
});
this.appendDummyInput()
.appendField(dropdown, 'TYPE')
.appendField(this.newQuote_(true))
.appendField(new FieldTextInput(''), 'TEXT')
.appendField(
fieldRegistry.fromJson({
type: 'field_input',
text: '',
}),
'TEXT')
.appendField(this.newQuote_(false));
this.setOutput(true, 'String');
this.setTooltip(function() {
@@ -696,9 +718,13 @@ const QUOTE_IMAGE_MIXIN = {
const isLeft = this.RTL ? !open : open;
const dataUri =
isLeft ? this.QUOTE_IMAGE_LEFT_DATAURI : this.QUOTE_IMAGE_RIGHT_DATAURI;
return new FieldImage(
dataUri, this.QUOTE_IMAGE_WIDTH, this.QUOTE_IMAGE_HEIGHT,
isLeft ? '\u201C' : '\u201D');
return fieldRegistry.fromJson({
type: 'field_image',
src: dataUri,
width: this.QUOTE_IMAGE_WIDTH,
height: this.QUOTE_IMAGE_HEIGHT,
alt: isLeft ? '\u201C' : '\u201D',
});
},
};

View File

@@ -1,325 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2006 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.
"""Calculates JavaScript dependencies without requiring Google's build system.
It iterates over a number of search paths and builds a dependency tree. With
the inputs provided, it walks the dependency tree and outputs all the files
required for compilation.
"""
import logging
import os
import re
import sys
_BASE_REGEX_STRING = '^\s*goog\.%s\(\s*[\'"](.+)[\'"]\s*\)'
req_regex = re.compile(_BASE_REGEX_STRING % 'require')
prov_regex = re.compile(_BASE_REGEX_STRING % 'provide')
ns_regex = re.compile('^ns:((\w+\.)*(\w+))$')
def IsValidFile(ref):
"""Returns true if the provided reference is a file and exists."""
return os.path.isfile(ref)
def IsJsFile(ref):
"""Returns true if the provided reference is a JavaScript file."""
return ref.endswith('.js')
def IsNamespace(ref):
"""Returns true if the provided reference is a namespace."""
return re.match(ns_regex, ref) is not None
def IsDirectory(ref):
"""Returns true if the provided reference is a directory."""
return os.path.isdir(ref)
def ExpandDirectories(refs):
"""Expands any directory references into inputs.
Description:
Looks for any directories in the provided references. Found directories
are recursively searched for .js files, which are then added to the result
list.
Args:
refs: a list of references such as files, directories, and namespaces
Returns:
A list of references with directories removed and replaced by any
.js files that are found in them. Also, the paths will be normalized.
"""
result = []
for ref in refs:
if IsDirectory(ref):
# Disable 'Unused variable' for subdirs
# pylint: disable=unused-variable
for (directory, subdirs, filenames) in os.walk(ref):
for filename in filenames:
if IsJsFile(filename):
result.append(os.path.join(directory, filename))
else:
result.append(ref)
return map(os.path.normpath, result)
class DependencyInfo(object):
"""Represents a dependency that is used to build and walk a tree."""
def __init__(self, filename):
self.filename = filename
self.provides = []
self.requires = []
def __str__(self):
return '%s Provides: %s Requires: %s' % (self.filename,
repr(self.provides),
repr(self.requires))
def BuildDependenciesFromFiles(files):
"""Build a list of dependencies from a list of files.
Description:
Takes a list of files, extracts their provides and requires, and builds
out a list of dependency objects.
Args:
files: a list of files to be parsed for goog.provides and goog.requires.
Returns:
A list of dependency objects, one for each file in the files argument.
"""
result = []
filenames = set()
for filename in files:
if filename in filenames:
continue
# Python 3 requires the file encoding to be specified
if (sys.version_info[0] < 3):
file_handle = open(filename, 'r')
else:
file_handle = open(filename, 'r', encoding='utf8')
try:
dep = CreateDependencyInfo(filename, file_handle)
result.append(dep)
finally:
file_handle.close()
filenames.add(filename)
return result
def CreateDependencyInfo(filename, source):
"""Create dependency info.
Args:
filename: Filename for source.
source: File-like object containing source.
Returns:
A DependencyInfo object with provides and requires filled.
"""
dep = DependencyInfo(filename)
for line in source:
if re.match(req_regex, line):
dep.requires.append(re.search(req_regex, line).group(1))
if re.match(prov_regex, line):
dep.provides.append(re.search(prov_regex, line).group(1))
return dep
def BuildDependencyHashFromDependencies(deps):
"""Builds a hash for searching dependencies by the namespaces they provide.
Description:
Dependency objects can provide multiple namespaces. This method enumerates
the provides of each dependency and adds them to a hash that can be used
to easily resolve a given dependency by a namespace it provides.
Args:
deps: a list of dependency objects used to build the hash.
Raises:
Exception: If a multiple files try to provide the same namepace.
Returns:
A hash table { namespace: dependency } that can be used to resolve a
dependency by a namespace it provides.
"""
dep_hash = {}
for dep in deps:
for provide in dep.provides:
if provide in dep_hash:
raise Exception('Duplicate provide (%s) in (%s, %s)' % (
provide,
dep_hash[provide].filename,
dep.filename))
dep_hash[provide] = dep
return dep_hash
def CalculateDependencies(paths, inputs):
"""Calculates the dependencies for given inputs.
Description:
This method takes a list of paths (files, directories) and builds a
searchable data structure based on the namespaces that each .js file
provides. It then parses through each input, resolving dependencies
against this data structure. The final output is a list of files,
including the inputs, that represent all of the code that is needed to
compile the given inputs.
Args:
paths: the references (files, directories) that are used to build the
dependency hash.
inputs: the inputs (files, directories, namespaces) that have dependencies
that need to be calculated.
Raises:
Exception: if a provided input is invalid.
Returns:
A list of all files, including inputs, that are needed to compile the given
inputs.
"""
deps = BuildDependenciesFromFiles(paths + inputs)
search_hash = BuildDependencyHashFromDependencies(deps)
result_list = []
seen_list = []
for input_file in inputs:
if IsNamespace(input_file):
namespace = re.search(ns_regex, input_file).group(1)
if namespace not in search_hash:
raise Exception('Invalid namespace (%s)' % namespace)
input_file = search_hash[namespace].filename
if not IsValidFile(input_file) or not IsJsFile(input_file):
raise Exception('Invalid file (%s)' % input_file)
seen_list.append(input_file)
file_handle = open(input_file, 'r')
try:
for line in file_handle:
if re.match(req_regex, line):
require = re.search(req_regex, line).group(1)
ResolveDependencies(require, search_hash, result_list, seen_list)
finally:
file_handle.close()
result_list.append(input_file)
return result_list
def FindClosureBasePath(paths):
"""Given a list of file paths, return Closure base.js path, if any.
Args:
paths: A list of paths.
Returns:
The path to Closure's base.js file including filename, if found.
"""
for path in paths:
pathname, filename = os.path.split(path)
if filename == 'base.js':
f = open(path)
is_base = False
# Sanity check that this is the Closure base file. Check that this
# is where goog is defined. This is determined by the @provideGoog
# flag.
for line in f:
if '@provideGoog' in line:
is_base = True
break
f.close()
if is_base:
return path
def ResolveDependencies(require, search_hash, result_list, seen_list):
"""Takes a given requirement and resolves all of the dependencies for it.
Description:
A given requirement may require other dependencies. This method
recursively resolves all dependencies for the given requirement.
Raises:
Exception: when require does not exist in the search_hash.
Args:
require: the namespace to resolve dependencies for.
search_hash: the data structure used for resolving dependencies.
result_list: a list of filenames that have been calculated as dependencies.
This variable is the output for this function.
seen_list: a list of filenames that have been 'seen'. This is required
for the dependency->dependent ordering.
"""
if require not in search_hash:
raise Exception('Missing provider for (%s)' % require)
dep = search_hash[require]
if not dep.filename in seen_list:
seen_list.append(dep.filename)
for sub_require in dep.requires:
ResolveDependencies(sub_require, search_hash, result_list, seen_list)
result_list.append(dep.filename)
def GetDepsLine(dep, base_path):
"""Returns a JS string for a dependency statement in the deps.js file.
Args:
dep: The dependency that we're printing.
base_path: The path to Closure's base.js including filename.
"""
return 'goog.addDependency("%s", %s, %s);' % (
GetRelpath(dep.filename, base_path), dep.provides, dep.requires)
def GetRelpath(path, start):
"""Return a relative path to |path| from |start|."""
# NOTE: Python 2.6 provides os.path.relpath, which has almost the same
# functionality as this function. Since we want to support 2.4, we have
# to implement it manually. :(
path_list = os.path.abspath(os.path.normpath(path)).split(os.sep)
start_list = os.path.abspath(
os.path.normpath(os.path.dirname(start))).split(os.sep)
common_prefix_count = 0
for i in range(0, min(len(path_list), len(start_list))):
if path_list[i] != start_list[i]:
break
common_prefix_count += 1
# Always use forward slashes, because this will get expanded to a url,
# not a file path.
return '/'.join(['..'] * (len(start_list) - common_prefix_count) +
path_list[common_prefix_count:])

View File

@@ -617,8 +617,10 @@ goog.declareModuleId = function(namespace) {
'within an ES6 module');
}
if (goog.moduleLoaderState_ && goog.moduleLoaderState_.moduleName) {
// throw new Error(
// 'goog.declareModuleId may only be called once per module.');
throw new Error(
'goog.declareModuleId may only be called once per module.' +
'This error can also be caused by circular imports, which ' +
'are not supported by debug module loader.');
}
if (namespace in goog.loadedModules_) {
throw new Error(
@@ -725,7 +727,7 @@ if (!COMPILED) {
// NOTE: We add goog.module as an implicit namespace as goog.module is defined
// here and because the existing module package has not been moved yet out of
// the goog.module namespace. This satisifies both the debug loader and
// the goog.module namespace. This satisfies both the debug loader and
// ahead-of-time dependency management.
}

View File

@@ -1,2 +1,8 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// eslint-disable-next-line
type AnyDuringMigration = any;

View File

@@ -35,7 +35,6 @@ import {Align, Input} from './input.js';
import {inputTypes} from './input_types.js';
import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js';
import type {IDeletable} from './interfaces/i_deletable.js';
import {ASTNode} from './keyboard_nav/ast_node.js';
import type {Mutator} from './mutator.js';
import * as Tooltip from './tooltip.js';
import * as arrayUtils from './utils/array.js';
@@ -50,8 +49,6 @@ import type {Workspace} from './workspace.js';
/**
* Class for one block.
* Not normally called directly, workspace.newBlock() is preferred.
*
* @alias Blockly.Block
*/
export class Block implements IASTNodeLocation, IDeletable {
/**
@@ -59,7 +56,7 @@ export class Block implements IASTNodeLocation, IDeletable {
* changes. This is usually only called from the constructor, the block type
* initializer function, or an extension initializer function.
*/
onchange?: ((p1: Abstract) => AnyDuringMigration)|null;
onchange?: ((p1: Abstract) => void)|null;
/** The language-neutral ID given to the collapsed input. */
static readonly COLLAPSED_INPUT_NAME: string = constants.COLLAPSED_INPUT_NAME;
@@ -93,39 +90,38 @@ export class Block implements IASTNodeLocation, IDeletable {
protected styleName_ = '';
/** An optional method called during initialization. */
init?: (() => AnyDuringMigration)|null = undefined;
init?: (() => void);
/** An optional method called during disposal. */
destroy?: (() => void) = undefined;
destroy?: (() => void);
/**
* An optional serialization method for defining how to serialize the
* mutation state to XML. This must be coupled with defining
* `domToMutation`.
*/
mutationToDom?: ((...p1: AnyDuringMigration[]) => Element)|null = undefined;
mutationToDom?: (...p1: AnyDuringMigration[]) => Element;
/**
* An optional deserialization method for defining how to deserialize the
* mutation state from XML. This must be coupled with defining
* `mutationToDom`.
*/
domToMutation?: ((p1: Element) => AnyDuringMigration)|null = undefined;
domToMutation?: (p1: Element) => void;
/**
* 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`.
*/
saveExtraState?: (() => AnyDuringMigration)|null = undefined;
saveExtraState?: () => AnyDuringMigration;
/**
* An optional serialization method for defining how to deserialize the
* block's extra state (eg mutation state) from something JSON compatible.
* This must be coupled with defining `saveExtraState`.
*/
loadExtraState?:
((p1: AnyDuringMigration) => AnyDuringMigration)|null = undefined;
loadExtraState?: (p1: AnyDuringMigration) => void;
/**
* An optional property for suppressing adding STATEMENT_PREFIX and
@@ -139,31 +135,25 @@ export class Block implements IASTNodeLocation, IDeletable {
* shown to the user, but are declared as global variables in the generated
* code.
*/
getDeveloperVariables?: (() => string[]) = undefined;
getDeveloperVariables?: () => string[];
/**
* An optional function that reconfigures the block based on the contents of
* the mutator dialog.
*/
compose?: ((p1: Block) => void) = undefined;
compose?: (p1: Block) => void;
/**
* An optional function that populates the mutator's dialog with
* this block's components.
*/
decompose?: ((p1: Workspace) => Block) = undefined;
decompose?: (p1: Workspace) => Block;
id: string;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Connection'.
outputConnection: Connection = null as AnyDuringMigration;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Connection'.
nextConnection: Connection = null as AnyDuringMigration;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Connection'.
previousConnection: Connection = null as AnyDuringMigration;
outputConnection: Connection|null = null;
nextConnection: Connection|null = null;
previousConnection: Connection|null = null;
inputList: Input[] = [];
inputsInline?: boolean = undefined;
inputsInline?: boolean;
private disabled = false;
tooltip: Tooltip.TipInfo = '';
contextMenu = true;
@@ -205,19 +195,17 @@ export class Block implements IASTNodeLocation, IDeletable {
protected isInsertionMarker_ = false;
/** Name of the type of hat. */
hat?: string = undefined;
hat?: string;
rendered: boolean|null = null;
/**
* String for block help, or function that returns a URL. Null for no help.
*/
// AnyDuringMigration because: Type 'null' is not assignable to type 'string
// | Function'.
helpUrl: string|Function = null as AnyDuringMigration;
helpUrl: string|Function|null = null;
/** A bound callback function to use when the parent workspace changes. */
private onchangeWrapper_: ((p1: Abstract) => AnyDuringMigration)|null = null;
private onchangeWrapper_: ((p1: Abstract) => void)|null = null;
/**
* A count of statement inputs on the block.
@@ -301,9 +289,7 @@ export class Block implements IASTNodeLocation, IDeletable {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(this));
}
} finally {
if (!existingGroup) {
eventUtils.setGroup(false);
}
eventUtils.setGroup(existingGroup);
// In case init threw, recordUndo flag should still be reset.
eventUtils.setRecordUndo(initialUndoFlag);
}
@@ -324,44 +310,45 @@ export class Block implements IASTNodeLocation, IDeletable {
* @suppress {checkTypes}
*/
dispose(healStack: boolean) {
if (this.isDeadOrDying()) {
return;
}
// Terminate onchange event calls.
if (this.isDeadOrDying()) return;
// Dispose of this change listener before unplugging.
// Technically not necessary due to the event firing delay.
// But future-proofing.
if (this.onchangeWrapper_) {
this.workspace.removeChangeListener(this.onchangeWrapper_);
}
this.unplug(healStack);
if (eventUtils.isEnabled()) {
// Constructing the delete event is costly. Only perform if necessary.
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_DELETE))(this));
}
eventUtils.disable();
this.workspace.removeTopBlock(this);
this.disposeInternal();
}
/**
* Disposes of this block without doing things required by the top block.
* E.g. does not fire events, unplug the block, etc.
*/
protected disposeInternal() {
if (this.isDeadOrDying()) return;
if (this.onchangeWrapper_) {
this.workspace.removeChangeListener(this.onchangeWrapper_);
}
eventUtils.disable();
try {
// This block is now at the top of the workspace.
// Remove this block from the workspace's list of top-most blocks.
this.workspace.removeTopBlock(this);
this.workspace.removeTypedBlock(this);
// Remove from block database.
this.workspace.removeBlockById(this.id);
this.disposing = true;
// First, dispose of all my children.
for (let i = this.childBlocks_.length - 1; i >= 0; i--) {
this.childBlocks_[i].dispose(false);
}
// Then dispose of myself.
// Dispose of all inputs and their fields.
for (let i = 0, input; input = this.inputList[i]; i++) {
input.dispose();
}
this.childBlocks_.forEach((c) => c.disposeInternal());
this.inputList.forEach((i) => i.dispose());
this.inputList.length = 0;
// Dispose of any remaining connections (next/previous/output).
const connections = this.getConnections_(true);
for (let i = 0, connection; connection = connections[i]; i++) {
connection.dispose();
}
this.getConnections_(true).forEach((c) => c.dispose());
} finally {
eventUtils.enable();
if (typeof this.destroy === 'function') {
@@ -424,7 +411,7 @@ export class Block implements IASTNodeLocation, IDeletable {
*/
private unplugFromRow_(opt_healStack?: boolean) {
let parentConnection = null;
if (this.outputConnection.isConnected()) {
if (this.outputConnection?.isConnected()) {
parentConnection = this.outputConnection.targetConnection;
// Disconnect from any superior block.
this.outputConnection.disconnect();
@@ -489,7 +476,7 @@ export class Block implements IASTNodeLocation, IDeletable {
*/
private unplugFromStack_(opt_healStack?: boolean) {
let previousTarget = null;
if (this.previousConnection.isConnected()) {
if (this.previousConnection?.isConnected()) {
// Remember the connection that any next statements need to connect to.
previousTarget = this.previousConnection.targetConnection;
// Detach this block from the parent's tree.
@@ -498,7 +485,7 @@ export class Block implements IASTNodeLocation, IDeletable {
const nextBlock = this.getNextBlock();
if (opt_healStack && nextBlock && !nextBlock.isShadow()) {
// Disconnect the next statement.
const nextTarget = this.nextConnection.targetConnection;
const nextTarget = this.nextConnection?.targetConnection ?? null;
nextTarget?.disconnect();
if (previousTarget &&
this.workspace.connectionChecker.canConnect(
@@ -797,6 +784,15 @@ export class Block implements IASTNodeLocation, IDeletable {
!this.workspace.options.readOnly;
}
/**
* Return whether this block's own deletable property is true or false.
*
* @returns True if the block's deletable property is true, false otherwise.
*/
isOwnDeletable(): boolean {
return this.deletable_;
}
/**
* Set whether this block is deletable or not.
*
@@ -810,12 +806,23 @@ export class Block implements IASTNodeLocation, IDeletable {
* Get whether this block is movable or not.
*
* @returns True if movable.
* @internal
*/
isMovable(): boolean {
return this.movable_ && !this.isShadow_ && !this.isDeadOrDying() &&
!this.workspace.options.readOnly;
}
/**
* Return whether this block's own movable property is true or false.
*
* @returns True if the block's movable property is true, false otherwise.
* @internal
*/
isOwnMovable(): boolean {
return this.movable_;
}
/**
* Set whether this block is movable or not.
*
@@ -884,12 +891,22 @@ export class Block implements IASTNodeLocation, IDeletable {
* Get whether this block is editable or not.
*
* @returns True if editable.
* @internal
*/
isEditable(): boolean {
return this.editable_ && !this.isDeadOrDying() &&
!this.workspace.options.readOnly;
}
/**
* Return whether this block's own editable property is true or false.
*
* @returns True if the block's editable property is true, false otherwise.
*/
isOwnEditable(): boolean {
return this.editable_;
}
/**
* Set whether this block is editable or not.
*
@@ -1024,7 +1041,7 @@ export class Block implements IASTNodeLocation, IDeletable {
* @param onchangeFn The callback to call when the block's workspace changes.
* @throws {Error} if onchangeFn is not falsey and not a function.
*/
setOnChange(onchangeFn: (p1: Abstract) => AnyDuringMigration) {
setOnChange(onchangeFn: (p1: Abstract) => void) {
if (onchangeFn && typeof onchangeFn !== 'function') {
throw Error('onchange must be a function.');
}
@@ -1066,11 +1083,12 @@ export class Block implements IASTNodeLocation, IDeletable {
* @returns List of variable ids.
*/
getVars(): string[] {
const vars = [];
const vars: string[] = [];
for (let i = 0, input; input = this.inputList[i]; i++) {
for (let j = 0, field; field = input.fieldRow[j]; j++) {
if (field.referencesVariables()) {
vars.push(field.getValue());
// NOTE: This only applies to `FieldVariable`, a `Field<string>`
vars.push(field.getValue() as string);
}
}
}
@@ -1190,9 +1208,7 @@ export class Block implements IASTNodeLocation, IDeletable {
'connection.');
}
this.previousConnection.dispose();
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Connection'.
this.previousConnection = null as AnyDuringMigration;
this.previousConnection = null;
}
}
}
@@ -1222,9 +1238,7 @@ export class Block implements IASTNodeLocation, IDeletable {
'connection.');
}
this.nextConnection.dispose();
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Connection'.
this.nextConnection = null as AnyDuringMigration;
this.nextConnection = null;
}
}
}
@@ -1253,9 +1267,7 @@ export class Block implements IASTNodeLocation, IDeletable {
'Must disconnect output value before removing connection.');
}
this.outputConnection.dispose();
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Connection'.
this.outputConnection = null as AnyDuringMigration;
this.outputConnection = null;
}
}
}
@@ -1386,21 +1398,52 @@ export class Block implements IASTNodeLocation, IDeletable {
* Create a human-readable text representation of this block and any children.
*
* @param opt_maxLength Truncate the string to this length.
* @param opt_emptyToken The placeholder string used to denote an empty field.
* @param opt_emptyToken The placeholder string used to denote an empty input.
* If not specified, '?' is used.
* @returns Text of block.
*/
toString(opt_maxLength?: number, opt_emptyToken?: string): string {
const tokens = this.toTokens(opt_emptyToken);
// Run through our tokens array and simplify expression to remove
// parentheses around single field blocks.
// E.g. ['repeat', '(', '10', ')', 'times', 'do', '?']
for (let i = 2; i < tokens.length; i++) {
if (tokens[i - 2] === '(' && tokens[i] === ')') {
tokens[i - 2] = tokens[i - 1];
tokens.splice(i - 1, 2);
}
}
// Join the text array, removing the spaces around added parentheses.
let prev = '';
let text: string = tokens.reduce((acc, curr) => {
const val = acc + ((prev === '(' || curr === ')') ? '' : ' ') + curr;
prev = curr[curr.length - 1];
return val;
}, '');
text = text.trim() || '???';
if (opt_maxLength) {
// TODO: Improve truncation so that text from this block is given
// priority. E.g. "1+2+3+4+5+6+7+8+9=0" should be "...6+7+8+9=0", not
// "1+2+3+4+5...". E.g. "1+2+3+4+5=6+7+8+9+0" should be "...4+5=6+7...".
if (text.length > opt_maxLength) {
text = text.substring(0, opt_maxLength - 3) + '...';
}
}
return text;
}
/**
* Converts this block into string tokens.
*
* @param emptyToken The token to use in place of an empty input.
* Defaults to '?'.
* @returns The array of string tokens representing this block.
*/
private toTokens(emptyToken = '?'): string[] {
const tokens = [];
const emptyFieldPlaceholder = opt_emptyToken || '?';
// Temporarily set flag to navigate to all fields.
const prevNavigateFields = ASTNode.NAVIGATE_ALL_FIELDS;
ASTNode.NAVIGATE_ALL_FIELDS = true;
let node = ASTNode.createBlockNode(this);
const rootNode = node;
/**
* Whether or not to add parentheses around an input.
*
@@ -1416,84 +1459,26 @@ export class Block implements IASTNodeLocation, IDeletable {
(checks.indexOf('Boolean') !== -1 || checks.indexOf('Number') !== -1);
}
/** Check that we haven't circled back to the original root node. */
function checkRoot() {
if (node && node.getType() === rootNode?.getType() &&
node.getLocation() === rootNode?.getLocation()) {
node = null;
for (const input of this.inputList) {
if (input.name == constants.COLLAPSED_INPUT_NAME) {
continue;
}
}
// Traverse the AST building up our text string.
while (node) {
switch (node.getType()) {
case ASTNode.types.INPUT: {
const connection = node.getLocation() as Connection;
if (!node.in()) {
tokens.push(emptyFieldPlaceholder);
} else if (shouldAddParentheses(connection)) {
tokens.push('(');
}
break;
}
case ASTNode.types.FIELD: {
const field = node.getLocation() as Field;
if (field.name !== constants.COLLAPSED_FIELD_NAME) {
tokens.push(field.getText());
}
break;
}
for (const field of input.fieldRow) {
tokens.push(field.getText());
}
const current = node;
node = current.in() || current.next();
if (!node) {
// Can't go in or next, keep going out until we can go next.
node = current.out();
checkRoot();
while (node && !node.next()) {
node = node.out();
checkRoot();
// If we hit an input on the way up, possibly close out parentheses.
if (node && node.getType() === ASTNode.types.INPUT &&
shouldAddParentheses(node.getLocation() as Connection)) {
tokens.push(')');
}
}
if (node) {
node = node.next();
if (input.connection) {
const child = input.connection.targetBlock();
if (child) {
const shouldAddParens = shouldAddParentheses(input.connection);
if (shouldAddParens) tokens.push('(');
tokens.push(...child.toTokens(emptyToken));
if (shouldAddParens) tokens.push(')');
} else {
tokens.push(emptyToken);
}
}
}
// Restore state of NAVIGATE_ALL_FIELDS.
ASTNode.NAVIGATE_ALL_FIELDS = prevNavigateFields;
// Run through our text array and simplify expression to remove parentheses
// around single field blocks.
// E.g. ['repeat', '(', '10', ')', 'times', 'do', '?']
for (let i = 2; i < tokens.length; i++) {
if (tokens[i - 2] === '(' && tokens[i] === ')') {
tokens[i - 2] = tokens[i - 1];
tokens.splice(i - 1, 2);
}
}
// Join the text array, removing spaces around added parentheses.
let text: string = tokens.reduce(function(acc, value) {
return acc + (acc.substr(-1) === '(' || value === ')' ? '' : ' ') + value;
}, '');
text = text.trim() || '???';
if (opt_maxLength) {
// TODO: Improve truncation so that text from this block is given
// priority. E.g. "1+2+3+4+5+6+7+8+9=0" should be "...6+7+8+9=0", not
// "1+2+3+4+5...". E.g. "1+2+3+4+5=6+7+8+9+0" should be "...4+5=6+7...".
if (text.length > opt_maxLength) {
text = text.substring(0, opt_maxLength - 3) + '...';
}
}
return text;
return tokens;
}
/**
@@ -1933,10 +1918,7 @@ export class Block implements IASTNodeLocation, IDeletable {
if (type === inputTypes.STATEMENT) {
this.statementInputCount++;
}
// AnyDuringMigration because: Argument of type 'Connection | null' is not
// assignable to parameter of type 'Connection'.
const input =
new Input(type, name, this, (connection as AnyDuringMigration));
const input = new Input(type, name, this, connection);
// Append input to list.
this.inputList.push(input);
return input;
@@ -2080,9 +2062,7 @@ export class Block implements IASTNodeLocation, IDeletable {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this, 'comment', null, this.commentModel.text, text));
this.commentModel.text = text;
// AnyDuringMigration because: Type 'string | null' is not assignable to
// type 'string | Comment'.
this.comment = text as AnyDuringMigration; // For backwards compatibility.
this.comment = text; // For backwards compatibility.
}
/**

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Methods animating a block on connection and disconnection.
*
* @namespace Blockly.blockAnimations
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.blockAnimations');
@@ -28,18 +23,20 @@ interface CloneRect {
/** PID of disconnect UI animation. There can only be one at a time. */
let disconnectPid: ReturnType<typeof setTimeout>|null = null;
/** SVG group of wobbling block. There can only be one at a time. */
let disconnectGroup: SVGElement|null = null;
/** The wobbling block. There can only be one at a time. */
let wobblingBlock: BlockSvg|null = null;
/**
* Play some UI effects (sound, animation) when disposing of a block.
*
* @param block The block being disposed of.
* @alias Blockly.blockAnimations.disposeUiEffect
* @internal
*/
export function disposeUiEffect(block: BlockSvg) {
// Disposing is going to take so long the animation won't play anyway.
if (block.getDescendants(false).length > 100) return;
const workspace = block.workspace;
const svgGroup = block.getSvgRoot();
workspace.getAudioManager().play('delete');
@@ -88,7 +85,6 @@ function disposeUiStep(
* Play some UI effects (sound, ripple) after a connection has been established.
*
* @param block The block being connected.
* @alias Blockly.blockAnimations.connectionUiEffect
* @internal
*/
export function connectionUiEffect(block: BlockSvg) {
@@ -135,8 +131,8 @@ function connectionUiStep(ripple: SVGElement, start: Date, scale: number) {
if (percent > 1) {
dom.removeNode(ripple);
} else {
ripple.setAttribute('r', (percent * 25 * scale).toString());
ripple.style.opacity = (1 - percent).toString();
ripple.setAttribute('r', String(percent * 25 * scale));
ripple.style.opacity = String(1 - percent);
disconnectPid = setTimeout(connectionUiStep, 10, ripple, start, scale);
}
}
@@ -145,7 +141,6 @@ function connectionUiStep(ripple: SVGElement, start: Date, scale: number) {
* Play some UI effects (sound, animation) when disconnecting a block.
*
* @param block The block being disconnected.
* @alias Blockly.blockAnimations.disconnectUiEffect
* @internal
*/
export function disconnectUiEffect(block: BlockSvg) {
@@ -163,18 +158,18 @@ export function disconnectUiEffect(block: BlockSvg) {
magnitude *= -1;
}
// Start the animation.
disconnectGroup = block.getSvgRoot();
disconnectUiStep(disconnectGroup, magnitude, new Date());
wobblingBlock = block;
disconnectUiStep(block, magnitude, new Date());
}
/**
* Animate a brief wiggle of a disconnected block.
*
* @param group SVG element to animate.
* @param block Block to animate.
* @param magnitude Maximum degrees skew (reversed for RTL).
* @param start Date of animation's start.
*/
function disconnectUiStep(group: SVGElement, magnitude: number, start: Date) {
function disconnectUiStep(block: BlockSvg, magnitude: number, start: Date) {
const DURATION = 200; // Milliseconds.
const WIGGLES = 3; // Half oscillations.
@@ -186,29 +181,25 @@ function disconnectUiStep(group: SVGElement, magnitude: number, start: Date) {
const val = Math.round(
Math.sin(percent * Math.PI * WIGGLES) * (1 - percent) * magnitude);
skew = `skewX(${val})`;
disconnectPid = setTimeout(disconnectUiStep, 10, group, magnitude, start);
disconnectPid = setTimeout(disconnectUiStep, 10, block, magnitude, start);
}
(group as AnyDuringMigration).skew_ = skew;
group.setAttribute(
'transform',
(group as AnyDuringMigration).translate_ +
(group as AnyDuringMigration).skew_);
block.getSvgRoot().setAttribute(
'transform', `${block.getTranslation()} ${skew}`);
}
/**
* Stop the disconnect UI animation immediately.
*
* @alias Blockly.blockAnimations.disconnectUiStop
* @internal
*/
export function disconnectUiStop() {
if (disconnectGroup) {
if (disconnectPid) {
clearTimeout(disconnectPid);
}
const group = disconnectGroup;
(group as AnyDuringMigration).skew_ = '';
group.setAttribute('transform', (group as AnyDuringMigration).translate_);
disconnectGroup = null;
if (!wobblingBlock) return;
if (disconnectPid) {
clearTimeout(disconnectPid);
disconnectPid = null;
}
wobblingBlock.getSvgRoot().setAttribute(
'transform', wobblingBlock.getTranslation());
wobblingBlock = null;
}

View File

@@ -34,8 +34,6 @@ import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Class for a block dragger. It moves blocks around the workspace when they
* are being dragged by a mouse or touch.
*
* @alias Blockly.BlockDragger
*/
export class BlockDragger implements IBlockDragger {
/** The top block in the stack that is being dragged. */
@@ -241,7 +239,7 @@ export class BlockDragger implements IBlockDragger {
// These are expensive and don't need to be done if we're deleting.
this.draggingBlock_.setDragging(false);
if (delta) { // !preventMove
this.updateBlockAfterMove_(delta);
this.updateBlockAfterMove_();
} else {
// Blocks dragged directly from a flyout may need to be bumped into
// bounds.
@@ -294,18 +292,14 @@ export class BlockDragger implements IBlockDragger {
/**
* Updates the necessary information to place a block at a certain location.
*
* @param delta The change in location from where the block started the drag
* to where it ended the drag.
*/
protected updateBlockAfterMove_(delta: Coordinate) {
this.draggingBlock_.moveConnections(delta.x, delta.y);
protected updateBlockAfterMove_() {
this.fireMoveEvent_();
if (this.draggedConnectionManager_.wouldConnectBlock()) {
// Applying connections also rerenders the relevant blocks.
this.draggedConnectionManager_.applyConnections();
} else {
this.draggingBlock_.render();
this.draggingBlock_.queueRender();
}
this.draggingBlock_.scheduleSnapAndBump();
}

View File

@@ -56,13 +56,12 @@ import * as svgMath from './utils/svg_math.js';
import {Warning} from './warning.js';
import type {Workspace} from './workspace.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import {queueRender} from './render_management.js';
/**
* Class for a block's SVG representation.
* Not normally called directly, workspace.newBlock() is preferred.
*
* @alias Blockly.BlockSvg
*/
export class BlockSvg extends Block implements IASTNodeLocationSvg,
IBoundedElement, ICopyable,
@@ -79,12 +78,11 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* the block.
*/
static readonly COLLAPSED_WARNING_ID = 'TEMP_COLLAPSED_WARNING_';
override decompose?: ((p1: Workspace) => BlockSvg);
override decompose?: (p1: Workspace) => BlockSvg;
// override compose?: ((p1: BlockSvg) => void)|null;
saveConnections?: ((p1: BlockSvg) => AnyDuringMigration);
saveConnections?: (p1: BlockSvg) => void;
customContextMenu?:
((p1: Array<ContextMenuOption|LegacyContextMenuOption>) =>
AnyDuringMigration)|null;
(p1: Array<ContextMenuOption|LegacyContextMenuOption>) => void;
/**
* An property used internally to reference the block's rendering debugger.
@@ -125,6 +123,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
/** @internal */
pathObject: IPathObject;
override rendered = false;
private visuallyDisabled = false;
/**
* Is this block currently rendering? Used to stop recursive render calls
@@ -144,6 +143,24 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
override previousConnection!: RenderedConnection;
private readonly useDragSurface_: boolean;
private translation = '';
/**
* The ID of the setTimeout callback for bumping neighbours, or 0 if no bump
* is currently scheduled.
*/
private bumpNeighboursPid = 0;
/**
* The location of the top left of this block (in workspace coordinates)
* relative to either its parent block, or the workspace origin if it has no
* parent.
*
* @internal
*/
relativeCoords = new Coordinate(0, 0);
/**
* @param workspace The block's workspace.
* @param prototypeName Name of the language object containing type-specific
@@ -154,36 +171,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
constructor(workspace: WorkspaceSvg, prototypeName: string, opt_id?: string) {
super(workspace, prototypeName, opt_id);
this.workspace = workspace;
/**
* An optional method called when a mutator dialog is first opened.
* This function must create and initialize a top-level block for the
* mutator dialog, and return it. This function should also populate this
* top-level block with any sub-blocks which are appropriate. This method
* must also be coupled with defining a `compose` method for the default
* mutation dialog button and UI to appear.
*/
this.decompose = this.decompose;
/**
* An optional method called when a mutator dialog saves its content.
* This function is called to modify the original block according to new
* settings. This method must also be coupled with defining a `decompose`
* method for the default mutation dialog button and UI to appear.
*/
this.compose = this.compose;
/**
* An optional method called by the default mutator UI which gives the block
* a chance to save information about what child blocks are connected to
* what mutated connections.
*/
this.saveConnections = this.saveConnections;
/** An optional method for defining custom block context menu items. */
this.customContextMenu = this.customContextMenu;
this.svgGroup_ = dom.createSvgElement(Svg.G, {});
(this.svgGroup_ as AnyDuringMigration).translate_ = '';
/** A block style object. */
this.style = workspace.getRenderer().getConstants().getBlockStyle(null);
@@ -199,7 +187,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
this.useDragSurface_ = !!workspace.getBlockDragSurface();
const svgPath = this.pathObject.svgPath;
(svgPath as AnyDuringMigration).tooltip = this;
(svgPath as any).tooltip = this;
Tooltip.bindMouseEvents(svgPath);
// Expose this block's ID on its top-level SVG group.
@@ -348,9 +336,6 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
const oldXY = this.getRelativeToSurfaceXY();
if (newParent) {
(newParent as BlockSvg).getSvgRoot().appendChild(svgRoot);
const newXY = this.getRelativeToSurfaceXY();
// Move the connections to match the child's new position.
this.moveConnections(newXY.x - oldXY.x, newXY.y - oldXY.y);
} else if (oldParent) {
// If we are losing a parent, we want to move our DOM element to the
// root of the workspace.
@@ -435,8 +420,18 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @param y The y coordinate of the translation in workspace units.
*/
translate(x: number, y: number) {
this.getSvgRoot().setAttribute(
'transform', 'translate(' + x + ',' + y + ')');
this.translation = `translate(${x}, ${y})`;
this.relativeCoords = new Coordinate(x, y);
this.getSvgRoot().setAttribute('transform', this.getTranslation());
}
/**
* Returns the SVG translation of this block.
*
* @internal
*/
getTranslation(): string {
return this.translation;
}
/**
@@ -506,13 +501,8 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
this.workspace.getBlockDragSurface()!.translateSurface(
newLoc.x, newLoc.y);
} else {
(this.svgGroup_ as AnyDuringMigration).translate_ =
'translate(' + newLoc.x + ',' + newLoc.y + ')';
(this.svgGroup_ as AnyDuringMigration)
.setAttribute(
'transform',
(this.svgGroup_ as AnyDuringMigration).translate_ +
(this.svgGroup_ as AnyDuringMigration).skew_);
this.translate(newLoc.x, newLoc.y);
this.getSvgRoot().setAttribute('transform', this.getTranslation());
}
}
@@ -598,13 +588,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
return;
}
super.setCollapsed(collapsed);
if (!collapsed) {
this.updateCollapsed_();
} else if (this.rendered) {
this.render();
// Don't bump neighbours. Users like to store collapsed functions together
// and bumping makes them go out of alignment.
}
this.updateCollapsed_();
}
/**
@@ -708,11 +692,8 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
if (this.workspace.options.readOnly || !this.contextMenu) {
return null;
}
// AnyDuringMigration because: Argument of type '{ block: this; }' is not
// assignable to parameter of type 'Scope'.
const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
ContextMenuRegistry.ScopeType.BLOCK,
{block: this} as AnyDuringMigration);
ContextMenuRegistry.ScopeType.BLOCK, {block: this});
// Allow the block to add or modify menuOptions.
if (this.customContextMenu) {
@@ -775,9 +756,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*/
setDragging(adding: boolean) {
if (adding) {
const group = this.getSvgRoot();
(group as AnyDuringMigration).translate_ = '';
(group as AnyDuringMigration).skew_ = '';
this.translation = '';
common.draggingConnections.push(...this.getConnections_(true));
dom.addClass(this.svgGroup_, 'blocklyDragging');
} else {
@@ -862,59 +841,39 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @suppress {checkTypes}
*/
override dispose(healStack?: boolean, animate?: boolean) {
if (this.isDeadOrDying()) {
return;
}
if (this.isDeadOrDying()) return;
Tooltip.dispose();
Tooltip.unbindMouseEvents(this.pathObject.svgPath);
dom.startTextWidthCache();
// Save the block's workspace temporarily so we can resize the
// contents once the block is disposed.
const blockWorkspace = this.workspace;
// If this block is being dragged, unlink the mouse events.
if (common.getSelected() === this) {
this.unselect();
this.workspace.cancelCurrentGesture();
}
// If this block has a context menu open, close it.
if (ContextMenu.getCurrentBlock() === this) {
ContextMenu.hide();
}
ContextMenu.hide();
if (animate && this.rendered) {
this.unplug(healStack);
blockAnimations.disposeUiEffect(this);
}
// Stop rerendering.
this.rendered = false;
// Clear pending warnings.
for (const n of this.warningTextDb.values()) {
clearTimeout(n);
}
this.warningTextDb.clear();
const icons = this.getIcons();
for (let i = 0; i < icons.length; i++) {
icons[i].dispose();
}
// Just deleting this block from the DOM would result in a memory leak as
// well as corruption of the connection database. Therefore we must
// methodically step through the blocks and carefully disassemble them.
if (common.getSelected() === this) {
common.setSelected(null);
}
super.dispose(!!healStack);
dom.removeNode(this.svgGroup_);
blockWorkspace.resizeContents();
// Sever JavaScript to DOM connections.
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGGElement'.
this.svgGroup_ = null as AnyDuringMigration;
dom.stopTextWidthCache();
}
/**
* Disposes of this block without doing things required by the top block.
* E.g. does trigger UI effects, remove nodes, etc.
*/
override disposeInternal() {
if (this.isDeadOrDying()) return;
super.disposeInternal();
this.rendered = false;
if (common.getSelected() === this) {
this.unselect();
this.workspace.cancelCurrentGesture();
}
[...this.warningTextDb.values()].forEach((n) => clearTimeout(n));
this.warningTextDb.clear();
this.getIcons().forEach((i) => i.dispose());
}
/**
@@ -987,15 +946,12 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @internal
*/
updateDisabled() {
const children = (this.getChildren(false));
const disabled = !this.isEnabled() || this.getInheritedDisabled();
if (this.visuallyDisabled === disabled) return;
this.applyColour();
if (this.isCollapsed()) {
return;
}
for (let i = 0, child; child = children[i]; i++) {
if (child.rendered) {
child.updateDisabled();
}
this.visuallyDisabled = disabled;
for (const child of this.getChildren(false)) {
child.updateDisabled();
}
}
@@ -1015,8 +971,6 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @param text The text, or null to delete.
*/
override setCommentText(text: string|null) {
// AnyDuringMigration because: Property 'get' does not exist on type
// '(name: string) => void'.
if (this.commentModel.text === text) {
return;
}
@@ -1291,7 +1245,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
super.setPreviousStatement(newBoolean, opt_check);
if (this.rendered) {
this.render();
this.queueRender();
this.bumpNeighbours();
}
}
@@ -1308,7 +1262,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
super.setNextStatement(newBoolean, opt_check);
if (this.rendered) {
this.render();
this.queueRender();
this.bumpNeighbours();
}
}
@@ -1324,7 +1278,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
super.setOutput(newBoolean, opt_check);
if (this.rendered) {
this.render();
this.queueRender();
this.bumpNeighbours();
}
}
@@ -1338,7 +1292,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
super.setInputsInline(newBoolean);
if (this.rendered) {
this.render();
this.queueRender();
this.bumpNeighbours();
}
}
@@ -1356,7 +1310,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
const removed = super.removeInput(name, opt_quiet);
if (this.rendered) {
this.render();
this.queueRender();
// Removing an input will cause the block to change shape.
this.bumpNeighbours();
}
@@ -1374,7 +1328,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
super.moveNumberedInputBefore(inputIndex, refIndex);
if (this.rendered) {
this.render();
this.queueRender();
// Moving an input will cause the block to change shape.
this.bumpNeighbours();
}
@@ -1392,7 +1346,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
const input = super.appendInput_(type, name);
if (this.rendered) {
this.render();
this.queueRender();
// Adding an input will cause the block to change shape.
this.bumpNeighbours();
}
@@ -1450,7 +1404,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*
* @param all If true, return all connections even hidden ones.
* Otherwise, for a non-rendered block return an empty list, and for a
* collapsed block don't return inputs connections.
* collapsed block don't return inputs connections.
* @returns Array of connections.
* @internal
*/
@@ -1542,7 +1496,16 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* up on screen, because that creates confusion for end-users.
*/
override bumpNeighbours() {
this.getRootBlock().bumpNeighboursInternal();
if (this.bumpNeighboursPid) return;
const group = eventUtils.getGroup();
this.bumpNeighboursPid = setTimeout(() => {
const oldGroup = eventUtils.getGroup();
eventUtils.setGroup(group);
this.getRootBlock().bumpNeighboursInternal();
eventUtils.setGroup(oldGroup);
this.bumpNeighboursPid = 0;
}, config.bumpDelay);
}
/**
@@ -1597,11 +1560,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
eventUtils.setGroup(false);
}, config.bumpDelay / 2);
setTimeout(() => {
eventUtils.setGroup(group);
this.bumpNeighbours();
eventUtils.setGroup(false);
}, config.bumpDelay);
this.bumpNeighbours();
}
/**
@@ -1650,7 +1609,17 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
}
/**
* Lays out and reflows a block based on its contents and settings.
* Triggers a rerender after a delay to allow for batching.
*
* @internal
*/
queueRender() {
queueRender(this);
}
/**
* Immediately lays out and reflows a block based on its contents and
* settings.
*
* @param opt_bubble If false, just render this block.
* If true, also render block's parent, grandparent, etc. Defaults to true.
@@ -1668,7 +1637,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
this.updateCollapsed_();
}
this.workspace.getRenderer().render(this);
this.updateConnectionLocations_();
this.updateConnectionLocations();
if (opt_bubble !== false) {
const parentBlock = this.getParent();
@@ -1687,6 +1656,43 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
}
}
/**
* Renders this block in a way that's compatible with the more efficient
* render management system.
*
* @internal
*/
renderEfficiently() {
this.rendered = true;
dom.startTextWidthCache();
if (this.isCollapsed()) {
this.updateCollapsed_();
}
this.workspace.getRenderer().render(this);
this.tightenChildrenEfficiently();
dom.stopTextWidthCache();
this.updateMarkers_();
}
/**
* Tightens all children of this block so they are snuggly rendered against
* their parent connections.
*
* Does not update connection locations, so that they can be updated more
* efficiently by the render management system.
*
* @internal
*/
tightenChildrenEfficiently() {
for (const input of this.inputList) {
const conn = input.connection as RenderedConnection;
if (conn) conn.tightenEfficiently();
}
if (this.nextConnection) this.nextConnection.tightenEfficiently();
}
/** Redraw any attached marker or cursor svgs if needed. */
protected updateMarkers_() {
if (this.workspace.keyboardAccessibilityMode && this.pathObject.cursorSvg) {
@@ -1696,14 +1702,21 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
// TODO(#4592): Update all markers on the block.
this.workspace.getMarker(MarkerManager.LOCAL_MARKER)!.draw();
}
for (const input of this.inputList) {
for (const field of input.fieldRow) {
field.updateMarkers_();
}
}
}
/**
* Update all of the connections on this block with the new locations
* calculated during rendering. Also move all of the connected blocks based
* on the new connection locations.
*
* @internal
*/
private updateConnectionLocations_() {
updateConnectionLocations() {
const blockTL = this.getRelativeToSurfaceXY();
// Don't tighten previous or output connections because they are inferior
// connections.

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* The top level namespace used to access the Blockly library.
*
* @namespace Blockly
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly');
@@ -53,19 +48,19 @@ import {DragTarget} from './drag_target.js';
import * as dropDownDiv from './dropdowndiv.js';
import * as Events from './events/events.js';
import * as Extensions from './extensions.js';
import {Field, FieldValidator} from './field.js';
import {FieldAngle, FieldAngleValidator} from './field_angle.js';
import {FieldCheckbox, FieldCheckboxValidator} from './field_checkbox.js';
import {FieldColour, FieldColourValidator} from './field_colour.js';
import {FieldDropdown, FieldDropdownValidator, MenuGenerator, MenuGeneratorFunction, MenuOption} from './field_dropdown.js';
import {FieldImage} from './field_image.js';
import {FieldLabel} from './field_label.js';
import {Field, FieldConfig, FieldValidator, UnattachedFieldError} from './field.js';
import {FieldAngle, FieldAngleConfig, FieldAngleFromJsonConfig, FieldAngleValidator} from './field_angle.js';
import {FieldCheckbox, FieldCheckboxConfig, FieldCheckboxFromJsonConfig, FieldCheckboxValidator} from './field_checkbox.js';
import {FieldColour, FieldColourConfig, FieldColourFromJsonConfig, FieldColourValidator} from './field_colour.js';
import {FieldDropdown, FieldDropdownConfig, FieldDropdownFromJsonConfig, FieldDropdownValidator, MenuGenerator, MenuGeneratorFunction, MenuOption} from './field_dropdown.js';
import {FieldImage, FieldImageConfig, FieldImageFromJsonConfig} from './field_image.js';
import {FieldLabel, FieldLabelConfig, FieldLabelFromJsonConfig} from './field_label.js';
import {FieldLabelSerializable} from './field_label_serializable.js';
import {FieldMultilineInput, FieldMultilineInputValidator} from './field_multilineinput.js';
import {FieldNumber, FieldNumberValidator} from './field_number.js';
import {FieldMultilineInput, FieldMultilineInputConfig, FieldMultilineInputFromJsonConfig, FieldMultilineInputValidator} from './field_multilineinput.js';
import {FieldNumber, FieldNumberConfig, FieldNumberFromJsonConfig, FieldNumberValidator} from './field_number.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInput, FieldTextInputValidator} from './field_textinput.js';
import {FieldVariable, FieldVariableValidator} from './field_variable.js';
import {FieldTextInput, FieldTextInputConfig, FieldTextInputFromJsonConfig, FieldTextInputValidator} from './field_textinput.js';
import {FieldVariable, FieldVariableConfig, FieldVariableFromJsonConfig, FieldVariableValidator} from './field_variable.js';
import {Flyout} from './flyout_base.js';
import {FlyoutButton} from './flyout_button.js';
import {HorizontalFlyout} from './flyout_horizontal.js';
@@ -99,6 +94,7 @@ import {IFlyout} from './interfaces/i_flyout.js';
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 {IPositionable} from './interfaces/i_positionable.js';
import {IRegistrable} from './interfaces/i_registrable.js';
import {ISelectable} from './interfaces/i_selectable.js';
@@ -106,6 +102,7 @@ import {ISelectableToolboxItem} from './interfaces/i_selectable_toolbox_item.js'
import {IStyleable} from './interfaces/i_styleable.js';
import {IToolbox} from './interfaces/i_toolbox.js';
import {IToolboxItem} from './interfaces/i_toolbox_item.js';
import {IVariableBackedParameterModel, isVariableBackedParameterModel} from './interfaces/i_variable_backed_parameter_model.js';
import * as internalConstants from './internal_constants.js';
import {ASTNode} from './keyboard_nav/ast_node.js';
import {BasicCursor} from './keyboard_nav/basic_cursor.js';
@@ -176,7 +173,6 @@ import {ZoomControls} from './zoom_controls.js';
* compiler to override this constant.
*
* @define {string}
* @alias Blockly.VERSION
*/
export const VERSION = 'uncompiled';
@@ -194,19 +190,16 @@ export const VERSION = 'uncompiled';
/**
* @see Blockly.Input.Align.LEFT
* @alias Blockly.ALIGN_LEFT
*/
export const ALIGN_LEFT = Align.LEFT;
/**
* @see Blockly.Input.Align.CENTRE
* @alias Blockly.ALIGN_CENTRE
*/
export const ALIGN_CENTRE = Align.CENTRE;
/**
* @see Blockly.Input.Align.RIGHT
* @alias Blockly.ALIGN_RIGHT
*/
export const ALIGN_RIGHT = Align.RIGHT;
/*
@@ -215,31 +208,26 @@ export const ALIGN_RIGHT = Align.RIGHT;
/**
* @see ConnectionType.INPUT_VALUE
* @alias Blockly.INPUT_VALUE
*/
export const INPUT_VALUE = ConnectionType.INPUT_VALUE;
/**
* @see ConnectionType.OUTPUT_VALUE
* @alias Blockly.OUTPUT_VALUE
*/
export const OUTPUT_VALUE = ConnectionType.OUTPUT_VALUE;
/**
* @see ConnectionType.NEXT_STATEMENT
* @alias Blockly.NEXT_STATEMENT
*/
export const NEXT_STATEMENT = ConnectionType.NEXT_STATEMENT;
/**
* @see ConnectionType.PREVIOUS_STATEMENT
* @alias Blockly.PREVIOUS_STATEMENT
*/
export const PREVIOUS_STATEMENT = ConnectionType.PREVIOUS_STATEMENT;
/**
* @see inputTypes.DUMMY_INPUT
* @alias Blockly.DUMMY_INPUT
*/
export const DUMMY_INPUT = inputTypes.DUMMY;
@@ -247,25 +235,21 @@ export const DUMMY_INPUT = inputTypes.DUMMY;
/**
* @see toolbox.Position.TOP
* @alias Blockly.TOOLBOX_AT_TOP
*/
export const TOOLBOX_AT_TOP = toolbox.Position.TOP;
/**
* @see toolbox.Position.BOTTOM
* @alias Blockly.TOOLBOX_AT_BOTTOM
*/
export const TOOLBOX_AT_BOTTOM = toolbox.Position.BOTTOM;
/**
* @see toolbox.Position.LEFT
* @alias Blockly.TOOLBOX_AT_LEFT
*/
export const TOOLBOX_AT_LEFT = toolbox.Position.LEFT;
/**
* @see toolbox.Position.RIGHT
* @alias Blockly.TOOLBOX_AT_RIGHT
*/
export const TOOLBOX_AT_RIGHT = toolbox.Position.RIGHT;
@@ -282,7 +266,6 @@ export const TOOLBOX_AT_RIGHT = toolbox.Position.RIGHT;
*
* @param workspace Any workspace in the SVG.
* @see Blockly.common.svgResize
* @alias Blockly.svgResize
*/
export const svgResize = common.svgResize;
@@ -291,7 +274,6 @@ export const svgResize = common.svgResize;
*
* @param opt_onlyClosePopups Whether only popups should be closed.
* @see Blockly.WorkspaceSvg.hideChaff
* @alias Blockly.hideChaff
*/
export function hideChaff(opt_onlyClosePopups?: boolean) {
(common.getMainWorkspace() as WorkspaceSvg).hideChaff(opt_onlyClosePopups);
@@ -303,14 +285,11 @@ export function hideChaff(opt_onlyClosePopups?: boolean) {
* Blockly instances on a page.
*
* @see Blockly.common.getMainWorkspace
* @alias Blockly.getMainWorkspace
*/
export const getMainWorkspace = common.getMainWorkspace;
/**
* Returns the currently selected copyable object.
*
* @alias Blockly.common.getSelected
*/
export const getSelected = common.getSelected;
@@ -320,7 +299,6 @@ export const getSelected = common.getSelected;
*
* @param jsonArray An array of JSON block definitions.
* @see Blockly.common.defineBlocksWithJsonArray
* @alias Blockly.defineBlocksWithJsonArray
*/
export const defineBlocksWithJsonArray = common.defineBlocksWithJsonArray;
@@ -332,7 +310,6 @@ export const defineBlocksWithJsonArray = common.defineBlocksWithJsonArray;
*
* @param container The container element.
* @see Blockly.common.setParentContainer
* @alias Blockly.setParentContainer
*/
export const setParentContainer = common.setParentContainer;
@@ -343,7 +320,6 @@ export const setParentContainer = common.setParentContainer;
* @param workspace The workspace to resize.
* @deprecated Use **workspace.resizeContents** instead.
* @see Blockly.WorkspaceSvg.resizeContents
* @alias Blockly.resizeSvgContents
*/
function resizeSvgContentsLocal(workspace: WorkspaceSvg) {
deprecation.warn(
@@ -359,7 +335,6 @@ export const resizeSvgContents = resizeSvgContentsLocal;
* @param toCopy Block or Workspace Comment to be copied.
* @deprecated Use **Blockly.clipboard.copy** instead.
* @see Blockly.clipboard.copy
* @alias Blockly.copy
*/
export function copy(toCopy: ICopyable) {
deprecation.warn(
@@ -374,7 +349,6 @@ export function copy(toCopy: ICopyable) {
* @returns True if the paste was successful, false otherwise.
* @deprecated Use **Blockly.clipboard.paste** instead.
* @see Blockly.clipboard.paste
* @alias Blockly.paste
*/
export function paste(): boolean {
deprecation.warn(
@@ -389,7 +363,6 @@ export function paste(): boolean {
* @param toDuplicate Block or Workspace Comment to be copied.
* @deprecated Use **Blockly.clipboard.duplicate** instead.
* @see Blockly.clipboard.duplicate
* @alias Blockly.duplicate
*/
export function duplicate(toDuplicate: ICopyable) {
deprecation.warn(
@@ -405,7 +378,6 @@ export function duplicate(toDuplicate: ICopyable) {
* @returns True if number, false otherwise.
* @deprecated Use **Blockly.utils.string.isNumber** instead.
* @see Blockly.utils.string.isNumber
* @alias Blockly.isNumber
*/
export function isNumber(str: string): boolean {
deprecation.warn(
@@ -421,7 +393,6 @@ export function isNumber(str: string): boolean {
* @returns RGB code, e.g. '#5ba65b'.
* @deprecated Use **Blockly.utils.colour.hueToHex** instead.
* @see Blockly.utils.colour.hueToHex
* @alias Blockly.hueToHex
*/
export function hueToHex(hue: number): string {
deprecation.warn(
@@ -443,7 +414,6 @@ export function hueToHex(hue: number): string {
* @returns Opaque data that can be passed to unbindEvent_.
* @deprecated Use **Blockly.browserEvents.bind** instead.
* @see Blockly.browserEvents.bind
* @alias Blockly.bindEvent_
*/
export function bindEvent_(
node: EventTarget, name: string, thisObject: Object|null,
@@ -462,7 +432,6 @@ export function bindEvent_(
* @returns The function call.
* @deprecated Use **Blockly.browserEvents.unbind** instead.
* @see browserEvents.unbind
* @alias Blockly.unbindEvent_
*/
export function unbindEvent_(bindData: browserEvents.Data): Function {
deprecation.warn(
@@ -488,7 +457,6 @@ export function unbindEvent_(bindData: browserEvents.Data): Function {
* @returns Opaque data that can be passed to unbindEvent_.
* @deprecated Use **Blockly.browserEvents.conditionalBind** instead.
* @see browserEvents.conditionalBind
* @alias Blockly.bindEventWithChecks_
*/
export function bindEventWithChecks_(
node: EventTarget, name: string, thisObject: Object|null, func: Function,
@@ -514,8 +482,6 @@ export const COLLAPSED_FIELD_NAME = constants.COLLAPSED_FIELD_NAME;
* String for use in the "custom" attribute of a category in toolbox XML.
* This string indicates that the category should be dynamically populated with
* variable blocks.
*
* @alias Blockly.VARIABLE_CATEGORY_NAME
*/
export const VARIABLE_CATEGORY_NAME: string = Variables.CATEGORY_NAME;
@@ -523,8 +489,6 @@ export const VARIABLE_CATEGORY_NAME: string = Variables.CATEGORY_NAME;
* String for use in the "custom" attribute of a category in toolbox XML.
* This string indicates that the category should be dynamically populated with
* variable blocks.
*
* @alias Blockly.VARIABLE_DYNAMIC_CATEGORY_NAME
*/
export const VARIABLE_DYNAMIC_CATEGORY_NAME: string =
VariablesDynamic.CATEGORY_NAME;
@@ -532,8 +496,6 @@ export const VARIABLE_DYNAMIC_CATEGORY_NAME: string =
* String for use in the "custom" attribute of a category in toolbox XML.
* This string indicates that the category should be dynamically populated with
* procedure blocks.
*
* @alias Blockly.PROCEDURE_CATEGORY_NAME
*/
export const PROCEDURE_CATEGORY_NAME: string = Procedures.CATEGORY_NAME;
@@ -643,24 +605,61 @@ export {Cursor};
export {DeleteArea};
export {DragTarget};
export const DropDownDiv = dropDownDiv;
export {Field, FieldValidator};
export {FieldAngle, FieldAngleValidator};
export {FieldCheckbox, FieldCheckboxValidator};
export {FieldColour, FieldColourValidator};
export {Field, FieldConfig, FieldValidator, UnattachedFieldError};
export {
FieldAngle,
FieldAngleConfig,
FieldAngleFromJsonConfig,
FieldAngleValidator,
};
export {
FieldCheckbox,
FieldCheckboxConfig,
FieldCheckboxFromJsonConfig,
FieldCheckboxValidator,
};
export {
FieldColour,
FieldColourConfig,
FieldColourFromJsonConfig,
FieldColourValidator,
};
export {
FieldDropdown,
FieldDropdownConfig,
FieldDropdownFromJsonConfig,
FieldDropdownValidator,
MenuGenerator,
MenuGeneratorFunction,
MenuOption,
};
export {FieldImage};
export {FieldLabel};
export {FieldImage, FieldImageConfig, FieldImageFromJsonConfig};
export {FieldLabel, FieldLabelConfig, FieldLabelFromJsonConfig};
export {FieldLabelSerializable};
export {FieldMultilineInput, FieldMultilineInputValidator};
export {FieldNumber, FieldNumberValidator};
export {FieldTextInput, FieldTextInputValidator};
export {FieldVariable, FieldVariableValidator};
export {
FieldMultilineInput,
FieldMultilineInputConfig,
FieldMultilineInputFromJsonConfig,
FieldMultilineInputValidator,
};
export {
FieldNumber,
FieldNumberConfig,
FieldNumberFromJsonConfig,
FieldNumberValidator,
};
export {
FieldTextInput,
FieldTextInputConfig,
FieldTextInputFromJsonConfig,
FieldTextInputValidator,
};
export {
FieldVariable,
FieldVariableConfig,
FieldVariableFromJsonConfig,
FieldVariableValidator,
};
export {Flyout};
export {FlyoutButton};
export {FlyoutMetricsManager};
@@ -693,6 +692,7 @@ export {IMetricsManager};
export {IMovable};
export {Input};
export {InsertionMarkerManager};
export {IObservable, isObservable};
export {IPositionable};
export {IRegistrable};
export {ISelectable};
@@ -700,6 +700,7 @@ export {ISelectableToolboxItem};
export {IStyleable};
export {IToolbox};
export {IToolboxItem};
export {IVariableBackedParameterModel, isVariableBackedParameterModel};
export {Marker};
export {MarkerManager};
export {Menu};

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Object that defines user-specified options for the workspace.
*
* @namespace Blockly.BlocklyOptions
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.BlocklyOptions');
@@ -19,8 +14,6 @@ import type {ToolboxDefinition} from './utils/toolbox.js';
/**
* Blockly options.
*
* @alias Blockly.BlocklyOptions
*/
export interface BlocklyOptions {
collapse?: boolean;

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* A mapping of block type names to block prototype objects.
*
* @namespace Blockly.blocks
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.blocks');
@@ -21,7 +16,5 @@ export type BlockDefinition = AnyDuringMigration;
/**
* A mapping of block type names to block prototype objects.
*
* @alias Blockly.blocks.Blocks
*/
export const Blocks: {[key: string]: BlockDefinition} = Object.create(null);

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Browser event handling.
*
* @namespace Blockly.browserEvents
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.browserEvents');
@@ -20,8 +15,6 @@ import * as userAgent from './utils/useragent.js';
/**
* Blockly opaque event data used to unbind events when using
* `bind` and `conditionalBind`.
*
* @alias Blockly.browserEvents.Data
*/
export type Data = [EventTarget, string, (e: Event) => void][];
@@ -54,7 +47,6 @@ const PAGE_MODE_MULTIPLIER = 125;
* simultaneous touches. False by default.
* @param opt_noPreventDefault No-op, deprecated and will be removed in v10.
* @returns Opaque data that can be passed to unbindEvent_.
* @alias Blockly.browserEvents.conditionalBind
*/
export function conditionalBind(
node: EventTarget, name: string, thisObject: Object|null, func: Function,
@@ -105,7 +97,6 @@ export function conditionalBind(
* @param thisObject The value of 'this' in the function.
* @param func Function to call when event is triggered.
* @returns Opaque data that can be passed to unbindEvent_.
* @alias Blockly.browserEvents.bind
*/
export function bind(
node: EventTarget, name: string, thisObject: Object|null,
@@ -142,7 +133,6 @@ export function bind(
* @param bindData Opaque data from bindEvent_.
* This list is emptied during the course of calling this function.
* @returns The function call.
* @alias Blockly.browserEvents.unbind
*/
export function unbind(bindData: Data): (e: Event) => void {
// Accessing an element of the last property of the array is unsafe if the
@@ -150,10 +140,7 @@ export function unbind(bindData: Data): (e: Event) => void {
// should only pass Data from bind or conditionalBind.
const callback = bindData[bindData.length - 1][2];
while (bindData.length) {
const bindDatum = bindData.pop();
const node = bindDatum![0];
const name = bindDatum![1];
const func = bindDatum![2];
const [node, name, func] = bindData.pop()!;
node.removeEventListener(name, func, false);
}
return callback;
@@ -164,7 +151,6 @@ export function unbind(bindData: Data): (e: Event) => void {
*
* @param e An event.
* @returns True if text input.
* @alias Blockly.browserEvents.isTargetInput
*/
export function isTargetInput(e: Event): boolean {
if (e.target instanceof HTMLElement) {
@@ -194,7 +180,6 @@ export function isTargetInput(e: Event): boolean {
*
* @param e Mouse event.
* @returns True if right-click.
* @alias Blockly.browserEvents.isRightButton
*/
export function isRightButton(e: MouseEvent): boolean {
if (e.ctrlKey && userAgent.MAC) {
@@ -213,7 +198,6 @@ export function isRightButton(e: MouseEvent): boolean {
* @param svg SVG element.
* @param matrix Inverted screen CTM to use.
* @returns Object with .x and .y properties.
* @alias Blockly.browserEvents.mouseToSvg
*/
export function mouseToSvg(
e: MouseEvent, svg: SVGSVGElement, matrix: SVGMatrix|null): SVGPoint {
@@ -232,7 +216,6 @@ export function mouseToSvg(
*
* @param e Mouse event.
* @returns Scroll delta object with .x and .y properties.
* @alias Blockly.browserEvents.getScrollDeltaPixels
*/
export function getScrollDeltaPixels(e: WheelEvent): {x: number, y: number} {
switch (e.deltaMode) {

View File

@@ -30,8 +30,6 @@ import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Class for UI bubble.
*
* @alias Blockly.Bubble
*/
export class Bubble implements IBubble {
/** Width of the border around the bubble. */
@@ -220,27 +218,26 @@ export class Bubble implements IBubble {
'blocklyResizeSE',
},
this.bubbleGroup);
const resizeSize = 2 * Bubble.BORDER_WIDTH;
const size = 2 * Bubble.BORDER_WIDTH;
dom.createSvgElement(
Svg.POLYGON,
{'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())},
Svg.POLYGON, {'points': `0,${size} ${size},${size} ${size},0`},
this.resizeGroup);
dom.createSvgElement(
Svg.LINE, {
'class': 'blocklyResizeLine',
'x1': resizeSize / 3,
'y1': resizeSize - 1,
'x2': resizeSize - 1,
'y2': resizeSize / 3,
'x1': size / 3,
'y1': size - 1,
'x2': size - 1,
'y2': size / 3,
},
this.resizeGroup);
dom.createSvgElement(
Svg.LINE, {
'class': 'blocklyResizeLine',
'x1': resizeSize * 2 / 3,
'y1': resizeSize - 1,
'x2': resizeSize - 1,
'y2': resizeSize * 2 / 3,
'x1': size * 2 / 3,
'y1': size - 1,
'x2': size - 1,
'y2': size * 2 / 3,
},
this.resizeGroup);
} else {
@@ -662,8 +659,8 @@ export class Bubble implements IBubble {
height = Math.max(height, doubleBorderWidth + 20);
this.width = width;
this.height = height;
this.bubbleBack?.setAttribute('width', width.toString());
this.bubbleBack?.setAttribute('height', height.toString());
this.bubbleBack?.setAttribute('width', `${width}`);
this.bubbleBack?.setAttribute('height', `${height}`);
if (this.resizeGroup) {
if (this.workspace_.RTL) {
// Mirror the resize group.
@@ -903,8 +900,7 @@ export class Bubble implements IBubble {
textElement = paragraphElement.childNodes[i] as SVGTSpanElement;
i++) {
textElement.setAttribute('text-anchor', 'end');
textElement.setAttribute(
'x', (maxWidth + Bubble.BORDER_WIDTH).toString());
textElement.setAttribute('x', String(maxWidth + Bubble.BORDER_WIDTH));
}
}
return bubble;

View File

@@ -28,8 +28,6 @@ import type {WorkspaceSvg} from './workspace_svg.js';
* Class for a bubble dragger. It moves things on the bubble canvas around the
* workspace when they are being dragged by a mouse or touch. These can be
* block comments, mutators, warnings, or workspace comments.
*
* @alias Blockly.BubbleDragger
*/
export class BubbleDragger {
/** Which drag target the mouse pointer is over, if any. */

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Utilities for bumping objects back into worksapce bounds.
*
* @namespace Blockly.bumpObjects
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.bumpObjects');
@@ -35,7 +30,6 @@ import type {WorkspaceSvg} from './workspace_svg.js';
* in workspace coordinates.
* @param object The object to bump.
* @returns True if block was bumped.
* @alias Blockly.bumpObjects.bumpIntoBounds
*/
function bumpObjectIntoBounds(
workspace: WorkspaceSvg, scrollMetrics: ContainerRegion,
@@ -87,7 +81,6 @@ export const bumpIntoBounds = bumpObjectIntoBounds;
*
* @param workspace The workspace to handle.
* @returns The event handler.
* @alias Blockly.bumpObjects.bumpIntoBoundsHandler
*/
export function bumpIntoBoundsHandler(workspace: WorkspaceSvg):
(p1: Abstract) => void {
@@ -107,7 +100,7 @@ export function bumpIntoBoundsHandler(workspace: WorkspaceSvg):
return;
}
// Handle undo.
const oldGroup = eventUtils.getGroup();
const existingGroup = eventUtils.getGroup() || false;
eventUtils.setGroup(e.group);
const wasBumped = bumpObjectIntoBounds(
@@ -118,9 +111,7 @@ export function bumpIntoBoundsHandler(workspace: WorkspaceSvg):
'Moved object in bounds but there was no' +
' event group. This may break undo.');
}
if (oldGroup !== null) {
eventUtils.setGroup(oldGroup);
}
eventUtils.setGroup(existingGroup);
} else if (e.type === eventUtils.VIEWPORT_CHANGE) {
const viewportEvent = (e as ViewportChange);
if (viewportEvent.scale && viewportEvent.oldScale &&
@@ -167,7 +158,6 @@ function extractObjectFromEvent(
* Bumps the top objects in the given workspace into bounds.
*
* @param workspace The workspace.
* @alias Blockly.bumpObjects.bumpTopObjectsIntoBounds
*/
export function bumpTopObjectsIntoBounds(workspace: WorkspaceSvg) {
const metricsManager = workspace.getMetricsManager();

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Blockly's internal clipboard for managing copy-paste.
*
* @namespace Blockly.clipboard
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.clipboard');
@@ -22,7 +17,6 @@ let copyData: CopyData|null = null;
* Copy a block or workspace comment onto the local clipboard.
*
* @param toCopy Block or Workspace Comment to be copied.
* @alias Blockly.clipboard.copy
* @internal
*/
export function copy(toCopy: ICopyable) {
@@ -40,7 +34,6 @@ function copyInternal(toCopy: ICopyable) {
* Paste a block or workspace comment on to the main workspace.
*
* @returns The pasted thing if the paste was successful, null otherwise.
* @alias Blockly.clipboard.paste
* @internal
*/
export function paste(): ICopyable|null {
@@ -66,7 +59,6 @@ export function paste(): ICopyable|null {
* @param toDuplicate Block or Workspace Comment to be duplicated.
* @returns The block or workspace comment that was duplicated, or null if the
* duplication failed.
* @alias Blockly.clipboard.duplicate
* @internal
*/
export function duplicate(toDuplicate: ICopyable): ICopyable|null {

View File

@@ -32,8 +32,6 @@ import {Svg} from './utils/svg.js';
/**
* Class for a comment.
*
* @alias Blockly.Comment
*/
export class Comment extends Icon {
private readonly model: CommentModel;
@@ -44,17 +42,12 @@ export class Comment extends Icon {
*/
private cachedText: string|null = '';
/** Mouse up event data. */
private onMouseUpWrapper: browserEvents.Data|null = null;
/** Wheel event data. */
private onWheelWrapper: browserEvents.Data|null = null;
/** Change event data. */
private onChangeWrapper: browserEvents.Data|null = null;
/** Input event data. */
private onInputWrapper: browserEvents.Data|null = null;
/**
* Array holding info needed to unbind events.
* Used for disposing.
* Ex: [[node, name, func], [node, name, func]].
*/
private boundEvents: browserEvents.Data[] = [];
/**
* The SVG element that contains the text edit area, or null if not created.
@@ -149,14 +142,14 @@ export class Comment extends Icon {
body.appendChild(textarea);
this.foreignObject!.appendChild(body);
this.onMouseUpWrapper = browserEvents.conditionalBind(
textarea, 'focus', this, this.startEdit, true);
this.boundEvents.push(browserEvents.conditionalBind(
textarea, 'focus', this, this.startEdit, true));
// Don't zoom with mousewheel.
this.onWheelWrapper = browserEvents.conditionalBind(
this.boundEvents.push(browserEvents.conditionalBind(
textarea, 'wheel', this, function(e: Event) {
e.stopPropagation();
});
this.onChangeWrapper = browserEvents.conditionalBind(
}));
this.boundEvents.push(browserEvents.conditionalBind(
textarea, 'change', this,
/**
* @param _e Unused event parameter.
@@ -167,15 +160,15 @@ export class Comment extends Icon {
this.getBlock(), 'comment', null, this.cachedText,
this.model.text));
}
});
this.onInputWrapper = browserEvents.conditionalBind(
}));
this.boundEvents.push(browserEvents.conditionalBind(
textarea, 'input', this,
/**
* @param _e Unused event parameter.
*/
function(this: Comment, _e: Event) {
this.model.text = textarea.value;
});
}));
setTimeout(textarea.focus.bind(textarea), 0);
@@ -279,22 +272,10 @@ export class Comment extends Icon {
* Dispose of the bubble.
*/
private disposeBubble() {
if (this.onMouseUpWrapper) {
browserEvents.unbind(this.onMouseUpWrapper);
this.onMouseUpWrapper = null;
}
if (this.onWheelWrapper) {
browserEvents.unbind(this.onWheelWrapper);
this.onWheelWrapper = null;
}
if (this.onChangeWrapper) {
browserEvents.unbind(this.onChangeWrapper);
this.onChangeWrapper = null;
}
if (this.onInputWrapper) {
browserEvents.unbind(this.onInputWrapper);
this.onInputWrapper = null;
for (const event of this.boundEvents) {
browserEvents.unbind(event);
}
this.boundEvents.length = 0;
if (this.bubble_) {
this.bubble_.dispose();
this.bubble_ = null;

View File

@@ -4,12 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Common functions used both internally and externally, but which
* must not be at the top level to avoid circular dependencies.
*
* @namespace Blockly.common
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.common');
@@ -79,7 +73,6 @@ let mainWorkspace: Workspace;
* page.
*
* @returns The main workspace.
* @alias Blockly.common.getMainWorkspace
*/
export function getMainWorkspace(): Workspace {
return mainWorkspace;
@@ -89,7 +82,6 @@ export function getMainWorkspace(): Workspace {
* Sets last used main workspace.
*
* @param workspace The most recently used top level workspace.
* @alias Blockly.common.setMainWorkspace
*/
export function setMainWorkspace(workspace: Workspace) {
mainWorkspace = workspace;
@@ -102,8 +94,6 @@ let selected: ICopyable|null = null;
/**
* Returns the currently selected copyable object.
*
* @alias Blockly.common.getSelected
*/
export function getSelected(): ICopyable|null {
return selected;
@@ -115,7 +105,6 @@ export function getSelected(): ICopyable|null {
* programmatically select a block, use `BlockSvg#select`.
*
* @param newSelection The newly selected block.
* @alias Blockly.common.setSelected
* @internal
*/
export function setSelected(newSelection: ICopyable|null) {
@@ -132,7 +121,6 @@ let parentContainer: Element|null;
* Tooltip.
*
* @returns The parent container.
* @alias Blockly.common.getParentContainer
*/
export function getParentContainer(): Element|null {
return parentContainer;
@@ -145,7 +133,6 @@ export function getParentContainer(): Element|null {
* This method is a NOP if called after the first `Blockly.inject`.
*
* @param newParent The container element.
* @alias Blockly.common.setParentContainer
*/
export function setParentContainer(newParent: Element) {
parentContainer = newParent;
@@ -159,7 +146,6 @@ export function setParentContainer(newParent: Element) {
* Record the height/width of the SVG image.
*
* @param workspace Any workspace in the SVG.
* @alias Blockly.common.svgResize
*/
export function svgResize(workspace: WorkspaceSvg) {
let mainWorkspace = workspace;
@@ -201,7 +187,6 @@ export const draggingConnections: Connection[] = [];
* statements (blocks that are not inside a value or statement input
* of the block).
* @returns Map of types to type counts for descendants of the bock.
* @alias Blockly.common.getBlockTypeCounts
*/
export function getBlockTypeCounts(
block: Block, opt_stripFollowing?: boolean): {[key: string]: number} {
@@ -243,7 +228,6 @@ function jsonInitFactory(jsonDef: AnyDuringMigration): () => void {
* by the Blockly Developer Tools.
*
* @param jsonArray An array of JSON block definitions.
* @alias Blockly.common.defineBlocksWithJsonArray
*/
export function defineBlocksWithJsonArray(jsonArray: AnyDuringMigration[]) {
TEST_ONLY.defineBlocksWithJsonArrayInternal(jsonArray);
@@ -263,7 +247,6 @@ function defineBlocksWithJsonArrayInternal(jsonArray: AnyDuringMigration[]) {
* @param jsonArray An array of JSON block definitions.
* @returns A map of the block
* definitions created.
* @alias Blockly.common.defineBlocksWithJsonArray
*/
export function createBlockDefinitionsFromJsonArray(
jsonArray: AnyDuringMigration[]): {[key: string]: BlockDefinition} {
@@ -292,7 +275,6 @@ export function createBlockDefinitionsFromJsonArray(
*
* @param blocks A map of block
* type names to block definitions.
* @alias Blockly.common.defineBlocks
*/
export function defineBlocks(blocks: {[key: string]: BlockDefinition}) {
// Iterate over own enumerable properties.

View File

@@ -43,8 +43,6 @@ class Capability<_T> {
/**
* Manager for all items registered with the workspace.
*
* @alias Blockly.ComponentManager
*/
export class ComponentManager {
static Capability = Capability;
@@ -120,7 +118,7 @@ export class ComponentManager {
'Plugin "' + id + 'already has capability "' + capability + '"');
return;
}
capability = String(capability).toLowerCase();
capability = `${capability}`.toLowerCase();
this.componentData.get(id)?.capabilities.push(capability);
this.capabilityToComponentIds.get(capability)?.push(id);
}
@@ -143,7 +141,7 @@ export class ComponentManager {
'" to remove');
return;
}
capability = String(capability).toLowerCase();
capability = `${capability}`.toLowerCase();
arrayUtils.removeElem(this.componentData.get(id)!.capabilities, capability);
arrayUtils.removeElem(this.capabilityToComponentIds.get(capability)!, id);
}
@@ -156,7 +154,7 @@ export class ComponentManager {
* @returns Whether the component has the capability.
*/
hasCapability<T>(id: string, capability: string|Capability<T>): boolean {
capability = String(capability).toLowerCase();
capability = `${capability}`.toLowerCase();
return this.componentData.has(id) &&
this.componentData.get(id)!.capabilities.indexOf(capability) !== -1;
}
@@ -180,7 +178,7 @@ export class ComponentManager {
*/
getComponents<T extends IComponent>(
capability: string|Capability<T>, sorted: boolean): T[] {
capability = String(capability).toLowerCase();
capability = `${capability}`.toLowerCase();
const componentIds = this.capabilityToComponentIds.get(capability);
if (!componentIds) {
return [];

View File

@@ -4,13 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* All the values that we expect developers to be able to change
* before injecting Blockly. Changing these values during run time is not
* generally recommended.
*
* @namespace Blockly.config
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.config');
@@ -39,7 +32,6 @@ export const config: Config = {
/**
* Number of pixels the mouse must move before a drag starts.
*
* @alias Blockly.config.dragRadius
*/
dragRadius: 5,
/**
@@ -47,20 +39,17 @@ export const config: Config = {
* flyout. Because the drag-intention is determined when this is reached, it
* is larger than dragRadius so that the drag-direction is clearer.
*
* @alias Blockly.config.flyoutDragRadius
*/
flyoutDragRadius: 10,
/**
* Maximum misalignment between connections for them to snap together.
*
* @alias Blockly.config.snapRadius
*/
snapRadius: DEFAULT_SNAP_RADIUS,
/**
* Maximum misalignment between connections for them to snap together.
* This should be the same as the snap radius.
*
* @alias Blockly.config.connectingSnapRadius
*/
connectingSnapRadius: DEFAULT_SNAP_RADIUS,
/**
@@ -69,13 +58,11 @@ export const config: Config = {
* this much closer to the matching connection on the block than it actually
* is.
*
* @alias Blockly.config.currentConnectionPreference
*/
currentConnectionPreference: 8,
/**
* Delay in ms between trigger and bumping unconnected block out of alignment.
*
* @alias Blockly.config.bumpDelay
*/
bumpDelay: 250,
};

View File

@@ -25,8 +25,6 @@ import * as Xml from './xml.js';
/**
* Class for a connection between blocks.
*
* @alias Blockly.Connection
*/
export class Connection implements IASTNodeLocationWithBlock {
/** Constants for checking whether two connections are compatible. */
@@ -95,7 +93,7 @@ export class Connection implements IASTNodeLocationWithBlock {
// Make sure the childConnection is available.
if (childConnection.isConnected()) {
childConnection.disconnect();
childConnection.disconnectInternal(false);
}
// Make sure the parentConnection is available.
@@ -106,7 +104,7 @@ export class Connection implements IASTNodeLocationWithBlock {
if (target!.isShadow()) {
target!.dispose(false);
} else {
this.disconnect();
this.disconnectInternal();
orphan = target;
}
this.applyShadowState_(shadowState);
@@ -129,8 +127,9 @@ export class Connection implements IASTNodeLocationWithBlock {
if (orphan) {
const orphanConnection = this.type === INPUT ? orphan.outputConnection :
orphan.previousConnection;
if (!orphanConnection) return;
const connection = Connection.getConnectionForOrphanedConnection(
childBlock, (orphanConnection));
childBlock, orphanConnection);
if (connection) {
orphanConnection.connect(connection);
} else {
@@ -151,7 +150,7 @@ export class Connection implements IASTNodeLocationWithBlock {
this.setShadowStateInternal_();
const targetBlock = this.targetBlock();
if (targetBlock) {
if (targetBlock && !targetBlock.isDeadOrDying()) {
// Disconnect the attached normal block.
targetBlock.unplug();
}
@@ -224,8 +223,8 @@ export class Connection implements IASTNodeLocationWithBlock {
const checker = this.getConnectionChecker();
if (checker.canConnect(this, otherConnection, false)) {
const eventGroup = eventUtils.getGroup();
if (!eventGroup) {
const existingGroup = eventUtils.getGroup();
if (!existingGroup) {
eventUtils.setGroup(true);
}
// Determine which block is superior (higher in the source stack).
@@ -236,75 +235,83 @@ export class Connection implements IASTNodeLocationWithBlock {
// Inferior block.
otherConnection.connect_(this);
}
if (!eventGroup) {
eventUtils.setGroup(false);
}
eventUtils.setGroup(existingGroup);
}
return this.isConnected();
}
/** Disconnect this connection. */
/**
* Disconnect this connection.
*/
disconnect() {
const otherConnection = this.targetConnection;
if (!otherConnection) {
throw Error('Source connection not connected.');
}
if (otherConnection.targetConnection !== this) {
throw Error('Target connection not connected to source connection.');
}
let parentBlock;
let childBlock;
let parentConnection;
if (this.isSuperior()) {
// Superior block.
parentBlock = this.sourceBlock_;
childBlock = otherConnection.getSourceBlock();
/* eslint-disable-next-line @typescript-eslint/no-this-alias */
parentConnection = this;
} else {
// Inferior block.
parentBlock = otherConnection.getSourceBlock();
childBlock = this.sourceBlock_;
parentConnection = otherConnection;
}
const eventGroup = eventUtils.getGroup();
if (!eventGroup) {
eventUtils.setGroup(true);
}
this.disconnectInternal_(parentBlock, childBlock);
if (!childBlock.isShadow()) {
// If we were disconnecting a shadow, no need to spawn a new one.
parentConnection.respawnShadow_();
}
if (!eventGroup) {
eventUtils.setGroup(false);
}
this.disconnectInternal();
}
/**
* Disconnect two blocks that are connected by this connection.
*
* @param parentBlock The superior block.
* @param childBlock The inferior block.
* @param setParent Whether to set the parent of the disconnected block or
* not, defaults to true.
* If you do not set the parent, ensure that a subsequent action does,
* otherwise the view and model will be out of sync.
*/
protected disconnectInternal_(parentBlock: Block, childBlock: Block) {
protected disconnectInternal(setParent = true) {
const {parentConnection, childConnection} =
this.getParentAndChildConnections();
if (!parentConnection || !childConnection) {
throw Error('Source connection not connected.');
}
const existingGroup = eventUtils.getGroup();
if (!existingGroup) {
eventUtils.setGroup(true);
}
let event;
if (eventUtils.isEnabled()) {
event =
new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock) as BlockMove;
event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(
childConnection.getSourceBlock()) as BlockMove;
}
const otherConnection = this.targetConnection;
if (otherConnection) {
otherConnection.targetConnection = null;
}
this.targetConnection = null;
childBlock.setParent(null);
if (setParent) childConnection.getSourceBlock().setParent(null);
if (event) {
event.recordNew();
eventUtils.fire(event);
}
if (!childConnection.getSourceBlock().isShadow()) {
// If we were disconnecting a shadow, no need to spawn a new one.
parentConnection.respawnShadow_();
}
eventUtils.setGroup(existingGroup);
}
/**
* Returns the parent connection (superior) and child connection (inferior)
* given this connection and the connection it is connected to.
*
* @returns The parent connection and child connection, given this connection
* and the connection it is connected to.
*/
protected getParentAndChildConnections():
{parentConnection?: Connection, childConnection?: Connection} {
if (!this.targetConnection) return {};
if (this.isSuperior()) {
return {
parentConnection: this,
childConnection: this.targetConnection,
};
}
return {
parentConnection: this.targetConnection,
childConnection: this,
};
}
/**
@@ -545,6 +552,7 @@ export class Connection implements IASTNodeLocationWithBlock {
}
} else if (target.isShadow()) {
target.dispose(false);
if (this.getSourceBlock().isDeadOrDying()) return;
this.respawnShadow_();
if (this.targetBlock() && this.targetBlock()!.isShadow()) {
this.serializeShadow_(this.targetBlock());
@@ -678,11 +686,11 @@ function getSingleConnection(block: Block, orphanBlock: Block): Connection|
null {
let foundConnection = null;
const output = orphanBlock.outputConnection;
const typeChecker = output.getConnectionChecker();
const typeChecker = output?.getConnectionChecker();
for (let i = 0, input; input = block.inputList[i]; i++) {
const connection = input.connection;
if (connection && typeChecker.canConnect(output, connection, false)) {
if (connection && typeChecker?.canConnect(output, connection, false)) {
if (foundConnection) {
return null; // More than one connection.
}

View File

@@ -24,8 +24,6 @@ import type {RenderedConnection} from './rendered_connection.js';
/**
* Class for connection type checking logic.
*
* @alias Blockly.ConnectionChecker
*/
export class ConnectionChecker implements IConnectionChecker {
/**
@@ -203,7 +201,7 @@ export class ConnectionChecker implements IConnectionChecker {
/**
* Check whether this connection can be made by dragging.
*
* @param a Connection to compare.
* @param a Connection to compare (on the block that's being dragged).
* @param b Connection to compare against.
* @param distance The maximum allowable distance between connections.
* @returns True if the connection is allowed during a drag.
@@ -250,6 +248,12 @@ export class ConnectionChecker implements IConnectionChecker {
!b.targetBlock()!.isShadow() && b.targetBlock()!.nextConnection) {
return false;
}
// Don't offer to splice into a stack where the connected block is
// immovable.
if (b.targetBlock() && !b.targetBlock()!.isMovable()) {
return false;
}
break;
}
default:

View File

@@ -24,8 +24,6 @@ import type {Coordinate} from './utils/coordinate.js';
* Database of connections.
* Connections are stored in order of their vertical component. This way
* connections in an area may be looked up quickly using a binary search.
*
* @alias Blockly.ConnectionDB
*/
export class ConnectionDB {
/** Array of connections sorted by y position in workspace units. */

View File

@@ -4,19 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* An enum for the possible types of connections.
*
* @namespace Blockly.ConnectionType
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ConnectionType');
/**
* Enum for the type of a connection or input.
*
* @alias Blockly.ConnectionType
*/
export enum ConnectionType {
// A right-facing value input. E.g. 'set item to' or 'return'.

View File

@@ -4,25 +4,16 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Blockly constants.
*
* @namespace Blockly.constants
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.constants');
/**
* The language-neutral ID given to the collapsed input.
*
* @alias Blockly.constants.COLLAPSED_INPUT_NAME
*/
export const COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT';
/**
* The language-neutral ID given to the collapsed field.
*
* @alias Blockly.constants.COLLAPSED_FIELD_NAME
*/
export const COLLAPSED_FIELD_NAME = '_TEMP_COLLAPSED_FIELD';

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Functionality for the right-click context menus.
*
* @namespace Blockly.ContextMenu
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ContextMenu');
@@ -44,7 +39,6 @@ const dummyOwner = {};
* Gets the block the context menu is currently attached to.
*
* @returns The block the context menu is attached to.
* @alias Blockly.ContextMenu.getCurrentBlock
*/
export function getCurrentBlock(): Block|null {
return currentBlock;
@@ -54,7 +48,6 @@ export function getCurrentBlock(): Block|null {
* Sets the block the context menu is currently attached to.
*
* @param block The block the context menu is attached to.
* @alias Blockly.ContextMenu.setCurrentBlock
*/
export function setCurrentBlock(block: Block|null) {
currentBlock = block;
@@ -71,7 +64,6 @@ let menu_: Menu|null = null;
* @param e Mouse event.
* @param options Array of menu options.
* @param rtl True if RTL, false if LTR.
* @alias Blockly.ContextMenu.show
*/
export function show(
e: Event, options: (ContextMenuOption|LegacyContextMenuOption)[],
@@ -120,11 +112,15 @@ function populate_(
if (option.enabled) {
const actionHandler = function() {
hide();
// If .scope does not exist on the option, then the callback will not
// be expecting a scope parameter, so there should be no problems. Just
// assume it is a ContextMenuOption and we'll pass undefined if it's
// not.
option.callback((option as ContextMenuOption).scope);
requestAnimationFrame(() => {
setTimeout(() => {
// If .scope does not exist on the option, then the callback
// will not be expecting a scope parameter, so there should be
// no problems. Just assume it is a ContextMenuOption and we'll
// pass undefined if it's not.
option.callback((option as ContextMenuOption).scope);
}, 0);
});
};
menuItem.onAction(actionHandler, {});
}
@@ -200,8 +196,6 @@ function haltPropagation(e: Event) {
/**
* Hide the context menu.
*
* @alias Blockly.ContextMenu.hide
*/
export function hide() {
WidgetDiv.hideIfOwner(dummyOwner);
@@ -210,8 +204,6 @@ export function hide() {
/**
* Dispose of the menu.
*
* @alias Blockly.ContextMenu.dispose
*/
export function dispose() {
if (menu_) {
@@ -227,7 +219,6 @@ export function dispose() {
* @param block Original block.
* @param xml XML representation of new block.
* @returns Function that creates a block.
* @alias Blockly.ContextMenu.callbackFactory
*/
export function callbackFactory(block: Block, xml: Element): Function {
return () => {
@@ -263,7 +254,6 @@ export function callbackFactory(block: Block, xml: Element): Function {
* right-click originated.
* @returns A menu option,
* containing text, enabled, and a callback.
* @alias Blockly.ContextMenu.commentDeleteOption
* @internal
*/
export function commentDeleteOption(comment: WorkspaceCommentSvg):
@@ -287,7 +277,6 @@ export function commentDeleteOption(comment: WorkspaceCommentSvg):
* right-click originated.
* @returns A menu option,
* containing text, enabled, and a callback.
* @alias Blockly.ContextMenu.commentDuplicateOption
* @internal
*/
export function commentDuplicateOption(comment: WorkspaceCommentSvg):
@@ -311,7 +300,6 @@ export function commentDuplicateOption(comment: WorkspaceCommentSvg):
* @returns A menu option, containing text, enabled, and a callback.
* @suppress {strictModuleDepCheck,checkTypes} Suppress checks while workspace
* comments are not bundled in.
* @alias Blockly.ContextMenu.workspaceCommentOption
* @internal
*/
export function workspaceCommentOption(

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Registers default context menu items.
*
* @namespace Blockly.ContextMenuItems
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ContextMenuItems');
@@ -20,14 +15,11 @@ import * as Events from './events/events.js';
import * as eventUtils from './events/utils.js';
import {inputTypes} from './input_types.js';
import {Msg} from './msg.js';
import * as idGenerator from './utils/idgenerator.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Option to undo previous action.
*
* @alias Blockly.ContextMenuItems.registerUndo
*/
export function registerUndo() {
const undoOption: RegistryItem = {
@@ -52,8 +44,6 @@ export function registerUndo() {
/**
* Option to redo previous action.
*
* @alias Blockly.ContextMenuItems.registerRedo
*/
export function registerRedo() {
const redoOption: RegistryItem = {
@@ -78,8 +68,6 @@ export function registerRedo() {
/**
* Option to clean up blocks.
*
* @alias Blockly.ContextMenuItems.registerCleanup
*/
export function registerCleanup() {
const cleanOption: RegistryItem = {
@@ -135,8 +123,6 @@ function toggleOption_(shouldCollapse: boolean, topBlocks: BlockSvg[]) {
/**
* Option to collapse all blocks.
*
* @alias Blockly.ContextMenuItems.registerCollapse
*/
export function registerCollapse() {
const collapseOption: RegistryItem = {
@@ -171,8 +157,6 @@ export function registerCollapse() {
/**
* Option to expand all blocks.
*
* @alias Blockly.ContextMenuItems.registerExpand
*/
export function registerExpand() {
const expandOption: RegistryItem = {
@@ -240,13 +224,18 @@ function getDeletableBlocks_(workspace: WorkspaceSvg): BlockSvg[] {
/**
* Deletes the given blocks. Used to delete all blocks in the workspace.
*
* @param deleteList list of blocks to delete.
* @param eventGroup event group ID with which all delete events should be
* associated.
* @param deleteList List of blocks to delete.
* @param eventGroup Event group ID with which all delete events should be
* associated. If not specified, create a new group.
*/
function deleteNext_(deleteList: BlockSvg[], eventGroup: string) {
function deleteNext_(deleteList: BlockSvg[], eventGroup?: string) {
const DELAY = 10;
eventUtils.setGroup(eventGroup);
if (eventGroup) {
eventUtils.setGroup(eventGroup);
} else {
eventUtils.setGroup(true);
eventGroup = eventUtils.getGroup();
}
const block = deleteList.shift();
if (block) {
if (!block.isDeadOrDying()) {
@@ -261,8 +250,6 @@ function deleteNext_(deleteList: BlockSvg[], eventGroup: string) {
/**
* Option to delete all blocks.
*
* @alias Blockly.ContextMenuItems.registerDeleteAll
*/
export function registerDeleteAll() {
const deleteOption: RegistryItem = {
@@ -273,10 +260,8 @@ export function registerDeleteAll() {
const deletableBlocksLength = getDeletableBlocks_(scope.workspace).length;
if (deletableBlocksLength === 1) {
return Msg['DELETE_BLOCK'];
} else {
return Msg['DELETE_X_BLOCKS'].replace(
'%1', String(deletableBlocksLength));
}
return Msg['DELETE_X_BLOCKS'].replace('%1', `${deletableBlocksLength}`);
},
preconditionFn(scope: Scope) {
if (!scope.workspace) {
@@ -291,16 +276,15 @@ export function registerDeleteAll() {
}
scope.workspace.cancelCurrentGesture();
const deletableBlocks = getDeletableBlocks_(scope.workspace);
const eventGroup = idGenerator.genUid();
if (deletableBlocks.length < 2) {
deleteNext_(deletableBlocks, eventGroup);
deleteNext_(deletableBlocks);
} else {
dialog.confirm(
Msg['DELETE_ALL_BLOCKS'].replace(
'%1', String(deletableBlocks.length)),
function(ok) {
if (ok) {
deleteNext_(deletableBlocks, eventGroup);
deleteNext_(deletableBlocks);
}
});
}
@@ -323,8 +307,6 @@ function registerWorkspaceOptions_() {
/**
* Option to duplicate a block.
*
* @alias Blockly.ContextMenuItems.registerDuplicate
*/
export function registerDuplicate() {
const duplicateOption: RegistryItem = {
@@ -355,8 +337,6 @@ export function registerDuplicate() {
/**
* Option to add or remove block-level comment.
*
* @alias Blockly.ContextMenuItems.registerComment
*/
export function registerComment() {
const commentOption: RegistryItem = {
@@ -393,8 +373,6 @@ export function registerComment() {
/**
* Option to inline variables.
*
* @alias Blockly.ContextMenuItems.registerInline
*/
export function registerInline() {
const inlineOption: RegistryItem = {
@@ -428,8 +406,6 @@ export function registerInline() {
/**
* Option to collapse or expand a block.
*
* @alias Blockly.ContextMenuItems.registerCollapseExpandBlock
*/
export function registerCollapseExpandBlock() {
const collapseExpandOption: RegistryItem = {
@@ -457,8 +433,6 @@ export function registerCollapseExpandBlock() {
/**
* Option to disable or enable a block.
*
* @alias Blockly.ContextMenuItems.registerDisable
*/
export function registerDisable() {
const disableOption: RegistryItem = {
@@ -479,14 +453,12 @@ export function registerDisable() {
},
callback(scope: Scope) {
const block = scope.block;
const group = eventUtils.getGroup();
if (!group) {
const existingGroup = eventUtils.getGroup();
if (!existingGroup) {
eventUtils.setGroup(true);
}
block!.setEnabled(!block!.isEnabled());
if (!group) {
eventUtils.setGroup(false);
}
eventUtils.setGroup(existingGroup);
},
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
id: 'blockDisable',
@@ -497,8 +469,6 @@ export function registerDisable() {
/**
* Option to delete a block.
*
* @alias Blockly.ContextMenuItems.registerDelete
*/
export function registerDelete() {
const deleteOption: RegistryItem = {
@@ -513,7 +483,7 @@ export function registerDelete() {
}
return descendantCount === 1 ?
Msg['DELETE_BLOCK'] :
Msg['DELETE_X_BLOCKS'].replace('%1', String(descendantCount));
Msg['DELETE_X_BLOCKS'].replace('%1', `${descendantCount}`);
},
preconditionFn(scope: Scope) {
if (!scope.block!.isInFlyout && scope.block!.isDeletable()) {
@@ -535,8 +505,6 @@ export function registerDelete() {
/**
* Option to open help for a block.
*
* @alias Blockly.ContextMenuItems.registerHelp
*/
export function registerHelp() {
const helpOption: RegistryItem = {
@@ -577,7 +545,6 @@ function registerBlockOptions_() {
* Registers all default context menu items. This should be called once per
* instance of ContextMenuRegistry.
*
* @alias Blockly.ContextMenuItems.registerDefaultOptions
* @internal
*/
export function registerDefaultOptions() {

View File

@@ -20,8 +20,6 @@ import type {WorkspaceSvg} from './workspace_svg.js';
* Class for the registry of context menu items. This is intended to be a
* singleton. You should not create a new instance, and only access this class
* from ContextMenuRegistry.registry.
*
* @alias Blockly.ContextMenuRegistry
*/
export class ContextMenuRegistry {
static registry: ContextMenuRegistry;

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Inject Blockly's CSS synchronously.
*
* @namespace Blockly.Css
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Css');
@@ -21,7 +16,6 @@ let injected = false;
* components such as fields and the toolbox to store separate CSS.
*
* @param cssContent Multiline CSS string or an array of single lines of CSS.
* @alias Blockly.Css.register
*/
export function register(cssContent: string) {
if (injected) {
@@ -40,7 +34,6 @@ export function register(cssContent: string) {
* @param hasCss If false, don't inject CSS (providing CSS becomes the
* document's responsibility).
* @param pathToMedia Path from page to the Blockly media directory.
* @alias Blockly.Css.inject
*/
export function inject(hasCss: boolean, pathToMedia: string) {
// Only inject the CSS once.
@@ -67,8 +60,6 @@ export function inject(hasCss: boolean, pathToMedia: string) {
/**
* The CSS content for Blockly.
*
* @alias Blockly.Css.content
*/
let content = `
.blocklySvg {
@@ -235,16 +226,11 @@ let content = `
}
.blocklyDraggable {
/* backup for browsers (e.g. IE11) that don't support grab */
cursor: url("<<<PATH>>>/handopen.cur"), auto;
cursor: grab;
cursor: -webkit-grab;
}
/* backup for browsers (e.g. IE11) that don't support grabbing */
.blocklyDragging {
/* backup for browsers (e.g. IE11) that don't support grabbing */
cursor: url("<<<PATH>>>/handclosed.cur"), auto;
cursor: grabbing;
cursor: -webkit-grabbing;
}
@@ -252,8 +238,6 @@ let content = `
/* Changes cursor on mouse down. Not effective in Firefox because of
https://bugzilla.mozilla.org/show_bug.cgi?id=771241 */
.blocklyDraggable:active {
/* backup for browsers (e.g. IE11) that don't support grabbing */
cursor: url("<<<PATH>>>/handclosed.cur"), auto;
cursor: grabbing;
cursor: -webkit-grabbing;
}
@@ -262,8 +246,6 @@ let content = `
ahead of block during a drag. This way the cursor is still a closed hand.
*/
.blocklyBlockDragSurface .blocklyDraggable {
/* backup for browsers (e.g. IE11) that don't support grabbing */
cursor: url("<<<PATH>>>/handclosed.cur"), auto;
cursor: grabbing;
cursor: -webkit-grabbing;
}
@@ -373,11 +355,15 @@ let content = `
box-sizing: border-box;
}
/* Edge and IE introduce a close icon when the input value is longer than a
certain length. This affects our sizing calculations of the text input.
Hiding the close icon to avoid that. */
.blocklyHtmlInput::-ms-clear {
display: none;
/* Remove the increase and decrease arrows on the field number editor */
input.blocklyHtmlInput[type=number]::-webkit-inner-spin-button,
input.blocklyHtmlInput[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
.blocklyMainBackground {

View File

@@ -22,8 +22,6 @@ import type {IDraggable} from './interfaces/i_draggable.js';
/**
* Abstract class for a component that can delete a block or bubble that is
* dropped on top of it.
*
* @alias Blockly.DeleteArea
*/
export class DeleteArea extends DragTarget implements IDeleteArea {
/**

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Wrapper functions around JS functions for showing alert/confirmation dialogs.
*
* @namespace Blockly.dialog
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.dialog');
@@ -37,7 +32,6 @@ let promptImplementation = function(
*
* @param message The message to display to the user.
* @param opt_callback The callback when the alert is dismissed.
* @alias Blockly.dialog.alert
*/
export function alert(message: string, opt_callback?: () => void) {
alertImplementation(message, opt_callback);
@@ -48,7 +42,6 @@ export function alert(message: string, opt_callback?: () => void) {
*
* @param alertFunction The function to be run.
* @see Blockly.dialog.alert
* @alias Blockly.dialog.setAlert
*/
export function setAlert(alertFunction: (p1: string, p2?: () => void) => void) {
alertImplementation = alertFunction;
@@ -60,7 +53,6 @@ export function setAlert(alertFunction: (p1: string, p2?: () => void) => void) {
*
* @param message The message to display to the user.
* @param callback The callback for handling user response.
* @alias Blockly.dialog.confirm
*/
export function confirm(message: string, callback: (p1: boolean) => void) {
TEST_ONLY.confirmInternal(message, callback);
@@ -79,7 +71,6 @@ function confirmInternal(message: string, callback: (p1: boolean) => void) {
*
* @param confirmFunction The function to be run.
* @see Blockly.dialog.confirm
* @alias Blockly.dialog.setConfirm
*/
export function setConfirm(
confirmFunction: (p1: string, p2: (p1: boolean) => void) => void) {
@@ -95,7 +86,6 @@ export function setConfirm(
* @param message The message to display to the user.
* @param defaultValue The value to initialize the prompt with.
* @param callback The callback for handling user response.
* @alias Blockly.dialog.prompt
*/
export function prompt(
message: string, defaultValue: string,
@@ -108,7 +98,6 @@ export function prompt(
*
* @param promptFunction The function to be run.
* @see Blockly.dialog.prompt
* @alias Blockly.dialog.setPrompt
*/
export function setPrompt(
promptFunction: (p1: string, p2: string, p3: (p1: string|null) => void) =>

View File

@@ -21,8 +21,6 @@ import type {Rect} from './utils/rect.js';
/**
* Abstract class for a component with custom behaviour when a block or bubble
* is dragged over or dropped on top of it.
*
* @alias Blockly.DragTarget
*/
export class DragTarget implements IDragTarget {
/**

View File

@@ -4,16 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Events fired as a result of actions in Blockly's editor.
*
* @namespace Blockly.Events
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events');
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js';
import {Abstract, AbstractEventJson} from './events_abstract.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
import {BlockChange, BlockChangeJson} from './events_block_change.js';
import {BlockCreate, BlockCreateJson} from './events_block_create.js';
@@ -28,16 +23,6 @@ import {CommentCreate, CommentCreateJson} from './events_comment_create.js';
import {CommentDelete} from './events_comment_delete.js';
import {CommentMove, CommentMoveJson} from './events_comment_move.js';
import {MarkerMove, MarkerMoveJson} from './events_marker_move.js';
import {ProcedureBase} from './events_procedure_base.js';
import {ProcedureChangeReturn} from './events_procedure_change_return.js';
import {ProcedureCreate} from './events_procedure_create.js';
import {ProcedureDelete} from './events_procedure_delete.js';
import {ProcedureEnable} from './events_procedure_enable.js';
import {ProcedureRename} from './events_procedure_rename.js';
import {ProcedureParameterBase} from './events_procedure_parameter_base.js';
import {ProcedureParameterCreate} from './events_procedure_parameter_create.js';
import {ProcedureParameterDelete} from './events_procedure_parameter_delete.js';
import {ProcedureParameterRename} from './events_procedure_parameter_rename.js';
import {Selected, SelectedJson} from './events_selected.js';
import {ThemeChange, ThemeChangeJson} from './events_theme_change.js';
import {ToolboxItemSelect, ToolboxItemSelectJson} from './events_toolbox_item_select.js';
@@ -54,7 +39,7 @@ import {FinishedLoading, FinishedLoadingJson} from './workspace_events.js';
// Events.
export const Abstract = AbstractEvent;
export {Abstract};
export {AbstractEventJson};
export {BubbleOpen};
export {BubbleOpenJson};
@@ -87,16 +72,6 @@ export {FinishedLoading};
export {FinishedLoadingJson};
export {MarkerMove};
export {MarkerMoveJson};
export {ProcedureBase};
export {ProcedureChangeReturn};
export {ProcedureCreate};
export {ProcedureDelete};
export {ProcedureEnable};
export {ProcedureRename};
export {ProcedureParameterBase};
export {ProcedureParameterCreate};
export {ProcedureParameterDelete};
export {ProcedureParameterRename};
export {Selected};
export {SelectedJson};
export {ThemeChange};

View File

@@ -22,8 +22,6 @@ import * as eventUtils from './utils.js';
/**
* Abstract class for an event.
*
* @alias Blockly.Events.Abstract
*/
export abstract class Abstract {
/**
@@ -34,7 +32,16 @@ export abstract class Abstract {
/** The workspace identifier for this event. */
workspaceId?: string = undefined;
/**
* An ID for the group of events this block is associated with.
*
* Groups define events that should be treated as an single action from the
* user's perspective, and should be undone together.
*/
group: string;
/** Whether this event is undoable or not. */
recordUndo: boolean;
/** Whether or not the event is a UI event. */
@@ -43,16 +50,8 @@ export abstract class Abstract {
/** Type of this event. */
type = '';
/** @alias Blockly.Events.Abstract */
constructor() {
/**
* The event group ID for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
*/
this.group = eventUtils.getGroup();
/** Sets whether the event should be added to the undo stack. */
this.recordUndo = eventUtils.getRecordUndo();
}
@@ -74,6 +73,9 @@ export abstract class Abstract {
* @param json JSON representation.
*/
fromJson(json: AbstractEventJson) {
deprecation.warn(
'Blockly.Events.Abstract.prototype.fromJson', 'version 9', 'version 10',
'Blockly.Events.fromJson');
this.isBlank = false;
this.group = json['group'] || '';
}
@@ -89,9 +91,6 @@ export abstract class Abstract {
*/
static fromJson(json: AbstractEventJson, workspace: Workspace, event: any):
Abstract {
deprecation.warn(
'Blockly.Events.Abstract.prototype.fromJson', 'version 9', 'version 10',
'Blockly.Events.fromJson');
event.isBlank = false;
event.group = json['group'] || '';
event.workspaceId = workspace.id;

View File

@@ -20,12 +20,12 @@ import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js
/**
* Abstract class for a block event.
*
* @alias Blockly.Events.BlockBase
* Abstract class for any event related to blocks.
*/
export class BlockBase extends AbstractEvent {
override isBlank = true;
/** The ID of the block associated with this event. */
blockId?: string;
/**
@@ -38,10 +38,7 @@ export class BlockBase extends AbstractEvent {
if (!opt_block) return;
/** The block ID for the block this event pertains to */
this.blockId = opt_block.id;
/** The workspace identifier for this event. */
this.workspaceId = opt_block.workspace.id;
}

View File

@@ -16,6 +16,7 @@ import type {Block} from '../block.js';
import type {BlockSvg} from '../block_svg.js';
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import * as utilsXml from '../utils/xml.js';
import {Workspace} from '../workspace.js';
import * as Xml from '../xml.js';
@@ -24,15 +25,24 @@ import * as eventUtils from './utils.js';
/**
* Class for a block change event.
*
* @alias Blockly.Events.BlockChange
* Notifies listeners when some element of a block has changed (e.g.
* field values, comments, etc).
*/
export class BlockChange extends BlockBase {
override type = eventUtils.BLOCK_CHANGE;
/**
* The element that changed; one of 'field', 'comment', 'collapsed',
* 'disabled', 'inline', or 'mutation'
*/
element?: string;
/** The name of the field that changed, if this is a change to a field. */
name?: string;
/** The original value of the element. */
oldValue: unknown;
/** The new value of the element. */
newValue: unknown;
/**
@@ -173,7 +183,8 @@ export class BlockChange extends BlockBase {
if (block.loadExtraState) {
block.loadExtraState(JSON.parse(value as string || '{}'));
} else if (block.domToMutation) {
block.domToMutation(Xml.textToDom(value as string || '<mutation/>'));
block.domToMutation(
utilsXml.textToDom(value as string || '<mutation/>'));
}
eventUtils.fire(
new BlockChange(block, 'mutation', null, oldState, value));

View File

@@ -16,6 +16,7 @@ import type {Block} from '../block.js';
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import * as blocks from '../serialization/blocks.js';
import * as utilsXml from '../utils/xml.js';
import * as Xml from '../xml.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
@@ -24,16 +25,21 @@ import {Workspace} from '../workspace.js';
/**
* Class for a block creation event.
*
* @alias Blockly.Events.BlockCreate
* Notifies listeners when a block (or connected stack of blocks) is
* created.
*/
export class BlockCreate extends BlockBase {
override type = eventUtils.BLOCK_CREATE;
/** The XML representation of the created block(s). */
xml?: Element|DocumentFragment;
ids?: string[];
/** The JSON respresentation of the created block(s). */
json?: blocks.State;
/** All of the IDs of created blocks. */
ids?: string[];
/** @param opt_block The created block. Undefined for a blank event. */
constructor(opt_block?: Block) {
super(opt_block);
@@ -50,7 +56,6 @@ export class BlockCreate extends BlockBase {
this.xml = Xml.blockToDomWithXY(opt_block);
this.ids = eventUtils.getDescendantIds(opt_block);
/** JSON representation of the block that was just created. */
this.json = blocks.save(opt_block, {addCoordinates: true}) as blocks.State;
}
@@ -95,7 +100,7 @@ export class BlockCreate extends BlockBase {
'Blockly.Events.BlockCreate.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.xml = Xml.textToDom(json['xml']);
this.xml = utilsXml.textToDom(json['xml']);
this.ids = json['ids'];
this.json = json['json'] as blocks.State;
if (json['recordUndo'] !== undefined) {
@@ -117,7 +122,7 @@ export class BlockCreate extends BlockBase {
const newEvent =
super.fromJson(json, workspace, event ?? new BlockCreate()) as
BlockCreate;
newEvent.xml = Xml.textToDom(json['xml']);
newEvent.xml = utilsXml.textToDom(json['xml']);
newEvent.ids = json['ids'];
newEvent.json = json['json'] as blocks.State;
if (json['recordUndo'] !== undefined) {

View File

@@ -16,6 +16,7 @@ import type {Block} from '../block.js';
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import * as blocks from '../serialization/blocks.js';
import * as utilsXml from '../utils/xml.js';
import * as Xml from '../xml.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
@@ -24,15 +25,22 @@ import {Workspace} from '../workspace.js';
/**
* Class for a block deletion event.
*
* @alias Blockly.Events.BlockDelete
* Notifies listeners when a block (or connected stack of blocks) is
* deleted.
*/
export class BlockDelete extends BlockBase {
/** The XML representation of the deleted block(s). */
oldXml?: Element|DocumentFragment;
ids?: string[];
wasShadow?: boolean;
/** The JSON respresentation of the deleted block(s). */
oldJson?: blocks.State;
/** All of the IDs of deleted blocks. */
ids?: string[];
/** True if the deleted block was a shadow block, false otherwise. */
wasShadow?: boolean;
override type = eventUtils.BLOCK_DELETE;
/** @param opt_block The deleted block. Undefined for a blank event. */
@@ -53,11 +61,7 @@ export class BlockDelete extends BlockBase {
this.oldXml = Xml.blockToDomWithXY(opt_block);
this.ids = eventUtils.getDescendantIds(opt_block);
/** Was the block that was just deleted a shadow? */
this.wasShadow = opt_block.isShadow();
/** JSON representation of the block that was just deleted. */
this.oldJson =
blocks.save(opt_block, {addCoordinates: true}) as blocks.State;
}
@@ -109,7 +113,7 @@ export class BlockDelete extends BlockBase {
'Blockly.Events.BlockDelete.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.oldXml = Xml.textToDom(json['oldXml']);
this.oldXml = utilsXml.textToDom(json['oldXml']);
this.ids = json['ids'];
this.wasShadow =
json['wasShadow'] || this.oldXml.tagName.toLowerCase() === 'shadow';
@@ -133,7 +137,7 @@ export class BlockDelete extends BlockBase {
const newEvent =
super.fromJson(json, workspace, event ?? new BlockDelete()) as
BlockDelete;
newEvent.oldXml = Xml.textToDom(json['oldXml']);
newEvent.oldXml = utilsXml.textToDom(json['oldXml']);
newEvent.ids = json['ids'];
newEvent.wasShadow =
json['wasShadow'] || newEvent.oldXml.tagName.toLowerCase() === 'shadow';

View File

@@ -22,14 +22,21 @@ import {Workspace} from '../workspace.js';
/**
* Class for a block drag event.
*
* @alias Blockly.Events.BlockDrag
* Notifies listeners when a block is being manually dragged/dropped.
*/
export class BlockDrag extends UiBase {
/** The ID of the top-level block being dragged. */
blockId?: string;
/** True if this is the start of a drag, false if this is the end of one. */
isStart?: boolean;
/**
* A list of all of the blocks (i.e. all descendants of the block associated
* with the block ID) being dragged.
*/
blocks?: Block[];
override type = eventUtils.BLOCK_DRAG;
/**
@@ -46,11 +53,7 @@ export class BlockDrag extends UiBase {
if (!opt_block) return;
this.blockId = opt_block.id;
/** Whether this is the start of a block drag. */
this.isStart = opt_isStart;
/** The blocks affected by this drag event. */
this.blocks = opt_blocks;
}

View File

@@ -30,18 +30,40 @@ interface BlockLocation {
}
/**
* Class for a block move event. Created before the move.
*
* @alias Blockly.Events.BlockMove
* Notifies listeners when a block is moved. This could be from one
* connection to another, or from one location on the workspace to another.
*/
export class BlockMove extends BlockBase {
override type = eventUtils.BLOCK_MOVE;
/** The ID of the old parent block. Undefined if it was a top-level block. */
oldParentId?: string;
/**
* The name of the old input. Undefined if it was a top-level block or the
* parent's next block.
*/
oldInputName?: string;
/**
* The old X and Y workspace coordinates of the block if it was a top level
* block. Undefined if it was not a top level block.
*/
oldCoordinate?: Coordinate;
/** The ID of the new parent block. Undefined if it is a top-level block. */
newParentId?: string;
/**
* The name of the new input. Undefined if it is a top-level block or the
* parent's next block.
*/
newInputName?: string;
/**
* The new X and Y workspace coordinates of the block if it is a top level
* block. Undefined if it is not a top level block.
*/
newCoordinate?: Coordinate;
/** @param opt_block The moved block. Undefined for a blank event. */
@@ -239,7 +261,7 @@ export class BlockMove extends BlockBase {
blockConnection = block.previousConnection;
}
let parentConnection;
const connectionType = blockConnection.type;
const connectionType = blockConnection?.type;
if (inputName) {
const input = parentBlock!.getInput(inputName);
if (input) {
@@ -248,7 +270,7 @@ export class BlockMove extends BlockBase {
} else if (connectionType === ConnectionType.PREVIOUS_STATEMENT) {
parentConnection = parentBlock!.nextConnection;
}
if (parentConnection) {
if (parentConnection && blockConnection) {
blockConnection.connect(parentConnection);
} else {
console.warn('Can\'t connect to non-existent input: ' + inputName);

View File

@@ -23,13 +23,17 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a bubble open event.
*
* @alias Blockly.Events.BubbleOpen
*/
export class BubbleOpen extends UiBase {
/** The ID of the block the bubble is attached to. */
blockId?: string;
/** True if the bubble is opening, false if closing. */
isOpen?: boolean;
/** The type of bubble; one of 'mutator', 'comment', or 'warning'. */
bubbleType?: BubbleType;
override type = eventUtils.BUBBLE_OPEN;
/**
@@ -46,11 +50,7 @@ export class BubbleOpen extends UiBase {
if (!opt_block) return;
this.blockId = opt_block.id;
/** Whether the bubble is opening (false if closing). */
this.isOpen = opt_isOpen;
/** The type of bubble. One of 'mutator', 'comment', or 'warning'. */
this.bubbleType = opt_bubbleType;
}

View File

@@ -23,12 +23,16 @@ import {Workspace} from '../workspace.js';
/**
* Class for a click event.
*
* @alias Blockly.Events.Click
* Notifies listeners that ome blockly element was clicked.
*/
export class Click extends UiBase {
/** The ID of the block that was clicked, if a block was clicked. */
blockId?: string;
/**
* The type of element that was clicked; one of 'block', 'workspace',
* or 'zoom_controls'.
*/
targetType?: ClickTarget;
override type = eventUtils.CLICK;
@@ -51,8 +55,6 @@ export class Click extends UiBase {
super(workspaceId);
this.blockId = opt_block ? opt_block.id : undefined;
/** The type of element targeted by this click event. */
this.targetType = opt_targetType;
}

View File

@@ -26,11 +26,11 @@ import type {Workspace} from '../workspace.js';
/**
* Abstract class for a comment event.
*
* @alias Blockly.Events.CommentBase
*/
export class CommentBase extends AbstractEvent {
override isBlank = true;
/** The ID of the comment that this event references. */
commentId?: string;
/**
@@ -44,20 +44,9 @@ export class CommentBase extends AbstractEvent {
if (!opt_comment) return;
/** The ID of the comment this event pertains to. */
this.commentId = opt_comment.id;
/** The workspace identifier for this event. */
this.workspaceId = opt_comment.workspace.id;
/**
* The event group ID for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
*/
this.group = eventUtils.getGroup();
/** Sets whether the event should be added to the undo stack. */
this.recordUndo = eventUtils.getRecordUndo();
}

View File

@@ -22,13 +22,16 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a comment change event.
*
* @alias Blockly.Events.CommentChange
* Notifies listeners that the contents of a workspace comment has changed.
*/
export class CommentChange extends CommentBase {
override type = eventUtils.COMMENT_CHANGE;
// TODO(#6774): We should remove underscores.
/** The previous contents of the comment. */
oldContents_?: string;
/** The new contents of the comment. */
newContents_?: string;
/**

View File

@@ -15,6 +15,7 @@ goog.declareModuleId('Blockly.Events.CommentCreate');
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import * as utilsXml from '../utils/xml.js';
import * as Xml from '../xml.js';
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
@@ -23,13 +24,12 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a comment creation event.
*
* @alias Blockly.Events.CommentCreate
* Notifies listeners that a workspace comment was created.
*/
export class CommentCreate extends CommentBase {
override type = eventUtils.COMMENT_CREATE;
/** The XML representation of the created workspace comment. */
xml?: Element|DocumentFragment;
/**
@@ -73,7 +73,7 @@ export class CommentCreate extends CommentBase {
'Blockly.Events.CommentCreate.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.xml = Xml.textToDom(json['xml']);
this.xml = utilsXml.textToDom(json['xml']);
}
/**
@@ -90,7 +90,7 @@ export class CommentCreate extends CommentBase {
const newEvent =
super.fromJson(json, workspace, event ?? new CommentCreate()) as
CommentCreate;
newEvent.xml = Xml.textToDom(json['xml']);
newEvent.xml = utilsXml.textToDom(json['xml']);
return newEvent;
}

View File

@@ -17,17 +17,18 @@ import type {WorkspaceComment} from '../workspace_comment.js';
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import * as eventUtils from './utils.js';
import * as utilsXml from '../utils/xml.js';
import * as Xml from '../xml.js';
import type {Workspace} from '../workspace.js';
/**
* Class for a comment deletion event.
*
* @alias Blockly.Events.CommentDelete
* Notifies listeners that a workspace comment has been deleted.
*/
export class CommentDelete extends CommentBase {
override type = eventUtils.COMMENT_DELETE;
/** The XML representation of the deleted workspace comment. */
xml?: Element;
/**
@@ -83,7 +84,7 @@ export class CommentDelete extends CommentBase {
const newEvent =
super.fromJson(json, workspace, event ?? new CommentDelete()) as
CommentDelete;
newEvent.xml = Xml.textToDom(json['xml']);
newEvent.xml = utilsXml.textToDom(json['xml']);
return newEvent;
}
}

View File

@@ -23,15 +23,19 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a comment move event. Created before the move.
*
* @alias Blockly.Events.CommentMove
* Notifies listeners that a workspace comment has moved.
*/
export class CommentMove extends CommentBase {
override type = eventUtils.COMMENT_MOVE;
/** The comment that is being moved. */
comment_?: WorkspaceComment;
// TODO(#6774): We should remove underscores.
/** The location of the comment before the move, in workspace coordinates. */
oldCoordinate_?: Coordinate;
/** The location after the move, in workspace coordinates. */
/** The location of the comment after the move, in workspace coordinates. */
newCoordinate_?: Coordinate;
/**
@@ -45,13 +49,8 @@ export class CommentMove extends CommentBase {
return; // Blank event to be populated by fromJson.
}
/**
* The comment that is being moved.
*/
this.comment_ = opt_comment;
/** The location before the move, in workspace coordinates. */
this.oldCoordinate_ = opt_comment.getXY();
this.oldCoordinate_ = opt_comment.getRelativeToSurfaceXY();
}
/**
@@ -69,7 +68,7 @@ export class CommentMove extends CommentBase {
'The comment is undefined. Pass a comment to ' +
'the constructor if you want to use the record functionality');
}
this.newCoordinate_ = this.comment_.getXY();
this.newCoordinate_ = this.comment_.getRelativeToSurfaceXY();
}
/**
@@ -179,7 +178,7 @@ export class CommentMove extends CommentBase {
'or call fromJson');
}
// TODO: Check if the comment is being dragged, and give up if so.
const current = comment.getXY();
const current = comment.getRelativeToSurfaceXY();
comment.moveBy(target.x - current.x, target.y - current.y);
}
}

View File

@@ -24,15 +24,26 @@ import * as eventUtils from './utils.js';
/**
* Class for a marker move event.
*
* @alias Blockly.Events.MarkerMove
* Notifies listeners that a marker (used for keyboard navigation) has
* moved.
*/
export class MarkerMove extends UiBase {
/** The ID of the block the marker is now on, if any. */
blockId?: string;
/** The old node the marker used to be on, if any. */
oldNode?: ASTNode;
/** The new node the marker is now on. */
newNode?: ASTNode;
/**
* True if this is a cursor event, false otherwise.
* For information about cursors vs markers see {@link
* https://blocklycodelabs.dev/codelabs/keyboard-navigation/index.html?index=..%2F..index#1}.
*/
isCursor?: boolean;
override type = eventUtils.MARKER_MOVE;
/**
@@ -54,16 +65,9 @@ export class MarkerMove extends UiBase {
}
super(workspaceId);
/** The block identifier for this event. */
this.blockId = opt_block?.id;
/** The old node the marker used to be on. */
this.oldNode = opt_oldNode || undefined;
/** The new node the marker is now on. */
this.newNode = opt_newNode;
/** Whether this is a cursor event. */
this.isCursor = isCursor;
}

View File

@@ -1,37 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js';
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import type {Workspace} from '../workspace.js';
/**
* The base event for an event associated with a procedure.
*/
export abstract class ProcedureBase extends AbstractEvent {
isBlank = false;
constructor(workspace: Workspace, public readonly model: IProcedureModel) {
super();
this.workspaceId = workspace.id;
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureBaseJson {
const json = super.toJson() as ProcedureBaseJson;
json['procedureId'] = this.model.getId();
return json;
}
}
export interface ProcedureBaseJson extends AbstractEventJson {
procedureId: string;
}

View File

@@ -1,87 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a procedure's return type/status changing.
*/
export class ProcedureChangeReturn extends ProcedureBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_CHANGE_RETURN;
/** The new type(s) the procedure's return has been set to. */
private newTypes: string[]|null;
/**
* @param oldTypes The type(s) the procedure's return was set to before it
* changed.
*/
constructor(
workpace: Workspace, model: IProcedureModel,
public readonly oldTypes: string[]|null) {
super(workpace, model);
this.newTypes = model.getReturnTypes();
}
run(forward: boolean) {
const procedureModel =
this.getEventWorkspace_().getProcedureMap().get(this.model.getId());
if (!procedureModel) {
throw new Error(
'Cannot change the type of a procedure that does not exist ' +
'in the procedure map');
}
if (forward) {
procedureModel.setReturnTypes(this.newTypes);
} else {
procedureModel.setReturnTypes(this.oldTypes);
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureChangeReturnJson {
const json = super.toJson() as ProcedureChangeReturnJson;
json['oldTypes'] = this.oldTypes;
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureChangeReturnJson, workspace: Workspace):
ProcedureChangeReturn {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure change return event because the ' +
'target procedure does not exist');
}
return new ProcedureChangeReturn(workspace, model, json['oldTypes']);
}
}
export interface ProcedureChangeReturnJson extends ProcedureBaseJson {
oldTypes: string[]|null;
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_CHANGE_RETURN,
ProcedureChangeReturn);

View File

@@ -1,74 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {ObservableParameterModel, ObservableProcedureModel} from '../procedures.js';
import * as registry from '../registry.js';
import {loadProcedure, saveProcedure, State as ProcedureState} from '../serialization/procedures.js';
import type {Workspace} from '../workspace.js';
import {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a procedure data model being created.
*/
export class ProcedureCreate extends ProcedureBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_CREATE;
constructor(workspace: Workspace, model: IProcedureModel) {
super(workspace, model);
}
run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const procedureMap = workspace.getProcedureMap();
const procedureModel = procedureMap.get(this.model.getId());
if (forward) {
if (procedureModel) return;
// TODO: This should add the model to the map instead of creating a dupe.
procedureMap.add(new ObservableProcedureModel(
workspace, this.model.getName(), this.model.getId()));
} else {
if (!procedureModel) return;
procedureMap.delete(this.model.getId());
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureCreateJson {
const json = super.toJson() as ProcedureCreateJson;
json['model'] = saveProcedure(this.model);
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureCreateJson, workspace: Workspace):
ProcedureCreate {
return new ProcedureCreate(
workspace,
loadProcedure(
ObservableProcedureModel, ObservableParameterModel, json['model'],
workspace));
}
}
export interface ProcedureCreateJson extends ProcedureBaseJson {
model: ProcedureState,
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_CREATE, ProcedureCreate);

View File

@@ -1,71 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {ObservableProcedureModel} from '../procedures.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a procedure data model being deleted.
*/
export class ProcedureDelete extends ProcedureBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_DELETE;
constructor(workspace: Workspace, model: IProcedureModel) {
super(workspace, model);
}
run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const procedureMap = workspace.getProcedureMap();
const procedureModel = procedureMap.get(this.model.getId());
if (forward) {
if (!procedureModel) return;
procedureMap.delete(this.model.getId());
} else {
if (procedureModel) return;
procedureMap.add(new ObservableProcedureModel(
workspace, this.model.getName(), this.model.getId()));
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureDeleteJson {
return super.toJson() as ProcedureDeleteJson;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureDeleteJson, workspace: Workspace):
ProcedureDelete {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure delete event because the ' +
'target procedure does not exist');
}
return new ProcedureDelete(workspace, model);
}
}
export interface ProcedureDeleteJson extends ProcedureBaseJson {}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_DELETE, ProcedureDelete);

View File

@@ -1,76 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a procedure data model being enabled or disabled.
*/
export class ProcedureEnable extends ProcedureBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_ENABLE;
private oldState: boolean;
private newState: boolean;
constructor(workspace: Workspace, model: IProcedureModel) {
super(workspace, model);
this.oldState = !model.getEnabled();
this.newState = model.getEnabled();
}
run(forward: boolean) {
const procedureModel =
this.getEventWorkspace_().getProcedureMap().get(this.model.getId());
if (!procedureModel) {
throw new Error(
'Cannot change the enabled state of a procedure that does not ' +
'exist in the procedure map');
}
if (forward) {
procedureModel.setEnabled(this.newState);
} else {
procedureModel.setEnabled(this.oldState);
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureEnableJson {
return super.toJson() as ProcedureEnableJson;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureEnableJson, workspace: Workspace):
ProcedureEnable {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure enable event because the ' +
'target procedure does not exist');
}
return new ProcedureEnable(workspace, model);
}
}
export interface ProcedureEnableJson extends ProcedureBaseJson {}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_ENABLE, ProcedureEnable);

View File

@@ -1,39 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {IParameterModel} from '../interfaces/i_parameter_model.js';
import {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import type {Workspace} from '../workspace.js';
/**
* The base event for an event associated with a procedure parameter.
*/
export abstract class ProcedureParameterBase extends ProcedureBase {
constructor(
workspace: Workspace, model: IProcedureModel,
public readonly parameter: IParameterModel) {
super(workspace, model);
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureParameterBaseJson {
const json = super.toJson() as ProcedureParameterBaseJson;
json['parameterId'] = this.model.getId();
return json;
}
}
export interface ProcedureParameterBaseJson extends ProcedureBaseJson {
parameterId: string,
}

View File

@@ -1,101 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IParameterModel} from '../interfaces/i_parameter_model.js';
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {ObservableParameterModel} from '../procedures/observable_parameter_model.js';
import * as registry from '../registry.js';
import {loadParameter, ParameterState, saveParameter} from '../serialization/procedures.js';
import type {Workspace} from '../workspace.js';
import {ProcedureParameterBase, ProcedureParameterBaseJson} from './events_procedure_parameter_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a parameter being added to a procedure.
*/
export class ProcedureParameterCreate extends ProcedureParameterBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_PARAMETER_CREATE;
/**
* @param parameter The parameter model that was just added to the procedure.
* @param index The index the parameter was inserted at.
*/
constructor(
workspace: Workspace, procedure: IProcedureModel,
parameter: IParameterModel, public readonly index: number) {
super(workspace, procedure, parameter);
}
run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const procedureMap = workspace.getProcedureMap();
const procedureModel = procedureMap.get(this.model.getId());
if (!procedureModel) {
throw new Error(
'Cannot add a parameter to a procedure that does not exist ' +
'in the procedure map');
}
const parameterModel = procedureModel.getParameter(this.index);
if (forward) {
if (this.parameterMatches(parameterModel)) return;
// TODO: This should just add the parameter instead of creating a dupe.
procedureModel.insertParameter(
new ObservableParameterModel(
workspace, this.parameter.getName(), this.parameter.getId()),
this.index);
} else {
if (!this.parameterMatches(parameterModel)) return;
procedureModel.deleteParameter(this.index);
}
}
parameterMatches(param: IParameterModel) {
return param && param.getId() === this.parameter.getId();
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureParameterCreateJson {
const json = super.toJson() as ProcedureParameterCreateJson;
json['parameter'] = saveParameter(this.parameter);
json['index'] = this.index;
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureParameterCreateJson, workspace: Workspace):
ProcedureParameterCreate {
const procedure = workspace.getProcedureMap().get(json['procedureId']);
if (!procedure) {
throw new Error(
'Cannot deserialize parameter create event because the ' +
'target procedure does not exist');
}
return new ProcedureParameterCreate(
workspace, procedure,
loadParameter(ObservableParameterModel, json['parameter'], workspace),
json['index']);
}
}
export interface ProcedureParameterCreateJson extends
ProcedureParameterBaseJson {
parameter: ParameterState, index: number,
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_PARAMETER_CREATE,
ProcedureParameterCreate);

View File

@@ -1,97 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IParameterModel} from '../interfaces/i_parameter_model.js';
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {ObservableParameterModel} from '../procedures/observable_parameter_model.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureParameterBase, ProcedureParameterBaseJson} from './events_procedure_parameter_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a parameter being removed from a procedure.
*/
export class ProcedureParameterDelete extends ProcedureParameterBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_PARAMETER_DELETE;
/**
* @param parameter The parameter model that was just removed from the
* procedure.
* @param index The index the parameter was at before it was removed.
*/
constructor(
workspace: Workspace, procedure: IProcedureModel,
parameter: IParameterModel, public readonly index: number) {
super(workspace, procedure, parameter);
}
run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const procedureMap = workspace.getProcedureMap();
const procedureModel = procedureMap.get(this.model.getId());
if (!procedureModel) {
throw new Error(
'Cannot add a parameter to a procedure that does not exist ' +
'in the procedure map');
}
const parameterModel = procedureModel.getParameter(this.index);
if (forward) {
if (!this.parameterMatches(parameterModel)) return;
procedureModel.deleteParameter(this.index);
} else {
if (this.parameterMatches(parameterModel)) return;
// TODO: this should just insert the model instead of creating a dupe.
procedureModel.insertParameter(
new ObservableParameterModel(
workspace, this.parameter.getName(), this.parameter.getId()),
this.index);
}
}
parameterMatches(param: IParameterModel) {
return param && param.getId() === this.parameter.getId();
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureParameterDeleteJson {
const json = super.toJson() as ProcedureParameterDeleteJson;
json['index'] = this.index;
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureParameterDeleteJson, workspace: Workspace):
ProcedureParameterDelete {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure delete event because the ' +
'target procedure does not exist');
}
const param = model.getParameter(json['index']);
return new ProcedureParameterDelete(workspace, model, param, json['index']);
}
}
export interface ProcedureParameterDeleteJson extends
ProcedureParameterBaseJson {
index: number;
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_PARAMETER_DELETE,
ProcedureParameterDelete);

View File

@@ -1,102 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IParameterModel} from '../interfaces/i_parameter_model.js';
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureParameterBase, ProcedureParameterBaseJson} from './events_procedure_parameter_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a parameter of a procedure being renamed.
*/
export class ProcedureParameterRename extends ProcedureParameterBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_PARAMETER_RENAME;
private readonly newName: string;
constructor(
workspace: Workspace, procedure: IProcedureModel,
parameter: IParameterModel, public readonly oldName: string) {
super(workspace, procedure, parameter);
this.newName = parameter.getName();
}
run(forward: boolean) {
const parameterModel = findMatchingParameter(
this.getEventWorkspace_(), this.model.getId(), this.parameter.getId());
if (!parameterModel) {
throw new Error(
'Cannot rename a parameter that does not exist ' +
'in the procedure map');
}
if (forward) {
parameterModel.setName(this.newName);
} else {
parameterModel.setName(this.oldName);
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureParameterRenameJson {
const json = super.toJson() as ProcedureParameterRenameJson;
json['oldName'] = this.oldName;
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureParameterRenameJson, workspace: Workspace):
ProcedureParameterRename {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure delete event because the ' +
'target procedure does not exist');
}
const param = findMatchingParameter(
workspace, json['procedureId'], json['parameterId']);
if (!param) {
throw new Error(
'Cannot deserialize parameter rename event because the ' +
'target parameter does not exist');
}
return new ProcedureParameterRename(
workspace, model, param, json['oldName']);
}
}
function findMatchingParameter(
workspace: Workspace, modelId: string, paramId: string): IParameterModel|
undefined {
const procedureModel = workspace.getProcedureMap().get(modelId);
if (!procedureModel) {
throw new Error(
'Cannot rename the parameter of a procedure that does not exist ' +
'in the procedure map');
}
return procedureModel.getParameters().find((p) => p.getId() === paramId);
}
export interface ProcedureParameterRenameJson extends
ProcedureParameterBaseJson {
oldName: string;
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_PARAMETER_RENAME,
ProcedureParameterRename);

View File

@@ -1,78 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a procedure being renamed.
*/
export class ProcedureRename extends ProcedureBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_RENAME;
private newName: string;
constructor(
workspace: Workspace, model: IProcedureModel,
public readonly oldName: string) {
super(workspace, model);
this.newName = model.getName();
}
run(forward: boolean) {
const procedureModel =
this.getEventWorkspace_().getProcedureMap().get(this.model.getId());
if (!procedureModel) {
throw new Error(
'Cannot change the type of a procedure that does not exist ' +
'in the procedure map');
}
if (forward) {
procedureModel.setName(this.newName);
} else {
procedureModel.setName(this.oldName);
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureRenameJson {
const json = super.toJson() as ProcedureRenameJson;
json['oldName'] = this.oldName;
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureRenameJson, workspace: Workspace):
ProcedureRename {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure rename event because the ' +
'target procedure does not exist');
}
return new ProcedureRename(workspace, model, json['oldName']);
}
}
export interface ProcedureRenameJson extends ProcedureBaseJson {
oldName: string;
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_RENAME, ProcedureRename);

View File

@@ -23,12 +23,18 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a selected event.
*
* @alias Blockly.Events.Selected
* Notifies listeners that a new element has been selected.
*/
export class Selected extends UiBase {
/** The id of the last selected selectable element. */
oldElementId?: string;
/**
* The id of the newly selected selectable element,
* or undefined if unselected.
*/
newElementId?: string;
override type = eventUtils.SELECTED;
/**
@@ -44,10 +50,7 @@ export class Selected extends UiBase {
opt_workspaceId?: string) {
super(opt_workspaceId);
/** The id of the last selected element. */
this.oldElementId = opt_oldElementId ?? undefined;
/** The id of the selected element. */
this.newElementId = opt_newElementId ?? undefined;
}

View File

@@ -21,12 +21,12 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a theme change event.
*
* @alias Blockly.Events.ThemeChange
* Notifies listeners that the workspace theme has changed.
*/
export class ThemeChange extends UiBase {
/** The name of the new theme that has been set. */
themeName?: string;
override type = eventUtils.THEME_CHANGE;
/**
@@ -36,8 +36,6 @@ export class ThemeChange extends UiBase {
*/
constructor(opt_themeName?: string, opt_workspaceId?: string) {
super(opt_workspaceId);
/** The theme name. */
this.themeName = opt_themeName;
}

View File

@@ -21,13 +21,15 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a toolbox item select event.
*
* @alias Blockly.Events.ToolboxItemSelect
* Notifies listeners that a toolbox item has been selected.
*/
export class ToolboxItemSelect extends UiBase {
/** The previously selected toolbox item. */
oldItem?: string;
/** The newly selected toolbox item. */
newItem?: string;
override type = eventUtils.TOOLBOX_ITEM_SELECT;
/**
@@ -42,11 +44,7 @@ export class ToolboxItemSelect extends UiBase {
opt_oldItem?: string|null, opt_newItem?: string|null,
opt_workspaceId?: string) {
super(opt_workspaceId);
/** The previously selected toolbox item. */
this.oldItem = opt_oldItem ?? undefined;
/** The newly selected toolbox item. */
this.newItem = opt_newItem ?? undefined;
}

View File

@@ -22,11 +22,13 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a trashcan open event.
*
* @alias Blockly.Events.TrashcanOpen
* Notifies listeners when the trashcan is opening or closing.
*/
export class TrashcanOpen extends UiBase {
/**
* True if the trashcan is currently opening (previously closed).
* False if it is currently closing (previously open).
*/
isOpen?: boolean;
override type = eventUtils.TRASHCAN_OPEN;
@@ -38,8 +40,6 @@ export class TrashcanOpen extends UiBase {
*/
constructor(opt_isOpen?: boolean, opt_workspaceId?: string) {
super(opt_workspaceId);
/** Whether the trashcan flyout is opening (false if closing). */
this.isOpen = opt_isOpen;
}

View File

@@ -23,7 +23,6 @@ import * as eventUtils from './utils.js';
* Class for a UI event.
*
* @deprecated December 2020. Instead use a more specific UI event.
* @alias Blockly.Events.Ui
*/
export class Ui extends UiBase {
blockId: AnyDuringMigration;

View File

@@ -22,8 +22,6 @@ import {Abstract as AbstractEvent} from './events_abstract.js';
* editing to work (e.g. scrolling the workspace, zooming, opening toolbox
* categories).
* UI events do not undo or redo.
*
* @alias Blockly.Events.UiBase
*/
export class UiBase extends AbstractEvent {
override isBlank = true;

View File

@@ -21,11 +21,10 @@ import type {Workspace} from '../workspace.js';
/**
* Abstract class for a variable event.
*
* @alias Blockly.Events.VarBase
*/
export class VarBase extends AbstractEvent {
override isBlank = true;
/** The ID of the variable this event references. */
varId?: string;
/**
@@ -37,10 +36,7 @@ export class VarBase extends AbstractEvent {
this.isBlank = typeof opt_variable === 'undefined';
if (!opt_variable) return;
/** The variable id for the variable this event pertains to. */
this.varId = opt_variable.getId();
/** The workspace identifier for this event. */
this.workspaceId = opt_variable.workspace.id;
}

View File

@@ -22,13 +22,15 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a variable creation event.
*
* @alias Blockly.Events.VarCreate
* Notifies listeners that a variable model has been created.
*/
export class VarCreate extends VarBase {
override type = eventUtils.VAR_CREATE;
/** The type of the variable that was created. */
varType?: string;
/** The name of the variable that was created. */
varName?: string;
/**
@@ -51,7 +53,7 @@ export class VarCreate extends VarBase {
*/
override toJson(): VarCreateJson {
const json = super.toJson() as VarCreateJson;
if (!this.varType) {
if (this.varType === undefined) {
throw new Error(
'The var type is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Classes for all types of variable events.
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarDelete');
@@ -22,13 +17,15 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a variable deletion event.
* Notifies listeners that a variable model has been deleted.
*
* @alias Blockly.Events.VarDelete
* @class
*/
export class VarDelete extends VarBase {
override type = eventUtils.VAR_DELETE;
/** The type of the variable that was deleted. */
varType?: string;
/** The name of the variable that was deleted. */
varName?: string;
/**
@@ -51,7 +48,7 @@ export class VarDelete extends VarBase {
*/
override toJson(): VarDeleteJson {
const json = super.toJson() as VarDeleteJson;
if (!this.varType) {
if (this.varType === undefined) {
throw new Error(
'The var type is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');

View File

@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Class for a variable rename event.
*
* @class
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarRename');
@@ -22,13 +17,17 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a variable rename event.
* Notifies listeners that a variable model was renamed.
*
* @alias Blockly.Events.VarRename
* @class
*/
export class VarRename extends VarBase {
override type = eventUtils.VAR_RENAME;
/** The previous name of the variable. */
oldName?: string;
/** The new name of the variable. */
newName?: string;
/**

View File

@@ -21,15 +21,30 @@ import type {Workspace} from '../workspace.js';
/**
* Class for a viewport change event.
* Notifies listeners that the workspace surface's position or scale has
* changed.
*
* @alias Blockly.Events.ViewportChange
* Does not notify when the workspace itself resizes.
*/
export class ViewportChange extends UiBase {
/**
* Top edge of the visible portion of the workspace, relative to the
* workspace origin.
*/
viewTop?: number;
/**
* The left edge of the visible portion of the workspace, relative to
* the workspace origin.
*/
viewLeft?: number;
/** The scale of the workpace. */
scale?: number;
/** The previous scale of the workspace. */
oldScale?: number;
override type = eventUtils.VIEWPORT_CHANGE;
/**
@@ -48,22 +63,9 @@ export class ViewportChange extends UiBase {
opt_workspaceId?: string, opt_oldScale?: number) {
super(opt_workspaceId);
/**
* Top-edge of the visible portion of the workspace, relative to the
* workspace origin.
*/
this.viewTop = opt_top;
/**
* Left-edge of the visible portion of the workspace, relative to the
* workspace origin.
*/
this.viewLeft = opt_left;
/** The scale of the workspace. */
this.scale = opt_scale;
/** The old scale of the workspace. */
this.oldScale = opt_oldScale;
}

View File

@@ -4,12 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Helper methods for events that are fired as a result of
* actions in Blockly's editor.
*
* @namespace Blockly.Events.utils
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.utils');
@@ -39,7 +33,6 @@ let recordUndo = true;
* Sets whether events should be added to the undo stack.
*
* @param newValue True if events should be added to the undo stack.
* @alias Blockly.Events.utils.setRecordUndo
*/
export function setRecordUndo(newValue: boolean) {
recordUndo = newValue;
@@ -49,7 +42,6 @@ export function setRecordUndo(newValue: boolean) {
* Returns whether or not events will be added to the undo stack.
*
* @returns True if events will be added to the undo stack.
* @alias Blockly.Events.utils.getRecordUndo
*/
export function getRecordUndo(): boolean {
return recordUndo;
@@ -60,218 +52,140 @@ let disabled = 0;
/**
* Name of event that creates a block. Will be deprecated for BLOCK_CREATE.
*
* @alias Blockly.Events.utils.CREATE
*/
export const CREATE = 'create';
/**
* Name of event that creates a block.
*
* @alias Blockly.Events.utils.BLOCK_CREATE
*/
export const BLOCK_CREATE = CREATE;
/**
* Name of event that deletes a block. Will be deprecated for BLOCK_DELETE.
*
* @alias Blockly.Events.utils.DELETE
*/
export const DELETE = 'delete';
/**
* Name of event that deletes a block.
*
* @alias Blockly.Events.utils.BLOCK_DELETE
*/
export const BLOCK_DELETE = DELETE;
/**
* Name of event that changes a block. Will be deprecated for BLOCK_CHANGE.
*
* @alias Blockly.Events.utils.CHANGE
*/
export const CHANGE = 'change';
/**
* Name of event that changes a block.
*
* @alias Blockly.Events.utils.BLOCK_CHANGE
*/
export const BLOCK_CHANGE = CHANGE;
/**
* Name of event that moves a block. Will be deprecated for BLOCK_MOVE.
*
* @alias Blockly.Events.utils.MOVE
*/
export const MOVE = 'move';
/**
* Name of event that moves a block.
*
* @alias Blockly.Events.utils.BLOCK_MOVE
*/
export const BLOCK_MOVE = MOVE;
/**
* Name of event that creates a variable.
*
* @alias Blockly.Events.utils.VAR_CREATE
*/
export const VAR_CREATE = 'var_create';
/**
* Name of event that deletes a variable.
*
* @alias Blockly.Events.utils.VAR_DELETE
*/
export const VAR_DELETE = 'var_delete';
/**
* Name of event that renames a variable.
*
* @alias Blockly.Events.utils.VAR_RENAME
*/
export const VAR_RENAME = 'var_rename';
/**
* Name of generic event that records a UI change.
*
* @alias Blockly.Events.utils.UI
*/
export const UI = 'ui';
/**
* Name of event that record a block drags a block.
*
* @alias Blockly.Events.utils.BLOCK_DRAG
*/
export const BLOCK_DRAG = 'drag';
/**
* Name of event that records a change in selected element.
*
* @alias Blockly.Events.utils.SELECTED
*/
export const SELECTED = 'selected';
/**
* Name of event that records a click.
*
* @alias Blockly.Events.utils.CLICK
*/
export const CLICK = 'click';
/**
* Name of event that records a marker move.
*
* @alias Blockly.Events.utils.MARKER_MOVE
*/
export const MARKER_MOVE = 'marker_move';
/**
* Name of event that records a bubble open.
*
* @alias Blockly.Events.utils.BUBBLE_OPEN
*/
export const BUBBLE_OPEN = 'bubble_open';
/**
* Name of event that records a trashcan open.
*
* @alias Blockly.Events.utils.TRASHCAN_OPEN
*/
export const TRASHCAN_OPEN = 'trashcan_open';
/**
* Name of event that records a toolbox item select.
*
* @alias Blockly.Events.utils.TOOLBOX_ITEM_SELECT
*/
export const TOOLBOX_ITEM_SELECT = 'toolbox_item_select';
/**
* Name of event that records a theme change.
*
* @alias Blockly.Events.utils.THEME_CHANGE
*/
export const THEME_CHANGE = 'theme_change';
/**
* Name of event that records a viewport change.
*
* @alias Blockly.Events.utils.VIEWPORT_CHANGE
*/
export const VIEWPORT_CHANGE = 'viewport_change';
/**
* Name of event that creates a comment.
*
* @alias Blockly.Events.utils.COMMENT_CREATE
*/
export const COMMENT_CREATE = 'comment_create';
/**
* Name of event that deletes a comment.
*
* @alias Blockly.Events.utils.COMMENT_DELETE
*/
export const COMMENT_DELETE = 'comment_delete';
/**
* Name of event that changes a comment.
*
* @alias Blockly.Events.utils.COMMENT_CHANGE
*/
export const COMMENT_CHANGE = 'comment_change';
/**
* Name of event that moves a comment.
*
* @alias Blockly.Events.utils.COMMENT_MOVE
*/
export const COMMENT_MOVE = 'comment_move';
/**
* Name of event that records a workspace load.
*
* @alias Blockly.Events.utils.FINISHED_LOADING
*/
export const FINISHED_LOADING = 'finished_loading';
/** Name of event that creates a procedure model. */
export const PROCEDURE_CREATE = 'procedure_create';
/** Name of event that deletes a procedure model. */
export const PROCEDURE_DELETE = 'procedure_delete';
/** Name of event that renames a procedure model. */
export const PROCEDURE_RENAME = 'procedure_rename';
/** Name of event that enables/disables a procedure model. */
export const PROCEDURE_ENABLE = 'procedure_enable';
/** Name of event that changes the returntype of a procedure model. */
export const PROCEDURE_CHANGE_RETURN = 'procedure_change_return';
/** Name of event that creates a procedure parameter. */
export const PROCEDURE_PARAMETER_CREATE = 'procedure_parameter_create';
/** Name of event that deletes a procedure parameter. */
export const PROCEDURE_PARAMETER_DELETE = 'procedure_parameter_delete';
/** Name of event that renames a procedure parameter. */
export const PROCEDURE_PARAMETER_RENAME = 'procedure_parameter_rename';
/**
* Type of events that cause objects to be bumped back into the visible
* portion of the workspace.
*
* Not to be confused with bumping so that disconnected connections do not
* appear connected.
*
* @alias Blockly.Events.utils.BumpEvent
*/
export type BumpEvent = BlockCreate|BlockMove|CommentCreate|CommentMove;
@@ -281,8 +195,6 @@ export type BumpEvent = BlockCreate|BlockMove|CommentCreate|CommentMove;
*
* Not to be confused with bumping so that disconnected connections do not
* appear connected.
*
* @alias Blockly.Events.utils.BUMP_EVENTS
*/
export const BUMP_EVENTS: string[] =
[BLOCK_CREATE, BLOCK_MOVE, COMMENT_CREATE, COMMENT_MOVE];
@@ -294,7 +206,6 @@ const FIRE_QUEUE: Abstract[] = [];
* Create a custom event and fire it.
*
* @param event Custom data for event.
* @alias Blockly.Events.utils.fire
*/
export function fire(event: Abstract) {
TEST_ONLY.fireInternal(event);
@@ -309,7 +220,17 @@ function fireInternal(event: Abstract) {
}
if (!FIRE_QUEUE.length) {
// First event added; schedule a firing of the event queue.
setTimeout(fireNow, 0);
try {
// If we are in a browser context, we want to make sure that the event
// fires after blocks have been rerendered this frame.
requestAnimationFrame(() => {
setTimeout(fireNow, 0);
});
} catch (e) {
// Otherwise we just want to delay so events can be coallesced.
// requestAnimationFrame will error triggering this.
setTimeout(fireNow, 0);
}
}
FIRE_QUEUE.push(event);
}
@@ -336,7 +257,6 @@ function fireNow() {
* @param queueIn Array of events.
* @param forward True if forward (redo), false if backward (undo).
* @returns Array of filtered events.
* @alias Blockly.Events.utils.filter
*/
export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
let queue = queueIn.slice();
@@ -419,8 +339,6 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
/**
* Modify pending undo events so that when they are fired they don't land
* in the undo stack. Called by Workspace.clearUndo.
*
* @alias Blockly.Events.utils.clearPendingUndo
*/
export function clearPendingUndo() {
for (let i = 0, event; event = FIRE_QUEUE[i]; i++) {
@@ -430,8 +348,6 @@ export function clearPendingUndo() {
/**
* Stop sending events. Every call to this function MUST also call enable.
*
* @alias Blockly.Events.utils.disable
*/
export function disable() {
disabled++;
@@ -440,8 +356,6 @@ export function disable() {
/**
* Start sending events. Unless events were already disabled when the
* corresponding call to disable was made.
*
* @alias Blockly.Events.utils.enable
*/
export function enable() {
disabled--;
@@ -451,7 +365,6 @@ export function enable() {
* Returns whether events may be fired or not.
*
* @returns True if enabled.
* @alias Blockly.Events.utils.isEnabled
*/
export function isEnabled(): boolean {
return disabled === 0;
@@ -461,7 +374,6 @@ export function isEnabled(): boolean {
* Current group.
*
* @returns ID string.
* @alias Blockly.Events.utils.getGroup
*/
export function getGroup(): string {
return group;
@@ -472,7 +384,6 @@ export function getGroup(): string {
*
* @param state True to start new group, false to end group.
* String to set group explicitly.
* @alias Blockly.Events.utils.setGroup
*/
export function setGroup(state: boolean|string) {
TEST_ONLY.setGroupInternal(state);
@@ -494,7 +405,6 @@ function setGroupInternal(state: boolean|string) {
*
* @param block The root block.
* @returns List of block IDs.
* @alias Blockly.Events.utils.getDescendantIds
* @internal
*/
export function getDescendantIds(block: Block): string[] {
@@ -513,7 +423,6 @@ export function getDescendantIds(block: Block): string[] {
* @param workspace Target workspace for event.
* @returns The event represented by the JSON.
* @throws {Error} if an event type is not found in the registry.
* @alias Blockly.Events.utils.fromJson
*/
export function fromJson(
json: AnyDuringMigration, workspace: Workspace): Abstract {
@@ -550,7 +459,6 @@ function eventClassHasStaticFromJson(eventClass: new (...p: any[]) => Abstract):
*
* @param eventType The type of the event to get.
* @returns The event class with the given type.
* @alias Blockly.Events.utils.get
*/
export function get(eventType: string):
(new (...p1: AnyDuringMigration[]) => Abstract) {
@@ -568,7 +476,6 @@ export function get(eventType: string):
* users don't try to re-enable disabled orphan blocks.
*
* @param event Custom data for event.
* @alias Blockly.Events.utils.disableOrphans
*/
export function disableOrphans(event: Abstract) {
if (event.type === MOVE || event.type === CREATE) {

View File

@@ -19,12 +19,8 @@ import * as eventUtils from './utils.js';
/**
* Class for a finished loading event.
* Used to notify the developer when the workspace has finished loading (i.e
* domToWorkspace).
* Finished loading events do not record undo or redo.
*
* @alias Blockly.Events.FinishedLoading
* Notifies listeners when the workspace has finished deserializing from
* JSON/XML.
*/
export class FinishedLoading extends AbstractEvent {
override isBlank = true;
@@ -37,12 +33,10 @@ export class FinishedLoading extends AbstractEvent {
*/
constructor(opt_workspace?: Workspace) {
super();
/** Whether or not the event is blank (to be populated by fromJson). */
this.isBlank = !!opt_workspace;
if (!opt_workspace) return;
/** The workspace identifier for this event. */
this.workspaceId = opt_workspace.id;
}

View File

@@ -4,14 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Extensions are functions that help initialize blocks, usually
* adding dynamic behavior such as onchange handlers and mutators. These
* are applied using Block.applyExtension(), or the JSON "extensions"
* array attribute.
*
* @namespace Blockly.Extensions
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Extensions');
@@ -36,7 +28,6 @@ export const TEST_ONLY = {allExtensions};
* @param initFn The function to initialize an extended block.
* @throws {Error} if the extension name is empty, the extension is already
* registered, or extensionFn is not a function.
* @alias Blockly.Extensions.register
*/
export function register(name: string, initFn: Function) {
if (typeof name !== 'string' || name.trim() === '') {
@@ -58,7 +49,6 @@ export function register(name: string, initFn: Function) {
* @param mixinObj The values to mix in.
* @throws {Error} if the extension name is empty or the extension is already
* registered.
* @alias Blockly.Extensions.registerMixin
*/
export function registerMixin(name: string, mixinObj: AnyDuringMigration) {
if (!mixinObj || typeof mixinObj !== 'object') {
@@ -81,7 +71,6 @@ export function registerMixin(name: string, mixinObj: AnyDuringMigration) {
* @param opt_blockList A list of blocks to appear in the flyout of the mutator
* dialog.
* @throws {Error} if the mutation is invalid or can't be applied to the block.
* @alias Blockly.Extensions.registerMutator
*/
export function registerMutator(
name: string, mixinObj: AnyDuringMigration,
@@ -113,7 +102,6 @@ export function registerMutator(
* Unregisters the extension registered with the given name.
*
* @param name The name of the extension to unregister.
* @alias Blockly.Extensions.unregister
*/
export function unregister(name: string) {
if (isRegistered(name)) {
@@ -129,7 +117,6 @@ export function unregister(name: string) {
*
* @param name The name of the extension to check for.
* @returns True if the extension is registered. False if it is not registered.
* @alias Blockly.Extensions.isRegistered
*/
export function isRegistered(name: string): boolean {
return !!allExtensions[name];
@@ -143,7 +130,6 @@ export function isRegistered(name: string): boolean {
* @param block The block to apply the named extension to.
* @param isMutator True if this extension defines a mutator.
* @throws {Error} if the extension is not found.
* @alias Blockly.Extensions.apply
*/
export function apply(name: string, block: Block, isMutator: boolean) {
const extensionFn = allExtensions[name];
@@ -387,7 +373,6 @@ export function runAfterPageLoad(fn: () => void) {
* lookup table.
* @param lookupTable The table of field values to tooltip text.
* @returns The extension function.
* @alias Blockly.Extensions.buildTooltipForDropdown
*/
export function buildTooltipForDropdown(
dropdownName: string, lookupTable: {[key: string]: string}): Function {
@@ -470,7 +455,6 @@ function checkDropdownOptionsInTable(
* placeholder.
* @param fieldName The field with the replacement text.
* @returns The extension function.
* @alias Blockly.Extensions.buildTooltipWithFieldText
*/
export function buildTooltipWithFieldText(
msgTemplate: string, fieldName: string): Function {

View File

@@ -35,7 +35,6 @@ import type {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
import {Rect} from './utils/rect.js';
import {Sentinel} from './utils/sentinel.js';
import {Size} from './utils/size.js';
import * as style from './utils/style.js';
import {Svg} from './utils/svg.js';
@@ -43,14 +42,28 @@ import * as userAgent from './utils/useragent.js';
import * as utilsXml from './utils/xml.js';
import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import * as Xml from './xml.js';
export type FieldValidator<T = any> = (value?: T) => T|null|undefined;
/**
* A function that is called to validate changes to the field's value before
* they are set.
*
* @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values}
* @param newValue The value to be validated.
* @returns One of three instructions for setting the new value: `T`, `null`,
* or `undefined`.
*
* - `T` to set this function's returned value instead of `newValue`.
*
* - `null` to invoke `doValueInvalid_` and not set a value.
*
* - `undefined` to set `newValue` as is.
*/
export type FieldValidator<T = any> = (newValue: T) => T|null|undefined;
/**
* Abstract class for an editable field.
*
* @alias Blockly.Field
* @typeParam T - The value stored on the field.
*/
export abstract class Field<T = any> implements IASTNodeLocationSvg,
IASTNodeLocationWithBlock,
@@ -75,7 +88,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* field's value or run configure_, and should allow a subclass to do that
* instead.
*/
static readonly SKIP_SETUP = new Sentinel();
static readonly SKIP_SETUP = Symbol('SKIP_SETUP');
/**
* Name of field. Unique within each block.
@@ -186,16 +199,16 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field value
* after their own constructors have run).
* @param opt_validator A function that is called to validate changes to the
* @param validator A function that is called to validate changes to the
* field's value. Takes in a value & returns a validated value, or null to
* abort the change.
* @param opt_config A map of options used to configure the field.
* @param config A map of options used to configure the field.
* Refer to the individual field's documentation for a list of properties
* this parameter supports.
*/
constructor(
value: T|Sentinel, opt_validator?: FieldValidator<T>|null,
opt_config?: FieldConfig) {
value: T|typeof Field.SKIP_SETUP, validator?: FieldValidator<T>|null,
config?: FieldConfig) {
/**
* A generic value possessed by the field.
* Should generally be non-null, only null when the field is created.
@@ -207,15 +220,13 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
/** The size of the area rendered by the field. */
this.size_ = new Size(0, 0);
if (value === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
if (value === Field.SKIP_SETUP) return;
if (config) {
this.configure_(config);
}
this.setValue(value);
if (opt_validator) {
this.setValidator(opt_validator);
if (validator) {
this.setValidator(validator);
}
}
@@ -372,7 +383,8 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* @internal
*/
fromXml(fieldElement: Element) {
this.setValue(fieldElement.textContent);
// Any because gremlins live here. No touchie!
this.setValue(fieldElement.textContent as any);
}
/**
@@ -384,7 +396,8 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* @internal
*/
toXml(fieldElement: Element): Element {
fieldElement.textContent = this.getValue();
// Any because gremlins live here. No touchie!
fieldElement.textContent = this.getValue() as any;
return fieldElement;
}
@@ -434,7 +447,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
callingClass.prototype.toXml !== this.toXml) {
const elem = utilsXml.createElement('field');
elem.setAttribute('name', this.name || '');
const text = Xml.domToText(this.toXml(elem));
const text = utilsXml.domToText(this.toXml(elem));
return text.replace(
' xmlns="https://developers.google.com/blockly/xml"', '');
}
@@ -456,7 +469,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
boolean {
if (callingClass.prototype.loadState === this.loadState &&
callingClass.prototype.fromXml !== this.fromXml) {
this.fromXml(Xml.textToDom(state as string));
this.fromXml(utilsXml.textToDom(state as string));
return true;
}
// Either they called this on purpose from their loadState, or they have
@@ -472,14 +485,11 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
dispose() {
dropDownDiv.hideIfOwner(this);
WidgetDiv.hideIfOwner(this);
Tooltip.unbindMouseEvents(this.getClickTarget_());
if (this.mouseDownWrapper_) {
browserEvents.unbind(this.mouseDownWrapper_);
if (!this.getSourceBlock()?.isDeadOrDying()) {
dom.removeNode(this.fieldGroup_);
}
dom.removeNode(this.fieldGroup_);
this.disposed = true;
}
@@ -619,7 +629,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
*
* @returns Validation function, or null.
*/
getValidator(): Function|null {
getValidator(): FieldValidator<T>|null {
return this.validator_;
}
@@ -698,14 +708,14 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* Calls showEditor_ when the field is clicked if the field is clickable.
* Do not override.
*
* @param opt_e Optional mouse event that triggered the field to open, or
* @param e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
* @sealed
* @internal
*/
showEditor(opt_e?: Event) {
showEditor(e?: Event) {
if (this.isClickable()) {
this.showEditor_(opt_e);
this.showEditor_(e);
}
}
@@ -722,11 +732,11 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
/**
* Updates the size of the field based on the text.
*
* @param opt_margin margin to use when positioning the text element.
* @param margin margin to use when positioning the text element.
*/
protected updateSize_(opt_margin?: number) {
protected updateSize_(margin?: number) {
const constants = this.getConstants();
const xOffset = opt_margin !== undefined ? opt_margin :
const xOffset = margin !== undefined ? margin :
this.borderRect_ ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING :
0;
let totalWidth = xOffset * 2;
@@ -766,17 +776,17 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
this.textElement_.setAttribute(
'x',
`${
String(
this.getSourceBlock()?.RTL ?
this.size_.width - contentWidth - xOffset :
xOffset}`);
xOffset));
this.textElement_.setAttribute(
'y',
`${
String(
constants!.FIELD_TEXT_BASELINE_CENTER ?
halfHeight :
halfHeight - constants!.FIELD_TEXT_HEIGHT / 2 +
constants!.FIELD_TEXT_BASELINE}`);
constants!.FIELD_TEXT_BASELINE));
}
/** Position a field's border rect after a size change. */
@@ -784,12 +794,12 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
if (!this.borderRect_) {
return;
}
this.borderRect_.setAttribute('width', `${this.size_.width}`);
this.borderRect_.setAttribute('height', `${this.size_.height}`);
this.borderRect_.setAttribute('width', String(this.size_.width));
this.borderRect_.setAttribute('height', String(this.size_.height));
this.borderRect_.setAttribute(
'rx', `${this.getConstants()!.FIELD_BORDER_RECT_RADIUS}`);
'rx', String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS));
this.borderRect_.setAttribute(
'ry', `${this.getConstants()!.FIELD_BORDER_RECT_RADIUS}`);
'ry', String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS));
}
/**
@@ -943,9 +953,8 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
forceRerender() {
this.isDirty_ = true;
if (this.sourceBlock_ && this.sourceBlock_.rendered) {
(this.sourceBlock_ as BlockSvg).render();
(this.sourceBlock_ as BlockSvg).queueRender();
(this.sourceBlock_ as BlockSvg).bumpNeighbours();
this.updateMarkers_();
}
}
@@ -965,41 +974,37 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
return;
}
let validatedValue = this.doClassValidation_(newValue);
// Class validators might accidentally forget to return, we'll ignore that.
newValue = this.processValidation_(newValue, validatedValue);
if (newValue instanceof Error) {
const classValidation = this.doClassValidation_(newValue);
const classValue = this.processValidation_(newValue, classValidation);
if (classValue instanceof Error) {
doLogging && console.log('invalid class validation, return');
return;
}
const localValidator = this.getValidator();
if (localValidator) {
validatedValue = localValidator.call(this, newValue);
// Local validators might accidentally forget to return, we'll ignore
// that.
newValue = this.processValidation_(newValue, validatedValue);
if (newValue instanceof Error) {
doLogging && console.log('invalid local validation, return');
return;
}
const localValidation = this.getValidator()?.call(this, classValue);
const localValue = this.processValidation_(classValue, localValidation);
if (localValue instanceof Error) {
doLogging && console.log('invalid local validation, return');
return;
}
const source = this.sourceBlock_;
if (source && source.disposed) {
doLogging && console.log('source disposed, return');
return;
}
const oldValue = this.getValue();
if (oldValue === newValue) {
if (oldValue === localValue) {
doLogging && console.log('same, doValueUpdate_, return');
this.doValueUpdate_(newValue);
this.doValueUpdate_(localValue);
return;
}
this.doValueUpdate_(newValue);
this.doValueUpdate_(localValue);
if (source && eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
source, 'field', this.name || null, oldValue, newValue));
source, 'field', this.name || null, oldValue, localValue));
}
if (this.isDirty_) {
this.forceRerender();
@@ -1015,8 +1020,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* @returns New value, or an Error object.
*/
private processValidation_(
newValue: AnyDuringMigration,
validatedValue: AnyDuringMigration): AnyDuringMigration {
newValue: AnyDuringMigration, validatedValue: T|null|undefined): T|Error {
if (validatedValue === null) {
this.doValueInvalid_(newValue);
if (this.isDirty_) {
@@ -1024,10 +1028,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
}
return Error();
}
if (validatedValue !== undefined) {
newValue = validatedValue;
}
return newValue;
return validatedValue === undefined ? newValue as T : validatedValue;
}
/**
@@ -1035,23 +1036,39 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
*
* @returns Current value.
*/
getValue(): AnyDuringMigration {
getValue(): T|null {
return this.value_;
}
/**
* Used to validate a value. Returns input by default. Can be overridden by
* subclasses, see FieldDropdown.
* Validate the changes to a field's value before they are set. See
* **FieldDropdown** for an example of subclass implementation.
*
* @param opt_newValue The value to be validated.
* @returns The validated value, same as input by default.
* **NOTE:** Validation returns one option between `T`, `null`, and
* `undefined`. **Field**'s implementation will never return `undefined`, but
* it is valid for a subclass to return `undefined` if the new value is
* compatible with `T`.
*
* @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values}
* @param newValue - The value to be validated.
* @returns One of three instructions for setting the new value: `T`, `null`,
* or `undefined`.
*
* - `T` to set this function's returned value instead of `newValue`.
*
* - `null` to invoke `doValueInvalid_` and not set a value.
*
* - `undefined` to set `newValue` as is.
*/
protected doClassValidation_(opt_newValue?: AnyDuringMigration):
AnyDuringMigration {
if (opt_newValue === null || opt_newValue === undefined) {
protected doClassValidation_(newValue: T): T|null|undefined;
protected doClassValidation_(newValue?: AnyDuringMigration): T|null;
protected doClassValidation_(newValue?: T|AnyDuringMigration): T|null
|undefined {
if (newValue === null || newValue === undefined) {
return null;
}
return opt_newValue;
return newValue as T;
}
/**
@@ -1060,7 +1077,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
*
* @param newValue The value to be saved.
*/
protected doValueUpdate_(newValue: AnyDuringMigration) {
protected doValueUpdate_(newValue: T) {
this.value_ = newValue;
this.isDirty_ = true;
}
@@ -1086,7 +1103,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
}
const gesture = (this.sourceBlock_.workspace as WorkspaceSvg).getGesture(e);
if (gesture) {
gesture.setStartField(this as Field);
gesture.setStartField(this);
}
}
@@ -1261,8 +1278,12 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
this.markerSvg_ = markerSvg;
}
/** Redraw any attached marker or cursor svgs if needed. */
protected updateMarkers_() {
/**
* Redraw any attached marker or cursor svgs if needed.
*
* @internal
*/
updateMarkers_() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
@@ -1288,6 +1309,8 @@ export interface FieldConfig {
/**
* For use by Field and descendants of Field. Constructors can change
* in descendants, though they should contain all of Field's prototype methods.
*
* @internal
*/
export type FieldProto = Pick<typeof Field, 'prototype'>;

View File

@@ -20,30 +20,24 @@ import {Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {FieldInput, FieldInputConfig, FieldInputValidator} from './field_input.js';
import * as dom from './utils/dom.js';
import {KeyCodes} from './utils/keycodes.js';
import * as math from './utils/math.js';
import type {Sentinel} from './utils/sentinel.js';
import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
export type FieldAngleValidator = FieldInputValidator<number>;
/**
* Class for an editable angle field.
*
* @alias Blockly.FieldAngle
*/
export class FieldAngle extends FieldInput<number> {
/**
* The default amount to round angles to when using a mouse or keyboard nav
* input. Must be a positive integer to support keyboard navigation.
*/
static readonly ROUND = 15;
/** Half the width of protractor image. */
static readonly HALF = 100 / 2;
/**
* Radius of protractor circle. Slightly smaller than protractor size since
* otherwise SVG crops off half the border at the edges.
*/
static readonly RADIUS: number = FieldAngle.HALF - 1;
/**
* Default property describing which direction makes an angle field's value
* increase. Angle increases clockwise (true) or counterclockwise (false).
@@ -64,88 +58,73 @@ export class FieldAngle extends FieldInput<number> {
static readonly WRAP = 360;
/**
* Radius of protractor circle. Slightly smaller than protractor size since
* otherwise SVG crops off half the border at the edges.
* The default amount to round angles to when using a mouse or keyboard nav
* input. Must be a positive integer to support keyboard navigation.
*/
static readonly RADIUS: number = FieldAngle.HALF - 1;
static readonly ROUND = 15;
/**
* Whether the angle should increase as the angle picker is moved clockwise
* (true) or counterclockwise (false).
*/
private clockwise_ = FieldAngle.CLOCKWISE;
private clockwise = FieldAngle.CLOCKWISE;
/**
* The offset of zero degrees (and all other angles).
*/
private offset_ = FieldAngle.OFFSET;
private offset = FieldAngle.OFFSET;
/**
* The maximum angle to allow before wrapping.
*/
private wrap_ = FieldAngle.WRAP;
private wrap = FieldAngle.WRAP;
/**
* The amount to round angles to when using a mouse or keyboard nav input.
*/
private round_ = FieldAngle.ROUND;
private round = FieldAngle.ROUND;
/** The angle picker's SVG element. */
private editor_: SVGSVGElement|null = null;
/**
* Array holding info needed to unbind events.
* Used for disposing.
* Ex: [[node, name, func], [node, name, func]].
*/
private boundEvents: browserEvents.Data[] = [];
/** The angle picker's gauge path depending on the value. */
gauge_: SVGPathElement|null = null;
/** Dynamic red line pointing at the value's angle. */
private line: SVGLineElement|null = null;
/** The angle picker's line drawn representing the value's angle. */
line_: SVGLineElement|null = null;
/** Dynamic pink area extending from 0 to the value's angle. */
private gauge: SVGPathElement|null = null;
/** The degree symbol for this field. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGTSpanElement'.
protected symbol_: SVGTSpanElement = null as AnyDuringMigration;
/** Wrapper click event data. */
private clickWrapper_: browserEvents.Data|null = null;
/** Surface click event data. */
private clickSurfaceWrapper_: browserEvents.Data|null = null;
/** Surface mouse move event data. */
private moveSurfaceWrapper_: browserEvents.Data|null = null;
protected symbol_: SVGTSpanElement|null = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
*/
override SERIALIZABLE = true;
/**
* @param opt_value The initial value of the field. Should cast to a number.
* @param value The initial value of the field. Should cast to a number.
* Defaults to 0. Also accepts Field.SKIP_SETUP if you wish to skip setup
* (only used by subclasses that want to handle configuration and setting
* the field value after their own constructors have run).
* @param opt_validator A function that is called to validate changes to the
* @param validator A function that is called to validate changes to the
* field's value. Takes in a number & returns a validated number, or null
* to abort the change.
* @param opt_config A map of options used to configure the field.
* @param config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/angle#creation}
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|number|Sentinel, opt_validator?: FieldAngleValidator,
opt_config?: FieldAngleConfig) {
value?: string|number|typeof Field.SKIP_SETUP,
validator?: FieldAngleValidator, config?: FieldAngleConfig) {
super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) {
return;
if (value === Field.SKIP_SETUP) return;
if (config) {
this.configure_(config);
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) {
this.setValidator(opt_validator);
this.setValue(value);
if (validator) {
this.setValidator(validator);
}
}
@@ -159,22 +138,22 @@ export class FieldAngle extends FieldInput<number> {
switch (config.mode) {
case Mode.COMPASS:
this.clockwise_ = true;
this.offset_ = 90;
this.clockwise = true;
this.offset = 90;
break;
case Mode.PROTRACTOR:
// This is the default mode, so we could do nothing. But just to
// future-proof, we'll set it anyway.
this.clockwise_ = false;
this.offset_ = 0;
this.clockwise = false;
this.offset = 0;
break;
}
// Allow individual settings to override the mode setting.
if (config.clockwise) this.clockwise_ = config.clockwise;
if (config.offset) this.offset_ = config.offset;
if (config.wrap) this.wrap_ = config.wrap;
if (config.round) this.round_ = config.round;
if (config.clockwise) this.clockwise = config.clockwise;
if (config.offset) this.offset = config.offset;
if (config.wrap) this.wrap = config.wrap;
if (config.round) this.round = config.round;
}
/**
@@ -184,32 +163,32 @@ export class FieldAngle extends FieldInput<number> {
*/
override initView() {
super.initView();
// Add the degree symbol to the left of the number, even in RTL (issue
// #2380)
// Add the degree symbol to the left of the number,
// even in RTL (issue #2380).
this.symbol_ = dom.createSvgElement(Svg.TSPAN, {});
this.symbol_.appendChild(document.createTextNode('°'));
this.getTextElement().appendChild(this.symbol_);
}
/** Updates the graph when the field rerenders. */
/** Updates the angle when the field rerenders. */
protected override render_() {
super.render_();
this.updateGraph_();
this.updateGraph();
}
/**
* Create and show the angle field's editor.
*
* @param opt_e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
* @param e Optional mouse event that triggered the field to open,
* or undefined if triggered programmatically.
*/
protected override showEditor_(opt_e?: Event) {
protected override showEditor_(e?: Event) {
// Mobile browsers have issues with in-line textareas (focus & keyboards).
const noFocus = userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD;
super.showEditor_(opt_e, noFocus);
super.showEditor_(e, noFocus);
this.dropdownCreate_();
dropDownDiv.getContentDiv().appendChild(this.editor_!);
const editor = this.dropdownCreate();
dropDownDiv.getContentDiv().appendChild(editor);
if (this.sourceBlock_ instanceof BlockSvg) {
dropDownDiv.setColour(
@@ -217,16 +196,17 @@ export class FieldAngle extends FieldInput<number> {
this.sourceBlock_.style.colourTertiary);
}
// AnyDuringMigration because: Argument of type 'this' is not assignable to
// parameter of type 'Field'.
dropDownDiv.showPositionedByField(
this as AnyDuringMigration, this.dropdownDispose_.bind(this));
dropDownDiv.showPositionedByField(this, this.dropdownDispose.bind(this));
this.updateGraph_();
this.updateGraph();
}
/** Create the angle dropdown editor. */
private dropdownCreate_() {
/**
* Creates the angle dropdown editor.
*
* @returns The newly created slider.
*/
private dropdownCreate(): SVGSVGElement {
const svg = dom.createSvgElement(Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
@@ -244,9 +224,9 @@ export class FieldAngle extends FieldInput<number> {
'class': 'blocklyAngleCircle',
},
svg);
this.gauge_ =
this.gauge =
dom.createSvgElement(Svg.PATH, {'class': 'blocklyAngleGauge'}, svg);
this.line_ = dom.createSvgElement(
this.line = dom.createSvgElement(
Svg.LINE, {
'x1': FieldAngle.HALF,
'y1': FieldAngle.HALF,
@@ -272,38 +252,30 @@ export class FieldAngle extends FieldInput<number> {
// The angle picker is different from other fields in that it updates on
// mousemove even if it's not in the middle of a drag. In future we may
// change this behaviour.
this.clickWrapper_ =
browserEvents.conditionalBind(svg, 'click', this, this.hide_);
this.boundEvents.push(
browserEvents.conditionalBind(svg, 'click', this, this.hide));
// On touch devices, the picker's value is only updated with a drag. Add
// a click handler on the drag surface to update the value if the surface
// is clicked.
this.clickSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'pointerdown', this, this.onMouseMove_, true);
this.moveSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'pointermove', this, this.onMouseMove_, true);
this.editor_ = svg;
this.boundEvents.push(browserEvents.conditionalBind(
circle, 'pointerdown', this, this.onMouseMove_, true));
this.boundEvents.push(browserEvents.conditionalBind(
circle, 'pointermove', this, this.onMouseMove_, true));
return svg;
}
/** Disposes of events and DOM-references belonging to the angle editor. */
private dropdownDispose_() {
if (this.clickWrapper_) {
browserEvents.unbind(this.clickWrapper_);
this.clickWrapper_ = null;
private dropdownDispose() {
for (const event of this.boundEvents) {
browserEvents.unbind(event);
}
if (this.clickSurfaceWrapper_) {
browserEvents.unbind(this.clickSurfaceWrapper_);
this.clickSurfaceWrapper_ = null;
}
if (this.moveSurfaceWrapper_) {
browserEvents.unbind(this.moveSurfaceWrapper_);
this.moveSurfaceWrapper_ = null;
}
this.gauge_ = null;
this.line_ = null;
this.boundEvents.length = 0;
this.gauge = null;
this.line = null;
}
/** Hide the editor. */
private hide_() {
private hide() {
dropDownDiv.hideIfOwner(this);
WidgetDiv.hide();
}
@@ -315,7 +287,7 @@ export class FieldAngle extends FieldInput<number> {
*/
protected onMouseMove_(e: PointerEvent) {
// Calculate angle.
const bBox = this.gauge_!.ownerSVGElement!.getBoundingClientRect();
const bBox = this.gauge!.ownerSVGElement!.getBoundingClientRect();
const dx = e.clientX - bBox.left - FieldAngle.HALF;
const dy = e.clientY - bBox.top - FieldAngle.HALF;
let angle = Math.atan(-dy / dx);
@@ -332,13 +304,13 @@ export class FieldAngle extends FieldInput<number> {
}
// Do offsetting.
if (this.clockwise_) {
angle = this.offset_ + 360 - angle;
if (this.clockwise) {
angle = this.offset + 360 - angle;
} else {
angle = 360 - (this.offset_ - angle);
angle = 360 - (this.offset - angle);
}
this.displayMouseOrKeyboardValue_(angle);
this.displayMouseOrKeyboardValue(angle);
}
/**
@@ -348,31 +320,31 @@ export class FieldAngle extends FieldInput<number> {
*
* @param angle New angle.
*/
private displayMouseOrKeyboardValue_(angle: number) {
if (this.round_) {
angle = Math.round(angle / this.round_) * this.round_;
private displayMouseOrKeyboardValue(angle: number) {
if (this.round) {
angle = Math.round(angle / this.round) * this.round;
}
angle = this.wrapValue_(angle);
angle = this.wrapValue(angle);
if (angle !== this.value_) {
this.setEditorValue_(angle);
}
}
/** Redraw the graph with the current angle. */
private updateGraph_() {
if (!this.gauge_) {
private updateGraph() {
if (!this.gauge || !this.line) {
return;
}
// Always display the input (i.e. getText) even if it is invalid.
let angleDegrees = Number(this.getText()) + this.offset_;
let angleDegrees = Number(this.getText()) + this.offset;
angleDegrees %= 360;
let angleRadians = math.toRadians(angleDegrees);
const path = ['M ', FieldAngle.HALF, ',', FieldAngle.HALF];
let x2 = FieldAngle.HALF;
let y2 = FieldAngle.HALF;
if (!isNaN(angleRadians)) {
const clockwiseFlag = Number(this.clockwise_);
const angle1 = math.toRadians(this.offset_);
const clockwiseFlag = Number(this.clockwise);
const angle1 = math.toRadians(this.offset);
const x1 = Math.cos(angle1) * FieldAngle.RADIUS;
const y1 = Math.sin(angle1) * -FieldAngle.RADIUS;
if (clockwiseFlag) {
@@ -390,13 +362,9 @@ export class FieldAngle extends FieldInput<number> {
' l ', x1, ',', y1, ' A ', FieldAngle.RADIUS, ',', FieldAngle.RADIUS,
' 0 ', largeFlag, ' ', clockwiseFlag, ' ', x2, ',', y2, ' z');
}
this.gauge_.setAttribute('d', path.join(''));
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.line_!.setAttribute('x2', x2 as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.line_!.setAttribute('y2', y2 as AnyDuringMigration);
this.gauge.setAttribute('d', path.join(''));
this.line.setAttribute('x2', `${x2}`);
this.line.setAttribute('y2', `${y2}`);
}
/**
@@ -404,31 +372,35 @@ export class FieldAngle extends FieldInput<number> {
*
* @param e Keyboard event.
*/
protected override onHtmlInputKeyDown_(e: Event) {
protected override onHtmlInputKeyDown_(e: KeyboardEvent) {
super.onHtmlInputKeyDown_(e);
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const keyboardEvent = e as KeyboardEvent;
let multiplier;
if (keyboardEvent.keyCode === KeyCodes.LEFT) {
// decrement (increment in RTL)
multiplier = block.RTL ? 1 : -1;
} else if (keyboardEvent.keyCode === KeyCodes.RIGHT) {
// increment (decrement in RTL)
multiplier = block.RTL ? -1 : 1;
} else if (keyboardEvent.keyCode === KeyCodes.DOWN) {
// decrement
multiplier = -1;
} else if (keyboardEvent.keyCode === KeyCodes.UP) {
// increment
multiplier = 1;
let multiplier = 0;
switch (e.key) {
case 'ArrowLeft':
// decrement (increment in RTL)
multiplier = block.RTL ? 1 : -1;
break;
case 'ArrowRight':
// increment (decrement in RTL)
multiplier = block.RTL ? -1 : 1;
break;
case 'ArrowDown':
// decrement
multiplier = -1;
break;
case 'ArrowUp':
// increment
multiplier = 1;
break;
}
if (multiplier) {
const value = this.getValue() as number;
this.displayMouseOrKeyboardValue_(value + multiplier * this.round_);
this.displayMouseOrKeyboardValue(value + multiplier * this.round);
e.preventDefault();
e.stopPropagation();
}
@@ -437,16 +409,15 @@ export class FieldAngle extends FieldInput<number> {
/**
* Ensure that the input value is a valid angle.
*
* @param opt_newValue The input value.
* @param newValue The input value.
* @returns A valid angle, or null if invalid.
*/
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
number|null {
const value = Number(opt_newValue);
protected override doClassValidation_(newValue?: any): number|null {
const value = Number(newValue);
if (isNaN(value) || !isFinite(value)) {
return null;
}
return this.wrapValue_(value);
return this.wrapValue(value);
}
/**
@@ -455,12 +426,12 @@ export class FieldAngle extends FieldInput<number> {
* @param value The value to wrap.
* @returns The wrapped value.
*/
private wrapValue_(value: number): number {
private wrapValue(value: number): number {
value %= 360;
if (value < 0) {
value += 360;
}
if (value > this.wrap_) {
if (value > this.wrap) {
value -= 360;
}
return value;
@@ -481,13 +452,20 @@ export class FieldAngle extends FieldInput<number> {
}
}
/** CSS for angle field. See css.js for use. */
fieldRegistry.register('field_angle', FieldAngle);
FieldAngle.prototype.DEFAULT_VALUE = 0;
/**
* CSS for angle field.
*/
Css.register(`
.blocklyAngleCircle {
stroke: #444;
stroke-width: 1;
fill: #ddd;
fill-opacity: .8;
fill-opacity: 0.8;
}
.blocklyAngleMarks {
@@ -497,7 +475,7 @@ Css.register(`
.blocklyAngleGauge {
fill: #f88;
fill-opacity: .8;
fill-opacity: 0.8;
pointer-events: none;
}
@@ -509,10 +487,6 @@ Css.register(`
}
`);
fieldRegistry.register('field_angle', FieldAngle);
FieldAngle.prototype.DEFAULT_VALUE = 0;
/**
* The two main modes of the angle field.
* Compass specifies:
@@ -549,3 +523,20 @@ export interface FieldAngleConfig extends FieldInputConfig {
export interface FieldAngleFromJsonConfig extends FieldAngleConfig {
angle?: number;
}
/**
* A function that is called to validate changes to the field's value before
* they are set.
*
* @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values}
* @param newValue The value to be validated.
* @returns One of three instructions for setting the new value: `T`, `null`,
* or `undefined`.
*
* - `T` to set this function's returned value instead of `newValue`.
*
* - `null` to invoke `doValueInvalid_` and not set a value.
*
* - `undefined` to set `newValue` as is.
*/
export type FieldAngleValidator = FieldInputValidator<number>;

View File

@@ -18,16 +18,14 @@ import './events/events_block_change.js';
import * as dom from './utils/dom.js';
import {Field, FieldConfig, FieldValidator} from './field.js';
import * as fieldRegistry from './field_registry.js';
import type {Sentinel} from './utils/sentinel.js';
export type FieldCheckboxValidator = FieldValidator<boolean>;
type BoolString = 'TRUE'|'FALSE';
type CheckboxBool = BoolString|boolean;
/**
* Class for a checkbox field.
*
* @alias Blockly.FieldCheckbox
*/
export class FieldCheckbox extends Field<boolean> {
export class FieldCheckbox extends Field<CheckboxBool> {
/** Default character for the checkmark. */
static readonly CHECK_CHAR = '✓';
private checkChar_: string;
@@ -42,26 +40,30 @@ export class FieldCheckbox extends Field<boolean> {
* Mouse cursor style when over the hotspot that initiates editability.
*/
override CURSOR = 'default';
override value_: AnyDuringMigration;
/**
* @param opt_value The initial value of the field. Should either be 'TRUE',
* NOTE: The default value is set in `Field`, so maintain that value instead
* of overwriting it here or in the constructor.
*/
override value_: boolean|null = this.value_;
/**
* @param value The initial value of the field. Should either be 'TRUE',
* 'FALSE' or a boolean. Defaults to 'FALSE'. Also accepts
* Field.SKIP_SETUP if you wish to skip setup (only used by subclasses
* that want to handle configuration and setting the field value after
* their own constructors have run).
* @param opt_validator A function that is called to validate changes to the
* @param validator A function that is called to validate changes to the
* field's value. Takes in a value ('TRUE' or 'FALSE') & returns a
* validated value ('TRUE' or 'FALSE'), or null to abort the change.
* @param opt_config A map of options used to configure the field.
* @param config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/checkbox#creation}
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|boolean|Sentinel,
opt_validator?: FieldCheckboxValidator,
opt_config?: FieldCheckboxConfig) {
value?: CheckboxBool|typeof Field.SKIP_SETUP,
validator?: FieldCheckboxValidator, config?: FieldCheckboxConfig) {
super(Field.SKIP_SETUP);
/**
@@ -70,15 +72,13 @@ export class FieldCheckbox extends Field<boolean> {
*/
this.checkChar_ = FieldCheckbox.CHECK_CHAR;
if (opt_value === Field.SKIP_SETUP) {
return;
if (value === Field.SKIP_SETUP) return;
if (config) {
this.configure_(config);
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) {
this.setValidator(opt_validator);
this.setValue(value);
if (validator) {
this.setValidator(validator);
}
}
@@ -149,15 +149,15 @@ export class FieldCheckbox extends Field<boolean> {
/**
* Ensure that the input value is valid ('TRUE' or 'FALSE').
*
* @param opt_newValue The input value.
* @param newValue The input value.
* @returns A valid value ('TRUE' or 'FALSE), or null if invalid.
*/
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null {
if (opt_newValue === true || opt_newValue === 'TRUE') {
protected override doClassValidation_(newValue?: AnyDuringMigration):
BoolString|null {
if (newValue === true || newValue === 'TRUE') {
return 'TRUE';
}
if (opt_newValue === false || opt_newValue === 'FALSE') {
if (newValue === false || newValue === 'FALSE') {
return 'FALSE';
}
return null;
@@ -169,7 +169,7 @@ export class FieldCheckbox extends Field<boolean> {
* @param newValue The value to be saved. The default validator guarantees
* that this is a either 'TRUE' or 'FALSE'.
*/
protected override doValueUpdate_(newValue: AnyDuringMigration) {
protected override doValueUpdate_(newValue: BoolString) {
this.value_ = this.convertValueToBool_(newValue);
// Update visual.
if (this.textElement_) {
@@ -182,7 +182,7 @@ export class FieldCheckbox extends Field<boolean> {
*
* @returns The value of this field.
*/
override getValue(): string {
override getValue(): BoolString {
return this.value_ ? 'TRUE' : 'FALSE';
}
@@ -191,8 +191,8 @@ export class FieldCheckbox extends Field<boolean> {
*
* @returns The boolean value of this field.
*/
getValueBoolean(): boolean {
return this.value_ as boolean;
getValueBoolean(): boolean|null {
return this.value_;
}
/**
@@ -213,12 +213,9 @@ export class FieldCheckbox extends Field<boolean> {
* @param value The value to convert.
* @returns The converted value.
*/
private convertValueToBool_(value: AnyDuringMigration): boolean {
if (typeof value === 'string') {
return value === 'TRUE';
} else {
return !!value;
}
private convertValueToBool_(value: CheckboxBool|null): boolean {
if (typeof value === 'string') return value === 'TRUE';
return !!value;
}
/**
@@ -253,3 +250,20 @@ export interface FieldCheckboxConfig extends FieldConfig {
export interface FieldCheckboxFromJsonConfig extends FieldCheckboxConfig {
checked?: boolean;
}
/**
* A function that is called to validate changes to the field's value before
* they are set.
*
* @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values}
* @param newValue The value to be validated.
* @returns One of three instructions for setting the new value: `T`, `null`,
* or `undefined`.
*
* - `T` to set this function's returned value instead of `newValue`.
*
* - `null` to invoke `doValueInvalid_` and not set a value.
*
* - `undefined` to set `newValue` as is.
*/
export type FieldCheckboxValidator = FieldValidator<CheckboxBool>;

View File

@@ -25,16 +25,10 @@ import * as fieldRegistry from './field_registry.js';
import * as aria from './utils/aria.js';
import * as colour from './utils/colour.js';
import * as idGenerator from './utils/idgenerator.js';
import {KeyCodes} from './utils/keycodes.js';
import type {Sentinel} from './utils/sentinel.js';
import {Size} from './utils/size.js';
export type FieldColourValidator = FieldValidator<string>;
/**
* Class for a colour input field.
*
* @alias Blockly.FieldColour
*/
export class FieldColour extends Field<string> {
/**
@@ -80,25 +74,17 @@ export class FieldColour extends Field<string> {
static COLUMNS = 7;
/** The field's colour picker element. */
private picker_: Element|null = null;
private picker: HTMLElement|null = null;
/** Index of the currently highlighted element. */
private highlightedIndex_: number|null = null;
private highlightedIndex: number|null = null;
/** Mouse click event data. */
private onClickWrapper_: browserEvents.Data|null = null;
/** Mouse move event data. */
private onMouseMoveWrapper_: browserEvents.Data|null = null;
/** Mouse enter event data. */
private onMouseEnterWrapper_: browserEvents.Data|null = null;
/** Mouse leave event data. */
private onMouseLeaveWrapper_: browserEvents.Data|null = null;
/** Key down event data. */
private onKeyDownWrapper_: browserEvents.Data|null = null;
/**
* Array holding info needed to unbind events.
* Used for disposing.
* Ex: [[node, name, func], [node, name, func]].
*/
private boundEvents: browserEvents.Data[] = [];
/**
* Serializable fields are saved by the serializer, non-serializable fields
@@ -117,55 +103,46 @@ export class FieldColour extends Field<string> {
protected override isDirty_ = false;
/** Array of colours used by this field. If null, use the global list. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'string[]'.
private colours_: string[] = null as AnyDuringMigration;
private colours: string[]|null = null;
/**
* Array of colour tooltips used by this field. If null, use the global
* list.
*/
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'string[]'.
private titles_: string[] = null as AnyDuringMigration;
private titles: string[]|null = null;
/**
* Number of colour columns used by this field. If 0, use the global
* setting. By default use the global constants for columns.
*/
private columns_ = 0;
override size_: AnyDuringMigration;
override clickTarget_: AnyDuringMigration;
override value_: AnyDuringMigration;
private columns = 0;
/**
* @param opt_value The initial value of the field. Should be in '#rrggbb'
* @param value The initial value of the field. Should be in '#rrggbb'
* format. Defaults to the first value in the default colour array. Also
* accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param opt_validator A function that is called to validate changes to the
* @param validator A function that is called to validate changes to the
* field's value. Takes in a colour string & returns a validated colour
* string ('#rrggbb' format), or null to abort the change.Blockly.
* @param opt_config A map of options used to configure the field.
* string ('#rrggbb' format), or null to abort the change.
* @param config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/colour}
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|Sentinel, opt_validator?: FieldColourValidator,
opt_config?: FieldColourConfig) {
value?: string|typeof Field.SKIP_SETUP, validator?: FieldColourValidator,
config?: FieldColourConfig) {
super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) {
return;
if (value === Field.SKIP_SETUP) return;
if (config) {
this.configure_(config);
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) {
this.setValidator(opt_validator);
this.setValue(value);
if (validator) {
this.setValidator(validator);
}
}
@@ -176,9 +153,9 @@ export class FieldColour extends Field<string> {
*/
protected override configure_(config: FieldColourConfig) {
super.configure_(config);
if (config.colourOptions) this.colours_ = config.colourOptions;
if (config.colourTitles) this.titles_ = config.colourTitles;
if (config.columns) this.columns_ = config.columns;
if (config.colourOptions) this.colours = config.colourOptions;
if (config.colourTitles) this.titles = config.colourTitles;
if (config.columns) this.columns = config.columns;
}
/**
@@ -198,6 +175,11 @@ export class FieldColour extends Field<string> {
}
}
/**
* Updates text field to match the colour/style of the block.
*
* @internal
*/
override applyColour() {
if (!this.getConstants()!.FIELD_COLOUR_FULL_BLOCK) {
if (this.borderRect_) {
@@ -213,15 +195,14 @@ export class FieldColour extends Field<string> {
/**
* Ensure that the input value is a valid colour.
*
* @param opt_newValue The input value.
* @param newValue The input value.
* @returns A valid colour, or null if invalid.
*/
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null {
if (typeof opt_newValue !== 'string') {
protected override doClassValidation_(newValue?: any): string|null {
if (typeof newValue !== 'string') {
return null;
}
return colour.parse(opt_newValue);
return colour.parse(newValue);
}
/**
@@ -230,15 +211,14 @@ export class FieldColour extends Field<string> {
* @param newValue The value to be saved. The default validator guarantees
* that this is a colour in '#rrggbb' format.
*/
protected override doValueUpdate_(newValue: AnyDuringMigration) {
protected override doValueUpdate_(newValue: string) {
this.value_ = newValue;
if (this.borderRect_) {
this.borderRect_.style.fill = newValue as string;
this.borderRect_.style.fill = newValue;
} else if (
this.sourceBlock_ && this.sourceBlock_.rendered &&
this.sourceBlock_ instanceof BlockSvg) {
this.sourceBlock_.pathObject.svgPath.setAttribute(
'fill', newValue as string);
this.sourceBlock_.pathObject.svgPath.setAttribute('fill', newValue);
this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff');
}
}
@@ -262,14 +242,14 @@ export class FieldColour extends Field<string> {
*
* @param colours Array of colours for this block, or null to use default
* (FieldColour.COLOURS).
* @param opt_titles Optional array of colour tooltips, or null to use default
* @param titles Optional array of colour tooltips, or null to use default
* (FieldColour.TITLES).
* @returns Returns itself (for method chaining).
*/
setColours(colours: string[], opt_titles?: string[]): FieldColour {
this.colours_ = colours;
if (opt_titles) {
this.titles_ = opt_titles;
setColours(colours: string[], titles?: string[]): FieldColour {
this.colours = colours;
if (titles) {
this.titles = titles;
}
return this;
}
@@ -282,23 +262,19 @@ export class FieldColour extends Field<string> {
* @returns Returns itself (for method chaining).
*/
setColumns(columns: number): FieldColour {
this.columns_ = columns;
this.columns = columns;
return this;
}
/** Create and show the colour field's editor. */
protected override showEditor_() {
this.dropdownCreate_();
// AnyDuringMigration because: Argument of type 'Element | null' is not
// assignable to parameter of type 'Node'.
dropDownDiv.getContentDiv().appendChild(this.picker_ as AnyDuringMigration);
this.dropdownCreate();
dropDownDiv.getContentDiv().appendChild(this.picker!);
dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this));
dropDownDiv.showPositionedByField(this, this.dropdownDispose.bind(this));
// Focus so we can start receiving keyboard events.
// AnyDuringMigration because: Property 'focus' does not exist on type
// 'Element'.
(this.picker_ as AnyDuringMigration)!.focus({preventScroll: true});
this.picker!.focus({preventScroll: true});
}
/**
@@ -306,7 +282,7 @@ export class FieldColour extends Field<string> {
*
* @param e Mouse event.
*/
private onClick_(e: PointerEvent) {
private onClick(e: PointerEvent) {
const cell = e.target as Element;
const colour = cell && cell.getAttribute('data-colour');
if (colour !== null) {
@@ -321,31 +297,35 @@ export class FieldColour extends Field<string> {
*
* @param e Keyboard event.
*/
private onKeyDown_(e: KeyboardEvent) {
let handled = false;
if (e.keyCode === KeyCodes.UP) {
this.moveHighlightBy_(0, -1);
handled = true;
} else if (e.keyCode === KeyCodes.DOWN) {
this.moveHighlightBy_(0, 1);
handled = true;
} else if (e.keyCode === KeyCodes.LEFT) {
this.moveHighlightBy_(-1, 0);
handled = true;
} else if (e.keyCode === KeyCodes.RIGHT) {
this.moveHighlightBy_(1, 0);
handled = true;
} else if (e.keyCode === KeyCodes.ENTER) {
// Select the highlighted colour.
const highlighted = this.getHighlighted_();
if (highlighted) {
const colour = highlighted && highlighted.getAttribute('data-colour');
if (colour !== null) {
this.setValue(colour);
private onKeyDown(e: KeyboardEvent) {
let handled = true;
let highlighted: HTMLElement|null;
switch (e.key) {
case 'ArrowUp':
this.moveHighlightBy(0, -1);
break;
case 'ArrowDown':
this.moveHighlightBy(0, 1);
break;
case 'ArrowLeft':
this.moveHighlightBy(-1, 0);
break;
case 'ArrowRight':
this.moveHighlightBy(1, 0);
break;
case 'Enter':
// Select the highlighted colour.
highlighted = this.getHighlighted();
if (highlighted) {
const colour = highlighted.getAttribute('data-colour');
if (colour !== null) {
this.setValue(colour);
}
}
}
dropDownDiv.hideWithoutAnimation();
handled = true;
dropDownDiv.hideWithoutAnimation();
break;
default:
handled = false;
}
if (handled) {
e.stopPropagation();
@@ -355,22 +335,22 @@ export class FieldColour extends Field<string> {
/**
* Move the currently highlighted position by dx and dy.
*
* @param dx Change of x
* @param dy Change of y
* @param dx Change of x.
* @param dy Change of y.
*/
private moveHighlightBy_(dx: number, dy: number) {
if (!this.highlightedIndex_) {
private moveHighlightBy(dx: number, dy: number) {
if (!this.highlightedIndex) {
return;
}
const colours = this.colours_ || FieldColour.COLOURS;
const columns = this.columns_ || FieldColour.COLUMNS;
const colours = this.colours || FieldColour.COLOURS;
const columns = this.columns || FieldColour.COLUMNS;
// Get the current x and y coordinates
let x = this.highlightedIndex_ % columns;
let y = Math.floor(this.highlightedIndex_ / columns);
// Get the current x and y coordinates.
let x = this.highlightedIndex % columns;
let y = Math.floor(this.highlightedIndex / columns);
// Add the offset
// Add the offset.
x += dx;
y += dy;
@@ -405,9 +385,9 @@ export class FieldColour extends Field<string> {
}
// Move the highlight to the new coordinates.
const cell = this.picker_!.childNodes[y].childNodes[x] as Element;
const cell = this.picker!.childNodes[y].childNodes[x] as Element;
const index = y * columns + x;
this.setHighlightedCell_(cell, index);
this.setHighlightedCell(cell, index);
}
/**
@@ -415,30 +395,26 @@ export class FieldColour extends Field<string> {
*
* @param e Mouse event.
*/
private onMouseMove_(e: PointerEvent) {
private onMouseMove(e: PointerEvent) {
const cell = e.target as Element;
const index = cell && Number(cell.getAttribute('data-index'));
if (index !== null && index !== this.highlightedIndex_) {
this.setHighlightedCell_(cell, index);
if (index !== null && index !== this.highlightedIndex) {
this.setHighlightedCell(cell, index);
}
}
/** Handle a mouse enter event. Focus the picker. */
private onMouseEnter_() {
// AnyDuringMigration because: Property 'focus' does not exist on type
// 'Element'.
(this.picker_ as AnyDuringMigration)!.focus({preventScroll: true});
private onMouseEnter() {
this.picker?.focus({preventScroll: true});
}
/**
* Handle a mouse leave event. Blur the picker and unhighlight
* the currently highlighted colour.
*/
private onMouseLeave_() {
// AnyDuringMigration because: Property 'blur' does not exist on type
// 'Element'.
(this.picker_ as AnyDuringMigration)!.blur();
const highlighted = this.getHighlighted_();
private onMouseLeave() {
this.picker?.blur();
const highlighted = this.getHighlighted();
if (highlighted) {
dom.removeClass(highlighted, 'blocklyColourHighlighted');
}
@@ -449,55 +425,53 @@ export class FieldColour extends Field<string> {
*
* @returns Highlighted item (null if none).
*/
private getHighlighted_(): HTMLElement|null {
if (!this.highlightedIndex_) {
private getHighlighted(): HTMLElement|null {
if (!this.highlightedIndex) {
return null;
}
const columns = this.columns_ || FieldColour.COLUMNS;
const x = this.highlightedIndex_ % columns;
const y = Math.floor(this.highlightedIndex_ / columns);
const row = this.picker_!.childNodes[y];
const columns = this.columns || FieldColour.COLUMNS;
const x = this.highlightedIndex % columns;
const y = Math.floor(this.highlightedIndex / columns);
const row = this.picker!.childNodes[y];
if (!row) {
return null;
}
const col = row.childNodes[x] as HTMLElement;
return col;
return row.childNodes[x] as HTMLElement;
}
/**
* Update the currently highlighted cell.
*
* @param cell the new cell to highlight
* @param index the index of the new cell
* @param cell The new cell to highlight.
* @param index The index of the new cell.
*/
private setHighlightedCell_(cell: Element, index: number) {
private setHighlightedCell(cell: Element, index: number) {
// Unhighlight the current item.
const highlighted = this.getHighlighted_();
const highlighted = this.getHighlighted();
if (highlighted) {
dom.removeClass(highlighted, 'blocklyColourHighlighted');
}
// Highlight new item.
dom.addClass(cell, 'blocklyColourHighlighted');
// Set new highlighted index.
this.highlightedIndex_ = index;
this.highlightedIndex = index;
// Update accessibility roles.
// AnyDuringMigration because: Argument of type 'string | null' is not
// assignable to parameter of type 'string | number | boolean | string[]'.
aria.setState(
this.picker_ as Element, aria.State.ACTIVEDESCENDANT,
cell.getAttribute('id') as AnyDuringMigration);
const cellId = cell.getAttribute('id');
if (cellId && this.picker) {
aria.setState(this.picker, aria.State.ACTIVEDESCENDANT, cellId);
}
}
/** Create a colour picker dropdown editor. */
private dropdownCreate_() {
const columns = this.columns_ || FieldColour.COLUMNS;
const colours = this.colours_ || FieldColour.COLOURS;
const titles = this.titles_ || FieldColour.TITLES;
private dropdownCreate() {
const columns = this.columns || FieldColour.COLUMNS;
const colours = this.colours || FieldColour.COLOURS;
const titles = this.titles || FieldColour.TITLES;
const selectedColour = this.getValue();
// Create the palette.
const table = (document.createElement('table'));
const table = document.createElement('table');
table.className = 'blocklyColourTable';
table.tabIndex = 0;
table.dir = 'ltr';
@@ -519,58 +493,40 @@ export class FieldColour extends Field<string> {
cell.setAttribute('data-colour', colours[i]);
cell.title = titles[i] || colours[i];
cell.id = idGenerator.getNextUniqueId();
// AnyDuringMigration because: Argument of type 'number' is not
// assignable to parameter of type 'string'.
cell.setAttribute('data-index', i as AnyDuringMigration);
cell.setAttribute('data-index', `${i}`);
aria.setRole(cell, aria.Role.GRIDCELL);
aria.setState(cell, aria.State.LABEL, colours[i]);
aria.setState(cell, aria.State.SELECTED, colours[i] === selectedColour);
cell.style.backgroundColor = colours[i];
if (colours[i] === selectedColour) {
cell.className = 'blocklyColourSelected';
this.highlightedIndex_ = i;
this.highlightedIndex = i;
}
}
// Configure event handler on the table to listen for any event in a cell.
this.onClickWrapper_ = browserEvents.conditionalBind(
table, 'pointerdown', this, this.onClick_, true);
this.onMouseMoveWrapper_ = browserEvents.conditionalBind(
table, 'pointermove', this, this.onMouseMove_, true);
this.onMouseEnterWrapper_ = browserEvents.conditionalBind(
table, 'pointerenter', this, this.onMouseEnter_, true);
this.onMouseLeaveWrapper_ = browserEvents.conditionalBind(
table, 'pointerleave', this, this.onMouseLeave_, true);
this.onKeyDownWrapper_ =
browserEvents.conditionalBind(table, 'keydown', this, this.onKeyDown_);
this.boundEvents.push(browserEvents.conditionalBind(
table, 'pointerdown', this, this.onClick, true));
this.boundEvents.push(browserEvents.conditionalBind(
table, 'pointermove', this, this.onMouseMove, true));
this.boundEvents.push(browserEvents.conditionalBind(
table, 'pointerenter', this, this.onMouseEnter, true));
this.boundEvents.push(browserEvents.conditionalBind(
table, 'pointerleave', this, this.onMouseLeave, true));
this.boundEvents.push(browserEvents.conditionalBind(
table, 'keydown', this, this.onKeyDown, false));
this.picker_ = table;
this.picker = table;
}
/** Disposes of events and DOM-references belonging to the colour editor. */
private dropdownDispose_() {
if (this.onClickWrapper_) {
browserEvents.unbind(this.onClickWrapper_);
this.onClickWrapper_ = null;
private dropdownDispose() {
for (const event of this.boundEvents) {
browserEvents.unbind(event);
}
if (this.onMouseMoveWrapper_) {
browserEvents.unbind(this.onMouseMoveWrapper_);
this.onMouseMoveWrapper_ = null;
}
if (this.onMouseEnterWrapper_) {
browserEvents.unbind(this.onMouseEnterWrapper_);
this.onMouseEnterWrapper_ = null;
}
if (this.onMouseLeaveWrapper_) {
browserEvents.unbind(this.onMouseLeaveWrapper_);
this.onMouseLeaveWrapper_ = null;
}
if (this.onKeyDownWrapper_) {
browserEvents.unbind(this.onKeyDownWrapper_);
this.onKeyDownWrapper_ = null;
}
this.picker_ = null;
this.highlightedIndex_ = null;
this.boundEvents.length = 0;
this.picker = null;
this.highlightedIndex = null;
}
/**
@@ -584,14 +540,19 @@ export class FieldColour extends Field<string> {
static fromJson(options: FieldColourFromJsonConfig): FieldColour {
// `this` might be a subclass of FieldColour if that class doesn't override
// the static fromJson method.
return new this(options['colour'], undefined, options);
return new this(options.colour, undefined, options);
}
}
/** The default value for this field. */
FieldColour.prototype.DEFAULT_VALUE = FieldColour.COLOURS[0];
/** CSS for colour picker. See css.js for use. */
fieldRegistry.register('field_colour', FieldColour);
/**
* CSS for colour picker.
*/
Css.register(`
.blocklyColourTable {
border-collapse: collapse;
@@ -601,7 +562,7 @@ Css.register(`
}
.blocklyColourTable>tr>td {
border: .5px solid #888;
border: 0.5px solid #888;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
@@ -612,7 +573,7 @@ Css.register(`
.blocklyColourTable>tr>td.blocklyColourHighlighted {
border-color: #eee;
box-shadow: 2px 2px 7px 2px rgba(0,0,0,.3);
box-shadow: 2px 2px 7px 2px rgba(0, 0, 0, 0.3);
position: relative;
}
@@ -623,8 +584,6 @@ Css.register(`
}
`);
fieldRegistry.register('field_colour', FieldColour);
/**
* Config options for the colour field.
*/
@@ -640,3 +599,20 @@ export interface FieldColourConfig extends FieldConfig {
export interface FieldColourFromJsonConfig extends FieldColourConfig {
colour?: string;
}
/**
* A function that is called to validate changes to the field's value before
* they are set.
*
* @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values}
* @param newValue The value to be validated.
* @returns One of three instructions for setting the new value: `T`, `null`,
* or `undefined`.
*
* - `T` to set this function's returned value instead of `newValue`.
*
* - `null` to invoke `doValueInvalid_` and not set a value.
*
* - `undefined` to set `newValue` as is.
*/
export type FieldColourValidator = FieldValidator<string>;

View File

@@ -20,20 +20,16 @@ import {Field, FieldConfig, FieldValidator, UnattachedFieldError} from './field.
import * as fieldRegistry from './field_registry.js';
import {Menu} from './menu.js';
import {MenuItem} from './menuitem.js';
import * as style from './utils/style.js';
import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
import * as utilsString from './utils/string.js';
import {Svg} from './utils/svg.js';
export type FieldDropdownValidator = FieldValidator<string>;
/**
* Class for an editable dropdown field.
*
* @alias Blockly.FieldDropdown
*/
export class FieldDropdown extends Field<string> {
/** Horizontal distance that a checkmark overhangs the dropdown. */
@@ -92,7 +88,7 @@ export class FieldDropdown extends Field<string> {
*/
override suffixField: string|null = null;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
private selectedOption_!: Array<string|ImageProperties>;
private selectedOption_!: MenuOption;
override clickTarget_: SVGElement|null = null;
/**
@@ -101,11 +97,11 @@ export class FieldDropdown extends Field<string> {
* if you wish to skip setup (only used by subclasses that want to handle
* configuration and setting the field value after their own constructors
* have run).
* @param opt_validator A function that is called to validate changes to the
* @param validator A function that is called to validate changes to the
* field's value. Takes in a language-neutral dropdown option & returns a
* validated language-neutral dropdown option, or null to abort the
* change.
* @param opt_config A map of options used to configure the field.
* @param config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/dropdown#creation}
* for a list of properties this parameter supports.
@@ -113,19 +109,19 @@ export class FieldDropdown extends Field<string> {
*/
constructor(
menuGenerator: MenuGenerator,
opt_validator?: FieldDropdownValidator,
opt_config?: FieldConfig,
validator?: FieldDropdownValidator,
config?: FieldDropdownConfig,
);
constructor(menuGenerator: Sentinel);
constructor(menuGenerator: typeof Field.SKIP_SETUP);
constructor(
menuGenerator: MenuGenerator|Sentinel,
opt_validator?: FieldDropdownValidator,
opt_config?: FieldConfig,
menuGenerator: MenuGenerator|typeof Field.SKIP_SETUP,
validator?: FieldDropdownValidator,
config?: FieldDropdownConfig,
) {
super(Field.SKIP_SETUP);
// If we pass SKIP_SETUP, don't do *anything* with the menu generator.
if (!isMenuGenerator(menuGenerator)) return;
if (menuGenerator === Field.SKIP_SETUP) return;
if (Array.isArray(menuGenerator)) {
validateOptions(menuGenerator);
@@ -143,12 +139,12 @@ export class FieldDropdown extends Field<string> {
*/
this.selectedOption_ = this.getOptions(false)[0];
if (opt_config) {
this.configure_(opt_config);
if (config) {
this.configure_(config);
}
this.setValue(this.selectedOption_[1]);
if (opt_validator) {
this.setValidator(opt_validator);
if (validator) {
this.setValidator(validator);
}
}
@@ -248,17 +244,17 @@ export class FieldDropdown extends Field<string> {
/**
* Create a dropdown menu under the text.
*
* @param opt_e Optional mouse event that triggered the field to open, or
* @param e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
*/
protected override showEditor_(opt_e?: MouseEvent) {
protected override showEditor_(e?: MouseEvent) {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
this.dropdownCreate_();
if (opt_e && typeof opt_e.clientX === 'number') {
this.menu_!.openingCoords = new Coordinate(opt_e.clientX, opt_e.clientY);
if (e && typeof e.clientX === 'number') {
this.menu_!.openingCoords = new Coordinate(e.clientX, e.clientY);
} else {
this.menu_!.openingCoords = null;
}
@@ -287,6 +283,9 @@ export class FieldDropdown extends Field<string> {
if (this.selectedMenuItem_) {
this.menu_!.setHighlighted(this.selectedMenuItem_);
style.scrollIntoContainerView(
this.selectedMenuItem_.getElement()!, dropDownDiv.getContentDiv(),
true);
}
this.applyColour();
@@ -332,7 +331,7 @@ export class FieldDropdown extends Field<string> {
/**
* Disposes of events and DOM-references belonging to the dropdown editor.
*/
private dropdownDispose_() {
protected dropdownDispose_() {
if (this.menu_) {
this.menu_.dispose();
}
@@ -372,20 +371,20 @@ export class FieldDropdown extends Field<string> {
/**
* Return a list of the options for this dropdown.
*
* @param opt_useCache For dynamic options, whether or not to use the cached
* @param useCache For dynamic options, whether or not to use the cached
* options or to re-generate them.
* @returns A non-empty array of option tuples:
* (human-readable text or image, language-neutral name).
* @throws {TypeError} If generated options are incorrectly structured.
*/
getOptions(opt_useCache?: boolean): MenuOption[] {
getOptions(useCache?: boolean): MenuOption[] {
if (!this.menuGenerator_) {
// A subclass improperly skipped setup without defining the menu
// generator.
throw TypeError('A menu generator was never defined.');
}
if (Array.isArray(this.menuGenerator_)) return this.menuGenerator_;
if (opt_useCache && this.generatedOptions_) return this.generatedOptions_;
if (useCache && this.generatedOptions_) return this.generatedOptions_;
this.generatedOptions_ = this.menuGenerator_();
validateOptions(this.generatedOptions_);
@@ -395,24 +394,23 @@ export class FieldDropdown extends Field<string> {
/**
* Ensure that the input value is a valid language-neutral option.
*
* @param opt_newValue The input value.
* @param newValue The input value.
* @returns A valid language-neutral option, or null if invalid.
*/
protected override doClassValidation_(opt_newValue?: MenuOption[1]): string
|null {
protected override doClassValidation_(newValue?: string): string|null {
const options = this.getOptions(true);
const isValueValid = options.some((option) => option[1] === opt_newValue);
const isValueValid = options.some((option) => option[1] === newValue);
if (!isValueValid) {
if (this.sourceBlock_) {
console.warn(
'Cannot set the dropdown\'s value to an unavailable option.' +
' Block type: ' + this.sourceBlock_.type +
', Field name: ' + this.name + ', Value: ' + opt_newValue);
', Field name: ' + this.name + ', Value: ' + newValue);
}
return null;
}
return opt_newValue as string;
return newValue as string;
}
/**
@@ -421,7 +419,7 @@ export class FieldDropdown extends Field<string> {
* @param newValue The value to be saved. The default validator guarantees
* that this is one of the valid dropdown options.
*/
protected override doValueUpdate_(newValue: MenuOption[1]) {
protected override doValueUpdate_(newValue: string) {
super.doValueUpdate_(newValue);
const options = this.getOptions(true);
for (let i = 0, option; option = options[i]; i++) {
@@ -465,7 +463,7 @@ export class FieldDropdown extends Field<string> {
// Show correct element.
const option = this.selectedOption_ && this.selectedOption_[0];
if (option && typeof option === 'object') {
this.renderSelectedImage_((option));
this.renderSelectedImage_(option);
} else {
this.renderSelectedText_();
}
@@ -486,8 +484,8 @@ export class FieldDropdown extends Field<string> {
this.imageElement_!.style.display = '';
this.imageElement_!.setAttributeNS(
dom.XLINK_NS, 'xlink:href', imageJson.src);
this.imageElement_!.setAttribute('height', `${imageJson.height}`);
this.imageElement_!.setAttribute('width', `${imageJson.width}`);
this.imageElement_!.setAttribute('height', String(imageJson.height));
this.imageElement_!.setAttribute('width', String(imageJson.width));
const imageHeight = Number(imageJson.height);
const imageWidth = Number(imageJson.width);
@@ -517,14 +515,13 @@ export class FieldDropdown extends Field<string> {
let arrowX = 0;
if (block.RTL) {
const imageX = xPadding + arrowWidth;
this.imageElement_!.setAttribute('x', imageX.toString());
this.imageElement_!.setAttribute('x', `${imageX}`);
} else {
arrowX = imageWidth + arrowWidth;
this.getTextElement().setAttribute('text-anchor', 'end');
this.imageElement_!.setAttribute('x', xPadding.toString());
this.imageElement_!.setAttribute('x', `${xPadding}`);
}
this.imageElement_!.setAttribute(
'y', (height / 2 - imageHeight / 2).toString());
this.imageElement_!.setAttribute('y', String(height / 2 - imageHeight / 2));
this.positionTextElement_(arrowX + xPadding, imageWidth + arrowWidth);
}
@@ -588,8 +585,8 @@ export class FieldDropdown extends Field<string> {
/**
* Use the `getText_` developer hook to override the field's text
* representation. Get the selected option text. If the selected option is an
* image we return the image alt text.
* representation. Get the selected option text. If the selected option is
* an image we return the image alt text.
*
* @returns Selected option text.
*/
@@ -654,13 +651,35 @@ export type MenuGeneratorFunction = (this: FieldDropdown) => MenuOption[];
*/
export type MenuGenerator = MenuOption[]|MenuGeneratorFunction;
/**
* Config options for the dropdown field.
*/
export type FieldDropdownConfig = FieldConfig;
/**
* fromJson config for the dropdown field.
*/
export interface FieldDropdownFromJsonConfig extends FieldConfig {
export interface FieldDropdownFromJsonConfig extends FieldDropdownConfig {
options?: MenuOption[];
}
/**
* A function that is called to validate changes to the field's value before
* they are set.
*
* @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values}
* @param newValue The value to be validated.
* @returns One of three instructions for setting the new value: `T`, `null`,
* or `undefined`.
*
* - `T` to set this function's returned value instead of `newValue`.
*
* - `null` to invoke `doValueInvalid_` and not set a value.
*
* - `undefined` to set `newValue` as is.
*/
export type FieldDropdownValidator = FieldValidator<string>;
/**
* The y offset from the top of the field to the top of the image, if an image
* is selected.
@@ -670,15 +689,6 @@ const IMAGE_Y_OFFSET = 5;
/** The total vertical padding above and below an image. */
const IMAGE_Y_PADDING: number = IMAGE_Y_OFFSET * 2;
/**
* NOTE: Because Sentinel is an empty class, proving a value is Sentinel does
* not resolve in TS that it isn't a MenuGenerator.
*/
function isMenuGenerator(menuGenerator: MenuGenerator|
Sentinel): menuGenerator is MenuGenerator {
return menuGenerator !== Field.SKIP_SETUP;
}
/**
* Factor out common words in statically defined options.
* Create prefix and/or suffix labels.

View File

@@ -16,14 +16,11 @@ import {Field, FieldConfig} from './field.js';
import * as fieldRegistry from './field_registry.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
import {Size} from './utils/size.js';
import {Svg} from './utils/svg.js';
/**
* Class for an image on a block.
*
* @alias Blockly.FieldImage
*/
export class FieldImage extends Field<string> {
/**
@@ -35,12 +32,10 @@ export class FieldImage extends Field<string> {
private readonly imageHeight_: number;
/** The function to be called when this field is clicked. */
private clickHandler_: ((p1: FieldImage) => AnyDuringMigration)|null = null;
private clickHandler_: ((p1: FieldImage) => void)|null = null;
/** The rendered field's image element. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGImageElement'.
private imageElement_: SVGImageElement = null as AnyDuringMigration;
private imageElement_: SVGImageElement|null = null;
/**
* Editable fields usually show some sort of UI indicating they are
@@ -60,7 +55,6 @@ export class FieldImage extends Field<string> {
/** Alt text of this image. */
private altText_ = '';
override value_: AnyDuringMigration;
/**
* @param src The URL of the image.
@@ -69,19 +63,19 @@ export class FieldImage extends Field<string> {
* after their own constructors have run).
* @param width Width of the image.
* @param height Height of the image.
* @param opt_alt Optional alt text for when block is collapsed.
* @param opt_onClick Optional function to be called when the image is
* clicked. If opt_onClick is defined, opt_alt must also be defined.
* @param opt_flipRtl Whether to flip the icon in RTL.
* @param opt_config A map of options used to configure the field.
* @param alt Optional alt text for when block is collapsed.
* @param onClick Optional function to be called when the image is
* clicked. If onClick is defined, alt must also be defined.
* @param flipRtl Whether to flip the icon in RTL.
* @param config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/image#creation}
* for a list of properties this parameter supports.
*/
constructor(
src: string|Sentinel, width: string|number, height: string|number,
opt_alt?: string, opt_onClick?: (p1: FieldImage) => AnyDuringMigration,
opt_flipRtl?: boolean, opt_config?: FieldImageConfig) {
src: string|typeof Field.SKIP_SETUP, width: string|number,
height: string|number, alt?: string, onClick?: (p1: FieldImage) => void,
flipRtl?: boolean, config?: FieldImageConfig) {
super(Field.SKIP_SETUP);
const imageHeight = Number(parsing.replaceMessageReferences(height));
@@ -105,19 +99,17 @@ export class FieldImage extends Field<string> {
*/
this.imageHeight_ = imageHeight;
if (typeof opt_onClick === 'function') {
this.clickHandler_ = opt_onClick;
if (typeof onClick === 'function') {
this.clickHandler_ = onClick;
}
if (src === Field.SKIP_SETUP) {
return;
}
if (src === Field.SKIP_SETUP) return;
if (opt_config) {
this.configure_(opt_config);
if (config) {
this.configure_(config);
} else {
this.flipRtl_ = !!opt_flipRtl;
this.altText_ = parsing.replaceMessageReferences(opt_alt) || '';
this.flipRtl_ = !!flipRtl;
this.altText_ = parsing.replaceMessageReferences(alt) || '';
}
this.setValue(parsing.replaceMessageReferences(src));
}
@@ -162,15 +154,14 @@ export class FieldImage extends Field<string> {
/**
* Ensure that the input value (the source URL) is a string.
*
* @param opt_newValue The input value.
* @param newValue The input value.
* @returns A string, or null if invalid.
*/
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null {
if (typeof opt_newValue !== 'string') {
protected override doClassValidation_(newValue?: any): string|null {
if (typeof newValue !== 'string') {
return null;
}
return opt_newValue;
return newValue;
}
/**
@@ -179,11 +170,11 @@ export class FieldImage extends Field<string> {
* @param newValue The value to be saved. The default validator guarantees
* that this is a string.
*/
protected override doValueUpdate_(newValue: AnyDuringMigration) {
protected override doValueUpdate_(newValue: string) {
this.value_ = newValue;
if (this.imageElement_) {
this.imageElement_.setAttributeNS(
dom.XLINK_NS, 'xlink:href', String(this.value_));
dom.XLINK_NS, 'xlink:href', this.value_);
}
}
@@ -227,7 +218,7 @@ export class FieldImage extends Field<string> {
* @param func The function that is called when the image is clicked, or null
* to remove.
*/
setOnClickHandler(func: ((p1: FieldImage) => AnyDuringMigration)|null) {
setOnClickHandler(func: ((p1: FieldImage) => void)|null) {
this.clickHandler_ = func;
}

View File

@@ -25,21 +25,24 @@ import {Field, FieldConfig, FieldValidator, UnattachedFieldError} from './field.
import {Msg} from './msg.js';
import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js';
import {KeyCodes} from './utils/keycodes.js';
import type {Sentinel} from './utils/sentinel.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js';
export type InputTypes = string|number;
export type FieldInputValidator<T extends InputTypes> = FieldValidator<T>;
/**
* Supported types for FieldInput subclasses.
*
* @internal
*/
type InputTypes = string|number;
/**
* Class for an editable text field.
* Abstract class for an editable input field.
*
* @alias Blockly.FieldInput
* @typeParam T - The value stored on the field.
* @internal
*/
export abstract class FieldInput<T extends InputTypes> extends Field<T> {
export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
/**
* Pixel size of input border radius.
* Should match blocklyText's border-radius in CSS.
@@ -83,38 +86,33 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
/** Mouse cursor style when over the hotspot that initiates the editor. */
override CURSOR = 'text';
override clickTarget_: AnyDuringMigration;
override value_: AnyDuringMigration;
override isDirty_: AnyDuringMigration;
/**
* @param opt_value The initial value of the field. Should cast to a string.
* @param value The initial value of the field. Should cast to a string.
* Defaults to an empty string if null or undefined. Also accepts
* Field.SKIP_SETUP if you wish to skip setup (only used by subclasses
* that want to handle configuration and setting the field value after
* their own constructors have run).
* @param opt_validator A function that is called to validate changes to the
* @param validator A function that is called to validate changes to the
* field's value. Takes in a string & returns a validated string, or null
* to abort the change.
* @param opt_config A map of options used to configure the field.
* @param config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/text-input#creation}
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|Sentinel, opt_validator?: FieldInputValidator<T>|null,
opt_config?: FieldInputConfig) {
value?: string|typeof Field.SKIP_SETUP,
validator?: FieldInputValidator<T>|null, config?: FieldInputConfig) {
super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) {
return;
if (value === Field.SKIP_SETUP) return;
if (config) {
this.configure_(config);
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) {
this.setValidator(opt_validator);
this.setValue(value);
if (validator) {
this.setValidator(validator);
}
}
@@ -161,20 +159,6 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
this.createTextElement_();
}
/**
* Ensure that the input value casts to a valid string.
*
* @param opt_newValue The input value.
* @returns A valid string, or null if invalid.
*/
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
AnyDuringMigration {
if (opt_newValue === null || opt_newValue === undefined) {
return null;
}
return String(opt_newValue);
}
/**
* Called by setValue if the text input is not valid. If the field is
* currently being edited it reverts value of the field to the previous
@@ -207,7 +191,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
* @param newValue The value to be saved. The default validator guarantees
* that this is a string.
*/
protected override doValueUpdate_(newValue: AnyDuringMigration) {
protected override doValueUpdate_(newValue: string|T) {
this.isDirty_ = true;
this.isTextValid_ = true;
this.value_ = newValue;
@@ -276,14 +260,13 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
* Shows a prompt editor for mobile browsers if the modalInputs option is
* enabled.
*
* @param _opt_e Optional mouse event that triggered the field to open, or
* @param _e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
* @param opt_quietInput True if editor should be created without focus.
* @param quietInput True if editor should be created without focus.
* Defaults to false.
*/
protected override showEditor_(_opt_e?: Event, opt_quietInput?: boolean) {
protected override showEditor_(_e?: Event, quietInput = false) {
this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace;
const quietInput = opt_quietInput || false;
if (!quietInput && this.workspace_.options.modalInputs &&
(userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD)) {
this.showPromptEditor_();
@@ -372,7 +355,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
div!.style.transition = 'box-shadow 0.25s ease 0s';
if (this.getConstants()!.FIELD_TEXTINPUT_BOX_SHADOW) {
div!.style.boxShadow =
'rgba(255, 255, 255, 0.3) 0 0 0 ' + 4 * scale + 'px';
'rgba(255, 255, 255, 0.3) 0 0 0 ' + (4 * scale) + 'px';
}
}
htmlInput.style.borderRadius = borderRadius;
@@ -380,7 +363,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
div!.appendChild(htmlInput);
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
htmlInput.setAttribute('data-untyped-default-value', this.value_);
htmlInput.setAttribute('data-untyped-default-value', String(this.value_));
this.resizeEditor_();
@@ -457,29 +440,19 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
*
* @param e Keyboard event.
*/
protected onHtmlInputKeyDown_(e: Event) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
if ((e as AnyDuringMigration).keyCode === KeyCodes.ENTER) {
protected onHtmlInputKeyDown_(e: KeyboardEvent) {
if (e.key === 'Enter') {
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.ESC) {
} else if (e.key === 'Escape') {
this.setValue(
this.htmlInput_!.getAttribute('data-untyped-default-value'));
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.TAB) {
} else if (e.key === 'Tab') {
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'shiftKey' does not exist on type
// 'Event'. AnyDuringMigration because: Argument of type 'this' is not
// assignable to parameter of type 'Field'.
(this.sourceBlock_ as BlockSvg)
.tab(this as AnyDuringMigration, !(e as AnyDuringMigration).shiftKey);
(this.sourceBlock_ as BlockSvg).tab(this, !e.shiftKey);
e.preventDefault();
}
}
@@ -567,7 +540,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
* @returns The text to show on the HTML input.
*/
protected getEditorText_(value: AnyDuringMigration): string {
return String(value);
return `${value}`;
}
/**
@@ -587,7 +560,28 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
/**
* Config options for the input field.
*
* @internal
*/
export interface FieldInputConfig extends FieldConfig {
spellcheck?: boolean;
}
/**
* A function that is called to validate changes to the field's value before
* they are set.
*
* @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values}
* @param newValue The value to be validated.
* @returns One of three instructions for setting the new value: `T`, `null`,
* or `undefined`.
*
* - `T` to set this function's returned value instead of `newValue`.
*
* - `null` to invoke `doValueInvalid_` and not set a value.
*
* - `undefined` to set `newValue` as is.
* @internal
*/
export type FieldInputValidator<T extends InputTypes> =
FieldValidator<string|T>;

View File

@@ -17,15 +17,12 @@ import * as dom from './utils/dom.js';
import {Field, FieldConfig} from './field.js';
import * as fieldRegistry from './field_registry.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
/**
* Class for a non-editable, non-serializable text field.
*
* @alias Blockly.FieldLabel
*/
export class FieldLabel extends Field<string> {
/** The html class name to use for this field. */
/** The HTML class name to use for this field. */
private class_: string|null = null;
/**
@@ -35,31 +32,29 @@ export class FieldLabel extends Field<string> {
override EDITABLE = false;
/**
* @param opt_value The initial value of the field. Should cast to a string.
* @param value The initial value of the field. Should cast to a string.
* Defaults to an empty string if null or undefined. Also accepts
* Field.SKIP_SETUP if you wish to skip setup (only used by subclasses
* that want to handle configuration and setting the field value after
* their own constructors have run).
* @param opt_class Optional CSS class for the field's text.
* @param opt_config A map of options used to configure the field.
* @param textClass Optional CSS class for the field's text.
* @param config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label#creation}
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|Sentinel, opt_class?: string,
opt_config?: FieldLabelConfig) {
value?: string|typeof Field.SKIP_SETUP, textClass?: string,
config?: FieldLabelConfig) {
super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
if (value === Field.SKIP_SETUP) return;
if (config) {
this.configure_(config);
} else {
this.class_ = opt_class || null;
this.class_ = textClass || null;
}
this.setValue(opt_value);
this.setValue(value);
}
protected override configure_(config: FieldLabelConfig) {
@@ -82,15 +77,15 @@ export class FieldLabel extends Field<string> {
/**
* Ensure that the input value casts to a valid string.
*
* @param opt_newValue The input value.
* @param newValue The input value.
* @returns A valid string, or null if invalid.
*/
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null {
if (opt_newValue === null || opt_newValue === undefined) {
protected override doClassValidation_(newValue?: AnyDuringMigration): string
|null {
if (newValue === null || newValue === undefined) {
return null;
}
return String(opt_newValue);
return `${newValue}`;
}
/**

View File

@@ -20,8 +20,6 @@ import * as parsing from './utils/parsing.js';
/**
* Class for a non-editable, serializable text field.
*
* @alias Blockly.FieldLabelSerializable
*/
export class FieldLabelSerializable extends FieldLabel {
/**
@@ -38,17 +36,16 @@ export class FieldLabelSerializable extends FieldLabel {
override SERIALIZABLE = true;
/**
* @param opt_value The initial value of the field. Should cast to a string.
* @param value The initial value of the field. Should cast to a string.
* Defaults to an empty string if null or undefined.
* @param opt_class Optional CSS class for the field's text.
* @param opt_config A map of options used to configure the field.
* @param textClass Optional CSS class for the field's text.
* @param config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label-serializable#creation}
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string, opt_class?: string, opt_config?: FieldLabelConfig) {
super(String(opt_value ?? ''), opt_class, opt_config);
constructor(value?: string, textClass?: string, config?: FieldLabelConfig) {
super(String(value ?? ''), textClass, config);
}
/**

View File

@@ -18,28 +18,20 @@ import * as fieldRegistry from './field_registry.js';
import {FieldTextInput, FieldTextInputConfig, FieldTextInputValidator} from './field_textinput.js';
import * as aria from './utils/aria.js';
import * as dom from './utils/dom.js';
import {KeyCodes} from './utils/keycodes.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
export type FieldMultilineInputValidator = FieldTextInputValidator;
/**
* Class for an editable text area field.
*
* @alias Blockly.FieldMultilineInput
*/
export class FieldMultilineInput extends FieldTextInput {
/**
* The SVG group element that will contain a text element for each text row
* when initialized.
*/
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGGElement'.
textGroup_: SVGGElement = null as AnyDuringMigration;
textGroup: SVGGElement|null = null;
/**
* Defines the maximum number of lines of field.
@@ -51,37 +43,41 @@ export class FieldMultilineInput extends FieldTextInput {
protected isOverflowedY_ = false;
/**
* @param opt_value The initial content of the field. Should cast to a string.
* @param value The initial content of the field. Should cast to a string.
* Defaults to an empty string if null or undefined. Also accepts
* Field.SKIP_SETUP if you wish to skip setup (only used by subclasses
* that want to handle configuration and setting the field value after
* their own constructors have run).
* @param opt_validator An optional function that is called to validate any
* @param validator An optional function that is called to validate any
* constraints on what the user entered. Takes the new text as an
* argument and returns either the accepted text, a replacement text, or
* null to abort the change.
* @param opt_config A map of options used to configure the field.
* @param config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/multiline-text-input#creation}
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|Sentinel, opt_validator?: FieldMultilineInputValidator,
opt_config?: FieldMultilineInputConfig) {
value?: string|typeof Field.SKIP_SETUP,
validator?: FieldMultilineInputValidator,
config?: FieldMultilineInputConfig) {
super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) {
return;
if (value === Field.SKIP_SETUP) return;
if (config) {
this.configure_(config);
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) {
this.setValidator(opt_validator);
this.setValue(value);
if (validator) {
this.setValidator(validator);
}
}
/**
* Configure the field based on the given map of options.
*
* @param config A map of options to configure the field based on.
*/
protected override configure_(config: FieldMultilineInputConfig) {
super.configure_(config);
if (config.maxLines) this.setMaxLines(config.maxLines);
@@ -118,6 +114,8 @@ export class FieldMultilineInput extends FieldTextInput {
/**
* Saves this field's value.
* This function only exists for subclasses of FieldMultilineInput which
* predate the load/saveState API and only define to/fromXml.
*
* @returns The state of this field.
* @internal
@@ -132,6 +130,8 @@ export class FieldMultilineInput extends FieldTextInput {
/**
* Sets the field's value based on the given state.
* This function only exists for subclasses of FieldMultilineInput which
* predate the load/saveState API and only define to/fromXml.
*
* @param state The state of the variable to assign to this variable field.
* @internal
@@ -150,7 +150,7 @@ export class FieldMultilineInput extends FieldTextInput {
*/
override initView() {
this.createBorderRect_();
this.textGroup_ = dom.createSvgElement(
this.textGroup = dom.createSvgElement(
Svg.G, {
'class': 'blocklyEditableText',
},
@@ -210,9 +210,11 @@ export class FieldMultilineInput extends FieldTextInput {
* @param newValue The value to be saved. The default validator guarantees
* that this is a string.
*/
protected override doValueUpdate_(newValue: AnyDuringMigration) {
protected override doValueUpdate_(newValue: string) {
super.doValueUpdate_(newValue);
this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_;
if (this.value_ !== null) {
this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_;
}
}
/** Updates the text of the textElement. */
@@ -223,8 +225,9 @@ export class FieldMultilineInput extends FieldTextInput {
}
// Remove all text group children.
let currentChild;
while (currentChild = this.textGroup_.firstChild) {
this.textGroup_.removeChild(currentChild);
const textGroup = this.textGroup;
while (currentChild = textGroup!.firstChild) {
textGroup!.removeChild(currentChild);
}
// Add in text elements into the group.
@@ -240,7 +243,7 @@ export class FieldMultilineInput extends FieldTextInput {
'y': y + this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING,
'dy': this.getConstants()!.FIELD_TEXT_BASELINE,
},
this.textGroup_);
textGroup);
span.appendChild(document.createTextNode(lines[i]));
y += lineHeight;
}
@@ -278,7 +281,7 @@ export class FieldMultilineInput extends FieldTextInput {
/** Updates the size of the field based on the text. */
protected override updateSize_() {
const nodes = this.textGroup_.childNodes;
const nodes = (this.textGroup as SVGElement).childNodes;
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE;
const fontWeight = this.getConstants()!.FIELD_TEXT_FONTWEIGHT;
const fontFamily = this.getConstants()!.FIELD_TEXT_FONTFAMILY;
@@ -300,7 +303,7 @@ export class FieldMultilineInput extends FieldTextInput {
// absolute longest line, even if it would be truncated after editing.
// Otherwise we would get wrong editor width when there are more
// lines than this.maxLines_.
const actualEditorLines = this.value_.split('\n');
const actualEditorLines = String(this.value_).split('\n');
const dummyTextElement = dom.createSvgElement(
Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'});
@@ -324,13 +327,8 @@ export class FieldMultilineInput extends FieldTextInput {
if (this.borderRect_) {
totalHeight += this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING * 2;
totalWidth += this.getConstants()!.FIELD_BORDER_RECT_X_PADDING * 2;
// AnyDuringMigration because: Argument of type 'number' is not
// assignable to parameter of type 'string'.
this.borderRect_.setAttribute('width', totalWidth as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not
// assignable to parameter of type 'string'.
this.borderRect_.setAttribute(
'height', totalHeight as AnyDuringMigration);
this.borderRect_.setAttribute('width', `${totalWidth}`);
this.borderRect_.setAttribute('height', `${totalHeight}`);
}
this.size_.width = totalWidth;
this.size_.height = totalHeight;
@@ -343,13 +341,13 @@ export class FieldMultilineInput extends FieldTextInput {
* Overrides the default behaviour to force rerender in order to
* correct block size, based on editor text.
*
* @param _opt_e Optional mouse event that triggered the field to open, or
* @param e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
* @param opt_quietInput True if editor should be created without focus.
* @param quietInput True if editor should be created without focus.
* Defaults to false.
*/
override showEditor_(_opt_e?: Event, opt_quietInput?: boolean) {
super.showEditor_(_opt_e, opt_quietInput);
override showEditor_(e?: Event, quietInput?: boolean) {
super.showEditor_(e, quietInput);
this.forceRerender();
}
@@ -364,10 +362,7 @@ export class FieldMultilineInput extends FieldTextInput {
const htmlInput = (document.createElement('textarea'));
htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
// AnyDuringMigration because: Argument of type 'boolean' is not assignable
// to parameter of type 'string'.
htmlInput.setAttribute(
'spellcheck', this.spellcheck_ as AnyDuringMigration);
htmlInput.setAttribute('spellcheck', String(this.spellcheck_));
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
div!.style.fontSize = fontSize;
htmlInput.style.fontSize = fontSize;
@@ -385,7 +380,7 @@ export class FieldMultilineInput extends FieldTextInput {
div!.appendChild(htmlInput);
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
htmlInput.setAttribute('data-untyped-default-value', this.value_);
htmlInput.setAttribute('data-untyped-default-value', String(this.value_));
htmlInput.setAttribute('data-old-value', '');
if (userAgent.GECKO) {
// In FF, ensure the browser reflows before resizing to avoid issue #2777.
@@ -428,10 +423,8 @@ export class FieldMultilineInput extends FieldTextInput {
*
* @param e Keyboard event.
*/
protected override onHtmlInputKeyDown_(e: Event) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
if ((e as AnyDuringMigration).keyCode !== KeyCodes.ENTER) {
protected override onHtmlInputKeyDown_(e: KeyboardEvent) {
if (e.key !== 'Enter') {
super.onHtmlInputKeyDown_(e);
}
}
@@ -454,7 +447,12 @@ export class FieldMultilineInput extends FieldTextInput {
}
}
/** CSS for multiline field. See css.js for use. */
fieldRegistry.register('field_multilinetext', FieldMultilineInput);
/**
* CSS for multiline field.
*/
Css.register(`
.blocklyHtmlTextAreaInput {
font-family: monospace;
@@ -469,8 +467,6 @@ Css.register(`
}
`);
fieldRegistry.register('field_multilinetext', FieldMultilineInput);
/**
* Config options for the multiline input field.
*/
@@ -485,3 +481,20 @@ export interface FieldMultilineInputFromJsonConfig extends
FieldMultilineInputConfig {
text?: string;
}
/**
* A function that is called to validate changes to the field's value before
* they are set.
*
* @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values}
* @param newValue The value to be validated.
* @returns One of three instructions for setting the new value: `T`, `null`,
* or `undefined`.
*
* - `T` to set this function's returned value instead of `newValue`.
*
* - `null` to invoke `doValueInvalid_` and not set a value.
*
* - `undefined` to set `newValue` as is.
*/
export type FieldMultilineInputValidator = FieldTextInputValidator;

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