chore: use prettier instead of clang-format (#7014)

* chore: add and configure prettier

* chore: remove clang-format

* chore: remove clang-format config

* chore: lint additional ts files

* chore: fix lint errors in blocks

* chore: add prettier-ignore where needed

* chore: ignore js blocks when formatting

* chore: fix playground html syntax

* chore: fix yaml spacing from merge

* chore: convert text blocks to use arrow functions

* chore: format everything with prettier

* chore: fix lint unused imports in blocks
This commit is contained in:
Maribeth Bottorff
2023-05-10 16:01:39 -07:00
committed by GitHub
parent af991f5e1b
commit 88ff901a72
425 changed files with 29170 additions and 21169 deletions

View File

@@ -1,3 +0,0 @@
Language: JavaScript
BasedOnStyle: Google
ColumnLimit: 80

View File

@@ -1,7 +1,11 @@
*_compressed*.js # Build Artifacts
/msg/* /msg/*
/build/* /build/*
/dist/* /dist/*
/typings/*
/docs/*
# Tests other than mocha unit tests
/tests/blocks/* /tests/blocks/*
/tests/themes/* /tests/themes/*
/tests/compile/* /tests/compile/*
@@ -11,10 +15,14 @@
/tests/screenshot/* /tests/screenshot/*
/tests/test_runner.js /tests/test_runner.js
/tests/workspace_svg/* /tests/workspace_svg/*
# Demos, scripts, misc
/node_modules/*
/generators/* /generators/*
/demos/* /demos/*
/appengine/* /appengine/*
/externs/* /externs/*
/closure/* /closure/*
/scripts/gulpfiles/* /scripts/gulpfiles/*
/typings/* CHANGELOG.md
PULL_REQUEST_TEMPLATE.md

View File

@@ -1,19 +1,4 @@
const rules = { 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': [ 'no-unused-vars': [
'warn', 'warn',
{ {
@@ -29,20 +14,12 @@ const rules = {
// Blockly uses single quotes except for JSON blobs, which must use double // Blockly uses single quotes except for JSON blobs, which must use double
// quotes. // quotes.
'quotes': ['off'], '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. // Blockly uses 'use strict' in files.
'strict': ['off'], 'strict': ['off'],
// Closure style allows redeclarations. // Closure style allows redeclarations.
'no-redeclare': ['off'], 'no-redeclare': ['off'],
'valid-jsdoc': ['error'], 'valid-jsdoc': ['error'],
'no-console': ['off'], 'no-console': ['off'],
'no-multi-spaces': ['error', {'ignoreEOLComments': true}],
'operator-linebreak': ['error', 'after'],
'spaced-comment': [ 'spaced-comment': [
'error', 'error',
'always', 'always',
@@ -61,27 +38,13 @@ const rules = {
'allow': ['^opt_', '^_opt_', '^testOnly_'], '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. // Blockly uses capital letters for some non-constructor namespaces.
// Keep them for legacy reasons. // Keep them for legacy reasons.
'new-cap': ['off'], '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 // Blockly uses objects as maps, but uses Object.create(null) to
// instantiate them. // instantiate them.
'guard-for-in': ['off'], 'guard-for-in': ['off'],
'prefer-spread': ['off'], 'prefer-spread': ['off'],
'comma-dangle': [
'error',
{
'arrays': 'always-multiline',
'objects': 'always-multiline',
'imports': 'always-multiline',
'exports': 'always-multiline',
'functions': 'ignore',
},
],
}; };
/** /**
@@ -92,10 +55,7 @@ const rules = {
function buildTSOverride({files, tsconfig}) { function buildTSOverride({files, tsconfig}) {
return { return {
'files': files, 'files': files,
'plugins': [ 'plugins': ['@typescript-eslint/eslint-plugin', 'jsdoc'],
'@typescript-eslint/eslint-plugin',
'jsdoc',
],
'settings': { 'settings': {
'jsdoc': { 'jsdoc': {
'mode': 'typescript', 'mode': 'typescript',
@@ -111,6 +71,7 @@ function buildTSOverride({files, tsconfig}) {
'extends': [ 'extends': [
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:jsdoc/recommended', 'plugin:jsdoc/recommended',
'prettier', // Extend again so that these rules are applied last
], ],
'rules': { 'rules': {
// TS rules // TS rules
@@ -130,8 +91,6 @@ function buildTSOverride({files, tsconfig}) {
'varsIgnorePattern': '^_', 'varsIgnorePattern': '^_',
}, },
], ],
'func-call-spacing': ['off'],
'@typescript-eslint/func-call-spacing': ['warn'],
// Temporarily disable. 23 problems. // Temporarily disable. 23 problems.
'@typescript-eslint/no-explicit-any': ['off'], '@typescript-eslint/no-explicit-any': ['off'],
// Temporarily disable. 128 problems. // Temporarily disable. 128 problems.
@@ -195,21 +154,15 @@ const eslintJSON = {
'goog': true, 'goog': true,
'exports': true, 'exports': true,
}, },
'extends': [ 'extends': ['eslint:recommended', 'google', 'prettier'],
'eslint:recommended',
'google',
],
// TypeScript-specific config. Uses above rules plus these. // TypeScript-specific config. Uses above rules plus these.
'overrides': [ 'overrides': [
buildTSOverride({ buildTSOverride({
files: ['./core/**/*.ts', './core/**/*.tsx'], files: ['./**/*.ts', './**/*.tsx'],
tsconfig: './tsconfig.json', tsconfig: './tsconfig.json',
}), }),
buildTSOverride({ buildTSOverride({
files: [ files: ['./tests/typescript/**/*.ts', './tests/typescript/**/*.tsx'],
'./tests/typescript/**/*.ts',
'./tests/typescript/**/*.tsx',
],
tsconfig: './tests/typescript/tsconfig.json', tsconfig: './tests/typescript/tsconfig.json',
}), }),
{ {

View File

@@ -1,6 +1,7 @@
# Contributing to Blockly # Contributing to Blockly
Want to contribute? Great! Want to contribute? Great!
- First, read this page (including the small print at the end). - First, read this page (including the small print at the end).
- Second, please make pull requests against develop, not master. If your patch - Second, please make pull requests against develop, not master. If your patch
needs to go into master immediately, include a note in your PR. needs to go into master immediately, include a note in your PR.
@@ -8,6 +9,7 @@ Want to contribute? Great!
For more information on style guide and other details, head over to the [Blockly Developers site](https://developers.google.com/blockly/guides/modify/contributing). For more information on style guide and other details, head over to the [Blockly Developers site](https://developers.google.com/blockly/guides/modify/contributing).
### Before you contribute ### Before you contribute
Before we can use your code, you must sign the Before we can use your code, you must sign the
[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)
(CLA), which you can do online. The CLA is necessary mainly because you own the (CLA), which you can do online. The CLA is necessary mainly because you own the
@@ -19,22 +21,26 @@ the CLA until after you've submitted your code for review and a member has
approved it, but you must do it before we can put your code into our codebase. approved it, but you must do it before we can put your code into our codebase.
### Larger changes ### Larger changes
Before you start working on a larger contribution, you should get in touch with Before you start working on a larger contribution, you should get in touch with
us first through the issue tracker with your idea so that we can help out and us first through the issue tracker with your idea so that we can help out and
possibly guide you. Coordinating up front makes it much easier to avoid possibly guide you. Coordinating up front makes it much easier to avoid
frustration later on. frustration later on.
### Code reviews ### Code reviews
All submissions, including submissions by project members, require review. We All submissions, including submissions by project members, require review. We
use Github pull requests for this purpose. use Github pull requests for this purpose.
### Browser compatibility ### Browser compatibility
We care strongly about making Blockly work on all browsers. As of 2022 we
support Edge, Chrome, Safari, and Firefox. We will not accept changes that only We care strongly about making Blockly work on all browsers. As of 2022 we
work on a subset of those browsers. You can check [caniuse.com](https://caniuse.com/) support Edge, Chrome, Safari, and Firefox. We will not accept changes that only
work on a subset of those browsers. You can check [caniuse.com](https://caniuse.com/)
for compatibility information. for compatibility information.
### The small print ### The small print
Contributions made by corporations are covered by a different agreement than Contributions made by corporations are covered by a different agreement than
the one above, the the one above, the
[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate). [Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).

View File

@@ -30,7 +30,7 @@ body:
value: | value: |
1. 1.
2. 2.
3. 3.
- type: textarea - type: textarea
id: stack-trace id: stack-trace
attributes: attributes:

View File

@@ -1,4 +1,3 @@
name: Documentation name: Documentation
description: Report an issue with our documentation description: Report an issue with our documentation
labels: 'issue: docs, issue: triage' labels: 'issue: docs, issue: triage'

View File

@@ -1,4 +1,3 @@
name: Feature request name: Feature request
description: Suggest an idea for this project description: Suggest an idea for this project
labels: 'issue: feature request, issue: triage' labels: 'issue: feature request, issue: triage'

View File

@@ -5,23 +5,23 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "npm" # See documentation for possible values - package-ecosystem: 'npm' # See documentation for possible values
directory: "/" # Location of package manifests directory: '/' # Location of package manifests
target-branch: "develop" target-branch: 'develop'
schedule: schedule:
interval: "weekly" interval: 'weekly'
commit-message: commit-message:
prefix: "chore(deps)" prefix: 'chore(deps)'
labels: labels:
- "PR: chore" - 'PR: chore'
- "PR: dependencies" - 'PR: dependencies'
- package-ecosystem: "github-actions" # See documentation for possible values - package-ecosystem: 'github-actions' # See documentation for possible values
directory: "/" directory: '/'
target-branch: "develop" target-branch: 'develop'
schedule: schedule:
interval: "weekly" interval: 'weekly'
commit-message: commit-message:
prefix: "chore(deps)" prefix: 'chore(deps)'
labels: labels:
- "PR: chore" - 'PR: chore'
- "PR: dependencies" - 'PR: dependencies'

14
.github/release.yml vendored
View File

@@ -4,7 +4,7 @@ changelog:
exclude: exclude:
labels: labels:
- ignore-for-release - ignore-for-release
- "PR: chore" - 'PR: chore'
authors: authors:
- dependabot - dependabot
categories: categories:
@@ -16,17 +16,17 @@ changelog:
- deprecation - deprecation
- title: New features ✨ - title: New features ✨
labels: labels:
- "PR: feature" - 'PR: feature'
- title: Bug fixes 🐛 - title: Bug fixes 🐛
labels: labels:
- "PR: fix" - 'PR: fix'
- title: Cleanup ♻️ - title: Cleanup ♻️
labels: labels:
- "PR: docs" - 'PR: docs'
- "PR: refactor" - 'PR: refactor'
- title: Reverted changes ⎌ - title: Reverted changes ⎌
labels: labels:
- "PR: revert" - 'PR: revert'
- title: Other changes - title: Other changes
labels: labels:
- "*" - '*'

View File

@@ -16,26 +16,26 @@ jobs:
requested-reviewer: requested-reviewer:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Assign requested reviewer - name: Assign requested reviewer
uses: actions/github-script@v6 uses: actions/github-script@v6
with: with:
script: | script: |
try { try {
if (context.payload.pull_request === undefined) { if (context.payload.pull_request === undefined) {
throw new Error("Can't get pull_request payload. " + throw new Error("Can't get pull_request payload. " +
'Check a request reviewer event was triggered.'); 'Check a request reviewer event was triggered.');
}
const reviewers = context.payload.pull_request.requested_reviewers;
// Assignees takes in a list of logins rather than the
// reviewer object.
const reviewerNames = reviewers.map(reviewer => reviewer.login);
const {number:issue_number} = context.payload.pull_request;
github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue_number,
assignees: reviewerNames
});
} catch (error) {
core.setFailed(error.message);
} }
const reviewers = context.payload.pull_request.requested_reviewers;
// Assignees takes in a list of logins rather than the
// reviewer object.
const reviewerNames = reviewers.map(reviewer => reviewer.login);
const {number:issue_number} = context.payload.pull_request;
github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue_number,
assignees: reviewerNames
});
} catch (error) {
core.setFailed(error.message);
}

View File

@@ -23,65 +23,67 @@ jobs:
# https://nodejs.org/en/about/releases/ # https://nodejs.org/en/about/releases/
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
persist-credentials: false persist-credentials: false
- name: Reconfigure git to use HTTP authentication - name: Reconfigure git to use HTTP authentication
run: > run: >
git config --global url."https://github.com/".insteadOf git config --global url."https://github.com/".insteadOf
ssh://git@github.com/ ssh://git@github.com/
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Npm Install - name: Npm Install
run: npm install run: npm install
- name: Linux Test Setup - name: Linux Test Setup
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: source ./tests/scripts/setup_linux_env.sh run: source ./tests/scripts/setup_linux_env.sh
- name: MacOS Test Setup - name: MacOS Test Setup
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: source ./tests/scripts/setup_osx_env.sh run: source ./tests/scripts/setup_osx_env.sh
- name: Run - name: Run
run: npm run test run: npm run test
env: env:
CI: true CI: true
lint: lint:
timeout-minutes: 5 timeout-minutes: 5
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 20.x node-version: 20.x
- name: Npm Install - name: Npm Install
run: npm install run: npm install
- name: Lint - name: Lint
run: npm run lint run: npm run lint
clang-formatter: format:
timeout-minutes: 5 timeout-minutes: 5
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: DoozyX/clang-format-lint-action@v0.16 - name: Use Node.js 20.x
with: uses: actions/setup-node@v3
source: 'core' with:
extensions: 'js,ts' node-version: 20.x
# This should be as close as possible to the version that the npm
# package supports. This can be found by running: - name: Npm Install
# npx clang-format --version. run: npm install
clangFormatVersion: 15
- name: Check Format
run: npm run format:check

View File

@@ -10,7 +10,8 @@ jobs:
steps: steps:
- uses: bcoe/conventional-release-labels@v1 - uses: bcoe/conventional-release-labels@v1
with: with:
type_labels: '{"feat": "PR: feature", "fix": "PR: fix", "breaking": "breaking type_labels:
'{"feat": "PR: feature", "fix": "PR: fix", "breaking": "breaking
change", "chore": "PR: chore", "docs": "PR: docs", "refactor": "PR: change", "chore": "PR: chore", "docs": "PR: docs", "refactor": "PR:
refactor", "revert": "PR: revert", "deprecate": "deprecation"}' refactor", "revert": "PR: revert", "deprecate": "deprecation"}'
ignored_types: '[]' ignored_types: '[]'

View File

@@ -23,4 +23,4 @@ jobs:
uses: github-actions-up-and-running/pr-comment@f1f8ab2bf00dce6880a369ce08758a60c61d6c0b uses: github-actions-up-and-running/pr-comment@f1f8ab2bf00dce6880a369ce08758a60c61d6c0b
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
message: "Thanks for the PR! The develop branch is currently frozen in preparation for the release so it may not be addressed until after release week." message: 'Thanks for the PR! The develop branch is currently frozen in preparation for the release so it may not be addressed until after release week.'

View File

@@ -12,7 +12,6 @@ on:
jobs: jobs:
tag-module-cleanup: tag-module-cleanup:
# Add the type: cleanup label # Add the type: cleanup label
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

31
.prettierignore Normal file
View File

@@ -0,0 +1,31 @@
# Build Artifacts
/msg/*
/build/*
/dist/*
/typings/*
/docs/*
# Tests other than mocha unit tests
/tests/blocks/*
/tests/themes/*
/tests/compile/*
/tests/jsunit/*
/tests/generators/*
/tests/mocha/webdriver.js
/tests/screenshot/*
/tests/test_runner.js
/tests/workspace_svg/*
# Demos, scripts, misc
/node_modules/*
/generators/*
/demos/*
/appengine/*
/externs/*
/closure/*
/scripts/gulpfiles/*
CHANGELOG.md
PULL_REQUEST_TEMPLATE.md
# Don't bother formatting js blocks since we're getting rid of them
/blocks/*.js

13
.prettierrc.js Normal file
View File

@@ -0,0 +1,13 @@
// This config attempts to match google-style code.
module.exports = {
// Prefer single quotes, but minimize escaping.
singleQuote: true,
// Some properties must be quoted to preserve closure compiler behavior.
// Don't ever change whether properties are quoted.
quoteProps: 'preserve',
// Don't add spaces around braces for object literals.
bracketSpacing: false,
// Put HTML tag closing brackets on same line as last attribute.
bracketSameLine: true,
};

View File

@@ -1,6 +1,6 @@
# Blockly # Blockly
Google's Blockly is a library that adds a visual code editor to web and mobile apps. The Blockly editor uses interlocking, graphical blocks to represent code concepts like variables, logical expressions, loops, and more. It allows users to apply programming principles without having to worry about syntax or the intimidation of a blinking cursor on the command line. All code is free and open source. Google's Blockly is a library that adds a visual code editor to web and mobile apps. The Blockly editor uses interlocking, graphical blocks to represent code concepts like variables, logical expressions, loops, and more. It allows users to apply programming principles without having to worry about syntax or the intimidation of a blinking cursor on the command line. All code is free and open source.
![](https://developers.google.com/blockly/images/sample.png) ![](https://developers.google.com/blockly/images/sample.png)
@@ -8,13 +8,13 @@ Google's Blockly is a library that adds a visual code editor to web and mobile a
Blockly has many resources for learning how to use the library. Start at our [Google Developers Site](https://developers.google.com/blockly) to read the documentation on how to get started, configure Blockly, and integrate it into your application. The developers site also contains links to: Blockly has many resources for learning how to use the library. Start at our [Google Developers Site](https://developers.google.com/blockly) to read the documentation on how to get started, configure Blockly, and integrate it into your application. The developers site also contains links to:
* [Getting Started article](https://developers.google.com/blockly/guides/get-started/web) - [Getting Started article](https://developers.google.com/blockly/guides/get-started/web)
* [Getting Started codelab](https://blocklycodelabs.dev/codelabs/getting-started/index.html#0) - [Getting Started codelab](https://blocklycodelabs.dev/codelabs/getting-started/index.html#0)
* [More codelabs](https://blocklycodelabs.dev/) - [More codelabs](https://blocklycodelabs.dev/)
* [Demos and plugins](https://google.github.io/blockly-samples/) - [Demos and plugins](https://google.github.io/blockly-samples/)
Help us focus our development efforts by telling us [what you are doing with Help us focus our development efforts by telling us [what you are doing with
Blockly](https://developers.google.com/blockly/registration). The questionnaire only takes Blockly](https://developers.google.com/blockly/registration). The questionnaire only takes
a few minutes and will help us better support the Blockly community. a few minutes and will help us better support the Blockly community.
### Installing Blockly ### Installing Blockly
@@ -28,8 +28,9 @@ npm install blockly
For more information on installing and using Blockly, see the [Getting Started article](https://developers.google.com/blockly/guides/get-started/web). For more information on installing and using Blockly, see the [Getting Started article](https://developers.google.com/blockly/guides/get-started/web).
### Getting Help ### Getting Help
* [Report a bug](https://developers.google.com/blockly/guides/modify/contribute/write_a_good_issue) or file a feature request on GitHub
* Ask a question, or search others' questions, on our [developer forum](https://groups.google.com/forum/#!forum/blockly). You can also drop by to say hello and show us your prototypes; collectively we have a lot of experience and can offer hints which will save you time. We actively monitor the forums and typically respond to questions within 2 working days. - [Report a bug](https://developers.google.com/blockly/guides/modify/contribute/write_a_good_issue) or file a feature request on GitHub
- Ask a question, or search others' questions, on our [developer forum](https://groups.google.com/forum/#!forum/blockly). You can also drop by to say hello and show us your prototypes; collectively we have a lot of experience and can offer hints which will save you time. We actively monitor the forums and typically respond to questions within 2 working days.
### blockly-samples ### blockly-samples
@@ -50,6 +51,7 @@ We now have a [beta release on npm](https://www.npmjs.com/package/blockly?active
```bash ```bash
npm install blockly@beta npm install blockly@beta
``` ```
As it is a beta channel, it may be less stable, and the APIs there are subject to change. As it is a beta channel, it may be less stable, and the APIs there are subject to change.
### Branches ### Branches
@@ -82,5 +84,5 @@ We typically triage all bugs within 2 working days, which includes adding any ap
## Good to Know ## Good to Know
* Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com) - Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com)
* We test browsers using [BrowserStack](https://browserstack.com) - We test browsers using [BrowserStack](https://browserstack.com)

View File

@@ -109,7 +109,7 @@
/** /**
* (REQUIRED) Whether to generate an API report. * (REQUIRED) Whether to generate an API report.
*/ */
"enabled": false, "enabled": false
/** /**
* The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce
@@ -195,7 +195,7 @@
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName> * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts" * DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts"
*/ */
"untrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>_rollup.d.ts", "untrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>_rollup.d.ts"
/** /**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release. * Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release.

View File

@@ -11,11 +11,12 @@
import * as goog from '../closure/goog/goog.js'; import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.colour'); goog.declareModuleId('Blockly.libraryBlocks.colour');
import type {BlockDefinition} from '../core/blocks.js'; import {
import {createBlockDefinitionsFromJsonArray, defineBlocks} from '../core/common.js'; createBlockDefinitionsFromJsonArray,
defineBlocks,
} from '../core/common.js';
import '../core/field_colour.js'; import '../core/field_colour.js';
/** /**
* A dictionary of the block definitions provided by this module. * A dictionary of the block definitions provided by this module.
*/ */
@@ -52,7 +53,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{ {
'type': 'colour_rgb', 'type': 'colour_rgb',
'message0': 'message0':
'%{BKY_COLOUR_RGB_TITLE} %{BKY_COLOUR_RGB_RED} %1 %{BKY_COLOUR_RGB_GREEN} %2 %{BKY_COLOUR_RGB_BLUE} %3', '%{BKY_COLOUR_RGB_TITLE} %{BKY_COLOUR_RGB_RED} %1 %{BKY_COLOUR_RGB_GREEN} %2 %{BKY_COLOUR_RGB_BLUE} %3',
'args0': [ 'args0': [
{ {
'type': 'input_value', 'type': 'input_value',
@@ -82,8 +83,9 @@ export const blocks = createBlockDefinitionsFromJsonArray([
// Block for blending two colours together. // Block for blending two colours together.
{ {
'type': 'colour_blend', 'type': 'colour_blend',
'message0': '%{BKY_COLOUR_BLEND_TITLE} %{BKY_COLOUR_BLEND_COLOUR1} ' + 'message0':
'%1 %{BKY_COLOUR_BLEND_COLOUR2} %2 %{BKY_COLOUR_BLEND_RATIO} %3', '%{BKY_COLOUR_BLEND_TITLE} %{BKY_COLOUR_BLEND_COLOUR1} ' +
'%1 %{BKY_COLOUR_BLEND_COLOUR2} %2 %{BKY_COLOUR_BLEND_RATIO} %3',
'args0': [ 'args0': [
{ {
'type': 'input_value', 'type': 'input_value',

View File

@@ -18,17 +18,17 @@ import {Align} from '../core/inputs/input.js';
import type {Block} from '../core/block.js'; import type {Block} from '../core/block.js';
import type {Connection} from '../core/connection.js'; import type {Connection} from '../core/connection.js';
import type {BlockSvg} from '../core/block_svg.js'; import type {BlockSvg} from '../core/block_svg.js';
import type {BlockDefinition} from '../core/blocks.js';
import {ConnectionType} from '../core/connection_type.js';
import type {FieldDropdown} from '../core/field_dropdown.js'; import type {FieldDropdown} from '../core/field_dropdown.js';
import {Msg} from '../core/msg.js'; import {Msg} from '../core/msg.js';
import {Mutator} from '../core/mutator.js'; import {Mutator} from '../core/mutator.js';
import type {Workspace} from '../core/workspace.js'; import type {Workspace} from '../core/workspace.js';
import {createBlockDefinitionsFromJsonArray, defineBlocks} from '../core/common.js'; import {
createBlockDefinitionsFromJsonArray,
defineBlocks,
} from '../core/common.js';
import '../core/field_dropdown.js'; import '../core/field_dropdown.js';
import {ValueInput} from '../core/inputs/value_input.js'; import {ValueInput} from '../core/inputs/value_input.js';
/** /**
* A dictionary of the block definitions provided by this module. * A dictionary of the block definitions provided by this module.
*/ */
@@ -117,9 +117,8 @@ export const blocks = createBlockDefinitionsFromJsonArray([
}, },
]); ]);
/** Type of a 'lists_create_with' block. */ /** Type of a 'lists_create_with' block. */
type CreateWithBlock = Block&ListCreateWithMixin; type CreateWithBlock = Block & ListCreateWithMixin;
interface ListCreateWithMixin extends ListCreateWithMixinType { interface ListCreateWithMixin extends ListCreateWithMixinType {
itemCount_: number; itemCount_: number;
} }
@@ -129,22 +128,22 @@ const LISTS_CREATE_WITH = {
/** /**
* Block for creating a list with any number of elements of any type. * Block for creating a list with any number of elements of any type.
*/ */
init: function(this: CreateWithBlock) { init: function (this: CreateWithBlock) {
this.setHelpUrl(Msg['LISTS_CREATE_WITH_HELPURL']); this.setHelpUrl(Msg['LISTS_CREATE_WITH_HELPURL']);
this.setStyle('list_blocks'); this.setStyle('list_blocks');
this.itemCount_ = 3; this.itemCount_ = 3;
this.updateShape_(); this.updateShape_();
this.setOutput(true, 'Array'); this.setOutput(true, 'Array');
this.setMutator(new Mutator( this.setMutator(
['lists_create_with_item'], new Mutator(['lists_create_with_item'], this as unknown as BlockSvg)
this as unknown as BlockSvg)); // BUG(#6905) ); // BUG(#6905)
this.setTooltip(Msg['LISTS_CREATE_WITH_TOOLTIP']); this.setTooltip(Msg['LISTS_CREATE_WITH_TOOLTIP']);
}, },
/** /**
* Create XML to represent list inputs. * Create XML to represent list inputs.
* Backwards compatible serialization implementation. * Backwards compatible serialization implementation.
*/ */
mutationToDom: function(this: CreateWithBlock): Element { mutationToDom: function (this: CreateWithBlock): Element {
const container = xmlUtils.createElement('mutation'); const container = xmlUtils.createElement('mutation');
container.setAttribute('items', String(this.itemCount_)); container.setAttribute('items', String(this.itemCount_));
return container; return container;
@@ -155,7 +154,7 @@ const LISTS_CREATE_WITH = {
* *
* @param xmlElement XML storage element. * @param xmlElement XML storage element.
*/ */
domToMutation: function(this: CreateWithBlock, xmlElement: Element) { domToMutation: function (this: CreateWithBlock, xmlElement: Element) {
const items = xmlElement.getAttribute('items'); const items = xmlElement.getAttribute('items');
if (!items) throw new TypeError('element did not have items'); if (!items) throw new TypeError('element did not have items');
this.itemCount_ = parseInt(items, 10); this.itemCount_ = parseInt(items, 10);
@@ -166,7 +165,7 @@ const LISTS_CREATE_WITH = {
* *
* @return The state of this block, ie the item count. * @return The state of this block, ie the item count.
*/ */
saveExtraState: function(this: CreateWithBlock): {itemCount: number} { saveExtraState: function (this: CreateWithBlock): {itemCount: number} {
return { return {
'itemCount': this.itemCount_, 'itemCount': this.itemCount_,
}; };
@@ -176,7 +175,7 @@ const LISTS_CREATE_WITH = {
* *
* @param state The state to apply to this block, ie the item count. * @param state The state to apply to this block, ie the item count.
*/ */
loadExtraState: function(this: CreateWithBlock, state: AnyDuringMigration) { loadExtraState: function (this: CreateWithBlock, state: AnyDuringMigration) {
this.itemCount_ = state['itemCount']; this.itemCount_ = state['itemCount'];
this.updateShape_(); this.updateShape_();
}, },
@@ -186,32 +185,37 @@ const LISTS_CREATE_WITH = {
* @param workspace Mutator's workspace. * @param workspace Mutator's workspace.
* @return Root block in mutator. * @return Root block in mutator.
*/ */
decompose: function(this: CreateWithBlock, workspace: Workspace): decompose: function (
ContainerBlock { this: CreateWithBlock,
const containerBlock = workspace: Workspace
workspace.newBlock('lists_create_with_container') as ContainerBlock; ): ContainerBlock {
(containerBlock as BlockSvg).initSvg(); const containerBlock = workspace.newBlock(
let connection = containerBlock.getInput('STACK')!.connection; 'lists_create_with_container'
for (let i = 0; i < this.itemCount_; i++) { ) as ContainerBlock;
const itemBlock = (containerBlock as BlockSvg).initSvg();
workspace.newBlock('lists_create_with_item') as ItemBlock; let connection = containerBlock.getInput('STACK')!.connection;
(itemBlock as BlockSvg).initSvg(); for (let i = 0; i < this.itemCount_; i++) {
if (!itemBlock.previousConnection) { const itemBlock = workspace.newBlock(
throw new Error('itemBlock has no previousConnection'); 'lists_create_with_item'
} ) as ItemBlock;
connection!.connect(itemBlock.previousConnection); (itemBlock as BlockSvg).initSvg();
connection = itemBlock.nextConnection; if (!itemBlock.previousConnection) {
} throw new Error('itemBlock has no previousConnection');
return containerBlock; }
}, connection!.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection;
}
return containerBlock;
},
/** /**
* Reconfigure this block based on the mutator dialog's components. * Reconfigure this block based on the mutator dialog's components.
* *
* @param containerBlock Root block in mutator. * @param containerBlock Root block in mutator.
*/ */
compose: function(this: CreateWithBlock, containerBlock: Block) { compose: function (this: CreateWithBlock, containerBlock: Block) {
let itemBlock: ItemBlock|null = let itemBlock: ItemBlock | null = containerBlock.getInputTargetBlock(
containerBlock.getInputTargetBlock('STACK') as ItemBlock; 'STACK'
) as ItemBlock;
// Count number of inputs. // Count number of inputs.
const connections: Connection[] = []; const connections: Connection[] = [];
while (itemBlock) { while (itemBlock) {
@@ -241,9 +245,10 @@ const LISTS_CREATE_WITH = {
* *
* @param containerBlock Root block in mutator. * @param containerBlock Root block in mutator.
*/ */
saveConnections: function(this: CreateWithBlock, containerBlock: Block) { saveConnections: function (this: CreateWithBlock, containerBlock: Block) {
let itemBlock: ItemBlock|null = let itemBlock: ItemBlock | null = containerBlock.getInputTargetBlock(
containerBlock.getInputTargetBlock('STACK') as ItemBlock; 'STACK'
) as ItemBlock;
let i = 0; let i = 0;
while (itemBlock) { while (itemBlock) {
if (itemBlock.isInsertionMarker()) { if (itemBlock.isInsertionMarker()) {
@@ -251,8 +256,8 @@ const LISTS_CREATE_WITH = {
continue; continue;
} }
const input = this.getInput('ADD' + i); const input = this.getInput('ADD' + i);
itemBlock.valueConnection_ = itemBlock.valueConnection_ = input?.connection!
input?.connection!.targetConnection as Connection; .targetConnection as Connection;
itemBlock = itemBlock.getNextBlock() as ItemBlock | null; itemBlock = itemBlock.getNextBlock() as ItemBlock | null;
i++; i++;
} }
@@ -260,12 +265,13 @@ const LISTS_CREATE_WITH = {
/** /**
* Modify this block to have the correct number of inputs. * Modify this block to have the correct number of inputs.
*/ */
updateShape_: function(this: CreateWithBlock) { updateShape_: function (this: CreateWithBlock) {
if (this.itemCount_ && this.getInput('EMPTY')) { if (this.itemCount_ && this.getInput('EMPTY')) {
this.removeInput('EMPTY'); this.removeInput('EMPTY');
} else if (!this.itemCount_ && !this.getInput('EMPTY')) { } else if (!this.itemCount_ && !this.getInput('EMPTY')) {
this.appendDummyInput('EMPTY').appendField( this.appendDummyInput('EMPTY').appendField(
Msg['LISTS_CREATE_EMPTY_TITLE']); Msg['LISTS_CREATE_EMPTY_TITLE']
);
} }
// Add new inputs. // Add new inputs.
for (let i = 0; i < this.itemCount_; i++) { for (let i = 0; i < this.itemCount_; i++) {
@@ -285,7 +291,7 @@ const LISTS_CREATE_WITH = {
blocks['lists_create_with'] = LISTS_CREATE_WITH; blocks['lists_create_with'] = LISTS_CREATE_WITH;
/** Type for a 'lists_create_with_container' block. */ /** Type for a 'lists_create_with_container' block. */
type ContainerBlock = Block&ContainerMutator; type ContainerBlock = Block & ContainerMutator;
interface ContainerMutator extends ContainerMutatorType {} interface ContainerMutator extends ContainerMutatorType {}
type ContainerMutatorType = typeof LISTS_CREATE_WITH_CONTAINER; type ContainerMutatorType = typeof LISTS_CREATE_WITH_CONTAINER;
@@ -293,10 +299,11 @@ const LISTS_CREATE_WITH_CONTAINER = {
/** /**
* Mutator block for list container. * Mutator block for list container.
*/ */
init: function(this: ContainerBlock) { init: function (this: ContainerBlock) {
this.setStyle('list_blocks'); this.setStyle('list_blocks');
this.appendDummyInput().appendField( this.appendDummyInput().appendField(
Msg['LISTS_CREATE_WITH_CONTAINER_TITLE_ADD']); Msg['LISTS_CREATE_WITH_CONTAINER_TITLE_ADD']
);
this.appendStatementInput('STACK'); this.appendStatementInput('STACK');
this.setTooltip(Msg['LISTS_CREATE_WITH_CONTAINER_TOOLTIP']); this.setTooltip(Msg['LISTS_CREATE_WITH_CONTAINER_TOOLTIP']);
this.contextMenu = false; this.contextMenu = false;
@@ -305,7 +312,7 @@ const LISTS_CREATE_WITH_CONTAINER = {
blocks['lists_create_with_container'] = LISTS_CREATE_WITH_CONTAINER; blocks['lists_create_with_container'] = LISTS_CREATE_WITH_CONTAINER;
/** Type for a 'lists_create_with_item' block. */ /** Type for a 'lists_create_with_item' block. */
type ItemBlock = Block&ItemMutator; type ItemBlock = Block & ItemMutator;
interface ItemMutator extends ItemMutatorType { interface ItemMutator extends ItemMutatorType {
valueConnection_?: Connection; valueConnection_?: Connection;
} }
@@ -315,7 +322,7 @@ const LISTS_CREATE_WITH_ITEM = {
/** /**
* Mutator block for adding items. * Mutator block for adding items.
*/ */
init: function(this: ItemBlock) { init: function (this: ItemBlock) {
this.setStyle('list_blocks'); this.setStyle('list_blocks');
this.appendDummyInput().appendField(Msg['LISTS_CREATE_WITH_ITEM_TITLE']); this.appendDummyInput().appendField(Msg['LISTS_CREATE_WITH_ITEM_TITLE']);
this.setPreviousStatement(true); this.setPreviousStatement(true);
@@ -327,7 +334,7 @@ const LISTS_CREATE_WITH_ITEM = {
blocks['lists_create_with_item'] = LISTS_CREATE_WITH_ITEM; blocks['lists_create_with_item'] = LISTS_CREATE_WITH_ITEM;
/** Type for a 'lists_indexOf' block. */ /** Type for a 'lists_indexOf' block. */
type IndexOfBlock = Block&IndexOfMutator; type IndexOfBlock = Block & IndexOfMutator;
interface IndexOfMutator extends IndexOfMutatorType {} interface IndexOfMutator extends IndexOfMutatorType {}
type IndexOfMutatorType = typeof LISTS_INDEXOF; type IndexOfMutatorType = typeof LISTS_INDEXOF;
@@ -335,7 +342,7 @@ const LISTS_INDEXOF = {
/** /**
* Block for finding an item in the list. * Block for finding an item in the list.
*/ */
init: function(this: IndexOfBlock) { init: function (this: IndexOfBlock) {
const OPERATORS = [ const OPERATORS = [
[Msg['LISTS_INDEX_OF_FIRST'], 'FIRST'], [Msg['LISTS_INDEX_OF_FIRST'], 'FIRST'],
[Msg['LISTS_INDEX_OF_LAST'], 'LAST'], [Msg['LISTS_INDEX_OF_LAST'], 'LAST'],
@@ -343,8 +350,9 @@ const LISTS_INDEXOF = {
this.setHelpUrl(Msg['LISTS_INDEX_OF_HELPURL']); this.setHelpUrl(Msg['LISTS_INDEX_OF_HELPURL']);
this.setStyle('list_blocks'); this.setStyle('list_blocks');
this.setOutput(true, 'Number'); this.setOutput(true, 'Number');
this.appendValueInput('VALUE').setCheck('Array').appendField( this.appendValueInput('VALUE')
Msg['LISTS_INDEX_OF_INPUT_IN_LIST']); .setCheck('Array')
.appendField(Msg['LISTS_INDEX_OF_INPUT_IN_LIST']);
const operatorsDropdown = fieldRegistry.fromJson({ const operatorsDropdown = fieldRegistry.fromJson({
type: 'field_dropdown', type: 'field_dropdown',
options: OPERATORS, options: OPERATORS,
@@ -352,18 +360,18 @@ const LISTS_INDEXOF = {
if (!operatorsDropdown) throw new Error('field_dropdown not found'); if (!operatorsDropdown) throw new Error('field_dropdown not found');
this.appendValueInput('FIND').appendField(operatorsDropdown, 'END'); this.appendValueInput('FIND').appendField(operatorsDropdown, 'END');
this.setInputsInline(true); this.setInputsInline(true);
// Assign 'this' to a variable for use in the tooltip closure below. this.setTooltip(() => {
const thisBlock = this;
this.setTooltip(function() {
return Msg['LISTS_INDEX_OF_TOOLTIP'].replace( return Msg['LISTS_INDEX_OF_TOOLTIP'].replace(
'%1', thisBlock.workspace.options.oneBasedIndex ? '0' : '-1'); '%1',
this.workspace.options.oneBasedIndex ? '0' : '-1'
);
}); });
}, },
}; };
blocks['lists_indexOf'] = LISTS_INDEXOF; blocks['lists_indexOf'] = LISTS_INDEXOF;
/** Type for a 'lists_getIndex' block. */ /** Type for a 'lists_getIndex' block. */
type GetIndexBlock = Block&GetIndexMutator; type GetIndexBlock = Block & GetIndexMutator;
interface GetIndexMutator extends GetIndexMutatorType { interface GetIndexMutator extends GetIndexMutatorType {
WHERE_OPTIONS: Array<[string, string]>; WHERE_OPTIONS: Array<[string, string]>;
} }
@@ -373,7 +381,7 @@ const LISTS_GETINDEX = {
/** /**
* Block for getting element at index. * Block for getting element at index.
*/ */
init: function(this: GetIndexBlock) { init: function (this: GetIndexBlock) {
const MODE = [ const MODE = [
[Msg['LISTS_GET_INDEX_GET'], 'GET'], [Msg['LISTS_GET_INDEX_GET'], 'GET'],
[Msg['LISTS_GET_INDEX_GET_REMOVE'], 'GET_REMOVE'], [Msg['LISTS_GET_INDEX_GET_REMOVE'], 'GET_REMOVE'],
@@ -393,18 +401,19 @@ const LISTS_GETINDEX = {
options: MODE, options: MODE,
}) as FieldDropdown; }) as FieldDropdown;
modeMenu.setValidator( modeMenu.setValidator(
/** @param value The input value. */ /** @param value The input value. */
function(this: FieldDropdown, value: string) { function (this: FieldDropdown, value: string) {
const isStatement = (value === 'REMOVE'); const isStatement = value === 'REMOVE';
(this.getSourceBlock() as GetIndexBlock) (this.getSourceBlock() as GetIndexBlock).updateStatement_(isStatement);
.updateStatement_(isStatement); return undefined;
return undefined; }
}); );
this.appendValueInput('VALUE').setCheck('Array').appendField( this.appendValueInput('VALUE')
Msg['LISTS_GET_INDEX_INPUT_IN_LIST']); .setCheck('Array')
.appendField(Msg['LISTS_GET_INDEX_INPUT_IN_LIST']);
this.appendDummyInput() this.appendDummyInput()
.appendField(modeMenu, 'MODE') .appendField(modeMenu, 'MODE')
.appendField('', 'SPACE'); .appendField('', 'SPACE');
this.appendDummyInput('AT'); this.appendDummyInput('AT');
if (Msg['LISTS_GET_INDEX_TAIL']) { if (Msg['LISTS_GET_INDEX_TAIL']) {
this.appendDummyInput('TAIL').appendField(Msg['LISTS_GET_INDEX_TAIL']); this.appendDummyInput('TAIL').appendField(Msg['LISTS_GET_INDEX_TAIL']);
@@ -412,11 +421,9 @@ const LISTS_GETINDEX = {
this.setInputsInline(true); this.setInputsInline(true);
this.setOutput(true); this.setOutput(true);
this.updateAt_(true); this.updateAt_(true);
// Assign 'this' to a variable for use in the tooltip closure below. this.setTooltip(() => {
const thisBlock = this; const mode = this.getFieldValue('MODE');
this.setTooltip(function() { const where = this.getFieldValue('WHERE');
const mode = thisBlock.getFieldValue('MODE');
const where = thisBlock.getFieldValue('WHERE');
let tooltip = ''; let tooltip = '';
switch (mode + ' ' + where) { switch (mode + ' ' + where) {
case 'GET FROM_START': case 'GET FROM_START':
@@ -460,12 +467,13 @@ const LISTS_GETINDEX = {
break; break;
} }
if (where === 'FROM_START' || where === 'FROM_END') { if (where === 'FROM_START' || where === 'FROM_END') {
const msg = (where === 'FROM_START') ? const msg =
Msg['LISTS_INDEX_FROM_START_TOOLTIP'] : where === 'FROM_START'
Msg['LISTS_INDEX_FROM_END_TOOLTIP']; ? Msg['LISTS_INDEX_FROM_START_TOOLTIP']
tooltip += ' ' + : Msg['LISTS_INDEX_FROM_END_TOOLTIP'];
msg.replace( tooltip +=
'%1', thisBlock.workspace.options.oneBasedIndex ? '#1' : '#0'); ' ' +
msg.replace('%1', this.workspace.options.oneBasedIndex ? '#1' : '#0');
} }
return tooltip; return tooltip;
}); });
@@ -476,7 +484,7 @@ const LISTS_GETINDEX = {
* *
* @return XML storage element. * @return XML storage element.
*/ */
mutationToDom: function(this: GetIndexBlock): Element { mutationToDom: function (this: GetIndexBlock): Element {
const container = xmlUtils.createElement('mutation'); const container = xmlUtils.createElement('mutation');
const isStatement = !this.outputConnection; const isStatement = !this.outputConnection;
container.setAttribute('statement', String(isStatement)); container.setAttribute('statement', String(isStatement));
@@ -489,12 +497,12 @@ const LISTS_GETINDEX = {
* *
* @param xmlElement XML storage element. * @param xmlElement XML storage element.
*/ */
domToMutation: function(this: GetIndexBlock, xmlElement: Element) { domToMutation: function (this: GetIndexBlock, xmlElement: Element) {
// Note: Until January 2013 this block did not have mutations, // Note: Until January 2013 this block did not have mutations,
// so 'statement' defaults to false and 'at' defaults to true. // so 'statement' defaults to false and 'at' defaults to true.
const isStatement = (xmlElement.getAttribute('statement') === 'true'); const isStatement = xmlElement.getAttribute('statement') === 'true';
this.updateStatement_(isStatement); this.updateStatement_(isStatement);
const isAt = (xmlElement.getAttribute('at') !== 'false'); const isAt = xmlElement.getAttribute('at') !== 'false';
this.updateAt_(isAt); this.updateAt_(isAt);
}, },
/** /**
@@ -503,9 +511,9 @@ const LISTS_GETINDEX = {
* *
* @return The state of this block, ie whether it's a statement. * @return The state of this block, ie whether it's a statement.
*/ */
saveExtraState: function(this: GetIndexBlock): ({ saveExtraState: function (this: GetIndexBlock): {
isStatement: boolean isStatement: boolean;
} | null) { } | null {
if (!this.outputConnection) { if (!this.outputConnection) {
return { return {
isStatement: true, isStatement: true,
@@ -520,7 +528,7 @@ const LISTS_GETINDEX = {
* @param state The state to apply to this block, ie whether it's a * @param state The state to apply to this block, ie whether it's a
* statement. * statement.
*/ */
loadExtraState: function(this: GetIndexBlock, state: AnyDuringMigration) { loadExtraState: function (this: GetIndexBlock, state: AnyDuringMigration) {
if (state['isStatement']) { if (state['isStatement']) {
this.updateStatement_(true); this.updateStatement_(true);
} else if (typeof state === 'string') { } else if (typeof state === 'string') {
@@ -535,7 +543,7 @@ const LISTS_GETINDEX = {
* @param newStatement True if the block should be a statement. * @param newStatement True if the block should be a statement.
* False if the block should be a value. * False if the block should be a value.
*/ */
updateStatement_: function(this: GetIndexBlock, newStatement: boolean) { updateStatement_: function (this: GetIndexBlock, newStatement: boolean) {
const oldStatement = !this.outputConnection; const oldStatement = !this.outputConnection;
if (newStatement !== oldStatement) { if (newStatement !== oldStatement) {
// TODO(#6920): The .unplug only has one parameter. // TODO(#6920): The .unplug only has one parameter.
@@ -556,7 +564,7 @@ const LISTS_GETINDEX = {
* *
* @param isAt True if the input should exist. * @param isAt True if the input should exist.
*/ */
updateAt_: function(this: GetIndexBlock, isAt: boolean) { updateAt_: function (this: GetIndexBlock, isAt: boolean) {
// Destroy old 'AT' and 'ORDINAL' inputs. // Destroy old 'AT' and 'ORDINAL' inputs.
this.removeInput('AT'); this.removeInput('AT');
this.removeInput('ORDINAL', true); this.removeInput('ORDINAL', true);
@@ -565,7 +573,8 @@ const LISTS_GETINDEX = {
this.appendValueInput('AT').setCheck('Number'); this.appendValueInput('AT').setCheck('Number');
if (Msg['ORDINAL_NUMBER_SUFFIX']) { if (Msg['ORDINAL_NUMBER_SUFFIX']) {
this.appendDummyInput('ORDINAL').appendField( this.appendDummyInput('ORDINAL').appendField(
Msg['ORDINAL_NUMBER_SUFFIX']); Msg['ORDINAL_NUMBER_SUFFIX']
);
} }
} else { } else {
this.appendDummyInput('AT'); this.appendDummyInput('AT');
@@ -575,24 +584,25 @@ const LISTS_GETINDEX = {
options: this.WHERE_OPTIONS, options: this.WHERE_OPTIONS,
}) as FieldDropdown; }) as FieldDropdown;
menu.setValidator( menu.setValidator(
/** /**
* @param value The input value. * @param value The input value.
* @return Null if the field has been replaced; otherwise undefined. * @return Null if the field has been replaced; otherwise undefined.
*/ */
function(this: FieldDropdown, value: string) { function (this: FieldDropdown, value: string) {
const newAt = (value === 'FROM_START') || (value === 'FROM_END'); const newAt = value === 'FROM_START' || value === 'FROM_END';
// The 'isAt' variable is available due to this function being a // The 'isAt' variable is available due to this function being a
// closure. // closure.
if (newAt !== isAt) { if (newAt !== isAt) {
const block = this.getSourceBlock() as GetIndexBlock; const block = this.getSourceBlock() as GetIndexBlock;
block.updateAt_(newAt); block.updateAt_(newAt);
// This menu has been destroyed and replaced. Update the // This menu has been destroyed and replaced. Update the
// replacement. // replacement.
block.setFieldValue(value, 'WHERE'); block.setFieldValue(value, 'WHERE');
return null; return null;
} }
return undefined; return undefined;
}); }
);
this.getInput('AT')!.appendField(menu, 'WHERE'); this.getInput('AT')!.appendField(menu, 'WHERE');
if (Msg['LISTS_GET_INDEX_TAIL']) { if (Msg['LISTS_GET_INDEX_TAIL']) {
this.moveInputBefore('TAIL', null); this.moveInputBefore('TAIL', null);
@@ -602,7 +612,7 @@ const LISTS_GETINDEX = {
blocks['lists_getIndex'] = LISTS_GETINDEX; blocks['lists_getIndex'] = LISTS_GETINDEX;
/** Type for a 'lists_setIndex' block. */ /** Type for a 'lists_setIndex' block. */
type SetIndexBlock = Block&SetIndexMutator; type SetIndexBlock = Block & SetIndexMutator;
interface SetIndexMutator extends SetIndexMutatorType { interface SetIndexMutator extends SetIndexMutatorType {
WHERE_OPTIONS: Array<[string, string]>; WHERE_OPTIONS: Array<[string, string]>;
} }
@@ -612,7 +622,7 @@ const LISTS_SETINDEX = {
/** /**
* Block for setting the element at index. * Block for setting the element at index.
*/ */
init: function(this: SetIndexBlock) { init: function (this: SetIndexBlock) {
const MODE = [ const MODE = [
[Msg['LISTS_SET_INDEX_SET'], 'SET'], [Msg['LISTS_SET_INDEX_SET'], 'SET'],
[Msg['LISTS_SET_INDEX_INSERT'], 'INSERT'], [Msg['LISTS_SET_INDEX_INSERT'], 'INSERT'],
@@ -626,15 +636,16 @@ const LISTS_SETINDEX = {
]; ];
this.setHelpUrl(Msg['LISTS_SET_INDEX_HELPURL']); this.setHelpUrl(Msg['LISTS_SET_INDEX_HELPURL']);
this.setStyle('list_blocks'); this.setStyle('list_blocks');
this.appendValueInput('LIST').setCheck('Array').appendField( this.appendValueInput('LIST')
Msg['LISTS_SET_INDEX_INPUT_IN_LIST']); .setCheck('Array')
.appendField(Msg['LISTS_SET_INDEX_INPUT_IN_LIST']);
const operationDropdown = fieldRegistry.fromJson({ const operationDropdown = fieldRegistry.fromJson({
type: 'field_dropdown', type: 'field_dropdown',
options: MODE, options: MODE,
}) as FieldDropdown; }) as FieldDropdown;
this.appendDummyInput() this.appendDummyInput()
.appendField(operationDropdown, 'MODE') .appendField(operationDropdown, 'MODE')
.appendField('', 'SPACE'); .appendField('', 'SPACE');
this.appendDummyInput('AT'); this.appendDummyInput('AT');
this.appendValueInput('TO').appendField(Msg['LISTS_SET_INDEX_INPUT_TO']); this.appendValueInput('TO').appendField(Msg['LISTS_SET_INDEX_INPUT_TO']);
this.setInputsInline(true); this.setInputsInline(true);
@@ -642,11 +653,9 @@ const LISTS_SETINDEX = {
this.setNextStatement(true); this.setNextStatement(true);
this.setTooltip(Msg['LISTS_SET_INDEX_TOOLTIP']); this.setTooltip(Msg['LISTS_SET_INDEX_TOOLTIP']);
this.updateAt_(true); this.updateAt_(true);
// Assign 'this' to a variable for use in the tooltip closure below. this.setTooltip(() => {
const thisBlock = this; const mode = this.getFieldValue('MODE');
this.setTooltip(function() { const where = this.getFieldValue('WHERE');
const mode = thisBlock.getFieldValue('MODE');
const where = thisBlock.getFieldValue('WHERE');
let tooltip = ''; let tooltip = '';
switch (mode + ' ' + where) { switch (mode + ' ' + where) {
case 'SET FROM_START': case 'SET FROM_START':
@@ -677,9 +686,12 @@ const LISTS_SETINDEX = {
break; break;
} }
if (where === 'FROM_START' || where === 'FROM_END') { if (where === 'FROM_START' || where === 'FROM_END') {
tooltip += ' ' + tooltip +=
Msg['LISTS_INDEX_FROM_START_TOOLTIP'].replace( ' ' +
'%1', thisBlock.workspace.options.oneBasedIndex ? '#1' : '#0'); Msg['LISTS_INDEX_FROM_START_TOOLTIP'].replace(
'%1',
this.workspace.options.oneBasedIndex ? '#1' : '#0'
);
} }
return tooltip; return tooltip;
}); });
@@ -689,7 +701,7 @@ const LISTS_SETINDEX = {
* *
* @return XML storage element. * @return XML storage element.
*/ */
mutationToDom: function(this: SetIndexBlock): Element { mutationToDom: function (this: SetIndexBlock): Element {
const container = xmlUtils.createElement('mutation'); const container = xmlUtils.createElement('mutation');
const isAt = this.getInput('AT') instanceof ValueInput; const isAt = this.getInput('AT') instanceof ValueInput;
container.setAttribute('at', String(isAt)); container.setAttribute('at', String(isAt));
@@ -700,10 +712,10 @@ const LISTS_SETINDEX = {
* *
* @param xmlElement XML storage element. * @param xmlElement XML storage element.
*/ */
domToMutation: function(this: SetIndexBlock, xmlElement: Element) { domToMutation: function (this: SetIndexBlock, xmlElement: Element) {
// Note: Until January 2013 this block did not have mutations, // Note: Until January 2013 this block did not have mutations,
// so 'at' defaults to true. // so 'at' defaults to true.
const isAt = (xmlElement.getAttribute('at') !== 'false'); const isAt = xmlElement.getAttribute('at') !== 'false';
this.updateAt_(isAt); this.updateAt_(isAt);
}, },
@@ -715,7 +727,7 @@ const LISTS_SETINDEX = {
* *
* @return The state of this block. * @return The state of this block.
*/ */
saveExtraState: function(this: SetIndexBlock): null { saveExtraState: function (this: SetIndexBlock): null {
return null; return null;
}, },
@@ -724,14 +736,14 @@ const LISTS_SETINDEX = {
* No extra state is needed or expected as it is already encoded in the * No extra state is needed or expected as it is already encoded in the
* dropdown values. * dropdown values.
*/ */
loadExtraState: function(this: SetIndexBlock) {}, loadExtraState: function (this: SetIndexBlock) {},
/** /**
* Create or delete an input for the numeric index. * Create or delete an input for the numeric index.
* *
* @param isAt True if the input should exist. * @param isAt True if the input should exist.
*/ */
updateAt_: function(this: SetIndexBlock, isAt: boolean) { updateAt_: function (this: SetIndexBlock, isAt: boolean) {
// Destroy old 'AT' and 'ORDINAL' input. // Destroy old 'AT' and 'ORDINAL' input.
this.removeInput('AT'); this.removeInput('AT');
this.removeInput('ORDINAL', true); this.removeInput('ORDINAL', true);
@@ -740,7 +752,8 @@ const LISTS_SETINDEX = {
this.appendValueInput('AT').setCheck('Number'); this.appendValueInput('AT').setCheck('Number');
if (Msg['ORDINAL_NUMBER_SUFFIX']) { if (Msg['ORDINAL_NUMBER_SUFFIX']) {
this.appendDummyInput('ORDINAL').appendField( this.appendDummyInput('ORDINAL').appendField(
Msg['ORDINAL_NUMBER_SUFFIX']); Msg['ORDINAL_NUMBER_SUFFIX']
);
} }
} else { } else {
this.appendDummyInput('AT'); this.appendDummyInput('AT');
@@ -750,24 +763,25 @@ const LISTS_SETINDEX = {
options: this.WHERE_OPTIONS, options: this.WHERE_OPTIONS,
}) as FieldDropdown; }) as FieldDropdown;
menu.setValidator( menu.setValidator(
/** /**
* @param value The input value. * @param value The input value.
* @return Null if the field has been replaced; otherwise undefined. * @return Null if the field has been replaced; otherwise undefined.
*/ */
function(this: FieldDropdown, value: string) { function (this: FieldDropdown, value: string) {
const newAt = (value === 'FROM_START') || (value === 'FROM_END'); const newAt = value === 'FROM_START' || value === 'FROM_END';
// The 'isAt' variable is available due to this function being a // The 'isAt' variable is available due to this function being a
// closure. // closure.
if (newAt !== isAt) { if (newAt !== isAt) {
const block = this.getSourceBlock() as SetIndexBlock; const block = this.getSourceBlock() as SetIndexBlock;
block.updateAt_(newAt); block.updateAt_(newAt);
// This menu has been destroyed and replaced. Update the // This menu has been destroyed and replaced. Update the
// replacement. // replacement.
block.setFieldValue(value, 'WHERE'); block.setFieldValue(value, 'WHERE');
return null; return null;
} }
return undefined; return undefined;
}); }
);
this.moveInputBefore('AT', 'TO'); this.moveInputBefore('AT', 'TO');
if (this.getInput('ORDINAL')) { if (this.getInput('ORDINAL')) {
this.moveInputBefore('ORDINAL', 'TO'); this.moveInputBefore('ORDINAL', 'TO');
@@ -779,7 +793,7 @@ const LISTS_SETINDEX = {
blocks['lists_setIndex'] = LISTS_SETINDEX; blocks['lists_setIndex'] = LISTS_SETINDEX;
/** Type for a 'lists_getSublist' block. */ /** Type for a 'lists_getSublist' block. */
type GetSublistBlock = Block&GetSublistMutator; type GetSublistBlock = Block & GetSublistMutator;
interface GetSublistMutator extends GetSublistMutatorType { interface GetSublistMutator extends GetSublistMutatorType {
WHERE_OPTIONS_1: Array<[string, string]>; WHERE_OPTIONS_1: Array<[string, string]>;
WHERE_OPTIONS_2: Array<[string, string]>; WHERE_OPTIONS_2: Array<[string, string]>;
@@ -790,7 +804,7 @@ const LISTS_GETSUBLIST = {
/** /**
* Block for getting sublist. * Block for getting sublist.
*/ */
init: function(this: GetSublistBlock) { init: function (this: GetSublistBlock) {
this['WHERE_OPTIONS_1'] = [ this['WHERE_OPTIONS_1'] = [
[Msg['LISTS_GET_SUBLIST_START_FROM_START'], 'FROM_START'], [Msg['LISTS_GET_SUBLIST_START_FROM_START'], 'FROM_START'],
[Msg['LISTS_GET_SUBLIST_START_FROM_END'], 'FROM_END'], [Msg['LISTS_GET_SUBLIST_START_FROM_END'], 'FROM_END'],
@@ -803,8 +817,9 @@ const LISTS_GETSUBLIST = {
]; ];
this.setHelpUrl(Msg['LISTS_GET_SUBLIST_HELPURL']); this.setHelpUrl(Msg['LISTS_GET_SUBLIST_HELPURL']);
this.setStyle('list_blocks'); this.setStyle('list_blocks');
this.appendValueInput('LIST').setCheck('Array').appendField( this.appendValueInput('LIST')
Msg['LISTS_GET_SUBLIST_INPUT_IN_LIST']); .setCheck('Array')
.appendField(Msg['LISTS_GET_SUBLIST_INPUT_IN_LIST']);
this.appendDummyInput('AT1'); this.appendDummyInput('AT1');
this.appendDummyInput('AT2'); this.appendDummyInput('AT2');
if (Msg['LISTS_GET_SUBLIST_TAIL']) { if (Msg['LISTS_GET_SUBLIST_TAIL']) {
@@ -821,7 +836,7 @@ const LISTS_GETSUBLIST = {
* *
* @return XML storage element. * @return XML storage element.
*/ */
mutationToDom: function(this: GetSublistBlock): Element { mutationToDom: function (this: GetSublistBlock): Element {
const container = xmlUtils.createElement('mutation'); const container = xmlUtils.createElement('mutation');
const isAt1 = this.getInput('AT1') instanceof ValueInput; const isAt1 = this.getInput('AT1') instanceof ValueInput;
container.setAttribute('at1', String(isAt1)); container.setAttribute('at1', String(isAt1));
@@ -834,9 +849,9 @@ const LISTS_GETSUBLIST = {
* *
* @param xmlElement XML storage element. * @param xmlElement XML storage element.
*/ */
domToMutation: function(this: GetSublistBlock, xmlElement: Element) { domToMutation: function (this: GetSublistBlock, xmlElement: Element) {
const isAt1 = (xmlElement.getAttribute('at1') === 'true'); const isAt1 = xmlElement.getAttribute('at1') === 'true';
const isAt2 = (xmlElement.getAttribute('at2') === 'true'); const isAt2 = xmlElement.getAttribute('at2') === 'true';
this.updateAt_(1, isAt1); this.updateAt_(1, isAt1);
this.updateAt_(2, isAt2); this.updateAt_(2, isAt2);
}, },
@@ -849,7 +864,7 @@ const LISTS_GETSUBLIST = {
* *
* @return The state of this block. * @return The state of this block.
*/ */
saveExtraState: function(this: GetSublistBlock): null { saveExtraState: function (this: GetSublistBlock): null {
return null; return null;
}, },
@@ -858,7 +873,7 @@ const LISTS_GETSUBLIST = {
* No extra state is needed or expected as it is already encoded in the * No extra state is needed or expected as it is already encoded in the
* dropdown values. * dropdown values.
*/ */
loadExtraState: function(this: GetSublistBlock) {}, loadExtraState: function (this: GetSublistBlock) {},
/** /**
* Create or delete an input for a numeric index. * Create or delete an input for a numeric index.
@@ -867,7 +882,7 @@ const LISTS_GETSUBLIST = {
* @param n Specify first or second input (1 or 2). * @param n Specify first or second input (1 or 2).
* @param isAt True if the input should exist. * @param isAt True if the input should exist.
*/ */
updateAt_: function(this: GetSublistBlock, n: 1|2, isAt: boolean) { updateAt_: function (this: GetSublistBlock, n: 1 | 2, isAt: boolean) {
// Create or delete an input for the numeric index. // Create or delete an input for the numeric index.
// Destroy old 'AT' and 'ORDINAL' inputs. // Destroy old 'AT' and 'ORDINAL' inputs.
this.removeInput('AT' + n); this.removeInput('AT' + n);
@@ -876,37 +891,37 @@ const LISTS_GETSUBLIST = {
if (isAt) { if (isAt) {
this.appendValueInput('AT' + n).setCheck('Number'); this.appendValueInput('AT' + n).setCheck('Number');
if (Msg['ORDINAL_NUMBER_SUFFIX']) { if (Msg['ORDINAL_NUMBER_SUFFIX']) {
this.appendDummyInput('ORDINAL' + n) this.appendDummyInput('ORDINAL' + n).appendField(
.appendField(Msg['ORDINAL_NUMBER_SUFFIX']); Msg['ORDINAL_NUMBER_SUFFIX']
);
} }
} else { } else {
this.appendDummyInput('AT' + n); this.appendDummyInput('AT' + n);
} }
const menu = fieldRegistry.fromJson({ const menu = fieldRegistry.fromJson({
type: 'field_dropdown', type: 'field_dropdown',
// TODO(#6920): Rewrite this so that clang-format doesn't make such an options:
// awful unreadable mess of it. this[('WHERE_OPTIONS_' + n) as 'WHERE_OPTIONS_1' | 'WHERE_OPTIONS_2'],
options: this
[('WHERE_OPTIONS_' + n) as ('WHERE_OPTIONS_1' | 'WHERE_OPTIONS_2')],
}) as FieldDropdown; }) as FieldDropdown;
menu.setValidator( menu.setValidator(
/** /**
* @param value The input value. * @param value The input value.
* @return Null if the field has been replaced; otherwise undefined. * @return Null if the field has been replaced; otherwise undefined.
*/ */
function(this: FieldDropdown, value: string) { function (this: FieldDropdown, value: string) {
const newAt = (value === 'FROM_START') || (value === 'FROM_END'); const newAt = value === 'FROM_START' || value === 'FROM_END';
// The 'isAt' variable is available due to this function being a // The 'isAt' variable is available due to this function being a
// closure. // closure.
if (newAt !== isAt) { if (newAt !== isAt) {
const block = this.getSourceBlock() as GetSublistBlock; const block = this.getSourceBlock() as GetSublistBlock;
block.updateAt_(n, newAt); block.updateAt_(n, newAt);
// This menu has been destroyed and replaced. // This menu has been destroyed and replaced.
// Update the replacement. // Update the replacement.
block.setFieldValue(value, 'WHERE' + n); block.setFieldValue(value, 'WHERE' + n);
return null; return null;
} }
}); }
);
this.getInput('AT' + n)!.appendField(menu, 'WHERE' + n); this.getInput('AT' + n)!.appendField(menu, 'WHERE' + n);
if (n === 1) { if (n === 1) {
this.moveInputBefore('AT1', 'AT2'); this.moveInputBefore('AT1', 'AT2');
@@ -921,13 +936,13 @@ const LISTS_GETSUBLIST = {
}; };
blocks['lists_getSublist'] = LISTS_GETSUBLIST; blocks['lists_getSublist'] = LISTS_GETSUBLIST;
type SortBlock = Block|typeof blocks['lists_sort']; type SortBlock = Block | (typeof blocks)['lists_sort'];
blocks['lists_sort'] = { blocks['lists_sort'] = {
/** /**
* Block for sorting a list. * Block for sorting a list.
*/ */
init: function(this: SortBlock) { init: function (this: SortBlock) {
this.jsonInit({ this.jsonInit({
'message0': '%{BKY_LISTS_SORT_TITLE}', 'message0': '%{BKY_LISTS_SORT_TITLE}',
'args0': [ 'args0': [
@@ -962,15 +977,13 @@ blocks['lists_sort'] = {
}, },
}; };
type SplitBlock = Block|typeof blocks['lists_split']; type SplitBlock = Block | (typeof blocks)['lists_split'];
blocks['lists_split'] = { blocks['lists_split'] = {
/** /**
* Block for splitting text into a list, or joining a list into text. * Block for splitting text into a list, or joining a list into text.
*/ */
init: function(this: SplitBlock) { init: function (this: SplitBlock) {
// Assign 'this' to a variable for use in the closures below.
const thisBlock = this;
const dropdown = fieldRegistry.fromJson({ const dropdown = fieldRegistry.fromJson({
type: 'field_dropdown', type: 'field_dropdown',
options: [ options: [
@@ -979,19 +992,21 @@ blocks['lists_split'] = {
], ],
}); });
if (!dropdown) throw new Error('field_dropdown not found'); if (!dropdown) throw new Error('field_dropdown not found');
dropdown.setValidator(function(newMode) { dropdown.setValidator((newMode) => {
thisBlock.updateType_(newMode); this.updateType_(newMode);
}); });
this.setHelpUrl(Msg['LISTS_SPLIT_HELPURL']); this.setHelpUrl(Msg['LISTS_SPLIT_HELPURL']);
this.setStyle('list_blocks'); this.setStyle('list_blocks');
this.appendValueInput('INPUT').setCheck('String').appendField( this.appendValueInput('INPUT')
dropdown, 'MODE'); .setCheck('String')
this.appendValueInput('DELIM').setCheck('String').appendField( .appendField(dropdown, 'MODE');
Msg['LISTS_SPLIT_WITH_DELIMITER']); this.appendValueInput('DELIM')
.setCheck('String')
.appendField(Msg['LISTS_SPLIT_WITH_DELIMITER']);
this.setInputsInline(true); this.setInputsInline(true);
this.setOutput(true, 'Array'); this.setOutput(true, 'Array');
this.setTooltip(function() { this.setTooltip(() => {
const mode = thisBlock.getFieldValue('MODE'); const mode = this.getFieldValue('MODE');
if (mode === 'SPLIT') { if (mode === 'SPLIT') {
return Msg['LISTS_SPLIT_TOOLTIP_SPLIT']; return Msg['LISTS_SPLIT_TOOLTIP_SPLIT'];
} else if (mode === 'JOIN') { } else if (mode === 'JOIN') {
@@ -1005,7 +1020,7 @@ blocks['lists_split'] = {
* *
* @param newMode Either 'SPLIT' or 'JOIN'. * @param newMode Either 'SPLIT' or 'JOIN'.
*/ */
updateType_: function(this: SplitBlock, newMode: string) { updateType_: function (this: SplitBlock, newMode: string) {
const mode = this.getFieldValue('MODE'); const mode = this.getFieldValue('MODE');
if (mode !== newMode) { if (mode !== newMode) {
const inputConnection = this.getInput('INPUT')!.connection; const inputConnection = this.getInput('INPUT')!.connection;
@@ -1034,7 +1049,7 @@ blocks['lists_split'] = {
* *
* @return XML storage element. * @return XML storage element.
*/ */
mutationToDom: function(this: SplitBlock): Element { mutationToDom: function (this: SplitBlock): Element {
const container = xmlUtils.createElement('mutation'); const container = xmlUtils.createElement('mutation');
container.setAttribute('mode', this.getFieldValue('MODE')); container.setAttribute('mode', this.getFieldValue('MODE'));
return container; return container;
@@ -1044,7 +1059,7 @@ blocks['lists_split'] = {
* *
* @param xmlElement XML storage element. * @param xmlElement XML storage element.
*/ */
domToMutation: function(this: SplitBlock, xmlElement: Element) { domToMutation: function (this: SplitBlock, xmlElement: Element) {
this.updateType_(xmlElement.getAttribute('mode')); this.updateType_(xmlElement.getAttribute('mode'));
}, },
@@ -1056,7 +1071,7 @@ blocks['lists_split'] = {
* *
* @return The state of this block. * @return The state of this block.
*/ */
saveExtraState: function(this: SplitBlock): null { saveExtraState: function (this: SplitBlock): null {
return null; return null;
}, },
@@ -1065,7 +1080,7 @@ blocks['lists_split'] = {
* No extra state is needed or expected as it is already encoded in the * No extra state is needed or expected as it is already encoded in the
* dropdown values. * dropdown values.
*/ */
loadExtraState: function(this: SplitBlock) {}, loadExtraState: function (this: SplitBlock) {},
}; };
// Register provided blocks. // Register provided blocks.

View File

@@ -14,13 +14,19 @@ goog.declareModuleId('Blockly.libraryBlocks.loops');
import type {Abstract as AbstractEvent} from '../core/events/events_abstract.js'; import type {Abstract as AbstractEvent} from '../core/events/events_abstract.js';
import type {Block} from '../core/block.js'; import type {Block} from '../core/block.js';
import * as ContextMenu from '../core/contextmenu.js'; import * as ContextMenu from '../core/contextmenu.js';
import type {ContextMenuOption, LegacyContextMenuOption} from '../core/contextmenu_registry.js'; import type {
ContextMenuOption,
LegacyContextMenuOption,
} from '../core/contextmenu_registry.js';
import * as Events from '../core/events/events.js'; import * as Events from '../core/events/events.js';
import * as Extensions from '../core/extensions.js'; import * as Extensions from '../core/extensions.js';
import * as Variables from '../core/variables.js'; import * as Variables from '../core/variables.js';
import * as xmlUtils from '../core/utils/xml.js'; import * as xmlUtils from '../core/utils/xml.js';
import {Msg} from '../core/msg.js'; import {Msg} from '../core/msg.js';
import {createBlockDefinitionsFromJsonArray, defineBlocks} from '../core/common.js'; import {
createBlockDefinitionsFromJsonArray,
defineBlocks,
} from '../core/common.js';
import '../core/field_dropdown.js'; import '../core/field_dropdown.js';
import '../core/field_label.js'; import '../core/field_label.js';
import '../core/field_number.js'; import '../core/field_number.js';
@@ -29,7 +35,6 @@ import '../core/warning.js';
import {FieldVariable} from '../core/field_variable.js'; import {FieldVariable} from '../core/field_variable.js';
import {WorkspaceSvg} from '../core/workspace_svg.js'; import {WorkspaceSvg} from '../core/workspace_svg.js';
/** /**
* A dictionary of the block definitions provided by this module. * A dictionary of the block definitions provided by this module.
*/ */
@@ -38,16 +43,20 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{ {
'type': 'controls_repeat_ext', 'type': 'controls_repeat_ext',
'message0': '%{BKY_CONTROLS_REPEAT_TITLE}', 'message0': '%{BKY_CONTROLS_REPEAT_TITLE}',
'args0': [{ 'args0': [
'type': 'input_value', {
'name': 'TIMES', 'type': 'input_value',
'check': 'Number', 'name': 'TIMES',
}], 'check': 'Number',
},
],
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1', 'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
'args1': [{ 'args1': [
'type': 'input_statement', {
'name': 'DO', 'type': 'input_statement',
}], 'name': 'DO',
},
],
'previousStatement': null, 'previousStatement': null,
'nextStatement': null, 'nextStatement': null,
'style': 'loop_blocks', 'style': 'loop_blocks',
@@ -59,18 +68,22 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{ {
'type': 'controls_repeat', 'type': 'controls_repeat',
'message0': '%{BKY_CONTROLS_REPEAT_TITLE}', 'message0': '%{BKY_CONTROLS_REPEAT_TITLE}',
'args0': [{ 'args0': [
'type': 'field_number', {
'name': 'TIMES', 'type': 'field_number',
'value': 10, 'name': 'TIMES',
'min': 0, 'value': 10,
'precision': 1, 'min': 0,
}], 'precision': 1,
},
],
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1', 'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
'args1': [{ 'args1': [
'type': 'input_statement', {
'name': 'DO', 'type': 'input_statement',
}], 'name': 'DO',
},
],
'previousStatement': null, 'previousStatement': null,
'nextStatement': null, 'nextStatement': null,
'style': 'loop_blocks', 'style': 'loop_blocks',
@@ -97,10 +110,12 @@ export const blocks = createBlockDefinitionsFromJsonArray([
}, },
], ],
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1', 'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
'args1': [{ 'args1': [
'type': 'input_statement', {
'name': 'DO', 'type': 'input_statement',
}], 'name': 'DO',
},
],
'previousStatement': null, 'previousStatement': null,
'nextStatement': null, 'nextStatement': null,
'style': 'loop_blocks', 'style': 'loop_blocks',
@@ -137,19 +152,18 @@ export const blocks = createBlockDefinitionsFromJsonArray([
}, },
], ],
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1', 'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
'args1': [{ 'args1': [
'type': 'input_statement', {
'name': 'DO', 'type': 'input_statement',
}], 'name': 'DO',
},
],
'inputsInline': true, 'inputsInline': true,
'previousStatement': null, 'previousStatement': null,
'nextStatement': null, 'nextStatement': null,
'style': 'loop_blocks', 'style': 'loop_blocks',
'helpUrl': '%{BKY_CONTROLS_FOR_HELPURL}', 'helpUrl': '%{BKY_CONTROLS_FOR_HELPURL}',
'extensions': [ 'extensions': ['contextMenu_newGetVariableBlock', 'controls_for_tooltip'],
'contextMenu_newGetVariableBlock',
'controls_for_tooltip',
],
}, },
// Block for 'for each' loop. // Block for 'for each' loop.
{ {
@@ -168,10 +182,12 @@ export const blocks = createBlockDefinitionsFromJsonArray([
}, },
], ],
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1', 'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
'args1': [{ 'args1': [
'type': 'input_statement', {
'name': 'DO', 'type': 'input_statement',
}], 'name': 'DO',
},
],
'previousStatement': null, 'previousStatement': null,
'nextStatement': null, 'nextStatement': null,
'style': 'loop_blocks', 'style': 'loop_blocks',
@@ -185,22 +201,21 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{ {
'type': 'controls_flow_statements', 'type': 'controls_flow_statements',
'message0': '%1', 'message0': '%1',
'args0': [{ 'args0': [
'type': 'field_dropdown', {
'name': 'FLOW', 'type': 'field_dropdown',
'options': [ 'name': 'FLOW',
['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}', 'BREAK'], 'options': [
['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}', 'CONTINUE'], ['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}', 'BREAK'],
], ['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}', 'CONTINUE'],
}], ],
},
],
'previousStatement': null, 'previousStatement': null,
'style': 'loop_blocks', 'style': 'loop_blocks',
'helpUrl': '%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}', 'helpUrl': '%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}',
'suppressPrefixSuffix': true, 'suppressPrefixSuffix': true,
'extensions': [ 'extensions': ['controls_flow_tooltip', 'controls_flow_in_loop_check'],
'controls_flow_tooltip',
'controls_flow_in_loop_check',
],
}, },
]); ]);
@@ -215,8 +230,9 @@ const WHILE_UNTIL_TOOLTIPS = {
}; };
Extensions.register( Extensions.register(
'controls_whileUntil_tooltip', 'controls_whileUntil_tooltip',
Extensions.buildTooltipForDropdown('MODE', WHILE_UNTIL_TOOLTIPS)); Extensions.buildTooltipForDropdown('MODE', WHILE_UNTIL_TOOLTIPS)
);
/** /**
* Tooltips for the 'controls_flow_statements' block, keyed by FLOW value. * Tooltips for the 'controls_flow_statements' block, keyed by FLOW value.
@@ -229,14 +245,15 @@ const BREAK_CONTINUE_TOOLTIPS = {
}; };
Extensions.register( Extensions.register(
'controls_flow_tooltip', 'controls_flow_tooltip',
Extensions.buildTooltipForDropdown('FLOW', BREAK_CONTINUE_TOOLTIPS)); Extensions.buildTooltipForDropdown('FLOW', BREAK_CONTINUE_TOOLTIPS)
);
/** Type of a block that has CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN */ /** Type of a block that has CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN */
type CustomContextMenuBlock = Block&CustomContextMenuMixin; type CustomContextMenuBlock = Block & CustomContextMenuMixin;
interface CustomContextMenuMixin extends CustomContextMenuMixinType {} interface CustomContextMenuMixin extends CustomContextMenuMixinType {}
type CustomContextMenuMixinType = type CustomContextMenuMixinType =
typeof CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN; typeof CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN;
/** /**
* Mixin to add a context menu item to create a 'variables_get' block. * Mixin to add a context menu item to create a 'variables_get' block.
@@ -249,9 +266,10 @@ const CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN = {
* *
* @param options List of menu options to add to. * @param options List of menu options to add to.
*/ */
customContextMenu: function( customContextMenu: function (
this: CustomContextMenuBlock, this: CustomContextMenuBlock,
options: Array<ContextMenuOption|LegacyContextMenuOption>) { options: Array<ContextMenuOption | LegacyContextMenuOption>
) {
if (this.isInFlyout) { if (this.isInFlyout) {
return; return;
} }
@@ -267,24 +285,26 @@ const CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN = {
options.push({ options.push({
enabled: true, enabled: true,
text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', varName), text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', varName),
callback: ContextMenu.callbackFactory(this, xmlBlock) callback: ContextMenu.callbackFactory(this, xmlBlock),
}); });
} }
}, },
}; };
Extensions.registerMixin( Extensions.registerMixin(
'contextMenu_newGetVariableBlock', 'contextMenu_newGetVariableBlock',
CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN); CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN
);
Extensions.register( Extensions.register(
'controls_for_tooltip', 'controls_for_tooltip',
Extensions.buildTooltipWithFieldText('%{BKY_CONTROLS_FOR_TOOLTIP}', 'VAR')); Extensions.buildTooltipWithFieldText('%{BKY_CONTROLS_FOR_TOOLTIP}', 'VAR')
);
Extensions.register( Extensions.register(
'controls_forEach_tooltip', 'controls_forEach_tooltip',
Extensions.buildTooltipWithFieldText( Extensions.buildTooltipWithFieldText('%{BKY_CONTROLS_FOREACH_TOOLTIP}', 'VAR')
'%{BKY_CONTROLS_FOREACH_TOOLTIP}', 'VAR')); );
/** /**
* List of block types that are loops and thus do not need warnings. * List of block types that are loops and thus do not need warnings.
@@ -310,7 +330,7 @@ export const loopTypes: Set<string> = new Set([
]); ]);
/** Type of a block that has CONTROL_FLOW_IN_LOOP_CHECK_MIXIN */ /** Type of a block that has CONTROL_FLOW_IN_LOOP_CHECK_MIXIN */
type ControlFlowInLoopBlock = Block&ControlFlowInLoopMixin; type ControlFlowInLoopBlock = Block & ControlFlowInLoopMixin;
interface ControlFlowInLoopMixin extends ControlFlowInLoopMixinType {} interface ControlFlowInLoopMixin extends ControlFlowInLoopMixinType {}
type ControlFlowInLoopMixinType = typeof CONTROL_FLOW_IN_LOOP_CHECK_MIXIN; type ControlFlowInLoopMixinType = typeof CONTROL_FLOW_IN_LOOP_CHECK_MIXIN;
@@ -324,23 +344,23 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
* *
* @returns The nearest surrounding loop, or null if none. * @returns The nearest surrounding loop, or null if none.
*/ */
getSurroundLoop: function(this: ControlFlowInLoopBlock): Block | getSurroundLoop: function (this: ControlFlowInLoopBlock): Block | null {
null { // eslint-disable-next-line @typescript-eslint/no-this-alias
let block: Block|null = this; let block: Block | null = this;
do { do {
if (loopTypes.has(block.type)) { if (loopTypes.has(block.type)) {
return block; return block;
} }
block = block.getSurroundParent(); block = block.getSurroundParent();
} while (block); } while (block);
return null; return null;
}, },
/** /**
* Called whenever anything on the workspace changes. * Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop. * Add warning if this flow block is not nested inside a loop.
*/ */
onchange: function(this: ControlFlowInLoopBlock, e: AbstractEvent) { onchange: function (this: ControlFlowInLoopBlock, e: AbstractEvent) {
const ws = this.workspace as WorkspaceSvg; const ws = this.workspace as WorkspaceSvg;
// Don't change state if: // Don't change state if:
// * It's at the start of a drag. // * It's at the start of a drag.
@@ -350,7 +370,8 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
} }
const enabled = !!this.getSurroundLoop(); const enabled = !!this.getSurroundLoop();
this.setWarningText( this.setWarningText(
enabled ? null : Msg['CONTROLS_FLOW_STATEMENTS_WARNING']); enabled ? null : Msg['CONTROLS_FLOW_STATEMENTS_WARNING']
);
if (!this.isInFlyout) { if (!this.isInFlyout) {
const group = Events.getGroup(); const group = Events.getGroup();
// Makes it so the move and the disable event get undone together. // Makes it so the move and the disable event get undone together.
@@ -362,7 +383,9 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
}; };
Extensions.registerMixin( Extensions.registerMixin(
'controls_flow_in_loop_check', CONTROL_FLOW_IN_LOOP_CHECK_MIXIN); 'controls_flow_in_loop_check',
CONTROL_FLOW_IN_LOOP_CHECK_MIXIN
);
// Register provided blocks. // Register provided blocks.
defineBlocks(blocks); defineBlocks(blocks);

View File

@@ -12,18 +12,18 @@ import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.libraryBlocks.math'); goog.declareModuleId('Blockly.libraryBlocks.math');
import * as Extensions from '../core/extensions.js'; import * as Extensions from '../core/extensions.js';
import type {Field} from '../core/field.js';
import type {FieldDropdown} from '../core/field_dropdown.js'; import type {FieldDropdown} from '../core/field_dropdown.js';
import * as xmlUtils from '../core/utils/xml.js'; import * as xmlUtils from '../core/utils/xml.js';
import type {Block} from '../core/block.js'; import type {Block} from '../core/block.js';
import type {BlockDefinition} from '../core/blocks.js'; import {
import {createBlockDefinitionsFromJsonArray, defineBlocks} from '../core/common.js'; createBlockDefinitionsFromJsonArray,
defineBlocks,
} from '../core/common.js';
import '../core/field_dropdown.js'; import '../core/field_dropdown.js';
import '../core/field_label.js'; import '../core/field_label.js';
import '../core/field_number.js'; import '../core/field_number.js';
import '../core/field_variable.js'; import '../core/field_variable.js';
/** /**
* A dictionary of the block definitions provided by this module. * A dictionary of the block definitions provided by this module.
*/ */
@@ -32,11 +32,13 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{ {
'type': 'math_number', 'type': 'math_number',
'message0': '%1', 'message0': '%1',
'args0': [{ 'args0': [
'type': 'field_number', {
'name': 'NUM', 'type': 'field_number',
'value': 0, 'name': 'NUM',
}], 'value': 0,
},
],
'output': 'Number', 'output': 'Number',
'helpUrl': '%{BKY_MATH_NUMBER_HELPURL}', 'helpUrl': '%{BKY_MATH_NUMBER_HELPURL}',
'style': 'math_blocks', 'style': 'math_blocks',
@@ -428,13 +430,13 @@ const TOOLTIPS_BY_OP = {
}; };
Extensions.register( Extensions.register(
'math_op_tooltip', 'math_op_tooltip',
Extensions.buildTooltipForDropdown('OP', TOOLTIPS_BY_OP)); Extensions.buildTooltipForDropdown('OP', TOOLTIPS_BY_OP)
);
/** Type of a block that has IS_DIVISBLEBY_MUTATOR_MIXIN */ /** Type of a block that has IS_DIVISBLEBY_MUTATOR_MIXIN */
type DivisiblebyBlock = Block&DivisiblebyMixin; type DivisiblebyBlock = Block & DivisiblebyMixin;
interface DivisiblebyMixin extends DivisiblebyMixinType {} interface DivisiblebyMixin extends DivisiblebyMixinType {}
;
type DivisiblebyMixinType = typeof IS_DIVISIBLEBY_MUTATOR_MIXIN; type DivisiblebyMixinType = typeof IS_DIVISIBLEBY_MUTATOR_MIXIN;
/** /**
@@ -452,9 +454,9 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
* *
* @returns XML storage element. * @returns XML storage element.
*/ */
mutationToDom: function(this: DivisiblebyBlock): Element { mutationToDom: function (this: DivisiblebyBlock): Element {
const container = xmlUtils.createElement('mutation'); const container = xmlUtils.createElement('mutation');
const divisorInput = (this.getFieldValue('PROPERTY') === 'DIVISIBLE_BY'); const divisorInput = this.getFieldValue('PROPERTY') === 'DIVISIBLE_BY';
container.setAttribute('divisor_input', String(divisorInput)); container.setAttribute('divisor_input', String(divisorInput));
return container; return container;
}, },
@@ -464,8 +466,8 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
* *
* @param xmlElement XML storage element. * @param xmlElement XML storage element.
*/ */
domToMutation: function(this: DivisiblebyBlock, xmlElement: Element) { domToMutation: function (this: DivisiblebyBlock, xmlElement: Element) {
const divisorInput = (xmlElement.getAttribute('divisor_input') === 'true'); const divisorInput = xmlElement.getAttribute('divisor_input') === 'true';
this.updateShape_(divisorInput); this.updateShape_(divisorInput);
}, },
@@ -479,7 +481,7 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
* *
* @param divisorInput True if this block has a divisor input. * @param divisorInput True if this block has a divisor input.
*/ */
updateShape_: function(this: DivisiblebyBlock, divisorInput: boolean) { updateShape_: function (this: DivisiblebyBlock, divisorInput: boolean) {
// Add or remove a Value Input. // Add or remove a Value Input.
const inputExists = this.getInput('DIVISOR'); const inputExists = this.getInput('DIVISOR');
if (divisorInput) { if (divisorInput) {
@@ -497,29 +499,32 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
* can update the block shape (add/remove divisor input) based on whether * can update the block shape (add/remove divisor input) based on whether
* property is "divisible by". * property is "divisible by".
*/ */
const IS_DIVISIBLE_MUTATOR_EXTENSION = function(this: DivisiblebyBlock) { const IS_DIVISIBLE_MUTATOR_EXTENSION = function (this: DivisiblebyBlock) {
this.getField('PROPERTY')!.setValidator( this.getField('PROPERTY')!.setValidator(
/** @param option The selected dropdown option. */ /** @param option The selected dropdown option. */
function(this: FieldDropdown, option: string) { function (this: FieldDropdown, option: string) {
const divisorInput = (option === 'DIVISIBLE_BY'); const divisorInput = option === 'DIVISIBLE_BY';
(this.getSourceBlock() as DivisiblebyBlock).updateShape_(divisorInput); (this.getSourceBlock() as DivisiblebyBlock).updateShape_(divisorInput);
return undefined; // FieldValidators can't be void. Use option as-is. return undefined; // FieldValidators can't be void. Use option as-is.
}); }
);
}; };
Extensions.registerMutator( Extensions.registerMutator(
'math_is_divisibleby_mutator', IS_DIVISIBLEBY_MUTATOR_MIXIN, 'math_is_divisibleby_mutator',
IS_DIVISIBLE_MUTATOR_EXTENSION); IS_DIVISIBLEBY_MUTATOR_MIXIN,
IS_DIVISIBLE_MUTATOR_EXTENSION
);
// Update the tooltip of 'math_change' block to reference the variable. // Update the tooltip of 'math_change' block to reference the variable.
Extensions.register( Extensions.register(
'math_change_tooltip', 'math_change_tooltip',
Extensions.buildTooltipWithFieldText('%{BKY_MATH_CHANGE_TOOLTIP}', 'VAR')); Extensions.buildTooltipWithFieldText('%{BKY_MATH_CHANGE_TOOLTIP}', 'VAR')
);
/** Type of a block that has LIST_MODES_MUTATOR_MIXIN */ /** Type of a block that has LIST_MODES_MUTATOR_MIXIN */
type ListModesBlock = Block&ListModesMixin; type ListModesBlock = Block & ListModesMixin;
interface ListModesMixin extends ListModesMixinType {} interface ListModesMixin extends ListModesMixinType {}
;
type ListModesMixinType = typeof LIST_MODES_MUTATOR_MIXIN; type ListModesMixinType = typeof LIST_MODES_MUTATOR_MIXIN;
/** /**
@@ -532,7 +537,7 @@ const LIST_MODES_MUTATOR_MIXIN = {
* *
* @param newOp Either 'MODE' or some op than returns a number. * @param newOp Either 'MODE' or some op than returns a number.
*/ */
updateType_: function(this: ListModesBlock, newOp: string) { updateType_: function (this: ListModesBlock, newOp: string) {
if (newOp === 'MODE') { if (newOp === 'MODE') {
this.outputConnection!.setCheck('Array'); this.outputConnection!.setCheck('Array');
} else { } else {
@@ -545,7 +550,7 @@ const LIST_MODES_MUTATOR_MIXIN = {
* *
* @returns XML storage element. * @returns XML storage element.
*/ */
mutationToDom: function(this: ListModesBlock): Element { mutationToDom: function (this: ListModesBlock): Element {
const container = xmlUtils.createElement('mutation'); const container = xmlUtils.createElement('mutation');
container.setAttribute('op', this.getFieldValue('OP')); container.setAttribute('op', this.getFieldValue('OP'));
return container; return container;
@@ -556,7 +561,7 @@ const LIST_MODES_MUTATOR_MIXIN = {
* *
* @param xmlElement XML storage element. * @param xmlElement XML storage element.
*/ */
domToMutation: function(this: ListModesBlock, xmlElement: Element) { domToMutation: function (this: ListModesBlock, xmlElement: Element) {
const op = xmlElement.getAttribute('op'); const op = xmlElement.getAttribute('op');
if (op === null) throw new TypeError('xmlElement had no op attribute'); if (op === null) throw new TypeError('xmlElement had no op attribute');
this.updateType_(op); this.updateType_(op);
@@ -572,17 +577,20 @@ const LIST_MODES_MUTATOR_MIXIN = {
* Extension to 'math_on_list' blocks that allows support of * Extension to 'math_on_list' blocks that allows support of
* modes operation (outputs a list of numbers). * modes operation (outputs a list of numbers).
*/ */
const LIST_MODES_MUTATOR_EXTENSION = function(this: ListModesBlock) { const LIST_MODES_MUTATOR_EXTENSION = function (this: ListModesBlock) {
this.getField( this.getField('OP')!.setValidator(
'OP')!.setValidator(function(this: ListModesBlock, newOp: string) { function (this: ListModesBlock, newOp: string) {
this.updateType_(newOp); this.updateType_(newOp);
return undefined; return undefined;
}.bind(this)); }.bind(this)
);
}; };
Extensions.registerMutator( Extensions.registerMutator(
'math_modes_of_list_mutator', LIST_MODES_MUTATOR_MIXIN, 'math_modes_of_list_mutator',
LIST_MODES_MUTATOR_EXTENSION); LIST_MODES_MUTATOR_MIXIN,
LIST_MODES_MUTATOR_EXTENSION
);
// Register provided blocks. // Register provided blocks.
defineBlocks(blocks); defineBlocks(blocks);

View File

@@ -18,18 +18,19 @@ import {Align} from '../core/inputs/input.js';
import type {Block} from '../core/block.js'; import type {Block} from '../core/block.js';
import type {BlockSvg} from '../core/block_svg.js'; import type {BlockSvg} from '../core/block_svg.js';
import {Connection} from '../core/connection.js'; import {Connection} from '../core/connection.js';
import {ConnectionType} from '../core/connection_type.js';
import {FieldImage} from '../core/field_image.js'; import {FieldImage} from '../core/field_image.js';
import {FieldDropdown} from '../core/field_dropdown.js'; import {FieldDropdown} from '../core/field_dropdown.js';
import {FieldTextInput} from '../core/field_textinput.js'; import {FieldTextInput} from '../core/field_textinput.js';
import {Msg} from '../core/msg.js'; import {Msg} from '../core/msg.js';
import {Mutator} from '../core/mutator.js'; import {Mutator} from '../core/mutator.js';
import type {Workspace} from '../core/workspace.js'; import type {Workspace} from '../core/workspace.js';
import {createBlockDefinitionsFromJsonArray, defineBlocks} from '../core/common.js'; import {
createBlockDefinitionsFromJsonArray,
defineBlocks,
} from '../core/common.js';
import '../core/field_multilineinput.js'; import '../core/field_multilineinput.js';
import '../core/field_variable.js'; import '../core/field_variable.js';
import { ValueInput } from '../core/inputs/value_input.js'; import {ValueInput} from '../core/inputs/value_input.js';
/** /**
* A dictionary of the block definitions provided by this module. * A dictionary of the block definitions provided by this module.
@@ -39,19 +40,18 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{ {
'type': 'text', 'type': 'text',
'message0': '%1', 'message0': '%1',
'args0': [{ 'args0': [
'type': 'field_input', {
'name': 'TEXT', 'type': 'field_input',
'text': '', 'name': 'TEXT',
}], 'text': '',
},
],
'output': 'String', 'output': 'String',
'style': 'text_blocks', 'style': 'text_blocks',
'helpUrl': '%{BKY_TEXT_TEXT_HELPURL}', 'helpUrl': '%{BKY_TEXT_TEXT_HELPURL}',
'tooltip': '%{BKY_TEXT_TEXT_TOOLTIP}', 'tooltip': '%{BKY_TEXT_TEXT_TOOLTIP}',
'extensions': [ 'extensions': ['text_quotes', 'parent_tooltip_when_inline'],
'text_quotes',
'parent_tooltip_when_inline',
],
}, },
{ {
'type': 'text_multiline', 'type': 'text_multiline',
@@ -60,15 +60,15 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{ {
'type': 'field_image', 'type': 'field_image',
'src': 'src':
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAARCAYAAADpP' + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAARCAYAAADpP' +
'U2iAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAdhgAAHYYBXaITgQAAABh0RVh0' + 'U2iAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAdhgAAHYYBXaITgQAAABh0RVh0' +
'U29mdHdhcmUAcGFpbnQubmV0IDQuMS42/U4J6AAAAP1JREFUOE+Vks0KQUEYhjm' + 'U29mdHdhcmUAcGFpbnQubmV0IDQuMS42/U4J6AAAAP1JREFUOE+Vks0KQUEYhjm' +
'RIja4ABtZ2dm5A3t3Ia6AUm7CylYuQRaUhZSlLZJiQbFAyRnPN33y01HOW08z88' + 'RIja4ABtZ2dm5A3t3Ia6AUm7CylYuQRaUhZSlLZJiQbFAyRnPN33y01HOW08z88' +
'73zpwzM4F3GWOCruvGIE4/rLaV+Nq1hVGMBqzhqlxgCys4wJA65xnogMHsQ5luj' + '73zpwzM4F3GWOCruvGIE4/rLaV+Nq1hVGMBqzhqlxgCys4wJA65xnogMHsQ5luj' +
'nYHTejBBCK2mE4abjCgMGhNxHgDFWjDSG07kdfVa2pZMf4ZyMAdWmpZMfYOsLiD' + 'nYHTejBBCK2mE4abjCgMGhNxHgDFWjDSG07kdfVa2pZMf4ZyMAdWmpZMfYOsLiD' +
'MYMjlMB+K613QISRhTnITnsYg5yUd0DETmEoMlkFOeIT/A58iyK5E18BuTBfgYX' + 'MYMjlMB+K613QISRhTnITnsYg5yUd0DETmEoMlkFOeIT/A58iyK5E18BuTBfgYX' +
'fwNJv4P9/oEBerLylOnRhygmGdPpTTBZAPkde61lbQe4moWUvYUZYLfUNftIY4z' + 'fwNJv4P9/oEBerLylOnRhygmGdPpTTBZAPkde61lbQe4moWUvYUZYLfUNftIY4z' +
'wA5X2Z9AYnQrEAAAAASUVORK5CYII=', 'wA5X2Z9AYnQrEAAAAASUVORK5CYII=',
'width': 12, 'width': 12,
'height': 17, 'height': 17,
'alt': '\u00B6', 'alt': '\u00B6',
@@ -83,9 +83,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([
'style': 'text_blocks', 'style': 'text_blocks',
'helpUrl': '%{BKY_TEXT_TEXT_HELPURL}', 'helpUrl': '%{BKY_TEXT_TEXT_HELPURL}',
'tooltip': '%{BKY_TEXT_TEXT_TOOLTIP}', 'tooltip': '%{BKY_TEXT_TEXT_TOOLTIP}',
'extensions': [ 'extensions': ['parent_tooltip_when_inline'],
'parent_tooltip_when_inline',
],
}, },
{ {
'type': 'text_join', 'type': 'text_join',
@@ -95,7 +93,6 @@ export const blocks = createBlockDefinitionsFromJsonArray([
'helpUrl': '%{BKY_TEXT_JOIN_HELPURL}', 'helpUrl': '%{BKY_TEXT_JOIN_HELPURL}',
'tooltip': '%{BKY_TEXT_JOIN_TOOLTIP}', 'tooltip': '%{BKY_TEXT_JOIN_TOOLTIP}',
'mutator': 'text_join_mutator', 'mutator': 'text_join_mutator',
}, },
{ {
'type': 'text_create_join_container', 'type': 'text_create_join_container',
@@ -139,9 +136,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([
'previousStatement': null, 'previousStatement': null,
'nextStatement': null, 'nextStatement': null,
'style': 'text_blocks', 'style': 'text_blocks',
'extensions': [ 'extensions': ['text_append_tooltip'],
'text_append_tooltip',
],
}, },
{ {
'type': 'text_length', 'type': 'text_length',
@@ -186,14 +181,8 @@ export const blocks = createBlockDefinitionsFromJsonArray([
'type': 'field_dropdown', 'type': 'field_dropdown',
'name': 'END', 'name': 'END',
'options': [ 'options': [
[ ['%{BKY_TEXT_INDEXOF_OPERATOR_FIRST}', 'FIRST'],
'%{BKY_TEXT_INDEXOF_OPERATOR_FIRST}', ['%{BKY_TEXT_INDEXOF_OPERATOR_LAST}', 'LAST'],
'FIRST',
],
[
'%{BKY_TEXT_INDEXOF_OPERATOR_LAST}',
'LAST',
],
], ],
}, },
{ {
@@ -206,13 +195,11 @@ export const blocks = createBlockDefinitionsFromJsonArray([
'style': 'text_blocks', 'style': 'text_blocks',
'helpUrl': '%{BKY_TEXT_INDEXOF_HELPURL}', 'helpUrl': '%{BKY_TEXT_INDEXOF_HELPURL}',
'inputsInline': true, 'inputsInline': true,
'extensions': [ 'extensions': ['text_indexOf_tooltip'],
'text_indexOf_tooltip',
],
}, },
{ {
'type': 'text_charAt', 'type': 'text_charAt',
'message0': '%{BKY_TEXT_CHARAT_TITLE}', // "in text %1 %2" 'message0': '%{BKY_TEXT_CHARAT_TITLE}', // "in text %1 %2"
'args0': [ 'args0': [
{ {
'type': 'input_value', 'type': 'input_value',
@@ -240,7 +227,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([
]); ]);
/** Type of a 'text_get_substring' block. */ /** Type of a 'text_get_substring' block. */
type GetSubstringBlock = Block&GetSubstringMixin; type GetSubstringBlock = Block & GetSubstringMixin;
interface GetSubstringMixin extends GetSubstringType { interface GetSubstringMixin extends GetSubstringType {
WHERE_OPTIONS_1: Array<[string, string]>; WHERE_OPTIONS_1: Array<[string, string]>;
WHERE_OPTIONS_2: Array<[string, string]>; WHERE_OPTIONS_2: Array<[string, string]>;
@@ -251,7 +238,7 @@ const GET_SUBSTRING_BLOCK = {
/** /**
* Block for getting substring. * Block for getting substring.
*/ */
init: function(this: GetSubstringBlock) { init: function (this: GetSubstringBlock) {
this['WHERE_OPTIONS_1'] = [ this['WHERE_OPTIONS_1'] = [
[Msg['TEXT_GET_SUBSTRING_START_FROM_START'], 'FROM_START'], [Msg['TEXT_GET_SUBSTRING_START_FROM_START'], 'FROM_START'],
[Msg['TEXT_GET_SUBSTRING_START_FROM_END'], 'FROM_END'], [Msg['TEXT_GET_SUBSTRING_START_FROM_END'], 'FROM_END'],
@@ -264,8 +251,9 @@ const GET_SUBSTRING_BLOCK = {
]; ];
this.setHelpUrl(Msg['TEXT_GET_SUBSTRING_HELPURL']); this.setHelpUrl(Msg['TEXT_GET_SUBSTRING_HELPURL']);
this.setStyle('text_blocks'); this.setStyle('text_blocks');
this.appendValueInput('STRING').setCheck('String').appendField( this.appendValueInput('STRING')
Msg['TEXT_GET_SUBSTRING_INPUT_IN_TEXT']); .setCheck('String')
.appendField(Msg['TEXT_GET_SUBSTRING_INPUT_IN_TEXT']);
this.appendDummyInput('AT1'); this.appendDummyInput('AT1');
this.appendDummyInput('AT2'); this.appendDummyInput('AT2');
if (Msg['TEXT_GET_SUBSTRING_TAIL']) { if (Msg['TEXT_GET_SUBSTRING_TAIL']) {
@@ -283,7 +271,7 @@ const GET_SUBSTRING_BLOCK = {
* *
* @returns XML storage element. * @returns XML storage element.
*/ */
mutationToDom: function(this: GetSubstringBlock): Element { mutationToDom: function (this: GetSubstringBlock): Element {
const container = xmlUtils.createElement('mutation'); const container = xmlUtils.createElement('mutation');
const isAt1 = this.getInput('AT1') instanceof ValueInput; const isAt1 = this.getInput('AT1') instanceof ValueInput;
container.setAttribute('at1', `${isAt1}`); container.setAttribute('at1', `${isAt1}`);
@@ -297,9 +285,9 @@ const GET_SUBSTRING_BLOCK = {
* *
* @param xmlElement XML storage element. * @param xmlElement XML storage element.
*/ */
domToMutation: function(this: GetSubstringBlock, xmlElement: Element) { domToMutation: function (this: GetSubstringBlock, xmlElement: Element) {
const isAt1 = (xmlElement.getAttribute('at1') === 'true'); const isAt1 = xmlElement.getAttribute('at1') === 'true';
const isAt2 = (xmlElement.getAttribute('at2') === 'true'); const isAt2 = xmlElement.getAttribute('at2') === 'true';
this.updateAt_(1, isAt1); this.updateAt_(1, isAt1);
this.updateAt_(2, isAt2); this.updateAt_(2, isAt2);
}, },
@@ -316,7 +304,7 @@ const GET_SUBSTRING_BLOCK = {
* @param n Which input to modify (either 1 or 2). * @param n Which input to modify (either 1 or 2).
* @param isAt True if the input includes a value connection, false otherwise. * @param isAt True if the input includes a value connection, false otherwise.
*/ */
updateAt_: function(this: GetSubstringBlock, n: 1|2, isAt: boolean) { updateAt_: function (this: GetSubstringBlock, n: 1 | 2, isAt: boolean) {
// Create or delete an input for the numeric index. // Create or delete an input for the numeric index.
// Destroy old 'AT' and 'ORDINAL' inputs. // Destroy old 'AT' and 'ORDINAL' inputs.
this.removeInput('AT' + n); this.removeInput('AT' + n);
@@ -325,8 +313,9 @@ const GET_SUBSTRING_BLOCK = {
if (isAt) { if (isAt) {
this.appendValueInput('AT' + n).setCheck('Number'); this.appendValueInput('AT' + n).setCheck('Number');
if (Msg['ORDINAL_NUMBER_SUFFIX']) { if (Msg['ORDINAL_NUMBER_SUFFIX']) {
this.appendDummyInput('ORDINAL' + n) this.appendDummyInput('ORDINAL' + n).appendField(
.appendField(Msg['ORDINAL_NUMBER_SUFFIX']); Msg['ORDINAL_NUMBER_SUFFIX']
);
} }
} else { } else {
this.appendDummyInput('AT' + n); this.appendDummyInput('AT' + n);
@@ -339,27 +328,28 @@ const GET_SUBSTRING_BLOCK = {
const menu = fieldRegistry.fromJson({ const menu = fieldRegistry.fromJson({
type: 'field_dropdown', type: 'field_dropdown',
options: options:
this[('WHERE_OPTIONS_' + n) as 'WHERE_OPTIONS_1' | 'WHERE_OPTIONS_2'], this[('WHERE_OPTIONS_' + n) as 'WHERE_OPTIONS_1' | 'WHERE_OPTIONS_2'],
}) as FieldDropdown; }) as FieldDropdown;
menu.setValidator( menu.setValidator(
/** /**
* @param value The input value. * @param value The input value.
* @return Null if the field has been replaced; otherwise undefined. * @return Null if the field has been replaced; otherwise undefined.
*/ */
function(this: FieldDropdown, value: any): null|undefined { function (this: FieldDropdown, value: any): null | undefined {
const newAt = (value === 'FROM_START') || (value === 'FROM_END'); const newAt = value === 'FROM_START' || value === 'FROM_END';
// The 'isAt' variable is available due to this function being a // The 'isAt' variable is available due to this function being a
// closure. // closure.
if (newAt !== isAt) { if (newAt !== isAt) {
const block = this.getSourceBlock() as GetSubstringBlock; const block = this.getSourceBlock() as GetSubstringBlock;
block.updateAt_(n, newAt); block.updateAt_(n, newAt);
// This menu has been destroyed and replaced. // This menu has been destroyed and replaced.
// Update the replacement. // Update the replacement.
block.setFieldValue(value, 'WHERE' + n); block.setFieldValue(value, 'WHERE' + n);
return null; return null;
} }
return undefined; return undefined;
}); }
);
this.getInput('AT' + n)!.appendField(menu, 'WHERE' + n); this.getInput('AT' + n)!.appendField(menu, 'WHERE' + n);
if (n === 1) { if (n === 1) {
@@ -377,7 +367,7 @@ blocks['text_changeCase'] = {
/** /**
* Block for changing capitalization. * Block for changing capitalization.
*/ */
init: function(this: Block) { init: function (this: Block) {
const OPERATORS = [ const OPERATORS = [
[Msg['TEXT_CHANGECASE_OPERATOR_UPPERCASE'], 'UPPERCASE'], [Msg['TEXT_CHANGECASE_OPERATOR_UPPERCASE'], 'UPPERCASE'],
[Msg['TEXT_CHANGECASE_OPERATOR_LOWERCASE'], 'LOWERCASE'], [Msg['TEXT_CHANGECASE_OPERATOR_LOWERCASE'], 'LOWERCASE'],
@@ -385,12 +375,15 @@ blocks['text_changeCase'] = {
]; ];
this.setHelpUrl(Msg['TEXT_CHANGECASE_HELPURL']); this.setHelpUrl(Msg['TEXT_CHANGECASE_HELPURL']);
this.setStyle('text_blocks'); this.setStyle('text_blocks');
this.appendValueInput('TEXT').setCheck('String').appendField( this.appendValueInput('TEXT')
.setCheck('String')
.appendField(
fieldRegistry.fromJson({ fieldRegistry.fromJson({
type: 'field_dropdown', type: 'field_dropdown',
options: OPERATORS, options: OPERATORS,
}) as FieldDropdown, }) as FieldDropdown,
'CASE'); 'CASE'
);
this.setOutput(true, 'String'); this.setOutput(true, 'String');
this.setTooltip(Msg['TEXT_CHANGECASE_TOOLTIP']); this.setTooltip(Msg['TEXT_CHANGECASE_TOOLTIP']);
}, },
@@ -400,7 +393,7 @@ blocks['text_trim'] = {
/** /**
* Block for trimming spaces. * Block for trimming spaces.
*/ */
init: function(this: Block) { init: function (this: Block) {
const OPERATORS = [ const OPERATORS = [
[Msg['TEXT_TRIM_OPERATOR_BOTH'], 'BOTH'], [Msg['TEXT_TRIM_OPERATOR_BOTH'], 'BOTH'],
[Msg['TEXT_TRIM_OPERATOR_LEFT'], 'LEFT'], [Msg['TEXT_TRIM_OPERATOR_LEFT'], 'LEFT'],
@@ -408,12 +401,15 @@ blocks['text_trim'] = {
]; ];
this.setHelpUrl(Msg['TEXT_TRIM_HELPURL']); this.setHelpUrl(Msg['TEXT_TRIM_HELPURL']);
this.setStyle('text_blocks'); this.setStyle('text_blocks');
this.appendValueInput('TEXT').setCheck('String').appendField( this.appendValueInput('TEXT')
.setCheck('String')
.appendField(
fieldRegistry.fromJson({ fieldRegistry.fromJson({
type: 'field_dropdown', type: 'field_dropdown',
options: OPERATORS, options: OPERATORS,
}) as FieldDropdown, }) as FieldDropdown,
'MODE'); 'MODE'
);
this.setOutput(true, 'String'); this.setOutput(true, 'String');
this.setTooltip(Msg['TEXT_TRIM_TOOLTIP']); this.setTooltip(Msg['TEXT_TRIM_TOOLTIP']);
}, },
@@ -423,7 +419,7 @@ blocks['text_print'] = {
/** /**
* Block for print statement. * Block for print statement.
*/ */
init: function(this: Block) { init: function (this: Block) {
this.jsonInit({ this.jsonInit({
'message0': Msg['TEXT_PRINT_TITLE'], 'message0': Msg['TEXT_PRINT_TITLE'],
'args0': [ 'args0': [
@@ -441,7 +437,7 @@ blocks['text_print'] = {
}, },
}; };
type PromptCommonBlock = Block&PromptCommonMixin; type PromptCommonBlock = Block & PromptCommonMixin;
interface PromptCommonMixin extends PromptCommonType {} interface PromptCommonMixin extends PromptCommonType {}
type PromptCommonType = typeof PROMPT_COMMON; type PromptCommonType = typeof PROMPT_COMMON;
@@ -455,7 +451,7 @@ const PROMPT_COMMON = {
* *
* @param newOp The new output type. Should be either 'TEXT' or 'NUMBER'. * @param newOp The new output type. Should be either 'TEXT' or 'NUMBER'.
*/ */
updateType_: function(this: PromptCommonBlock, newOp: string) { updateType_: function (this: PromptCommonBlock, newOp: string) {
this.outputConnection!.setCheck(newOp === 'NUMBER' ? 'Number' : 'String'); this.outputConnection!.setCheck(newOp === 'NUMBER' ? 'Number' : 'String');
}, },
/** /**
@@ -464,7 +460,7 @@ const PROMPT_COMMON = {
* *
* @returns XML storage element. * @returns XML storage element.
*/ */
mutationToDom: function(this: PromptCommonBlock): Element { mutationToDom: function (this: PromptCommonBlock): Element {
const container = xmlUtils.createElement('mutation'); const container = xmlUtils.createElement('mutation');
container.setAttribute('type', this.getFieldValue('TYPE')); container.setAttribute('type', this.getFieldValue('TYPE'));
return container; return container;
@@ -475,7 +471,7 @@ const PROMPT_COMMON = {
* *
* @param xmlElement XML storage element. * @param xmlElement XML storage element.
*/ */
domToMutation: function(this: PromptCommonBlock, xmlElement: Element) { domToMutation: function (this: PromptCommonBlock, xmlElement: Element) {
this.updateType_(xmlElement.getAttribute('type')!); this.updateType_(xmlElement.getAttribute('type')!);
}, },
}; };
@@ -485,29 +481,27 @@ blocks['text_prompt_ext'] = {
/** /**
* Block for prompt function (external message). * Block for prompt function (external message).
*/ */
init: function(this: PromptCommonBlock) { init: function (this: PromptCommonBlock) {
const TYPES = [ const TYPES = [
[Msg['TEXT_PROMPT_TYPE_TEXT'], 'TEXT'], [Msg['TEXT_PROMPT_TYPE_TEXT'], 'TEXT'],
[Msg['TEXT_PROMPT_TYPE_NUMBER'], 'NUMBER'], [Msg['TEXT_PROMPT_TYPE_NUMBER'], 'NUMBER'],
]; ];
this.setHelpUrl(Msg['TEXT_PROMPT_HELPURL']); this.setHelpUrl(Msg['TEXT_PROMPT_HELPURL']);
this.setStyle('text_blocks'); this.setStyle('text_blocks');
// Assign 'this' to a variable for use in the closures below.
const thisBlock = this;
const dropdown = fieldRegistry.fromJson({ const dropdown = fieldRegistry.fromJson({
type: 'field_dropdown', type: 'field_dropdown',
options: TYPES, options: TYPES,
}) as FieldDropdown; }) as FieldDropdown;
dropdown.setValidator(function(this: FieldDropdown, newOp: string) { dropdown.setValidator((newOp: string) => {
thisBlock.updateType_(newOp); this.updateType_(newOp);
return undefined; // FieldValidators can't be void. Use option as-is. return undefined; // FieldValidators can't be void. Use option as-is.
}); });
this.appendValueInput('TEXT').appendField(dropdown, 'TYPE'); this.appendValueInput('TEXT').appendField(dropdown, 'TYPE');
this.setOutput(true, 'String'); this.setOutput(true, 'String');
this.setTooltip(function() { this.setTooltip(() => {
return (thisBlock.getFieldValue('TYPE') === 'TEXT') ? return this.getFieldValue('TYPE') === 'TEXT'
Msg['TEXT_PROMPT_TOOLTIP_TEXT'] : ? Msg['TEXT_PROMPT_TOOLTIP_TEXT']
Msg['TEXT_PROMPT_TOOLTIP_NUMBER']; : Msg['TEXT_PROMPT_TOOLTIP_NUMBER'];
}); });
}, },
@@ -517,7 +511,7 @@ blocks['text_prompt_ext'] = {
// XML hooks are kept for backwards compatibility. // XML hooks are kept for backwards compatibility.
}; };
type PromptBlock = Block&PromptCommonMixin&QuoteImageMixin; type PromptBlock = Block & PromptCommonMixin & QuoteImageMixin;
const TEXT_PROMPT_BLOCK = { const TEXT_PROMPT_BLOCK = {
...PROMPT_COMMON, ...PROMPT_COMMON,
@@ -525,40 +519,39 @@ const TEXT_PROMPT_BLOCK = {
* Block for prompt function (internal message). * Block for prompt function (internal message).
* The 'text_prompt_ext' block is preferred as it is more flexible. * The 'text_prompt_ext' block is preferred as it is more flexible.
*/ */
init: function(this: PromptBlock) { init: function (this: PromptBlock) {
this.mixin(QUOTE_IMAGE_MIXIN); this.mixin(QUOTE_IMAGE_MIXIN);
const TYPES = [ const TYPES = [
[Msg['TEXT_PROMPT_TYPE_TEXT'], 'TEXT'], [Msg['TEXT_PROMPT_TYPE_TEXT'], 'TEXT'],
[Msg['TEXT_PROMPT_TYPE_NUMBER'], 'NUMBER'], [Msg['TEXT_PROMPT_TYPE_NUMBER'], 'NUMBER'],
]; ];
// Assign 'this' to a variable for use in the closures below.
const thisBlock = this;
this.setHelpUrl(Msg['TEXT_PROMPT_HELPURL']); this.setHelpUrl(Msg['TEXT_PROMPT_HELPURL']);
this.setStyle('text_blocks'); this.setStyle('text_blocks');
const dropdown = fieldRegistry.fromJson({ const dropdown = fieldRegistry.fromJson({
type: 'field_dropdown', type: 'field_dropdown',
options: TYPES, options: TYPES,
}) as FieldDropdown; }) as FieldDropdown;
dropdown.setValidator(function(this: FieldDropdown, newOp: string) { dropdown.setValidator((newOp: string) => {
thisBlock.updateType_(newOp); this.updateType_(newOp);
return undefined; // FieldValidators can't be void. Use option as-is. return undefined; // FieldValidators can't be void. Use option as-is.
}); });
this.appendDummyInput() this.appendDummyInput()
.appendField(dropdown, 'TYPE') .appendField(dropdown, 'TYPE')
.appendField(this.newQuote_(true)) .appendField(this.newQuote_(true))
.appendField( .appendField(
fieldRegistry.fromJson({ fieldRegistry.fromJson({
type: 'field_input', type: 'field_input',
text: '', text: '',
}) as FieldTextInput, }) as FieldTextInput,
'TEXT') 'TEXT'
.appendField(this.newQuote_(false)); )
.appendField(this.newQuote_(false));
this.setOutput(true, 'String'); this.setOutput(true, 'String');
this.setTooltip(function() { this.setTooltip(() => {
return (thisBlock.getFieldValue('TYPE') === 'TEXT') ? return this.getFieldValue('TYPE') === 'TEXT'
Msg['TEXT_PROMPT_TOOLTIP_TEXT'] : ? Msg['TEXT_PROMPT_TOOLTIP_TEXT']
Msg['TEXT_PROMPT_TOOLTIP_NUMBER']; : Msg['TEXT_PROMPT_TOOLTIP_NUMBER'];
}); });
}, },
}; };
@@ -569,7 +562,7 @@ blocks['text_count'] = {
/** /**
* Block for counting how many times one string appears within another string. * Block for counting how many times one string appears within another string.
*/ */
init: function(this: Block) { init: function (this: Block) {
this.jsonInit({ this.jsonInit({
'message0': Msg['TEXT_COUNT_MESSAGE0'], 'message0': Msg['TEXT_COUNT_MESSAGE0'],
'args0': [ 'args0': [
@@ -597,7 +590,7 @@ blocks['text_replace'] = {
/** /**
* Block for replacing one string with another in the text. * Block for replacing one string with another in the text.
*/ */
init: function(this: Block) { init: function (this: Block) {
this.jsonInit({ this.jsonInit({
'message0': Msg['TEXT_REPLACE_MESSAGE0'], 'message0': Msg['TEXT_REPLACE_MESSAGE0'],
'args0': [ 'args0': [
@@ -630,7 +623,7 @@ blocks['text_reverse'] = {
/** /**
* Block for reversing a string. * Block for reversing a string.
*/ */
init: function(this: Block) { init: function (this: Block) {
this.jsonInit({ this.jsonInit({
'message0': Msg['TEXT_REVERSE_MESSAGE0'], 'message0': Msg['TEXT_REVERSE_MESSAGE0'],
'args0': [ 'args0': [
@@ -650,7 +643,7 @@ blocks['text_reverse'] = {
}; };
/** Type of a block that has QUOTE_IMAGE_MIXIN */ /** Type of a block that has QUOTE_IMAGE_MIXIN */
type QuoteImageBlock = Block&QuoteImageMixin; type QuoteImageBlock = Block & QuoteImageMixin;
interface QuoteImageMixin extends QuoteImageMixinType {} interface QuoteImageMixin extends QuoteImageMixinType {}
type QuoteImageMixinType = typeof QUOTE_IMAGE_MIXIN; type QuoteImageMixinType = typeof QUOTE_IMAGE_MIXIN;
@@ -660,21 +653,21 @@ const QUOTE_IMAGE_MIXIN = {
* quote). * quote).
*/ */
QUOTE_IMAGE_LEFT_DATAURI: QUOTE_IMAGE_LEFT_DATAURI:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAA' + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAA' +
'n0lEQVQI1z3OMa5BURSF4f/cQhAKjUQhuQmFNwGJEUi0RKN5rU7FHKhpjEH3TEMtkdBSCY' + 'n0lEQVQI1z3OMa5BURSF4f/cQhAKjUQhuQmFNwGJEUi0RKN5rU7FHKhpjEH3TEMtkdBSCY' +
'1EIv8r7nFX9e29V7EBAOvu7RPjwmWGH/VuF8CyN9/OAdvqIXYLvtRaNjx9mMTDyo+NjAN1' + '1EIv8r7nFX9e29V7EBAOvu7RPjwmWGH/VuF8CyN9/OAdvqIXYLvtRaNjx9mMTDyo+NjAN1' +
'HNcl9ZQ5oQMM3dgDUqDo1l8DzvwmtZN7mnD+PkmLa+4mhrxVA9fRowBWmVBhFy5gYEjKMf' + 'HNcl9ZQ5oQMM3dgDUqDo1l8DzvwmtZN7mnD+PkmLa+4mhrxVA9fRowBWmVBhFy5gYEjKMf' +
'z9AylsaRRgGzvZAAAAAElFTkSuQmCC', 'z9AylsaRRgGzvZAAAAAElFTkSuQmCC',
/** /**
* Image data URI of an LTR closing double quote (same as RTL opening double * Image data URI of an LTR closing double quote (same as RTL opening double
* quote). * quote).
*/ */
QUOTE_IMAGE_RIGHT_DATAURI: QUOTE_IMAGE_RIGHT_DATAURI:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAA' + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAA' +
'qUlEQVQI1z3KvUpCcRiA8ef9E4JNHhI0aFEacm1o0BsI0Slx8wa8gLauoDnoBhq7DcfWhg' + 'qUlEQVQI1z3KvUpCcRiA8ef9E4JNHhI0aFEacm1o0BsI0Slx8wa8gLauoDnoBhq7DcfWhg' +
'gONDmJJgqCPA7neJ7p934EOOKOnM8Q7PDElo/4x4lFb2DmuUjcUzS3URnGib9qaPNbuXvB' + 'gONDmJJgqCPA7neJ7p934EOOKOnM8Q7PDElo/4x4lFb2DmuUjcUzS3URnGib9qaPNbuXvB' +
'O3sGPHJDRG6fGVdMSeWDP2q99FQdFrz26Gu5Tq7dFMzUvbXy8KXeAj57cOklgA+u1B5Aos' + 'O3sGPHJDRG6fGVdMSeWDP2q99FQdFrz26Gu5Tq7dFMzUvbXy8KXeAj57cOklgA+u1B5Aos' +
'lLtGIHQMaCVnwDnADZIFIrXsoXrgAAAABJRU5ErkJggg==', 'lLtGIHQMaCVnwDnADZIFIrXsoXrgAAAABJRU5ErkJggg==',
/** /**
* Pixel width of QUOTE_IMAGE_LEFT_DATAURI and QUOTE_IMAGE_RIGHT_DATAURI. * Pixel width of QUOTE_IMAGE_LEFT_DATAURI and QUOTE_IMAGE_RIGHT_DATAURI.
*/ */
@@ -689,7 +682,7 @@ const QUOTE_IMAGE_MIXIN = {
* *
* @param fieldName The name of the field to wrap with quotes. * @param fieldName The name of the field to wrap with quotes.
*/ */
quoteField_: function(this: QuoteImageBlock, fieldName: string) { quoteField_: function (this: QuoteImageBlock, fieldName: string) {
for (let i = 0, input; (input = this.inputList[i]); i++) { for (let i = 0, input; (input = this.inputList[i]); i++) {
for (let j = 0, field; (field = input.fieldRow[j]); j++) { for (let j = 0, field; (field = input.fieldRow[j]); j++) {
if (fieldName === field.name) { if (fieldName === field.name) {
@@ -700,7 +693,8 @@ const QUOTE_IMAGE_MIXIN = {
} }
} }
console.warn( console.warn(
'field named "' + fieldName + '" not found in ' + this.toDevString()); 'field named "' + fieldName + '" not found in ' + this.toDevString()
);
}, },
/** /**
@@ -711,10 +705,11 @@ const QUOTE_IMAGE_MIXIN = {
* Otherwise, a closing quote is used (” in LTR). * Otherwise, a closing quote is used (” in LTR).
* @returns The new field. * @returns The new field.
*/ */
newQuote_: function(this: QuoteImageBlock, open: boolean): FieldImage { newQuote_: function (this: QuoteImageBlock, open: boolean): FieldImage {
const isLeft = this.RTL ? !open : open; const isLeft = this.RTL ? !open : open;
const dataUri = const dataUri = isLeft
isLeft ? this.QUOTE_IMAGE_LEFT_DATAURI : this.QUOTE_IMAGE_RIGHT_DATAURI; ? this.QUOTE_IMAGE_LEFT_DATAURI
: this.QUOTE_IMAGE_RIGHT_DATAURI;
return fieldRegistry.fromJson({ return fieldRegistry.fromJson({
type: 'field_image', type: 'field_image',
src: dataUri, src: dataUri,
@@ -728,20 +723,20 @@ const QUOTE_IMAGE_MIXIN = {
/** /**
* Wraps TEXT field with images of double quote characters. * Wraps TEXT field with images of double quote characters.
*/ */
const QUOTES_EXTENSION = function(this: QuoteImageBlock) { const QUOTES_EXTENSION = function (this: QuoteImageBlock) {
this.mixin(QUOTE_IMAGE_MIXIN); this.mixin(QUOTE_IMAGE_MIXIN);
this.quoteField_('TEXT'); this.quoteField_('TEXT');
}; };
/** Type of a block that has TEXT_JOIN_MUTATOR_MIXIN */ /** Type of a block that has TEXT_JOIN_MUTATOR_MIXIN */
type JoinMutatorBlock = Block&JoinMutatorMixin&QuoteImageMixin; type JoinMutatorBlock = Block & JoinMutatorMixin & QuoteImageMixin;
interface JoinMutatorMixin extends JoinMutatorMixinType {} interface JoinMutatorMixin extends JoinMutatorMixinType {}
type JoinMutatorMixinType = typeof JOIN_MUTATOR_MIXIN; type JoinMutatorMixinType = typeof JOIN_MUTATOR_MIXIN;
/** Type of a item block in the text_join_mutator bubble. */ /** Type of a item block in the text_join_mutator bubble. */
type JoinItemBlock = BlockSvg&JoinItemMixin; type JoinItemBlock = BlockSvg & JoinItemMixin;
interface JoinItemMixin { interface JoinItemMixin {
valueConnection_: Connection|null valueConnection_: Connection | null;
} }
/** /**
@@ -755,7 +750,7 @@ const JOIN_MUTATOR_MIXIN = {
* *
* @returns XML storage element. * @returns XML storage element.
*/ */
mutationToDom: function(this: JoinMutatorBlock): Element { mutationToDom: function (this: JoinMutatorBlock): Element {
const container = xmlUtils.createElement('mutation'); const container = xmlUtils.createElement('mutation');
container.setAttribute('items', `${this.itemCount_}`); container.setAttribute('items', `${this.itemCount_}`);
return container; return container;
@@ -766,7 +761,7 @@ const JOIN_MUTATOR_MIXIN = {
* *
* @param xmlElement XML storage element. * @param xmlElement XML storage element.
*/ */
domToMutation: function(this: JoinMutatorBlock, xmlElement: Element) { domToMutation: function (this: JoinMutatorBlock, xmlElement: Element) {
this.itemCount_ = parseInt(xmlElement.getAttribute('items')!, 10); this.itemCount_ = parseInt(xmlElement.getAttribute('items')!, 10);
this.updateShape_(); this.updateShape_();
}, },
@@ -775,7 +770,7 @@ const JOIN_MUTATOR_MIXIN = {
* *
* @returns The state of this block, ie the item count. * @returns The state of this block, ie the item count.
*/ */
saveExtraState: function(this: JoinMutatorBlock): {itemCount: number;} { saveExtraState: function (this: JoinMutatorBlock): {itemCount: number} {
return { return {
'itemCount': this.itemCount_, 'itemCount': this.itemCount_,
}; };
@@ -785,7 +780,7 @@ const JOIN_MUTATOR_MIXIN = {
* *
* @param state The state to apply to this block, ie the item count. * @param state The state to apply to this block, ie the item count.
*/ */
loadExtraState: function(this: JoinMutatorBlock, state: {[x: string]: any;}) { loadExtraState: function (this: JoinMutatorBlock, state: {[x: string]: any}) {
this.itemCount_ = state['itemCount']; this.itemCount_ = state['itemCount'];
this.updateShape_(); this.updateShape_();
}, },
@@ -795,14 +790,16 @@ const JOIN_MUTATOR_MIXIN = {
* @param workspace Mutator's workspace. * @param workspace Mutator's workspace.
* @returns Root block in mutator. * @returns Root block in mutator.
*/ */
decompose: function(this: JoinMutatorBlock, workspace: Workspace): Block { decompose: function (this: JoinMutatorBlock, workspace: Workspace): Block {
const containerBlock = const containerBlock = workspace.newBlock(
workspace.newBlock('text_create_join_container') as BlockSvg; 'text_create_join_container'
) as BlockSvg;
containerBlock.initSvg(); containerBlock.initSvg();
let connection = containerBlock.getInput('STACK')!.connection!; let connection = containerBlock.getInput('STACK')!.connection!;
for (let i = 0; i < this.itemCount_; i++) { for (let i = 0; i < this.itemCount_; i++) {
const itemBlock = const itemBlock = workspace.newBlock(
workspace.newBlock('text_create_join_item') as JoinItemBlock; 'text_create_join_item'
) as JoinItemBlock;
itemBlock.initSvg(); itemBlock.initSvg();
connection.connect(itemBlock.previousConnection); connection.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection; connection = itemBlock.nextConnection;
@@ -814,9 +811,10 @@ const JOIN_MUTATOR_MIXIN = {
* *
* @param containerBlock Root block in mutator. * @param containerBlock Root block in mutator.
*/ */
compose: function(this: JoinMutatorBlock, containerBlock: Block) { compose: function (this: JoinMutatorBlock, containerBlock: Block) {
let itemBlock = let itemBlock = containerBlock.getInputTargetBlock(
containerBlock.getInputTargetBlock('STACK') as JoinItemBlock; 'STACK'
) as JoinItemBlock;
// Count number of inputs. // Count number of inputs.
const connections = []; const connections = [];
while (itemBlock) { while (itemBlock) {
@@ -846,7 +844,7 @@ const JOIN_MUTATOR_MIXIN = {
* *
* @param containerBlock Root block in mutator. * @param containerBlock Root block in mutator.
*/ */
saveConnections: function(this: JoinMutatorBlock, containerBlock: Block) { saveConnections: function (this: JoinMutatorBlock, containerBlock: Block) {
let itemBlock = containerBlock.getInputTargetBlock('STACK'); let itemBlock = containerBlock.getInputTargetBlock('STACK');
let i = 0; let i = 0;
while (itemBlock) { while (itemBlock) {
@@ -856,7 +854,7 @@ const JOIN_MUTATOR_MIXIN = {
} }
const input = this.getInput('ADD' + i); const input = this.getInput('ADD' + i);
(itemBlock as JoinItemBlock).valueConnection_ = (itemBlock as JoinItemBlock).valueConnection_ =
input && input.connection!.targetConnection; input && input.connection!.targetConnection;
itemBlock = itemBlock.getNextBlock(); itemBlock = itemBlock.getNextBlock();
i++; i++;
} }
@@ -864,13 +862,13 @@ const JOIN_MUTATOR_MIXIN = {
/** /**
* Modify this block to have the correct number of inputs. * Modify this block to have the correct number of inputs.
*/ */
updateShape_: function(this: JoinMutatorBlock) { updateShape_: function (this: JoinMutatorBlock) {
if (this.itemCount_ && this.getInput('EMPTY')) { if (this.itemCount_ && this.getInput('EMPTY')) {
this.removeInput('EMPTY'); this.removeInput('EMPTY');
} else if (!this.itemCount_ && !this.getInput('EMPTY')) { } else if (!this.itemCount_ && !this.getInput('EMPTY')) {
this.appendDummyInput('EMPTY') this.appendDummyInput('EMPTY')
.appendField(this.newQuote_(true)) .appendField(this.newQuote_(true))
.appendField(this.newQuote_(false)); .appendField(this.newQuote_(false));
} }
// Add new inputs. // Add new inputs.
for (let i = 0; i < this.itemCount_; i++) { for (let i = 0; i < this.itemCount_; i++) {
@@ -891,7 +889,7 @@ const JOIN_MUTATOR_MIXIN = {
/** /**
* Performs final setup of a text_join block. * Performs final setup of a text_join block.
*/ */
const JOIN_EXTENSION = function(this: JoinMutatorBlock) { const JOIN_EXTENSION = function (this: JoinMutatorBlock) {
// Add the quote mixin for the itemCount_ = 0 case. // Add the quote mixin for the itemCount_ = 0 case.
this.mixin(QUOTE_IMAGE_MIXIN); this.mixin(QUOTE_IMAGE_MIXIN);
// Initialize the mutator values. // Initialize the mutator values.
@@ -903,22 +901,24 @@ const JOIN_EXTENSION = function(this: JoinMutatorBlock) {
// Update the tooltip of 'text_append' block to reference the variable. // Update the tooltip of 'text_append' block to reference the variable.
Extensions.register( Extensions.register(
'text_append_tooltip', 'text_append_tooltip',
Extensions.buildTooltipWithFieldText('%{BKY_TEXT_APPEND_TOOLTIP}', 'VAR')); Extensions.buildTooltipWithFieldText('%{BKY_TEXT_APPEND_TOOLTIP}', 'VAR')
);
/** /**
* Update the tooltip of 'text_append' block to reference the variable. * Update the tooltip of 'text_append' block to reference the variable.
*/ */
const INDEXOF_TOOLTIP_EXTENSION = function(this: Block) { const INDEXOF_TOOLTIP_EXTENSION = function (this: Block) {
this.setTooltip(() => { this.setTooltip(() => {
return Msg['TEXT_INDEXOF_TOOLTIP'].replace( return Msg['TEXT_INDEXOF_TOOLTIP'].replace(
'%1', this.workspace.options.oneBasedIndex ? '0' : '-1'); '%1',
this.workspace.options.oneBasedIndex ? '0' : '-1'
);
}); });
}; };
/** Type of a block that has TEXT_CHARAT_MUTATOR_MIXIN */ /** Type of a block that has TEXT_CHARAT_MUTATOR_MIXIN */
type CharAtBlock = Block&CharAtMixin; type CharAtBlock = Block & CharAtMixin;
interface CharAtMixin extends CharAtMixinType {} interface CharAtMixin extends CharAtMixinType {}
type CharAtMixinType = typeof CHARAT_MUTATOR_MIXIN; type CharAtMixinType = typeof CHARAT_MUTATOR_MIXIN;
@@ -933,7 +933,7 @@ const CHARAT_MUTATOR_MIXIN = {
* *
* @returns XML storage element. * @returns XML storage element.
*/ */
mutationToDom: function(this: CharAtBlock): Element { mutationToDom: function (this: CharAtBlock): Element {
const container = xmlUtils.createElement('mutation'); const container = xmlUtils.createElement('mutation');
container.setAttribute('at', `${this.isAt_}`); container.setAttribute('at', `${this.isAt_}`);
return container; return container;
@@ -944,10 +944,10 @@ const CHARAT_MUTATOR_MIXIN = {
* *
* @param xmlElement XML storage element. * @param xmlElement XML storage element.
*/ */
domToMutation: function(this: CharAtBlock, xmlElement: Element) { domToMutation: function (this: CharAtBlock, xmlElement: Element) {
// Note: Until January 2013 this block did not have mutations, // Note: Until January 2013 this block did not have mutations,
// so 'at' defaults to true. // so 'at' defaults to true.
const isAt = (xmlElement.getAttribute('at') !== 'false'); const isAt = xmlElement.getAttribute('at') !== 'false';
this.updateAt_(isAt); this.updateAt_(isAt);
}, },
@@ -961,7 +961,7 @@ const CHARAT_MUTATOR_MIXIN = {
* *
* @param isAt True if the input should exist. * @param isAt True if the input should exist.
*/ */
updateAt_: function(this: CharAtBlock, isAt: boolean) { updateAt_: function (this: CharAtBlock, isAt: boolean) {
// Destroy old 'AT' and 'ORDINAL' inputs. // Destroy old 'AT' and 'ORDINAL' inputs.
this.removeInput('AT', true); this.removeInput('AT', true);
this.removeInput('ORDINAL', true); this.removeInput('ORDINAL', true);
@@ -970,7 +970,8 @@ const CHARAT_MUTATOR_MIXIN = {
this.appendValueInput('AT').setCheck('Number'); this.appendValueInput('AT').setCheck('Number');
if (Msg['ORDINAL_NUMBER_SUFFIX']) { if (Msg['ORDINAL_NUMBER_SUFFIX']) {
this.appendDummyInput('ORDINAL').appendField( this.appendDummyInput('ORDINAL').appendField(
Msg['ORDINAL_NUMBER_SUFFIX']); Msg['ORDINAL_NUMBER_SUFFIX']
);
} }
} }
if (Msg['TEXT_CHARAT_TAIL']) { if (Msg['TEXT_CHARAT_TAIL']) {
@@ -985,30 +986,29 @@ const CHARAT_MUTATOR_MIXIN = {
/** /**
* Does the initial mutator update of text_charAt and adds the tooltip * Does the initial mutator update of text_charAt and adds the tooltip
*/ */
const CHARAT_EXTENSION = function(this: CharAtBlock) { const CHARAT_EXTENSION = function (this: CharAtBlock) {
const dropdown = this.getField('WHERE') as FieldDropdown; const dropdown = this.getField('WHERE') as FieldDropdown;
dropdown.setValidator(function(this: FieldDropdown, value: any) { dropdown.setValidator(function (this: FieldDropdown, value: any) {
const newAt = (value === 'FROM_START') || (value === 'FROM_END'); const newAt = value === 'FROM_START' || value === 'FROM_END';
const block = this.getSourceBlock() as CharAtBlock; const block = this.getSourceBlock() as CharAtBlock;
if (newAt !== block.isAt_) { if (newAt !== block.isAt_) {
block.updateAt_(newAt); block.updateAt_(newAt);
} }
return undefined; // FieldValidators can't be void. Use option as-is. return undefined; // FieldValidators can't be void. Use option as-is.
}); });
this.updateAt_(true); this.updateAt_(true);
// Assign 'this' to a variable for use in the tooltip closure below. this.setTooltip(() => {
const thisBlock = this; const where = this.getFieldValue('WHERE');
this.setTooltip(function() {
const where = thisBlock.getFieldValue('WHERE');
let tooltip = Msg['TEXT_CHARAT_TOOLTIP']; let tooltip = Msg['TEXT_CHARAT_TOOLTIP'];
if (where === 'FROM_START' || where === 'FROM_END') { if (where === 'FROM_START' || where === 'FROM_END') {
const msg = (where === 'FROM_START') ? const msg =
Msg['LISTS_INDEX_FROM_START_TOOLTIP'] : where === 'FROM_START'
Msg['LISTS_INDEX_FROM_END_TOOLTIP']; ? Msg['LISTS_INDEX_FROM_START_TOOLTIP']
: Msg['LISTS_INDEX_FROM_END_TOOLTIP'];
if (msg) { if (msg) {
tooltip += ' ' + tooltip +=
msg.replace( ' ' +
'%1', thisBlock.workspace.options.oneBasedIndex ? '#1' : '#0'); msg.replace('%1', this.workspace.options.oneBasedIndex ? '#1' : '#0');
} }
} }
return tooltip; return tooltip;
@@ -1020,10 +1020,16 @@ Extensions.register('text_indexOf_tooltip', INDEXOF_TOOLTIP_EXTENSION);
Extensions.register('text_quotes', QUOTES_EXTENSION); Extensions.register('text_quotes', QUOTES_EXTENSION);
Extensions.registerMutator( Extensions.registerMutator(
'text_join_mutator', JOIN_MUTATOR_MIXIN, JOIN_EXTENSION); 'text_join_mutator',
JOIN_MUTATOR_MIXIN,
JOIN_EXTENSION
);
Extensions.registerMutator( Extensions.registerMutator(
'text_charAt_mutator', CHARAT_MUTATOR_MIXIN, CHARAT_EXTENSION); 'text_charAt_mutator',
CHARAT_MUTATOR_MIXIN,
CHARAT_EXTENSION
);
// Register provided blocks. // Register provided blocks.
defineBlocks(blocks); defineBlocks(blocks);

View File

@@ -17,14 +17,19 @@ import * as Extensions from '../core/extensions.js';
import * as Variables from '../core/variables.js'; import * as Variables from '../core/variables.js';
import * as xmlUtils from '../core/utils/xml.js'; import * as xmlUtils from '../core/utils/xml.js';
import type {Block} from '../core/block.js'; import type {Block} from '../core/block.js';
import type {ContextMenuOption, LegacyContextMenuOption} from '../core/contextmenu_registry.js'; import type {
ContextMenuOption,
LegacyContextMenuOption,
} from '../core/contextmenu_registry.js';
import {FieldVariable} from '../core/field_variable.js'; import {FieldVariable} from '../core/field_variable.js';
import {Msg} from '../core/msg.js'; import {Msg} from '../core/msg.js';
import type {WorkspaceSvg} from '../core/workspace_svg.js'; import type {WorkspaceSvg} from '../core/workspace_svg.js';
import {createBlockDefinitionsFromJsonArray, defineBlocks} from '../core/common.js'; import {
createBlockDefinitionsFromJsonArray,
defineBlocks,
} from '../core/common.js';
import '../core/field_label.js'; import '../core/field_label.js';
/** /**
* A dictionary of the block definitions provided by this module. * A dictionary of the block definitions provided by this module.
*/ */
@@ -71,10 +76,10 @@ export const blocks = createBlockDefinitionsFromJsonArray([
]); ]);
/** Type of a block that has CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN */ /** Type of a block that has CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN */
type VariableBlock = Block&VariableMixin; type VariableBlock = Block & VariableMixin;
interface VariableMixin extends VariableMixinType {} interface VariableMixin extends VariableMixinType {}
type VariableMixinType = type VariableMixinType =
typeof CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN; typeof CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN;
/** /**
* Mixin to add context menu items to create getter/setter blocks for this * Mixin to add context menu items to create getter/setter blocks for this
@@ -87,9 +92,10 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
* *
* @param options List of menu options to add to. * @param options List of menu options to add to.
*/ */
customContextMenu: function( customContextMenu: function (
this: VariableBlock, this: VariableBlock,
options: Array<ContextMenuOption|LegacyContextMenuOption>) { options: Array<ContextMenuOption | LegacyContextMenuOption>
) {
if (!this.isInFlyout) { if (!this.isInFlyout) {
let oppositeType; let oppositeType;
let contextMenuMsg; let contextMenuMsg;
@@ -113,12 +119,14 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
options.push({ options.push({
enabled: this.workspace.remainingCapacity() > 0, enabled: this.workspace.remainingCapacity() > 0,
text: contextMenuMsg.replace('%1', name), text: contextMenuMsg.replace('%1', name),
callback: ContextMenu.callbackFactory(this, xmlBlock) callback: ContextMenu.callbackFactory(this, xmlBlock),
}); });
// Getter blocks have the option to rename or delete that variable. // Getter blocks have the option to rename or delete that variable.
} else { } else {
if (this.type === 'variables_get' || if (
this.type === 'variables_get_reporter') { this.type === 'variables_get' ||
this.type === 'variables_get_reporter'
) {
const renameOption = { const renameOption = {
text: Msg['RENAME_VARIABLE'], text: Msg['RENAME_VARIABLE'],
enabled: true, enabled: true,
@@ -144,8 +152,10 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
* @param block The block with the variable to rename. * @param block The block with the variable to rename.
* @returns A function that renames the variable. * @returns A function that renames the variable.
*/ */
const renameOptionCallbackFactory = function(block: VariableBlock): () => void { const renameOptionCallbackFactory = function (
return function() { block: VariableBlock
): () => void {
return function () {
const workspace = block.workspace; const workspace = block.workspace;
const variableField = block.getField('VAR') as FieldVariable; const variableField = block.getField('VAR') as FieldVariable;
const variable = variableField.getVariable()!; const variable = variableField.getVariable()!;
@@ -160,8 +170,10 @@ const renameOptionCallbackFactory = function(block: VariableBlock): () => void {
* @param block The block with the variable to delete. * @param block The block with the variable to delete.
* @returns A function that deletes the variable. * @returns A function that deletes the variable.
*/ */
const deleteOptionCallbackFactory = function(block: VariableBlock): () => void { const deleteOptionCallbackFactory = function (
return function() { block: VariableBlock
): () => void {
return function () {
const workspace = block.workspace; const workspace = block.workspace;
const variableField = block.getField('VAR') as FieldVariable; const variableField = block.getField('VAR') as FieldVariable;
const variable = variableField.getVariable()!; const variable = variableField.getVariable()!;
@@ -171,8 +183,9 @@ const deleteOptionCallbackFactory = function(block: VariableBlock): () => void {
}; };
Extensions.registerMixin( Extensions.registerMixin(
'contextMenu_variableSetterGetter', 'contextMenu_variableSetterGetter',
CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN); CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN
);
// Register provided blocks. // Register provided blocks.
defineBlocks(blocks); defineBlocks(blocks);

View File

@@ -18,14 +18,19 @@ import * as Variables from '../core/variables.js';
import * as xml from '../core/utils/xml.js'; import * as xml from '../core/utils/xml.js';
import {Abstract as AbstractEvent} from '../core/events/events_abstract.js'; import {Abstract as AbstractEvent} from '../core/events/events_abstract.js';
import type {Block} from '../core/block.js'; import type {Block} from '../core/block.js';
import type {ContextMenuOption, LegacyContextMenuOption} from '../core/contextmenu_registry.js'; import type {
ContextMenuOption,
LegacyContextMenuOption,
} from '../core/contextmenu_registry.js';
import {FieldVariable} from '../core/field_variable.js'; import {FieldVariable} from '../core/field_variable.js';
import {Msg} from '../core/msg.js'; import {Msg} from '../core/msg.js';
import type {WorkspaceSvg} from '../core/workspace_svg.js'; import type {WorkspaceSvg} from '../core/workspace_svg.js';
import {createBlockDefinitionsFromJsonArray, defineBlocks} from '../core/common.js'; import {
createBlockDefinitionsFromJsonArray,
defineBlocks,
} from '../core/common.js';
import '../core/field_label.js'; import '../core/field_label.js';
/** /**
* A dictionary of the block definitions provided by this module. * A dictionary of the block definitions provided by this module.
*/ */
@@ -34,11 +39,13 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{ {
'type': 'variables_get_dynamic', 'type': 'variables_get_dynamic',
'message0': '%1', 'message0': '%1',
'args0': [{ 'args0': [
'type': 'field_variable', {
'name': 'VAR', 'type': 'field_variable',
'variable': '%{BKY_VARIABLES_DEFAULT_NAME}', 'name': 'VAR',
}], 'variable': '%{BKY_VARIABLES_DEFAULT_NAME}',
},
],
'output': null, 'output': null,
'style': 'variable_dynamic_blocks', 'style': 'variable_dynamic_blocks',
'helpUrl': '%{BKY_VARIABLES_GET_HELPURL}', 'helpUrl': '%{BKY_VARIABLES_GET_HELPURL}',
@@ -70,10 +77,10 @@ export const blocks = createBlockDefinitionsFromJsonArray([
]); ]);
/** Type of a block that has CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN */ /** Type of a block that has CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN */
type VariableBlock = Block&VariableMixin; type VariableBlock = Block & VariableMixin;
interface VariableMixin extends VariableMixinType {} interface VariableMixin extends VariableMixinType {}
type VariableMixinType = type VariableMixinType =
typeof CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN; typeof CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN;
/** /**
* Mixin to add context menu items to create getter/setter blocks for this * Mixin to add context menu items to create getter/setter blocks for this
@@ -86,9 +93,10 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
* *
* @param options List of menu options to add to. * @param options List of menu options to add to.
*/ */
customContextMenu: function( customContextMenu: function (
this: VariableBlock, this: VariableBlock,
options: Array<ContextMenuOption|LegacyContextMenuOption>) { options: Array<ContextMenuOption | LegacyContextMenuOption>
) {
// Getter blocks have the option to create a setter block, and vice versa. // Getter blocks have the option to create a setter block, and vice versa.
if (!this.isInFlyout) { if (!this.isInFlyout) {
let oppositeType; let oppositeType;
@@ -116,11 +124,13 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
options.push({ options.push({
enabled: this.workspace.remainingCapacity() > 0, enabled: this.workspace.remainingCapacity() > 0,
text: contextMenuMsg.replace('%1', name), text: contextMenuMsg.replace('%1', name),
callback: ContextMenu.callbackFactory(this, xmlBlock) callback: ContextMenu.callbackFactory(this, xmlBlock),
}); });
} else { } else {
if (this.type === 'variables_get_dynamic' || if (
this.type === 'variables_get_reporter_dynamic') { this.type === 'variables_get_dynamic' ||
this.type === 'variables_get_reporter_dynamic'
) {
const renameOption = { const renameOption = {
text: Msg['RENAME_VARIABLE'], text: Msg['RENAME_VARIABLE'],
enabled: true, enabled: true,
@@ -143,7 +153,7 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
* *
* @param _e Change event. * @param _e Change event.
*/ */
onchange: function(this: VariableBlock, _e: AbstractEvent) { onchange: function (this: VariableBlock, _e: AbstractEvent) {
const id = this.getFieldValue('VAR'); const id = this.getFieldValue('VAR');
const variableModel = Variables.getVariable(this.workspace, id)!; const variableModel = Variables.getVariable(this.workspace, id)!;
if (this.type === 'variables_get_dynamic') { if (this.type === 'variables_get_dynamic') {
@@ -161,8 +171,8 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
* @param block The block with the variable to rename. * @param block The block with the variable to rename.
* @returns A function that renames the variable. * @returns A function that renames the variable.
*/ */
const renameOptionCallbackFactory = function(block: VariableBlock) { const renameOptionCallbackFactory = function (block: VariableBlock) {
return function() { return function () {
const workspace = block.workspace; const workspace = block.workspace;
const variableField = block.getField('VAR') as FieldVariable; const variableField = block.getField('VAR') as FieldVariable;
const variable = variableField.getVariable()!; const variable = variableField.getVariable()!;
@@ -177,8 +187,8 @@ const renameOptionCallbackFactory = function(block: VariableBlock) {
* @param block The block with the variable to delete. * @param block The block with the variable to delete.
* @returns A function that deletes the variable. * @returns A function that deletes the variable.
*/ */
const deleteOptionCallbackFactory = function(block: VariableBlock) { const deleteOptionCallbackFactory = function (block: VariableBlock) {
return function() { return function () {
const workspace = block.workspace; const workspace = block.workspace;
const variableField = block.getField('VAR') as FieldVariable; const variableField = block.getField('VAR') as FieldVariable;
const variable = variableField.getVariable()!; const variable = variableField.getVariable()!;
@@ -188,8 +198,9 @@ const deleteOptionCallbackFactory = function(block: VariableBlock) {
}; };
Extensions.registerMixin( Extensions.registerMixin(
'contextMenu_variableDynamicSetterGetter', 'contextMenu_variableDynamicSetterGetter',
CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN); CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN
);
// Register provided blocks. // Register provided blocks.
defineBlocks(blocks); defineBlocks(blocks);

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@ import type {BlockSvg} from './block_svg.js';
import * as dom from './utils/dom.js'; import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js'; import {Svg} from './utils/svg.js';
/** A bounding box for a cloned block. */ /** A bounding box for a cloned block. */
interface CloneRect { interface CloneRect {
x: number; x: number;
@@ -21,11 +20,10 @@ interface CloneRect {
} }
/** PID of disconnect UI animation. There can only be one at a time. */ /** PID of disconnect UI animation. There can only be one at a time. */
let disconnectPid: ReturnType<typeof setTimeout>|null = null; let disconnectPid: ReturnType<typeof setTimeout> | null = null;
/** The wobbling block. There can only be one at a time. */ /** The wobbling block. There can only be one at a time. */
let wobblingBlock: BlockSvg|null = null; let wobblingBlock: BlockSvg | null = null;
/** /**
* Play some UI effects (sound, animation) when disposing of a block. * Play some UI effects (sound, animation) when disposing of a block.
@@ -46,8 +44,12 @@ export function disposeUiEffect(block: BlockSvg) {
const clone: SVGGElement = svgGroup.cloneNode(true) as SVGGElement; const clone: SVGGElement = svgGroup.cloneNode(true) as SVGGElement;
clone.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')'); clone.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')');
workspace.getParentSvg().appendChild(clone); workspace.getParentSvg().appendChild(clone);
const cloneRect = const cloneRect = {
{'x': xy.x, 'y': xy.y, 'width': block.width, 'height': block.height}; 'x': xy.x,
'y': xy.y,
'width': block.width,
'height': block.height,
};
disposeUiStep(clone, cloneRect, workspace.RTL, new Date(), workspace.scale); disposeUiStep(clone, cloneRect, workspace.RTL, new Date(), workspace.scale);
} }
/** /**
@@ -62,21 +64,25 @@ export function disposeUiEffect(block: BlockSvg) {
* @param workspaceScale Scale of workspace. * @param workspaceScale Scale of workspace.
*/ */
function disposeUiStep( function disposeUiStep(
clone: Element, rect: CloneRect, rtl: boolean, start: Date, clone: Element,
workspaceScale: number) { rect: CloneRect,
rtl: boolean,
start: Date,
workspaceScale: number
) {
const ms = new Date().getTime() - start.getTime(); const ms = new Date().getTime() - start.getTime();
const percent = ms / 150; const percent = ms / 150;
if (percent > 1) { if (percent > 1) {
dom.removeNode(clone); dom.removeNode(clone);
} else { } else {
const x = const x =
rect.x + (rtl ? -1 : 1) * rect.width * workspaceScale / 2 * percent; rect.x + (((rtl ? -1 : 1) * rect.width * workspaceScale) / 2) * percent;
const y = rect.y + rect.height * workspaceScale * percent; const y = rect.y + rect.height * workspaceScale * percent;
const scale = (1 - percent) * workspaceScale; const scale = (1 - percent) * workspaceScale;
clone.setAttribute( clone.setAttribute(
'transform', 'transform',
'translate(' + x + ',' + y + ')' + 'translate(' + x + ',' + y + ')' + ' scale(' + scale + ')'
' scale(' + scale + ')'); );
setTimeout(disposeUiStep, 10, clone, rect, rtl, start, workspaceScale); setTimeout(disposeUiStep, 10, clone, rect, rtl, start, workspaceScale);
} }
} }
@@ -92,7 +98,7 @@ export function connectionUiEffect(block: BlockSvg) {
const scale = workspace.scale; const scale = workspace.scale;
workspace.getAudioManager().play('click'); workspace.getAudioManager().play('click');
if (scale < 1) { if (scale < 1) {
return; // Too small to care about visual effects. return; // Too small to care about visual effects.
} }
// Determine the absolute coordinates of the inferior block. // Determine the absolute coordinates of the inferior block.
const xy = workspace.getSvgXY(block.getSvgRoot()); const xy = workspace.getSvgXY(block.getSvgRoot());
@@ -105,15 +111,17 @@ export function connectionUiEffect(block: BlockSvg) {
xy.y += 3 * scale; xy.y += 3 * scale;
} }
const ripple = dom.createSvgElement( const ripple = dom.createSvgElement(
Svg.CIRCLE, { Svg.CIRCLE,
'cx': xy.x, {
'cy': xy.y, 'cx': xy.x,
'r': 0, 'cy': xy.y,
'fill': 'none', 'r': 0,
'stroke': '#888', 'fill': 'none',
'stroke-width': 10, 'stroke': '#888',
}, 'stroke-width': 10,
workspace.getParentSvg()); },
workspace.getParentSvg()
);
// Start the animation. // Start the animation.
connectionUiStep(ripple, new Date(), scale); connectionUiStep(ripple, new Date(), scale);
} }
@@ -147,13 +155,13 @@ export function disconnectUiEffect(block: BlockSvg) {
disconnectUiStop(); disconnectUiStop();
block.workspace.getAudioManager().play('disconnect'); block.workspace.getAudioManager().play('disconnect');
if (block.workspace.scale < 1) { if (block.workspace.scale < 1) {
return; // Too small to care about visual effects. return; // Too small to care about visual effects.
} }
// Horizontal distance for bottom of block to wiggle. // Horizontal distance for bottom of block to wiggle.
const DISPLACEMENT = 10; const DISPLACEMENT = 10;
// Scale magnitude of skew to height of block. // Scale magnitude of skew to height of block.
const height = block.getHeightWidth().height; const height = block.getHeightWidth().height;
let magnitude = Math.atan(DISPLACEMENT / height) / Math.PI * 180; let magnitude = (Math.atan(DISPLACEMENT / height) / Math.PI) * 180;
if (!block.RTL) { if (!block.RTL) {
magnitude *= -1; magnitude *= -1;
} }
@@ -170,8 +178,8 @@ export function disconnectUiEffect(block: BlockSvg) {
* @param start Date of animation's start. * @param start Date of animation's start.
*/ */
function disconnectUiStep(block: BlockSvg, magnitude: number, start: Date) { function disconnectUiStep(block: BlockSvg, magnitude: number, start: Date) {
const DURATION = 200; // Milliseconds. const DURATION = 200; // Milliseconds.
const WIGGLES = 3; // Half oscillations. const WIGGLES = 3; // Half oscillations.
const ms = new Date().getTime() - start.getTime(); const ms = new Date().getTime() - start.getTime();
const percent = ms / DURATION; const percent = ms / DURATION;
@@ -179,13 +187,15 @@ function disconnectUiStep(block: BlockSvg, magnitude: number, start: Date) {
let skew = ''; let skew = '';
if (percent <= 1) { if (percent <= 1) {
const val = Math.round( const val = Math.round(
Math.sin(percent * Math.PI * WIGGLES) * (1 - percent) * magnitude); Math.sin(percent * Math.PI * WIGGLES) * (1 - percent) * magnitude
);
skew = `skewX(${val})`; skew = `skewX(${val})`;
disconnectPid = setTimeout(disconnectUiStep, 10, block, magnitude, start); disconnectPid = setTimeout(disconnectUiStep, 10, block, magnitude, start);
} }
block.getSvgRoot().setAttribute( block
'transform', `${block.getTranslation()} ${skew}`); .getSvgRoot()
.setAttribute('transform', `${block.getTranslation()} ${skew}`);
} }
/** /**
@@ -199,7 +209,8 @@ export function disconnectUiStop() {
clearTimeout(disconnectPid); clearTimeout(disconnectPid);
disconnectPid = null; disconnectPid = null;
} }
wobblingBlock.getSvgRoot().setAttribute( wobblingBlock
'transform', wobblingBlock.getTranslation()); .getSvgRoot()
.setAttribute('transform', wobblingBlock.getTranslation());
wobblingBlock = null; wobblingBlock = null;
} }

View File

@@ -23,7 +23,6 @@ import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js'; import {Svg} from './utils/svg.js';
import * as svgMath from './utils/svg_math.js'; import * as svgMath from './utils/svg_math.js';
/** /**
* Class for a drag surface for the currently dragged block. This is a separate * Class for a drag surface for the currently dragged block. This is a separate
* SVG that contains only the currently moving block, or nothing. * SVG that contains only the currently moving block, or nothing.
@@ -65,14 +64,16 @@ export class BlockDragSurfaceSvg {
/** @param container Containing element. */ /** @param container Containing element. */
constructor(private readonly container: Element) { constructor(private readonly container: Element) {
this.svg = dom.createSvgElement( this.svg = dom.createSvgElement(
Svg.SVG, { Svg.SVG,
'xmlns': dom.SVG_NS, {
'xmlns:html': dom.HTML_NS, 'xmlns': dom.SVG_NS,
'xmlns:xlink': dom.XLINK_NS, 'xmlns:html': dom.HTML_NS,
'version': '1.1', 'xmlns:xlink': dom.XLINK_NS,
'class': 'blocklyBlockDragSurface', 'version': '1.1',
}, 'class': 'blocklyBlockDragSurface',
this.container); },
this.container
);
this.dragGroup = dom.createSvgElement(Svg.G, {}, this.svg); this.dragGroup = dom.createSvgElement(Svg.G, {}, this.svg);
} }
@@ -120,8 +121,9 @@ export class BlockDragSurfaceSvg {
this.childSurfaceXY.x = roundX; this.childSurfaceXY.x = roundX;
this.childSurfaceXY.y = roundY; this.childSurfaceXY.y = roundY;
this.dragGroup.setAttribute( this.dragGroup.setAttribute(
'transform', 'transform',
'translate(' + roundX + ',' + roundY + ') scale(' + scale + ')'); 'translate(' + roundX + ',' + roundY + ') scale(' + scale + ')'
);
} }
/** /**
@@ -200,7 +202,7 @@ export class BlockDragSurfaceSvg {
* *
* @returns Drag surface block DOM element, or null if no blocks exist. * @returns Drag surface block DOM element, or null if no blocks exist.
*/ */
getCurrentBlock(): Element|null { getCurrentBlock(): Element | null {
return this.dragGroup.firstChild as Element; return this.dragGroup.firstChild as Element;
} }

View File

@@ -30,7 +30,6 @@ import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js'; import * as dom from './utils/dom.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
/** /**
* Class for a block dragger. It moves blocks around the workspace when they * Class for a block dragger. It moves blocks around the workspace when they
* are being dragged by a mouse or touch. * are being dragged by a mouse or touch.
@@ -44,7 +43,7 @@ export class BlockDragger implements IBlockDragger {
protected workspace_: WorkspaceSvg; protected workspace_: WorkspaceSvg;
/** Which drag area the mouse pointer is over, if any. */ /** Which drag area the mouse pointer is over, if any. */
private dragTarget_: IDragTarget|null = null; private dragTarget_: IDragTarget | null = null;
/** Whether the block would be deleted if dropped immediately. */ /** Whether the block would be deleted if dropped immediately. */
protected wouldDeleteBlock_ = false; protected wouldDeleteBlock_ = false;
@@ -59,8 +58,9 @@ export class BlockDragger implements IBlockDragger {
this.draggingBlock_ = block; this.draggingBlock_ = block;
/** Object that keeps track of connections on dragged blocks. */ /** Object that keeps track of connections on dragged blocks. */
this.draggedConnectionManager_ = this.draggedConnectionManager_ = new InsertionMarkerManager(
new InsertionMarkerManager(this.draggingBlock_); this.draggingBlock_
);
this.workspace_ = workspace; this.workspace_ = workspace;
@@ -136,9 +136,11 @@ export class BlockDragger implements IBlockDragger {
*/ */
protected shouldDisconnect_(healStack: boolean): boolean { protected shouldDisconnect_(healStack: boolean): boolean {
return !!( return !!(
this.draggingBlock_.getParent() || this.draggingBlock_.getParent() ||
healStack && this.draggingBlock_.nextConnection && (healStack &&
this.draggingBlock_.nextConnection.targetBlock()); this.draggingBlock_.nextConnection &&
this.draggingBlock_.nextConnection.targetBlock())
);
} }
/** /**
@@ -149,7 +151,9 @@ export class BlockDragger implements IBlockDragger {
* at mouse down, in pixel units. * at mouse down, in pixel units.
*/ */
protected disconnectBlock_( protected disconnectBlock_(
healStack: boolean, currentDragDeltaXY: Coordinate) { healStack: boolean,
currentDragDeltaXY: Coordinate
) {
this.draggingBlock_.unplug(healStack); this.draggingBlock_.unplug(healStack);
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta); const newLoc = Coordinate.sum(this.startXY_, delta);
@@ -162,7 +166,10 @@ export class BlockDragger implements IBlockDragger {
/** Fire a UI event at the start of a block drag. */ /** Fire a UI event at the start of a block drag. */
protected fireDragStartEvent_() { protected fireDragStartEvent_() {
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))( const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
this.draggingBlock_, true, this.draggingBlock_.getDescendants(false)); this.draggingBlock_,
true,
this.draggingBlock_.getDescendants(false)
);
eventUtils.fire(event); eventUtils.fire(event);
} }
@@ -217,10 +224,11 @@ export class BlockDragger implements IBlockDragger {
blockAnimation.disconnectUiStop(); blockAnimation.disconnectUiStop();
const preventMove = !!this.dragTarget_ && const preventMove =
this.dragTarget_.shouldPreventMove(this.draggingBlock_); !!this.dragTarget_ &&
this.dragTarget_.shouldPreventMove(this.draggingBlock_);
let newLoc: Coordinate; let newLoc: Coordinate;
let delta: Coordinate|null = null; let delta: Coordinate | null = null;
if (preventMove) { if (preventMove) {
newLoc = this.startXY_; newLoc = this.startXY_;
} else { } else {
@@ -238,15 +246,17 @@ export class BlockDragger implements IBlockDragger {
if (!deleted) { if (!deleted) {
// These are expensive and don't need to be done if we're deleting. // These are expensive and don't need to be done if we're deleting.
this.draggingBlock_.setDragging(false); this.draggingBlock_.setDragging(false);
if (delta) { // !preventMove if (delta) {
// !preventMove
this.updateBlockAfterMove_(); this.updateBlockAfterMove_();
} else { } else {
// Blocks dragged directly from a flyout may need to be bumped into // Blocks dragged directly from a flyout may need to be bumped into
// bounds. // bounds.
bumpObjects.bumpIntoBounds( bumpObjects.bumpIntoBounds(
this.draggingBlock_.workspace, this.draggingBlock_.workspace,
this.workspace_.getMetricsManager().getScrollMetrics(true), this.workspace_.getMetricsManager().getScrollMetrics(true),
this.draggingBlock_); this.draggingBlock_
);
} }
} }
this.workspace_.setResizesEnabled(true); this.workspace_.setResizesEnabled(true);
@@ -262,8 +272,10 @@ export class BlockDragger implements IBlockDragger {
* @returns New location after drag. delta is in workspace units. newLocation * @returns New location after drag. delta is in workspace units. newLocation
* is the new coordinate where the block should end up. * is the new coordinate where the block should end up.
*/ */
protected getNewLocationAfterDrag_(currentDragDeltaXY: Coordinate): protected getNewLocationAfterDrag_(currentDragDeltaXY: Coordinate): {
{delta: Coordinate, newLocation: Coordinate} { delta: Coordinate;
newLocation: Coordinate;
} {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLocation = Coordinate.sum(this.startXY_, delta); const newLocation = Coordinate.sum(this.startXY_, delta);
return { return {
@@ -307,7 +319,10 @@ export class BlockDragger implements IBlockDragger {
/** Fire a UI event at the end of a block drag. */ /** Fire a UI event at the end of a block drag. */
protected fireDragEndEvent_() { protected fireDragEndEvent_() {
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))( const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
this.draggingBlock_, false, this.draggingBlock_.getDescendants(false)); this.draggingBlock_,
false,
this.draggingBlock_.getDescendants(false)
);
eventUtils.fire(event); eventUtils.fire(event);
} }
@@ -322,21 +337,25 @@ export class BlockDragger implements IBlockDragger {
const toolbox = this.workspace_.getToolbox(); const toolbox = this.workspace_.getToolbox();
if (toolbox) { if (toolbox) {
const style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' : const style = this.draggingBlock_.isDeletable()
'blocklyToolboxGrab'; ? 'blocklyToolboxDelete'
: 'blocklyToolboxGrab';
// AnyDuringMigration because: Property 'removeStyle' does not exist on // AnyDuringMigration because: Property 'removeStyle' does not exist on
// type 'IToolbox'. // type 'IToolbox'.
if (isEnd && if (
typeof (toolbox as AnyDuringMigration).removeStyle === 'function') { isEnd &&
typeof (toolbox as AnyDuringMigration).removeStyle === 'function'
) {
// AnyDuringMigration because: Property 'removeStyle' does not exist on // AnyDuringMigration because: Property 'removeStyle' does not exist on
// type 'IToolbox'. // type 'IToolbox'.
(toolbox as AnyDuringMigration).removeStyle(style); (toolbox as AnyDuringMigration).removeStyle(style);
// AnyDuringMigration because: Property 'addStyle' does not exist on // AnyDuringMigration because: Property 'addStyle' does not exist on
// type 'IToolbox'. // type 'IToolbox'.
} else if ( } else if (
!isEnd && !isEnd &&
typeof (toolbox as AnyDuringMigration).addStyle === 'function') { typeof (toolbox as AnyDuringMigration).addStyle === 'function'
) {
// AnyDuringMigration because: Property 'addStyle' does not exist on // AnyDuringMigration because: Property 'addStyle' does not exist on
// type 'IToolbox'. // type 'IToolbox'.
(toolbox as AnyDuringMigration).addStyle(style); (toolbox as AnyDuringMigration).addStyle(style);
@@ -348,7 +367,8 @@ export class BlockDragger implements IBlockDragger {
protected fireMoveEvent_() { protected fireMoveEvent_() {
if (this.draggingBlock_.isDeadOrDying()) return; if (this.draggingBlock_.isDeadOrDying()) return;
const event = new (eventUtils.get(eventUtils.BLOCK_MOVE))( const event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(
this.draggingBlock_) as BlockMove; this.draggingBlock_
) as BlockMove;
event.setReason(['drag']); event.setReason(['drag']);
event.oldCoordinate = this.startXY_; event.oldCoordinate = this.startXY_;
event.recordNew(); event.recordNew();
@@ -374,8 +394,9 @@ export class BlockDragger implements IBlockDragger {
*/ */
protected pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate { protected pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate {
const result = new Coordinate( const result = new Coordinate(
pixelCoord.x / this.workspace_.scale, pixelCoord.x / this.workspace_.scale,
pixelCoord.y / this.workspace_.scale); pixelCoord.y / this.workspace_.scale
);
if (this.workspace_.isMutator) { if (this.workspace_.isMutator) {
// If we're in a mutator, its scale is always 1, purely because of some // If we're in a mutator, its scale is always 1, purely because of some
// oddities in our rendering optimizations. The actual scale is the same // oddities in our rendering optimizations. The actual scale is the same
@@ -408,8 +429,10 @@ export class BlockDragger implements IBlockDragger {
*/ */
getInsertionMarkers(): BlockSvg[] { getInsertionMarkers(): BlockSvg[] {
// No insertion markers with the old style of dragged connection managers. // No insertion markers with the old style of dragged connection managers.
if (this.draggedConnectionManager_ && if (
this.draggedConnectionManager_.getInsertionMarkers) { this.draggedConnectionManager_ &&
this.draggedConnectionManager_.getInsertionMarkers
) {
return this.draggedConnectionManager_.getInsertionMarkers(); return this.draggedConnectionManager_.getInsertionMarkers();
} }
return []; return [];
@@ -433,15 +456,15 @@ export interface IconPositionData {
function initIconData(block: BlockSvg): IconPositionData[] { function initIconData(block: BlockSvg): IconPositionData[] {
// Build a list of icons that need to be moved and where they started. // Build a list of icons that need to be moved and where they started.
const dragIconData = []; const dragIconData = [];
const descendants = (block.getDescendants(false)); const descendants = block.getDescendants(false);
for (let i = 0, descendant; descendant = descendants[i]; i++) { for (let i = 0, descendant; (descendant = descendants[i]); i++) {
const icons = descendant.getIcons(); const icons = descendant.getIcons();
for (let j = 0; j < icons.length; j++) { for (let j = 0; j < icons.length; j++) {
const data = { const data = {
// Coordinate with x and y properties (workspace // Coordinate with x and y properties (workspace
// coordinates). // coordinates).
location: icons[j].getIconLocation(), // Blockly.Icon location: icons[j].getIconLocation(), // Blockly.Icon
icon: icons[j], icon: icons[j],
}; };
dragIconData.push(data); dragIconData.push(data);

View File

@@ -25,7 +25,11 @@ import type {Connection} from './connection.js';
import {ConnectionType} from './connection_type.js'; import {ConnectionType} from './connection_type.js';
import * as constants from './constants.js'; import * as constants from './constants.js';
import * as ContextMenu from './contextmenu.js'; import * as ContextMenu from './contextmenu.js';
import {ContextMenuOption, ContextMenuRegistry, LegacyContextMenuOption} from './contextmenu_registry.js'; import {
ContextMenuOption,
ContextMenuRegistry,
LegacyContextMenuOption,
} from './contextmenu_registry.js';
import type {BlockMove} from './events/events_block_move.js'; import type {BlockMove} from './events/events_block_move.js';
import * as eventUtils from './events/utils.js'; import * as eventUtils from './events/utils.js';
import type {Field} from './field.js'; import type {Field} from './field.js';
@@ -58,14 +62,14 @@ import type {Workspace} from './workspace.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
import {queueRender} from './render_management.js'; import {queueRender} from './render_management.js';
/** /**
* Class for a block's SVG representation. * Class for a block's SVG representation.
* Not normally called directly, workspace.newBlock() is preferred. * Not normally called directly, workspace.newBlock() is preferred.
*/ */
export class BlockSvg extends Block implements IASTNodeLocationSvg, export class BlockSvg
IBoundedElement, ICopyable, extends Block
IDraggable { implements IASTNodeLocationSvg, IBoundedElement, ICopyable, IDraggable
{
/** /**
* Constant for identifying rows that are to be rendered inline. * Constant for identifying rows that are to be rendered inline.
* Don't collide with Blockly.inputTypes. * Don't collide with Blockly.inputTypes.
@@ -81,15 +85,16 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
override decompose?: (p1: Workspace) => BlockSvg; override decompose?: (p1: Workspace) => BlockSvg;
// override compose?: ((p1: BlockSvg) => void)|null; // override compose?: ((p1: BlockSvg) => void)|null;
saveConnections?: (p1: BlockSvg) => void; saveConnections?: (p1: BlockSvg) => void;
customContextMenu?: customContextMenu?: (
(p1: Array<ContextMenuOption|LegacyContextMenuOption>) => void; p1: Array<ContextMenuOption | LegacyContextMenuOption>
) => void;
/** /**
* An property used internally to reference the block's rendering debugger. * An property used internally to reference the block's rendering debugger.
* *
* @internal * @internal
*/ */
renderingDebugger: BlockRenderingDebug|null = null; renderingDebugger: BlockRenderingDebug | null = null;
/** /**
* Height of this block, not including any statement blocks above or below. * Height of this block, not including any statement blocks above or below.
@@ -110,13 +115,13 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
private warningTextDb = new Map<string, ReturnType<typeof setTimeout>>(); private warningTextDb = new Map<string, ReturnType<typeof setTimeout>>();
/** Block's mutator icon (if any). */ /** Block's mutator icon (if any). */
mutator: Mutator|null = null; mutator: Mutator | null = null;
/** Block's comment icon (if any). */ /** Block's comment icon (if any). */
private commentIcon_: Comment|null = null; private commentIcon_: Comment | null = null;
/** Block's warning icon (if any). */ /** Block's warning icon (if any). */
warning: Warning|null = null; warning: Warning | null = null;
private svgGroup_: SVGGElement; private svgGroup_: SVGGElement;
style: BlockStyle; style: BlockStyle;
@@ -160,7 +165,6 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*/ */
relativeCoords = new Coordinate(0, 0); relativeCoords = new Coordinate(0, 0);
/** /**
* @param workspace The block's workspace. * @param workspace The block's workspace.
* @param prototypeName Name of the language object containing type-specific * @param prototypeName Name of the language object containing type-specific
@@ -177,8 +181,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
this.style = workspace.getRenderer().getConstants().getBlockStyle(null); this.style = workspace.getRenderer().getConstants().getBlockStyle(null);
/** The renderer's path object. */ /** The renderer's path object. */
this.pathObject = this.pathObject = workspace
workspace.getRenderer().makePathObject(this.svgGroup_, this.style); .getRenderer()
.makePathObject(this.svgGroup_, this.style);
/** /**
* Whether to move the block to the drag surface when it is dragged. * Whether to move the block to the drag surface when it is dragged.
@@ -204,7 +209,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
if (!this.workspace.rendered) { if (!this.workspace.rendered) {
throw TypeError('Workspace is headless.'); throw TypeError('Workspace is headless.');
} }
for (let i = 0, input; input = this.inputList[i]; i++) { for (let i = 0, input; (input = this.inputList[i]); i++) {
input.init(); input.init();
} }
const icons = this.getIcons(); const icons = this.getIcons();
@@ -216,7 +221,11 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
const svg = this.getSvgRoot(); const svg = this.getSvgRoot();
if (!this.workspace.options.readOnly && !this.eventsInit_ && svg) { if (!this.workspace.options.readOnly && !this.eventsInit_ && svg) {
browserEvents.conditionalBind( browserEvents.conditionalBind(
svg, 'pointerdown', this, this.onMouseDown_); svg,
'pointerdown',
this,
this.onMouseDown_
);
} }
this.eventsInit_ = true; this.eventsInit_ = true;
@@ -230,7 +239,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* *
* @returns #RRGGBB string. * @returns #RRGGBB string.
*/ */
getColourSecondary(): string|undefined { getColourSecondary(): string | undefined {
return this.style.colourSecondary; return this.style.colourSecondary;
} }
@@ -239,7 +248,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* *
* @returns #RRGGBB string. * @returns #RRGGBB string.
*/ */
getColourTertiary(): string|undefined { getColourTertiary(): string | undefined {
return this.style.colourTertiary; return this.style.colourTertiary;
} }
@@ -268,7 +277,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
} }
} }
const event = new (eventUtils.get(eventUtils.SELECTED))( const event = new (eventUtils.get(eventUtils.SELECTED))(
oldId, this.id, this.workspace.id); oldId,
this.id,
this.workspace.id
);
eventUtils.fire(event); eventUtils.fire(event);
common.setSelected(this); common.setSelected(this);
this.addSelect(); this.addSelect();
@@ -283,7 +295,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
return; return;
} }
const event = new (eventUtils.get(eventUtils.SELECTED))( const event = new (eventUtils.get(eventUtils.SELECTED))(
this.id, null, this.workspace.id); this.id,
null,
this.workspace.id
);
event.workspaceId = this.workspace.id; event.workspaceId = this.workspace.id;
eventUtils.fire(event); eventUtils.fire(event);
common.setSelected(null); common.setSelected(null);
@@ -315,7 +330,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @param newParent New parent block. * @param newParent New parent block.
* @internal * @internal
*/ */
override setParent(newParent: this|null) { override setParent(newParent: this | null) {
const oldParent = this.parentBlock_; const oldParent = this.parentBlock_;
if (newParent === oldParent) { if (newParent === oldParent) {
return; return;
@@ -359,9 +374,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
let x = 0; let x = 0;
let y = 0; let y = 0;
const dragSurfaceGroup = this.useDragSurface_ ? const dragSurfaceGroup = this.useDragSurface_
this.workspace.getBlockDragSurface()!.getGroup() : ? this.workspace.getBlockDragSurface()!.getGroup()
null; : null;
let element: SVGElement = this.getSvgRoot(); let element: SVGElement = this.getSvgRoot();
if (element) { if (element) {
@@ -372,17 +387,22 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
y += xy.y; y += xy.y;
// If this element is the current element on the drag surface, include // If this element is the current element on the drag surface, include
// the translation of the drag surface itself. // the translation of the drag surface itself.
if (this.useDragSurface_ && if (
this.workspace.getBlockDragSurface()!.getCurrentBlock() === this.useDragSurface_ &&
element) { this.workspace.getBlockDragSurface()!.getCurrentBlock() === element
const surfaceTranslation = ) {
this.workspace.getBlockDragSurface()!.getSurfaceTranslation(); const surfaceTranslation = this.workspace
.getBlockDragSurface()!
.getSurfaceTranslation();
x += surfaceTranslation.x; x += surfaceTranslation.x;
y += surfaceTranslation.y; y += surfaceTranslation.y;
} }
element = element.parentNode as SVGElement; element = element.parentNode as SVGElement;
} while (element && element !== this.workspace.getCanvas() && } while (
element !== dragSurfaceGroup); element &&
element !== this.workspace.getCanvas() &&
element !== dragSurfaceGroup
);
} }
return new Coordinate(x, y); return new Coordinate(x, y);
} }
@@ -399,9 +419,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
throw Error('Block has parent'); throw Error('Block has parent');
} }
const eventsEnabled = eventUtils.isEnabled(); const eventsEnabled = eventUtils.isEnabled();
let event: BlockMove|null = null; let event: BlockMove | null = null;
if (eventsEnabled) { if (eventsEnabled) {
event = new (eventUtils.get(eventUtils.BLOCK_MOVE))!(this) as BlockMove; event = new (eventUtils.get(eventUtils.BLOCK_MOVE)!)(this) as BlockMove;
reason && event.setReason(reason); reason && event.setReason(reason);
} }
const xy = this.getRelativeToSurfaceXY(); const xy = this.getRelativeToSurfaceXY();
@@ -487,8 +507,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
} }
// Translate to current position, turning off 3d. // Translate to current position, turning off 3d.
this.translate(newXY.x, newXY.y); this.translate(newXY.x, newXY.y);
this.workspace.getBlockDragSurface()!.clearAndHide( this.workspace
this.workspace.getCanvas()); .getBlockDragSurface()!
.clearAndHide(this.workspace.getCanvas());
} }
/** /**
@@ -501,8 +522,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*/ */
moveDuringDrag(newLoc: Coordinate) { moveDuringDrag(newLoc: Coordinate) {
if (this.useDragSurface_) { if (this.useDragSurface_) {
this.workspace.getBlockDragSurface()!.translateSurface( this.workspace
newLoc.x, newLoc.y); .getBlockDragSurface()!
.translateSurface(newLoc.x, newLoc.y);
} else { } else {
this.translate(newLoc.x, newLoc.y); this.translate(newLoc.x, newLoc.y);
this.getSvgRoot().setAttribute('transform', this.getTranslation()); this.getSvgRoot().setAttribute('transform', this.getTranslation());
@@ -520,29 +542,31 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
/** Snap this block to the nearest grid point. */ /** Snap this block to the nearest grid point. */
snapToGrid() { snapToGrid() {
if (this.isDeadOrDying()) { if (this.isDeadOrDying()) {
return; // Deleted block. return; // Deleted block.
} }
if (this.workspace.isDragging()) { if (this.workspace.isDragging()) {
return; // Don't bump blocks during a drag.; return; // Don't bump blocks during a drag.;
} }
if (this.getParent()) { if (this.getParent()) {
return; // Only snap top-level blocks. return; // Only snap top-level blocks.
} }
if (this.isInFlyout) { if (this.isInFlyout) {
return; // Don't move blocks around in a flyout. return; // Don't move blocks around in a flyout.
} }
const grid = this.workspace.getGrid(); const grid = this.workspace.getGrid();
if (!grid || !grid.shouldSnap()) { if (!grid || !grid.shouldSnap()) {
return; // Config says no snapping. return; // Config says no snapping.
} }
const spacing = grid.getSpacing(); const spacing = grid.getSpacing();
const half = spacing / 2; const half = spacing / 2;
const xy = this.getRelativeToSurfaceXY(); const xy = this.getRelativeToSurfaceXY();
const dx = const dx = Math.round(
Math.round(Math.round((xy.x - half) / spacing) * spacing + half - xy.x); Math.round((xy.x - half) / spacing) * spacing + half - xy.x
const dy = );
Math.round(Math.round((xy.y - half) / spacing) * spacing + half - xy.y); const dy = Math.round(
Math.round((xy.y - half) / spacing) * spacing + half - xy.y
);
if (dx || dy) { if (dx || dy) {
this.moveBy(dx, dy, ['snap']); this.moveBy(dx, dy, ['snap']);
} }
@@ -576,7 +600,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*/ */
markDirty() { markDirty() {
this.pathObject.constants = this.workspace.getRenderer().getConstants(); this.pathObject.constants = this.workspace.getRenderer().getConstants();
for (let i = 0, input; input = this.inputList[i]; i++) { for (let i = 0, input; (input = this.inputList[i]); i++) {
input.markDirty(); input.markDirty();
} }
} }
@@ -603,7 +627,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
const collapsedInputName = constants.COLLAPSED_INPUT_NAME; const collapsedInputName = constants.COLLAPSED_INPUT_NAME;
const collapsedFieldName = constants.COLLAPSED_FIELD_NAME; const collapsedFieldName = constants.COLLAPSED_FIELD_NAME;
for (let i = 0, input; input = this.inputList[i]; i++) { for (let i = 0, input; (input = this.inputList[i]); i++) {
if (input.name !== collapsedInputName) { if (input.name !== collapsedInputName) {
input.setVisible(!collapsed); input.setVisible(!collapsed);
} }
@@ -616,7 +640,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
} }
const icons = this.getIcons(); const icons = this.getIcons();
for (let i = 0, icon; icon = icons[i]; i++) { for (let i = 0, icon; (icon = icons[i]); i++) {
icon.setVisible(false); icon.setVisible(false);
} }
@@ -626,8 +650,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
field.setValue(text); field.setValue(text);
return; return;
} }
const input = this.getInput(collapsedInputName) || const input =
this.appendDummyInput(collapsedInputName); this.getInput(collapsedInputName) ||
this.appendDummyInput(collapsedInputName);
input.appendField(new FieldLabel(text), collapsedFieldName); input.appendField(new FieldLabel(text), collapsedFieldName);
} }
@@ -679,7 +704,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*/ */
showHelp() { showHelp() {
const url = const url =
typeof this.helpUrl === 'function' ? this.helpUrl() : this.helpUrl; typeof this.helpUrl === 'function' ? this.helpUrl() : this.helpUrl;
if (url) { if (url) {
window.open(url); window.open(url);
} }
@@ -690,13 +715,16 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* *
* @returns Context menu options or null if no menu. * @returns Context menu options or null if no menu.
*/ */
protected generateContextMenu(): protected generateContextMenu(): Array<
Array<ContextMenuOption|LegacyContextMenuOption>|null { ContextMenuOption | LegacyContextMenuOption
> | null {
if (this.workspace.options.readOnly || !this.contextMenu) { if (this.workspace.options.readOnly || !this.contextMenu) {
return null; return null;
} }
const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
ContextMenuRegistry.ScopeType.BLOCK, {block: this}); ContextMenuRegistry.ScopeType.BLOCK,
{block: this}
);
// Allow the block to add or modify menuOptions. // Allow the block to add or modify menuOptions.
if (this.customContextMenu) { if (this.customContextMenu) {
@@ -815,12 +843,13 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*/ */
override setInsertionMarker(insertionMarker: boolean) { override setInsertionMarker(insertionMarker: boolean) {
if (this.isInsertionMarker_ === insertionMarker) { if (this.isInsertionMarker_ === insertionMarker) {
return; // No change. return; // No change.
} }
this.isInsertionMarker_ = insertionMarker; this.isInsertionMarker_ = insertionMarker;
if (this.isInsertionMarker_) { if (this.isInsertionMarker_) {
this.setColour( this.setColour(
this.workspace.getRenderer().getConstants().INSERTION_MARKER_COLOUR); this.workspace.getRenderer().getConstants().INSERTION_MARKER_COLOUR
);
this.pathObject.updateInsertionMarker(true); this.pathObject.updateInsertionMarker(true);
} }
} }
@@ -897,8 +926,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
// (https://github.com/google/blockly/issues/4832) // (https://github.com/google/blockly/issues/4832)
this.dispose(false, true); this.dispose(false, true);
} else { } else {
this.dispose(/* heal */ this.dispose(/* heal */ true, true);
true, true);
} }
eventUtils.setGroup(false); eventUtils.setGroup(false);
} }
@@ -909,14 +937,15 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @returns Copy metadata, or null if the block is an insertion marker. * @returns Copy metadata, or null if the block is an insertion marker.
* @internal * @internal
*/ */
toCopyData(): CopyData|null { toCopyData(): CopyData | null {
if (this.isInsertionMarker_) { if (this.isInsertionMarker_) {
return null; return null;
} }
return { return {
saveInfo: saveInfo: blocks.save(this, {
blocks.save(this, {addCoordinates: true, addNextBlocks: false}) as addCoordinates: true,
blocks.State, addNextBlocks: false,
}) as blocks.State,
source: this.workspace, source: this.workspace,
typeCounts: common.getBlockTypeCounts(this, true), typeCounts: common.getBlockTypeCounts(this, true),
}; };
@@ -935,8 +964,8 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
icons[i].applyColour(); icons[i].applyColour();
} }
for (let x = 0, input; input = this.inputList[x]; x++) { for (let x = 0, input; (input = this.inputList[x]); x++) {
for (let y = 0, field; field = input.fieldRow[y]; y++) { for (let y = 0, field; (field = input.fieldRow[y]); y++) {
field.applyColour(); field.applyColour();
} }
} }
@@ -969,7 +998,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* *
* @returns The comment icon attached to this block, or null. * @returns The comment icon attached to this block, or null.
*/ */
getCommentIcon(): Comment|null { getCommentIcon(): Comment | null {
return this.commentIcon_; return this.commentIcon_;
} }
@@ -978,7 +1007,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* *
* @param text The text, or null to delete. * @param text The text, or null to delete.
*/ */
override setCommentText(text: string|null) { override setCommentText(text: string | null) {
if (this.commentModel.text === text) { if (this.commentModel.text === text) {
return; return;
} }
@@ -993,11 +1022,11 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
} }
if (shouldHaveComment) { if (shouldHaveComment) {
this.commentIcon_ = new Comment(this); this.commentIcon_ = new Comment(this);
this.comment = this.commentIcon_; // For backwards compatibility. this.comment = this.commentIcon_; // For backwards compatibility.
} else { } else {
this.commentIcon_!.dispose(); this.commentIcon_!.dispose();
this.commentIcon_ = null; this.commentIcon_ = null;
this.comment = null; // For backwards compatibility. this.comment = null; // For backwards compatibility.
} }
if (this.rendered) { if (this.rendered) {
// Icons must force an immediate render so that bubbles can be opened // Icons must force an immediate render so that bubbles can be opened
@@ -1015,7 +1044,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @param opt_id An optional ID for the warning text to be able to maintain * @param opt_id An optional ID for the warning text to be able to maintain
* multiple warnings. * multiple warnings.
*/ */
override setWarningText(text: string|null, opt_id?: string) { override setWarningText(text: string | null, opt_id?: string) {
const id = opt_id || ''; const id = opt_id || '';
if (!id) { if (!id) {
// Kill all previous pending processes, this edit supersedes them all. // Kill all previous pending processes, this edit supersedes them all.
@@ -1031,12 +1060,15 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
if (this.workspace.isDragging()) { if (this.workspace.isDragging()) {
// Don't change the warning text during a drag. // Don't change the warning text during a drag.
// Wait until the drag finishes. // Wait until the drag finishes.
this.warningTextDb.set(id, setTimeout(() => { this.warningTextDb.set(
if (!this.isDeadOrDying()) { id,
this.warningTextDb.delete(id); setTimeout(() => {
this.setWarningText(text, id); if (!this.isDeadOrDying()) {
} this.warningTextDb.delete(id);
}, 100)); this.setWarningText(text, id);
}
}, 100)
);
return; return;
} }
if (this.isInFlyout) { if (this.isInFlyout) {
@@ -1056,14 +1088,16 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
} }
if (collapsedParent) { if (collapsedParent) {
collapsedParent.setWarningText( collapsedParent.setWarningText(
Msg['COLLAPSED_WARNINGS_WARNING'], BlockSvg.COLLAPSED_WARNING_ID); Msg['COLLAPSED_WARNINGS_WARNING'],
BlockSvg.COLLAPSED_WARNING_ID
);
} }
if (!this.warning) { if (!this.warning) {
this.warning = new Warning(this); this.warning = new Warning(this);
changedState = true; changedState = true;
} }
this.warning!.setText((text), id); this.warning!.setText(text, id);
} else { } else {
// Dispose all warnings if no ID is given. // Dispose all warnings if no ID is given.
if (this.warning && !id) { if (this.warning && !id) {
@@ -1093,7 +1127,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* *
* @param mutator A mutator dialog instance or null to remove. * @param mutator A mutator dialog instance or null to remove.
*/ */
override setMutator(mutator: Mutator|null) { override setMutator(mutator: Mutator | null) {
if (this.mutator && this.mutator !== mutator) { if (this.mutator && this.mutator !== mutator) {
this.mutator.dispose(); this.mutator.dispose();
} }
@@ -1186,11 +1220,12 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* *
* @param colour HSV hue value, or #RRGGBB string. * @param colour HSV hue value, or #RRGGBB string.
*/ */
override setColour(colour: number|string) { override setColour(colour: number | string) {
super.setColour(colour); super.setColour(colour);
const styleObj = const styleObj = this.workspace
this.workspace.getRenderer().getConstants().getBlockStyleForColour( .getRenderer()
this.colour_); .getConstants()
.getBlockStyleForColour(this.colour_);
this.pathObject.setStyle(styleObj.style); this.pathObject.setStyle(styleObj.style);
this.style = styleObj.style; this.style = styleObj.style;
@@ -1206,9 +1241,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @throws {Error} if the block style does not exist. * @throws {Error} if the block style does not exist.
*/ */
override setStyle(blockStyleName: string) { override setStyle(blockStyleName: string) {
const blockStyle = const blockStyle = this.workspace
this.workspace.getRenderer().getConstants().getBlockStyle( .getRenderer()
blockStyleName); .getConstants()
.getBlockStyle(blockStyleName);
this.styleName_ = blockStyleName; this.styleName_ = blockStyleName;
if (blockStyle) { if (blockStyle) {
@@ -1234,7 +1270,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*/ */
bringToFront() { bringToFront() {
/* eslint-disable-next-line @typescript-eslint/no-this-alias */ /* eslint-disable-next-line @typescript-eslint/no-this-alias */
let block: this|null = this; let block: this | null = this;
do { do {
const root = block.getSvgRoot(); const root = block.getSvgRoot();
const parent = root.parentNode; const parent = root.parentNode;
@@ -1255,7 +1291,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* if any type could be connected. * if any type could be connected.
*/ */
override setPreviousStatement( override setPreviousStatement(
newBoolean: boolean, opt_check?: string|string[]|null) { newBoolean: boolean,
opt_check?: string | string[] | null
) {
super.setPreviousStatement(newBoolean, opt_check); super.setPreviousStatement(newBoolean, opt_check);
if (this.rendered) { if (this.rendered) {
@@ -1272,7 +1310,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* if any type could be connected. * if any type could be connected.
*/ */
override setNextStatement( override setNextStatement(
newBoolean: boolean, opt_check?: string|string[]|null) { newBoolean: boolean,
opt_check?: string | string[] | null
) {
super.setNextStatement(newBoolean, opt_check); super.setNextStatement(newBoolean, opt_check);
if (this.rendered) { if (this.rendered) {
@@ -1288,7 +1328,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @param opt_check Returned type or list of returned types. Null or * @param opt_check Returned type or list of returned types. Null or
* undefined if any type could be returned (e.g. variable get). * undefined if any type could be returned (e.g. variable get).
*/ */
override setOutput(newBoolean: boolean, opt_check?: string|string[]|null) { override setOutput(
newBoolean: boolean,
opt_check?: string | string[] | null
) {
super.setOutput(newBoolean, opt_check); super.setOutput(newBoolean, opt_check);
if (this.rendered) { if (this.rendered) {
@@ -1372,14 +1415,14 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*/ */
setConnectionTracking(track: boolean) { setConnectionTracking(track: boolean) {
if (this.previousConnection) { if (this.previousConnection) {
(this.previousConnection).setTracking(track); this.previousConnection.setTracking(track);
} }
if (this.outputConnection) { if (this.outputConnection) {
(this.outputConnection).setTracking(track); this.outputConnection.setTracking(track);
} }
if (this.nextConnection) { if (this.nextConnection) {
(this.nextConnection).setTracking(track); this.nextConnection.setTracking(track);
const child = (this.nextConnection).targetBlock(); const child = this.nextConnection.targetBlock();
if (child) { if (child) {
child.setConnectionTracking(track); child.setConnectionTracking(track);
} }
@@ -1428,7 +1471,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
myConnections.push(this.nextConnection); myConnections.push(this.nextConnection);
} }
if (all || !this.collapsed_) { if (all || !this.collapsed_) {
for (let i = 0, input; input = this.inputList[i]; i++) { for (let i = 0, input; (input = this.inputList[i]); i++) {
if (input.connection) { if (input.connection) {
myConnections.push(input.connection as RenderedConnection); myConnections.push(input.connection as RenderedConnection);
} }
@@ -1448,8 +1491,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @returns The last next connection on the stack, or null. * @returns The last next connection on the stack, or null.
* @internal * @internal
*/ */
override lastConnectionInStack(ignoreShadows: boolean): RenderedConnection override lastConnectionInStack(
|null { ignoreShadows: boolean
): RenderedConnection | null {
return super.lastConnectionInStack(ignoreShadows) as RenderedConnection; return super.lastConnectionInStack(ignoreShadows) as RenderedConnection;
} }
@@ -1463,8 +1507,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @returns The matching connection on this block, or null. * @returns The matching connection on this block, or null.
* @internal * @internal
*/ */
override getMatchingConnection(otherBlock: Block, conn: Connection): override getMatchingConnection(
RenderedConnection|null { otherBlock: Block,
conn: Connection
): RenderedConnection | null {
return super.getMatchingConnection(otherBlock, conn) as RenderedConnection; return super.getMatchingConnection(otherBlock, conn) as RenderedConnection;
} }
@@ -1483,7 +1529,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* *
* @returns The next statement block or null. * @returns The next statement block or null.
*/ */
override getNextBlock(): BlockSvg|null { override getNextBlock(): BlockSvg | null {
return super.getNextBlock() as BlockSvg; return super.getNextBlock() as BlockSvg;
} }
@@ -1492,7 +1538,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* *
* @returns The previous statement block or null. * @returns The previous statement block or null.
*/ */
override getPreviousBlock(): BlockSvg|null { override getPreviousBlock(): BlockSvg | null {
return super.getPreviousBlock() as BlockSvg; return super.getPreviousBlock() as BlockSvg;
} }
@@ -1520,8 +1566,11 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*/ */
private bumpNeighboursInternal() { private bumpNeighboursInternal() {
const root = this.getRootBlock(); const root = this.getRootBlock();
if (this.isDeadOrDying() || this.workspace.isDragging() || if (
root.isInFlyout) { this.isDeadOrDying() ||
this.workspace.isDragging() ||
root.isInFlyout
) {
return; return;
} }
@@ -1581,12 +1630,15 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @internal * @internal
*/ */
positionNearConnection( positionNearConnection(
sourceConnection: RenderedConnection, sourceConnection: RenderedConnection,
targetConnection: RenderedConnection) { targetConnection: RenderedConnection
) {
// We only need to position the new block if it's before the existing one, // We only need to position the new block if it's before the existing one,
// otherwise its position is set by the previous block. // otherwise its position is set by the previous block.
if (sourceConnection.type === ConnectionType.NEXT_STATEMENT || if (
sourceConnection.type === ConnectionType.INPUT_VALUE) { sourceConnection.type === ConnectionType.NEXT_STATEMENT ||
sourceConnection.type === ConnectionType.INPUT_VALUE
) {
const dx = targetConnection.x - sourceConnection.x; const dx = targetConnection.x - sourceConnection.x;
const dy = targetConnection.y - sourceConnection.y; const dy = targetConnection.y - sourceConnection.y;
@@ -1598,7 +1650,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @returns The first statement connection or null. * @returns The first statement connection or null.
* @internal * @internal
*/ */
override getFirstStatementConnection(): RenderedConnection|null { override getFirstStatementConnection(): RenderedConnection | null {
return super.getFirstStatementConnection() as RenderedConnection | null; return super.getFirstStatementConnection() as RenderedConnection | null;
} }
@@ -1636,7 +1688,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*/ */
render(opt_bubble?: boolean) { render(opt_bubble?: boolean) {
if (this.renderIsInProgress_) { if (this.renderIsInProgress_) {
return; // Don't allow recursive renders. return; // Don't allow recursive renders.
} }
this.renderIsInProgress_ = true; this.renderIsInProgress_ = true;
try { try {
@@ -1784,15 +1836,16 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @returns Object with height and width properties in workspace units. * @returns Object with height and width properties in workspace units.
* @internal * @internal
*/ */
getHeightWidth(): {height: number, width: number} { getHeightWidth(): {height: number; width: number} {
let height = this.height; let height = this.height;
let width = this.width; let width = this.width;
// Recursively add size of subsequent blocks. // Recursively add size of subsequent blocks.
const nextBlock = this.getNextBlock(); const nextBlock = this.getNextBlock();
if (nextBlock) { if (nextBlock) {
const nextHeightWidth = nextBlock.getHeightWidth(); const nextHeightWidth = nextBlock.getHeightWidth();
const tabHeight = const tabHeight = this.workspace
this.workspace.getRenderer().getConstants().NOTCH_HEIGHT; .getRenderer()
.getConstants().NOTCH_HEIGHT;
height += nextHeightWidth.height - tabHeight; height += nextHeightWidth.height - tabHeight;
width = Math.max(width, nextHeightWidth.width); width = Math.max(width, nextHeightWidth.width);
} }

View File

@@ -48,19 +48,75 @@ import {DragTarget} from './drag_target.js';
import * as dropDownDiv from './dropdowndiv.js'; import * as dropDownDiv from './dropdowndiv.js';
import * as Events from './events/events.js'; import * as Events from './events/events.js';
import * as Extensions from './extensions.js'; import * as Extensions from './extensions.js';
import {Field, FieldConfig, FieldValidator, UnattachedFieldError} from './field.js'; import {
import {FieldAngle, FieldAngleConfig, FieldAngleFromJsonConfig, FieldAngleValidator} from './field_angle.js'; Field,
import {FieldCheckbox, FieldCheckboxConfig, FieldCheckboxFromJsonConfig, FieldCheckboxValidator} from './field_checkbox.js'; FieldConfig,
import {FieldColour, FieldColourConfig, FieldColourFromJsonConfig, FieldColourValidator} from './field_colour.js'; FieldValidator,
import {FieldDropdown, FieldDropdownConfig, FieldDropdownFromJsonConfig, FieldDropdownValidator, MenuGenerator, MenuGeneratorFunction, MenuOption} from './field_dropdown.js'; UnattachedFieldError,
import {FieldImage, FieldImageConfig, FieldImageFromJsonConfig} from './field_image.js'; } from './field.js';
import {FieldLabel, FieldLabelConfig, FieldLabelFromJsonConfig} from './field_label.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 {FieldLabelSerializable} from './field_label_serializable.js';
import {FieldMultilineInput, FieldMultilineInputConfig, FieldMultilineInputFromJsonConfig, FieldMultilineInputValidator} from './field_multilineinput.js'; import {
import {FieldNumber, FieldNumberConfig, FieldNumberFromJsonConfig, FieldNumberValidator} from './field_number.js'; 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 * as fieldRegistry from './field_registry.js';
import {FieldTextInput, FieldTextInputConfig, FieldTextInputFromJsonConfig, FieldTextInputValidator} from './field_textinput.js'; import {
import {FieldVariable, FieldVariableConfig, FieldVariableFromJsonConfig, FieldVariableValidator} from './field_variable.js'; FieldTextInput,
FieldTextInputConfig,
FieldTextInputFromJsonConfig,
FieldTextInputValidator,
} from './field_textinput.js';
import {
FieldVariable,
FieldVariableConfig,
FieldVariableFromJsonConfig,
FieldVariableValidator,
} from './field_variable.js';
import {Flyout} from './flyout_base.js'; import {Flyout} from './flyout_base.js';
import {FlyoutButton} from './flyout_button.js'; import {FlyoutButton} from './flyout_button.js';
import {HorizontalFlyout} from './flyout_horizontal.js'; import {HorizontalFlyout} from './flyout_horizontal.js';
@@ -105,7 +161,10 @@ import {ISelectableToolboxItem} from './interfaces/i_selectable_toolbox_item.js'
import {IStyleable} from './interfaces/i_styleable.js'; import {IStyleable} from './interfaces/i_styleable.js';
import {IToolbox} from './interfaces/i_toolbox.js'; import {IToolbox} from './interfaces/i_toolbox.js';
import {IToolboxItem} from './interfaces/i_toolbox_item.js'; import {IToolboxItem} from './interfaces/i_toolbox_item.js';
import {IVariableBackedParameterModel, isVariableBackedParameterModel} from './interfaces/i_variable_backed_parameter_model.js'; import {
IVariableBackedParameterModel,
isVariableBackedParameterModel,
} from './interfaces/i_variable_backed_parameter_model.js';
import * as internalConstants from './internal_constants.js'; import * as internalConstants from './internal_constants.js';
import {ASTNode} from './keyboard_nav/ast_node.js'; import {ASTNode} from './keyboard_nav/ast_node.js';
import {BasicCursor} from './keyboard_nav/basic_cursor.js'; import {BasicCursor} from './keyboard_nav/basic_cursor.js';
@@ -163,11 +222,13 @@ import {WorkspaceComment} from './workspace_comment.js';
import {WorkspaceCommentSvg} from './workspace_comment_svg.js'; import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import {WorkspaceDragSurfaceSvg} from './workspace_drag_surface_svg.js'; import {WorkspaceDragSurfaceSvg} from './workspace_drag_surface_svg.js';
import {WorkspaceDragger} from './workspace_dragger.js'; import {WorkspaceDragger} from './workspace_dragger.js';
import {resizeSvgContents as realResizeSvgContents, WorkspaceSvg} from './workspace_svg.js'; import {
resizeSvgContents as realResizeSvgContents,
WorkspaceSvg,
} from './workspace_svg.js';
import * as Xml from './xml.js'; import * as Xml from './xml.js';
import {ZoomControls} from './zoom_controls.js'; import {ZoomControls} from './zoom_controls.js';
/** /**
* Blockly core version. * Blockly core version.
* This constant is overridden by the build script (npm run build) to the value * This constant is overridden by the build script (npm run build) to the value
@@ -327,8 +388,11 @@ export const setParentContainer = common.setParentContainer;
*/ */
function resizeSvgContentsLocal(workspace: WorkspaceSvg) { function resizeSvgContentsLocal(workspace: WorkspaceSvg) {
deprecation.warn( deprecation.warn(
'Blockly.resizeSvgContents', 'December 2021', 'December 2022', 'Blockly.resizeSvgContents',
'Blockly.WorkspaceSvg.resizeSvgContents'); 'December 2021',
'December 2022',
'Blockly.WorkspaceSvg.resizeSvgContents'
);
realResizeSvgContents(workspace); realResizeSvgContents(workspace);
} }
export const resizeSvgContents = resizeSvgContentsLocal; export const resizeSvgContents = resizeSvgContentsLocal;
@@ -342,8 +406,11 @@ export const resizeSvgContents = resizeSvgContentsLocal;
*/ */
export function copy(toCopy: ICopyable) { export function copy(toCopy: ICopyable) {
deprecation.warn( deprecation.warn(
'Blockly.copy', 'December 2021', 'December 2022', 'Blockly.copy',
'Blockly.clipboard.copy'); 'December 2021',
'December 2022',
'Blockly.clipboard.copy'
);
clipboard.copy(toCopy); clipboard.copy(toCopy);
} }
@@ -356,8 +423,11 @@ export function copy(toCopy: ICopyable) {
*/ */
export function paste(): boolean { export function paste(): boolean {
deprecation.warn( deprecation.warn(
'Blockly.paste', 'December 2021', 'December 2022', 'Blockly.paste',
'Blockly.clipboard.paste'); 'December 2021',
'December 2022',
'Blockly.clipboard.paste'
);
return !!clipboard.paste(); return !!clipboard.paste();
} }
@@ -370,8 +440,11 @@ export function paste(): boolean {
*/ */
export function duplicate(toDuplicate: ICopyable) { export function duplicate(toDuplicate: ICopyable) {
deprecation.warn( deprecation.warn(
'Blockly.duplicate', 'December 2021', 'December 2022', 'Blockly.duplicate',
'Blockly.clipboard.duplicate'); 'December 2021',
'December 2022',
'Blockly.clipboard.duplicate'
);
clipboard.duplicate(toDuplicate); clipboard.duplicate(toDuplicate);
} }
@@ -385,8 +458,11 @@ export function duplicate(toDuplicate: ICopyable) {
*/ */
export function isNumber(str: string): boolean { export function isNumber(str: string): boolean {
deprecation.warn( deprecation.warn(
'Blockly.isNumber', 'December 2021', 'December 2022', 'Blockly.isNumber',
'Blockly.utils.string.isNumber'); 'December 2021',
'December 2022',
'Blockly.utils.string.isNumber'
);
return utils.string.isNumber(str); return utils.string.isNumber(str);
} }
@@ -400,8 +476,11 @@ export function isNumber(str: string): boolean {
*/ */
export function hueToHex(hue: number): string { export function hueToHex(hue: number): string {
deprecation.warn( deprecation.warn(
'Blockly.hueToHex', 'December 2021', 'December 2022', 'Blockly.hueToHex',
'Blockly.utils.colour.hueToHex'); 'December 2021',
'December 2022',
'Blockly.utils.colour.hueToHex'
);
return colour.hueToHex(hue); return colour.hueToHex(hue);
} }
@@ -420,11 +499,17 @@ export function hueToHex(hue: number): string {
* @see Blockly.browserEvents.bind * @see Blockly.browserEvents.bind
*/ */
export function bindEvent_( export function bindEvent_(
node: EventTarget, name: string, thisObject: Object|null, node: EventTarget,
func: Function): browserEvents.Data { name: string,
thisObject: Object | null,
func: Function
): browserEvents.Data {
deprecation.warn( deprecation.warn(
'Blockly.bindEvent_', 'December 2021', 'December 2022', 'Blockly.bindEvent_',
'Blockly.browserEvents.bind'); 'December 2021',
'December 2022',
'Blockly.browserEvents.bind'
);
return browserEvents.bind(node, name, thisObject, func); return browserEvents.bind(node, name, thisObject, func);
} }
@@ -439,8 +524,11 @@ export function bindEvent_(
*/ */
export function unbindEvent_(bindData: browserEvents.Data): Function { export function unbindEvent_(bindData: browserEvents.Data): Function {
deprecation.warn( deprecation.warn(
'Blockly.unbindEvent_', 'December 2021', 'December 2022', 'Blockly.unbindEvent_',
'Blockly.browserEvents.unbind'); 'December 2021',
'December 2022',
'Blockly.browserEvents.unbind'
);
return browserEvents.unbind(bindData); return browserEvents.unbind(bindData);
} }
@@ -463,14 +551,26 @@ export function unbindEvent_(bindData: browserEvents.Data): Function {
* @see browserEvents.conditionalBind * @see browserEvents.conditionalBind
*/ */
export function bindEventWithChecks_( export function bindEventWithChecks_(
node: EventTarget, name: string, thisObject: Object|null, func: Function, node: EventTarget,
opt_noCaptureIdentifier?: boolean, name: string,
_opt_noPreventDefault?: boolean): browserEvents.Data { thisObject: Object | null,
func: Function,
opt_noCaptureIdentifier?: boolean,
_opt_noPreventDefault?: boolean
): browserEvents.Data {
deprecation.warn( deprecation.warn(
'Blockly.bindEventWithChecks_', 'December 2021', 'December 2022', 'Blockly.bindEventWithChecks_',
'Blockly.browserEvents.conditionalBind'); 'December 2021',
'December 2022',
'Blockly.browserEvents.conditionalBind'
);
return browserEvents.conditionalBind( return browserEvents.conditionalBind(
node, name, thisObject, func, opt_noCaptureIdentifier); node,
name,
thisObject,
func,
opt_noCaptureIdentifier
);
} }
// Aliases to allow external code to access these values for legacy reasons. // Aliases to allow external code to access these values for legacy reasons.
@@ -495,7 +595,7 @@ export const VARIABLE_CATEGORY_NAME: string = Variables.CATEGORY_NAME;
* variable blocks. * variable blocks.
*/ */
export const VARIABLE_DYNAMIC_CATEGORY_NAME: string = export const VARIABLE_DYNAMIC_CATEGORY_NAME: string =
VariablesDynamic.CATEGORY_NAME; VariablesDynamic.CATEGORY_NAME;
/** /**
* String for use in the "custom" attribute of a category in toolbox XML. * String for use in the "custom" attribute of a category in toolbox XML.
* This string indicates that the category should be dynamically populated with * This string indicates that the category should be dynamically populated with
@@ -503,58 +603,62 @@ export const VARIABLE_DYNAMIC_CATEGORY_NAME: string =
*/ */
export const PROCEDURE_CATEGORY_NAME: string = Procedures.CATEGORY_NAME; export const PROCEDURE_CATEGORY_NAME: string = Procedures.CATEGORY_NAME;
// Context for why we need to monkey-patch in these functions (internal): // Context for why we need to monkey-patch in these functions (internal):
// https://docs.google.com/document/d/1MbO0LEA-pAyx1ErGLJnyUqTLrcYTo-5zga9qplnxeXo/edit?usp=sharing&resourcekey=0-5h_32-i-dHwHjf_9KYEVKg // https://docs.google.com/document/d/1MbO0LEA-pAyx1ErGLJnyUqTLrcYTo-5zga9qplnxeXo/edit?usp=sharing&resourcekey=0-5h_32-i-dHwHjf_9KYEVKg
// clang-format off // clang-format off
Workspace.prototype.newBlock = Workspace.prototype.newBlock = function (
function(prototypeName: string, opt_id?: string): Block { prototypeName: string,
return new Block(this, prototypeName, opt_id); opt_id?: string
}; ): Block {
return new Block(this, prototypeName, opt_id);
};
WorkspaceSvg.prototype.newBlock = WorkspaceSvg.prototype.newBlock = function (
function(prototypeName: string, opt_id?: string): BlockSvg { prototypeName: string,
return new BlockSvg(this, prototypeName, opt_id); opt_id?: string
}; ): BlockSvg {
return new BlockSvg(this, prototypeName, opt_id);
};
WorkspaceSvg.newTrashcan = function(workspace: WorkspaceSvg): Trashcan { WorkspaceSvg.newTrashcan = function (workspace: WorkspaceSvg): Trashcan {
return new Trashcan(workspace); return new Trashcan(workspace);
}; };
WorkspaceCommentSvg.prototype.showContextMenu = WorkspaceCommentSvg.prototype.showContextMenu = function (
function(this: WorkspaceCommentSvg, e: Event) { this: WorkspaceCommentSvg,
if (this.workspace.options.readOnly) { e: Event
return; ) {
} if (this.workspace.options.readOnly) {
const menuOptions = []; return;
}
const menuOptions = [];
if (this.isDeletable() && this.isMovable()) { if (this.isDeletable() && this.isMovable()) {
menuOptions.push(ContextMenu.commentDuplicateOption(this)); menuOptions.push(ContextMenu.commentDuplicateOption(this));
menuOptions.push(ContextMenu.commentDeleteOption(this)); menuOptions.push(ContextMenu.commentDeleteOption(this));
} }
ContextMenu.show(e, menuOptions, this.RTL); ContextMenu.show(e, menuOptions, this.RTL);
}; };
Mutator.prototype.newWorkspaceSvg = Mutator.prototype.newWorkspaceSvg = function (options: Options): WorkspaceSvg {
function(options: Options): WorkspaceSvg { return new WorkspaceSvg(options);
return new WorkspaceSvg(options); };
};
Names.prototype.populateProcedures = Names.prototype.populateProcedures = function (
function(this: Names, workspace: Workspace) { this: Names,
const procedures = Procedures.allProcedures(workspace); workspace: Workspace
// Flatten the return vs no-return procedure lists. ) {
const flattenedProcedures = const procedures = Procedures.allProcedures(workspace);
procedures[0].concat(procedures[1]); // Flatten the return vs no-return procedure lists.
for (let i = 0; i < flattenedProcedures.length; i++) { const flattenedProcedures = procedures[0].concat(procedures[1]);
this.getName(flattenedProcedures[i][0], Names.NameType.PROCEDURE); for (let i = 0; i < flattenedProcedures.length; i++) {
} this.getName(flattenedProcedures[i][0], Names.NameType.PROCEDURE);
}; }
};
// clang-format on // clang-format on
// Re-export submodules that no longer declareLegacyNamespace. // Re-export submodules that no longer declareLegacyNamespace.
export {browserEvents}; export {browserEvents};
export {ContextMenu}; export {ContextMenu};
@@ -668,9 +772,9 @@ export {Flyout};
export {FlyoutButton}; export {FlyoutButton};
export {FlyoutMetricsManager}; export {FlyoutMetricsManager};
export {CodeGenerator}; export {CodeGenerator};
export {CodeGenerator as Generator}; // Deprecated name, October 2022. export {CodeGenerator as Generator}; // Deprecated name, October 2022.
export {Gesture}; export {Gesture};
export {Gesture as TouchGesture}; // Remove in v10. export {Gesture as TouchGesture}; // Remove in v10.
export {Grid}; export {Grid};
export {HorizontalFlyout}; export {HorizontalFlyout};
export {IASTNodeLocation}; export {IASTNodeLocation};

View File

@@ -11,7 +11,6 @@ import type {Theme, ITheme} from './theme.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
import type {ToolboxDefinition} from './utils/toolbox.js'; import type {ToolboxDefinition} from './utils/toolbox.js';
/** /**
* Blockly options. * Blockly options.
*/ */
@@ -32,14 +31,14 @@ export interface BlocklyOptions {
renderer?: string; renderer?: string;
rendererOverrides?: {[rendererConstant: string]: any}; rendererOverrides?: {[rendererConstant: string]: any};
rtl?: boolean; rtl?: boolean;
scrollbars?: ScrollbarOptions|boolean; scrollbars?: ScrollbarOptions | boolean;
sounds?: boolean; sounds?: boolean;
theme?: Theme|string|ITheme; theme?: Theme | string | ITheme;
toolbox?: string|ToolboxDefinition|Element; toolbox?: string | ToolboxDefinition | Element;
toolboxPosition?: string; toolboxPosition?: string;
trashcan?: boolean; trashcan?: boolean;
maxTrashcanContents?: number; maxTrashcanContents?: number;
plugins?: {[key: string]: (new(...p1: any[]) => any)|string}; plugins?: {[key: string]: (new (...p1: any[]) => any) | string};
zoom?: ZoomOptions; zoom?: ZoomOptions;
parentWorkspace?: WorkspaceSvg; parentWorkspace?: WorkspaceSvg;
} }
@@ -53,7 +52,7 @@ export interface GridOptions {
export interface MoveOptions { export interface MoveOptions {
drag?: boolean; drag?: boolean;
scrollbars?: boolean|ScrollbarOptions; scrollbars?: boolean | ScrollbarOptions;
wheel?: boolean; wheel?: boolean;
} }

View File

@@ -7,7 +7,6 @@
import * as goog from '../closure/goog/goog.js'; import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.blocks'); goog.declareModuleId('Blockly.blocks');
/** /**
* A block definition. For now this very loose, but it can potentially * A block definition. For now this very loose, but it can potentially
* be refined e.g. by replacing this typedef with a class definition. * be refined e.g. by replacing this typedef with a class definition.

View File

@@ -11,7 +11,6 @@ import * as Touch from './touch.js';
import * as deprecation from './utils/deprecation.js'; import * as deprecation from './utils/deprecation.js';
import * as userAgent from './utils/useragent.js'; import * as userAgent from './utils/useragent.js';
/** /**
* Blockly opaque event data used to unbind events when using * Blockly opaque event data used to unbind events when using
* `bind` and `conditionalBind`. * `bind` and `conditionalBind`.
@@ -49,12 +48,19 @@ const PAGE_MODE_MULTIPLIER = 125;
* @returns Opaque data that can be passed to unbindEvent_. * @returns Opaque data that can be passed to unbindEvent_.
*/ */
export function conditionalBind( export function conditionalBind(
node: EventTarget, name: string, thisObject: Object|null, func: Function, node: EventTarget,
opt_noCaptureIdentifier?: boolean, opt_noPreventDefault?: boolean): Data { name: string,
thisObject: Object | null,
func: Function,
opt_noCaptureIdentifier?: boolean,
opt_noPreventDefault?: boolean
): Data {
if (opt_noPreventDefault !== undefined) { if (opt_noPreventDefault !== undefined) {
deprecation.warn( deprecation.warn(
'The opt_noPreventDefault argument of conditionalBind', 'version 9', 'The opt_noPreventDefault argument of conditionalBind',
'version 10'); 'version 9',
'version 10'
);
} }
/** /**
* *
@@ -99,8 +105,11 @@ export function conditionalBind(
* @returns Opaque data that can be passed to unbindEvent_. * @returns Opaque data that can be passed to unbindEvent_.
*/ */
export function bind( export function bind(
node: EventTarget, name: string, thisObject: Object|null, node: EventTarget,
func: Function): Data { name: string,
thisObject: Object | null,
func: Function
): Data {
/** /**
* *
* @param e * @param e
@@ -154,17 +163,24 @@ export function unbind(bindData: Data): (e: Event) => void {
*/ */
export function isTargetInput(e: Event): boolean { export function isTargetInput(e: Event): boolean {
if (e.target instanceof HTMLElement) { if (e.target instanceof HTMLElement) {
if (e.target.isContentEditable || if (
e.target.getAttribute('data-is-text-input') === 'true') { e.target.isContentEditable ||
e.target.getAttribute('data-is-text-input') === 'true'
) {
return true; return true;
} }
if (e.target instanceof HTMLInputElement) { if (e.target instanceof HTMLInputElement) {
const target = e.target; const target = e.target;
return target.type === 'text' || target.type === 'number' || return (
target.type === 'email' || target.type === 'password' || target.type === 'text' ||
target.type === 'search' || target.type === 'tel' || target.type === 'number' ||
target.type === 'url'; target.type === 'email' ||
target.type === 'password' ||
target.type === 'search' ||
target.type === 'tel' ||
target.type === 'url'
);
} }
if (e.target instanceof HTMLTextAreaElement) { if (e.target instanceof HTMLTextAreaElement) {
@@ -200,7 +216,10 @@ export function isRightButton(e: MouseEvent): boolean {
* @returns Object with .x and .y properties. * @returns Object with .x and .y properties.
*/ */
export function mouseToSvg( export function mouseToSvg(
e: MouseEvent, svg: SVGSVGElement, matrix: SVGMatrix|null): SVGPoint { e: MouseEvent,
svg: SVGSVGElement,
matrix: SVGMatrix | null
): SVGPoint {
const svgPoint = svg.createSVGPoint(); const svgPoint = svg.createSVGPoint();
svgPoint.x = e.clientX; svgPoint.x = e.clientX;
svgPoint.y = e.clientY; svgPoint.y = e.clientY;
@@ -217,17 +236,17 @@ export function mouseToSvg(
* @param e Mouse event. * @param e Mouse event.
* @returns Scroll delta object with .x and .y properties. * @returns Scroll delta object with .x and .y properties.
*/ */
export function getScrollDeltaPixels(e: WheelEvent): {x: number, y: number} { export function getScrollDeltaPixels(e: WheelEvent): {x: number; y: number} {
switch (e.deltaMode) { switch (e.deltaMode) {
case 0x00: // Pixel mode. case 0x00: // Pixel mode.
default: default:
return {x: e.deltaX, y: e.deltaY}; return {x: e.deltaX, y: e.deltaY};
case 0x01: // Line mode. case 0x01: // Line mode.
return { return {
x: e.deltaX * LINE_MODE_MULTIPLIER, x: e.deltaX * LINE_MODE_MULTIPLIER,
y: e.deltaY * LINE_MODE_MULTIPLIER, y: e.deltaY * LINE_MODE_MULTIPLIER,
}; };
case 0x02: // Page mode. case 0x02: // Page mode.
return { return {
x: e.deltaX * PAGE_MODE_MULTIPLIER, x: e.deltaX * PAGE_MODE_MULTIPLIER,
y: e.deltaY * PAGE_MODE_MULTIPLIER, y: e.deltaY * PAGE_MODE_MULTIPLIER,

View File

@@ -27,7 +27,6 @@ import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js'; import * as userAgent from './utils/useragent.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
/** /**
* Class for UI bubble. * Class for UI bubble.
*/ */
@@ -54,10 +53,10 @@ export class Bubble implements IBubble {
static ANCHOR_RADIUS = 8; static ANCHOR_RADIUS = 8;
/** Mouse up event data. */ /** Mouse up event data. */
private static onMouseUpWrapper: browserEvents.Data|null = null; private static onMouseUpWrapper: browserEvents.Data | null = null;
/** Mouse move event data. */ /** Mouse move event data. */
private static onMouseMoveWrapper: browserEvents.Data|null = null; private static onMouseMoveWrapper: browserEvents.Data | null = null;
workspace_: WorkspaceSvg; workspace_: WorkspaceSvg;
content_: SVGElement; content_: SVGElement;
@@ -67,18 +66,18 @@ export class Bubble implements IBubble {
private readonly rendered: boolean; private readonly rendered: boolean;
/** The SVG group containing all parts of the bubble. */ /** The SVG group containing all parts of the bubble. */
private bubbleGroup: SVGGElement|null = null; private bubbleGroup: SVGGElement | null = null;
/** /**
* The SVG path for the arrow from the bubble to the icon on the block. * The SVG path for the arrow from the bubble to the icon on the block.
*/ */
private bubbleArrow: SVGPathElement|null = null; private bubbleArrow: SVGPathElement | null = null;
/** The SVG rect for the main body of the bubble. */ /** The SVG rect for the main body of the bubble. */
private bubbleBack: SVGRectElement|null = null; private bubbleBack: SVGRectElement | null = null;
/** The SVG group for the resize hash marks on some bubbles. */ /** The SVG group for the resize hash marks on some bubbles. */
private resizeGroup: SVGGElement|null = null; private resizeGroup: SVGGElement | null = null;
/** Absolute coordinate of anchor point, in workspace coordinates. */ /** Absolute coordinate of anchor point, in workspace coordinates. */
private anchorXY!: Coordinate; private anchorXY!: Coordinate;
@@ -106,16 +105,16 @@ export class Bubble implements IBubble {
private autoLayout = true; private autoLayout = true;
/** Method to call on resize of bubble. */ /** Method to call on resize of bubble. */
private resizeCallback: (() => void)|null = null; private resizeCallback: (() => void) | null = null;
/** Method to call on move of bubble. */ /** Method to call on move of bubble. */
private moveCallback: (() => void)|null = null; private moveCallback: (() => void) | null = null;
/** Mouse down on bubbleBack event data. */ /** Mouse down on bubbleBack event data. */
private onMouseDownBubbleWrapper: browserEvents.Data|null = null; private onMouseDownBubbleWrapper: browserEvents.Data | null = null;
/** Mouse down on resizeGroup event data. */ /** Mouse down on resizeGroup event data. */
private onMouseDownResizeWrapper: browserEvents.Data|null = null; private onMouseDownResizeWrapper: browserEvents.Data | null = null;
/** /**
* Describes whether this bubble has been disposed of (nodes and event * Describes whether this bubble has been disposed of (nodes and event
@@ -135,9 +134,13 @@ export class Bubble implements IBubble {
* @param bubbleHeight Height of bubble, or null if not resizable. * @param bubbleHeight Height of bubble, or null if not resizable.
*/ */
constructor( constructor(
workspace: WorkspaceSvg, content: SVGElement, shape: SVGElement, workspace: WorkspaceSvg,
anchorXY: Coordinate, bubbleWidth: number|null, content: SVGElement,
bubbleHeight: number|null) { shape: SVGElement,
anchorXY: Coordinate,
bubbleWidth: number | null,
bubbleHeight: number | null
) {
this.rendered = false; this.rendered = false;
this.workspace_ = workspace; this.workspace_ = workspace;
this.content_ = content; this.content_ = content;
@@ -151,7 +154,8 @@ export class Bubble implements IBubble {
const canvas = workspace.getBubbleCanvas(); const canvas = workspace.getBubbleCanvas();
canvas.appendChild( canvas.appendChild(
this.createDom(content, !!(bubbleWidth && bubbleHeight))); this.createDom(content, !!(bubbleWidth && bubbleHeight))
);
this.setAnchorLocation(anchorXY); this.setAnchorLocation(anchorXY);
if (!bubbleWidth || !bubbleHeight) { if (!bubbleWidth || !bubbleHeight) {
@@ -192,8 +196,10 @@ export class Bubble implements IBubble {
*/ */
this.bubbleGroup = dom.createSvgElement(Svg.G, {}); this.bubbleGroup = dom.createSvgElement(Svg.G, {});
let filter: {filter?: string} = { let filter: {filter?: string} = {
'filter': 'url(#' + 'filter':
this.workspace_.getRenderer().getConstants().embossFilterId + ')', 'url(#' +
this.workspace_.getRenderer().getConstants().embossFilterId +
')',
}; };
if (userAgent.JavaFx) { if (userAgent.JavaFx) {
// Multiple reports that JavaFX can't handle filters. // Multiple reports that JavaFX can't handle filters.
@@ -203,53 +209,70 @@ export class Bubble implements IBubble {
const bubbleEmboss = dom.createSvgElement(Svg.G, filter, this.bubbleGroup); const bubbleEmboss = dom.createSvgElement(Svg.G, filter, this.bubbleGroup);
this.bubbleArrow = dom.createSvgElement(Svg.PATH, {}, bubbleEmboss); this.bubbleArrow = dom.createSvgElement(Svg.PATH, {}, bubbleEmboss);
this.bubbleBack = dom.createSvgElement( this.bubbleBack = dom.createSvgElement(
Svg.RECT, { Svg.RECT,
'class': 'blocklyDraggable', {
'x': 0, 'class': 'blocklyDraggable',
'y': 0, 'x': 0,
'rx': Bubble.BORDER_WIDTH, 'y': 0,
'ry': Bubble.BORDER_WIDTH, 'rx': Bubble.BORDER_WIDTH,
}, 'ry': Bubble.BORDER_WIDTH,
bubbleEmboss); },
bubbleEmboss
);
if (hasResize) { if (hasResize) {
this.resizeGroup = dom.createSvgElement( this.resizeGroup = dom.createSvgElement(
Svg.G, { Svg.G,
'class': this.workspace_.RTL ? 'blocklyResizeSW' : {
'blocklyResizeSE', 'class': this.workspace_.RTL ? 'blocklyResizeSW' : 'blocklyResizeSE',
}, },
this.bubbleGroup); this.bubbleGroup
);
const size = 2 * Bubble.BORDER_WIDTH; const size = 2 * Bubble.BORDER_WIDTH;
dom.createSvgElement( dom.createSvgElement(
Svg.POLYGON, {'points': `0,${size} ${size},${size} ${size},0`}, Svg.POLYGON,
this.resizeGroup); {'points': `0,${size} ${size},${size} ${size},0`},
this.resizeGroup
);
dom.createSvgElement( dom.createSvgElement(
Svg.LINE, { Svg.LINE,
'class': 'blocklyResizeLine', {
'x1': size / 3, 'class': 'blocklyResizeLine',
'y1': size - 1, 'x1': size / 3,
'x2': size - 1, 'y1': size - 1,
'y2': size / 3, 'x2': size - 1,
}, 'y2': size / 3,
this.resizeGroup); },
this.resizeGroup
);
dom.createSvgElement( dom.createSvgElement(
Svg.LINE, { Svg.LINE,
'class': 'blocklyResizeLine', {
'x1': size * 2 / 3, 'class': 'blocklyResizeLine',
'y1': size - 1, 'x1': (size * 2) / 3,
'x2': size - 1, 'y1': size - 1,
'y2': size * 2 / 3, 'x2': size - 1,
}, 'y2': (size * 2) / 3,
this.resizeGroup); },
this.resizeGroup
);
} else { } else {
this.resizeGroup = null; this.resizeGroup = null;
} }
if (!this.workspace_.options.readOnly) { if (!this.workspace_.options.readOnly) {
this.onMouseDownBubbleWrapper = browserEvents.conditionalBind( this.onMouseDownBubbleWrapper = browserEvents.conditionalBind(
this.bubbleBack, 'pointerdown', this, this.bubbleMouseDown); this.bubbleBack,
'pointerdown',
this,
this.bubbleMouseDown
);
if (this.resizeGroup) { if (this.resizeGroup) {
this.onMouseDownResizeWrapper = browserEvents.conditionalBind( this.onMouseDownResizeWrapper = browserEvents.conditionalBind(
this.resizeGroup, 'pointerdown', this, this.resizeMouseDown); this.resizeGroup,
'pointerdown',
this,
this.resizeMouseDown
);
} }
} }
this.bubbleGroup.appendChild(content); this.bubbleGroup.appendChild(content);
@@ -329,14 +352,25 @@ export class Bubble implements IBubble {
} }
// Left-click (or middle click) // Left-click (or middle click)
this.workspace_.startDrag( this.workspace_.startDrag(
e, e,
new Coordinate( new Coordinate(
this.workspace_.RTL ? -this.width : this.width, this.height)); this.workspace_.RTL ? -this.width : this.width,
this.height
)
);
Bubble.onMouseUpWrapper = browserEvents.conditionalBind( Bubble.onMouseUpWrapper = browserEvents.conditionalBind(
document, 'pointerup', this, Bubble.bubbleMouseUp); document,
'pointerup',
this,
Bubble.bubbleMouseUp
);
Bubble.onMouseMoveWrapper = browserEvents.conditionalBind( Bubble.onMouseMoveWrapper = browserEvents.conditionalBind(
document, 'pointermove', this, this.resizeMouseMove); document,
'pointermove',
this,
this.resizeMouseMove
);
this.workspace_.hideChaff(); this.workspace_.hideChaff();
// This event has been handled. No need to bubble up to the document. // This event has been handled. No need to bubble up to the document.
e.stopPropagation(); e.stopPropagation();
@@ -406,8 +440,9 @@ export class Bubble implements IBubble {
/** Position the bubble so that it does not fall off-screen. */ /** Position the bubble so that it does not fall off-screen. */
private layoutBubble() { private layoutBubble() {
// Get the metrics in workspace units. // Get the metrics in workspace units.
const viewMetrics = const viewMetrics = this.workspace_
this.workspace_.getMetricsManager().getViewMetrics(true); .getMetricsManager()
.getViewMetrics(true);
const optimalLeft = this.getOptimalRelativeLeft(viewMetrics); const optimalLeft = this.getOptimalRelativeLeft(viewMetrics);
const optimalTop = this.getOptimalRelativeTop(viewMetrics); const optimalTop = this.getOptimalRelativeTop(viewMetrics);
@@ -415,30 +450,35 @@ export class Bubble implements IBubble {
const topPosition = { const topPosition = {
x: optimalLeft, x: optimalLeft,
y: -this.height - y: (-this.height -
this.workspace_.getRenderer().getConstants().MIN_BLOCK_HEIGHT as this.workspace_.getRenderer().getConstants()
number, .MIN_BLOCK_HEIGHT) as number,
}; };
const startPosition = {x: -this.width - 30, y: optimalTop}; const startPosition = {x: -this.width - 30, y: optimalTop};
const endPosition = {x: bbox.width, y: optimalTop}; const endPosition = {x: bbox.width, y: optimalTop};
const bottomPosition = {x: optimalLeft, y: bbox.height}; const bottomPosition = {x: optimalLeft, y: bbox.height};
const closerPosition = const closerPosition =
bbox.width < bbox.height ? endPosition : bottomPosition; bbox.width < bbox.height ? endPosition : bottomPosition;
const fartherPosition = const fartherPosition =
bbox.width < bbox.height ? bottomPosition : endPosition; bbox.width < bbox.height ? bottomPosition : endPosition;
const topPositionOverlap = this.getOverlap(topPosition, viewMetrics); const topPositionOverlap = this.getOverlap(topPosition, viewMetrics);
const startPositionOverlap = this.getOverlap(startPosition, viewMetrics); const startPositionOverlap = this.getOverlap(startPosition, viewMetrics);
const closerPositionOverlap = this.getOverlap(closerPosition, viewMetrics); const closerPositionOverlap = this.getOverlap(closerPosition, viewMetrics);
const fartherPositionOverlap = const fartherPositionOverlap = this.getOverlap(
this.getOverlap(fartherPosition, viewMetrics); fartherPosition,
viewMetrics
);
// Set the position to whichever position shows the most of the bubble, // Set the position to whichever position shows the most of the bubble,
// with tiebreaks going in the order: top > start > close > far. // with tiebreaks going in the order: top > start > close > far.
const mostOverlap = Math.max( const mostOverlap = Math.max(
topPositionOverlap, startPositionOverlap, closerPositionOverlap, topPositionOverlap,
fartherPositionOverlap); startPositionOverlap,
closerPositionOverlap,
fartherPositionOverlap
);
if (topPositionOverlap === mostOverlap) { if (topPositionOverlap === mostOverlap) {
this.relativeLeft = topPosition.x; this.relativeLeft = topPosition.x;
this.relativeTop = topPosition.y; this.relativeTop = topPosition.y;
@@ -472,12 +512,14 @@ export class Bubble implements IBubble {
* @returns The percentage of the bubble that is visible. * @returns The percentage of the bubble that is visible.
*/ */
private getOverlap( private getOverlap(
relativeMin: {x: number, y: number}, relativeMin: {x: number; y: number},
viewMetrics: ContainerRegion): number { viewMetrics: ContainerRegion
): number {
// The position of the top-left corner of the bubble in workspace units. // The position of the top-left corner of the bubble in workspace units.
const bubbleMin = { const bubbleMin = {
x: this.workspace_.RTL ? this.anchorXY.x - relativeMin.x - this.width : x: this.workspace_.RTL
relativeMin.x + this.anchorXY.x, ? this.anchorXY.x - relativeMin.x - this.width
: relativeMin.x + this.anchorXY.x,
y: relativeMin.y + this.anchorXY.y, y: relativeMin.y + this.anchorXY.y,
}; };
// The position of the bottom-right corner of the bubble in workspace units. // The position of the bottom-right corner of the bubble in workspace units.
@@ -499,13 +541,16 @@ export class Bubble implements IBubble {
y: viewMetrics.top + viewMetrics.height, y: viewMetrics.top + viewMetrics.height,
}; };
const overlapWidth = Math.min(bubbleMax.x, workspaceMax.x) - const overlapWidth =
Math.max(bubbleMin.x, workspaceMin.x); Math.min(bubbleMax.x, workspaceMax.x) -
const overlapHeight = Math.min(bubbleMax.y, workspaceMax.y) - Math.max(bubbleMin.x, workspaceMin.x);
Math.max(bubbleMin.y, workspaceMin.y); const overlapHeight =
Math.min(bubbleMax.y, workspaceMax.y) -
Math.max(bubbleMin.y, workspaceMin.y);
return Math.max( return Math.max(
0, 0,
Math.min(1, overlapWidth * overlapHeight / (this.width * this.height))); Math.min(1, (overlapWidth * overlapHeight) / (this.width * this.height))
);
} }
/** /**
@@ -532,9 +577,10 @@ export class Bubble implements IBubble {
const bubbleLeft = bubbleRight - this.width; const bubbleLeft = bubbleRight - this.width;
const workspaceRight = viewMetrics.left + viewMetrics.width; const workspaceRight = viewMetrics.left + viewMetrics.width;
const workspaceLeft = viewMetrics.left + const workspaceLeft =
// Thickness in workspace units. viewMetrics.left +
Scrollbar.scrollbarThickness / this.workspace_.scale; // Thickness in workspace units.
Scrollbar.scrollbarThickness / this.workspace_.scale;
if (bubbleLeft < workspaceLeft) { if (bubbleLeft < workspaceLeft) {
// Slide the bubble right until it is onscreen. // Slide the bubble right until it is onscreen.
@@ -548,9 +594,11 @@ export class Bubble implements IBubble {
const bubbleRight = bubbleLeft + this.width; const bubbleRight = bubbleLeft + this.width;
const workspaceLeft = viewMetrics.left; const workspaceLeft = viewMetrics.left;
const workspaceRight = viewMetrics.left + viewMetrics.width - const workspaceRight =
// Thickness in workspace units. viewMetrics.left +
Scrollbar.scrollbarThickness / this.workspace_.scale; viewMetrics.width -
// Thickness in workspace units.
Scrollbar.scrollbarThickness / this.workspace_.scale;
if (bubbleLeft < workspaceLeft) { if (bubbleLeft < workspaceLeft) {
// Slide the bubble right until it is onscreen. // Slide the bubble right until it is onscreen.
@@ -585,9 +633,10 @@ export class Bubble implements IBubble {
const bubbleTop = this.anchorXY.y + relativeTop; const bubbleTop = this.anchorXY.y + relativeTop;
const bubbleBottom = bubbleTop + this.height; const bubbleBottom = bubbleTop + this.height;
const workspaceTop = viewMetrics.top; const workspaceTop = viewMetrics.top;
const workspaceBottom = viewMetrics.top + const workspaceBottom =
viewMetrics.height - // Thickness in workspace units. viewMetrics.top +
Scrollbar.scrollbarThickness / this.workspace_.scale; viewMetrics.height - // Thickness in workspace units.
Scrollbar.scrollbarThickness / this.workspace_.scale;
const anchorY = this.anchorXY.y; const anchorY = this.anchorXY.y;
if (bubbleTop < workspaceTop) { if (bubbleTop < workspaceTop) {
@@ -622,7 +671,9 @@ export class Bubble implements IBubble {
*/ */
moveTo(x: number, y: number) { moveTo(x: number, y: number) {
this.bubbleGroup?.setAttribute( this.bubbleGroup?.setAttribute(
'transform', 'translate(' + x + ',' + y + ')'); 'transform',
'translate(' + x + ',' + y + ')'
);
} }
/** /**
@@ -666,14 +717,22 @@ export class Bubble implements IBubble {
// Mirror the resize group. // Mirror the resize group.
const resizeSize = 2 * Bubble.BORDER_WIDTH; const resizeSize = 2 * Bubble.BORDER_WIDTH;
this.resizeGroup.setAttribute( this.resizeGroup.setAttribute(
'transform', 'transform',
'translate(' + resizeSize + ',' + (height - doubleBorderWidth) + 'translate(' +
') scale(-1 1)'); resizeSize +
',' +
(height - doubleBorderWidth) +
') scale(-1 1)'
);
} else { } else {
this.resizeGroup.setAttribute( this.resizeGroup.setAttribute(
'transform', 'transform',
'translate(' + (width - doubleBorderWidth) + ',' + 'translate(' +
(height - doubleBorderWidth) + ')'); (width - doubleBorderWidth) +
',' +
(height - doubleBorderWidth) +
')'
);
} }
} }
if (this.autoLayout) { if (this.autoLayout) {
@@ -724,7 +783,7 @@ export class Bubble implements IBubble {
// Calculate the thickness of the base of the arrow. // Calculate the thickness of the base of the arrow.
const bubbleSize = this.getBubbleSize(); const bubbleSize = this.getBubbleSize();
let thickness = let thickness =
(bubbleSize.width + bubbleSize.height) / Bubble.ARROW_THICKNESS; (bubbleSize.width + bubbleSize.height) / Bubble.ARROW_THICKNESS;
thickness = Math.min(thickness, bubbleSize.width, bubbleSize.height) / 4; thickness = Math.min(thickness, bubbleSize.width, bubbleSize.height) / 4;
// Back the tip of the arrow off of the anchor. // Back the tip of the arrow off of the anchor.
@@ -743,16 +802,38 @@ export class Bubble implements IBubble {
if (swirlAngle > Math.PI * 2) { if (swirlAngle > Math.PI * 2) {
swirlAngle -= Math.PI * 2; swirlAngle -= Math.PI * 2;
} }
const swirlRise = Math.sin(swirlAngle) * hypotenuse / Bubble.ARROW_BEND; const swirlRise = (Math.sin(swirlAngle) * hypotenuse) / Bubble.ARROW_BEND;
const swirlRun = Math.cos(swirlAngle) * hypotenuse / Bubble.ARROW_BEND; const swirlRun = (Math.cos(swirlAngle) * hypotenuse) / Bubble.ARROW_BEND;
steps.push('M' + baseX1 + ',' + baseY1); steps.push('M' + baseX1 + ',' + baseY1);
steps.push( steps.push(
'C' + (baseX1 + swirlRun) + ',' + (baseY1 + swirlRise) + ' ' + 'C' +
relAnchorX + ',' + relAnchorY + ' ' + relAnchorX + ',' + relAnchorY); (baseX1 + swirlRun) +
',' +
(baseY1 + swirlRise) +
' ' +
relAnchorX +
',' +
relAnchorY +
' ' +
relAnchorX +
',' +
relAnchorY
);
steps.push( steps.push(
'C' + relAnchorX + ',' + relAnchorY + ' ' + (baseX2 + swirlRun) + 'C' +
',' + (baseY2 + swirlRise) + ' ' + baseX2 + ',' + baseY2); relAnchorX +
',' +
relAnchorY +
' ' +
(baseX2 + swirlRun) +
',' +
(baseY2 + swirlRise) +
' ' +
baseX2 +
',' +
baseY2
);
} }
steps.push('z'); steps.push('z');
this.bubbleArrow?.setAttribute('d', steps.join(' ')); this.bubbleArrow?.setAttribute('d', steps.join(' '));
@@ -813,10 +894,11 @@ export class Bubble implements IBubble {
*/ */
getRelativeToSurfaceXY(): Coordinate { getRelativeToSurfaceXY(): Coordinate {
return new Coordinate( return new Coordinate(
this.workspace_.RTL ? this.workspace_.RTL
-this.relativeLeft + this.anchorXY.x - this.width : ? -this.relativeLeft + this.anchorXY.x - this.width
this.anchorXY.x + this.relativeLeft, : this.anchorXY.x + this.relativeLeft,
this.anchorXY.y + this.relativeTop); this.anchorXY.y + this.relativeTop
);
} }
/** /**
@@ -868,7 +950,10 @@ export class Bubble implements IBubble {
const lines = text.split('\n'); const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const tspanElement = dom.createSvgElement( const tspanElement = dom.createSvgElement(
Svg.TSPAN, {'dy': '1em', 'x': Bubble.BORDER_WIDTH}, paragraph); Svg.TSPAN,
{'dy': '1em', 'x': Bubble.BORDER_WIDTH},
paragraph
);
const textNode = document.createTextNode(lines[i]); const textNode = document.createTextNode(lines[i]);
tspanElement.appendChild(textNode); tspanElement.appendChild(textNode);
} }
@@ -885,20 +970,29 @@ export class Bubble implements IBubble {
* @internal * @internal
*/ */
static createNonEditableBubble( static createNonEditableBubble(
paragraphElement: SVGTextElement, block: BlockSvg, paragraphElement: SVGTextElement,
iconXY: Coordinate): Bubble { block: BlockSvg,
iconXY: Coordinate
): Bubble {
const bubble = new Bubble( const bubble = new Bubble(
block.workspace!, paragraphElement, block.pathObject.svgPath, (iconXY), block.workspace!,
null, null); paragraphElement,
block.pathObject.svgPath,
iconXY,
null,
null
);
// Expose this bubble's block's ID on its top-level SVG group. // Expose this bubble's block's ID on its top-level SVG group.
bubble.setSvgId(block.id); bubble.setSvgId(block.id);
if (block.RTL) { if (block.RTL) {
// Right-align the paragraph. // Right-align the paragraph.
// This cannot be done until the bubble is rendered on screen. // This cannot be done until the bubble is rendered on screen.
const maxWidth = paragraphElement.getBBox().width; const maxWidth = paragraphElement.getBBox().width;
for (let i = 0, textElement; for (
textElement = paragraphElement.childNodes[i] as SVGTSpanElement; let i = 0, textElement;
i++) { (textElement = paragraphElement.childNodes[i] as SVGTSpanElement);
i++
) {
textElement.setAttribute('text-anchor', 'end'); textElement.setAttribute('text-anchor', 'end');
textElement.setAttribute('x', String(maxWidth + Bubble.BORDER_WIDTH)); textElement.setAttribute('x', String(maxWidth + Bubble.BORDER_WIDTH));
} }

View File

@@ -23,7 +23,6 @@ import {Coordinate} from './utils/coordinate.js';
import {WorkspaceCommentSvg} from './workspace_comment_svg.js'; import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
/** /**
* Class for a bubble dragger. It moves things on the bubble canvas around the * 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 * workspace when they are being dragged by a mouse or touch. These can be
@@ -31,12 +30,12 @@ import type {WorkspaceSvg} from './workspace_svg.js';
*/ */
export class BubbleDragger { export class BubbleDragger {
/** Which drag target the mouse pointer is over, if any. */ /** Which drag target the mouse pointer is over, if any. */
private dragTarget_: IDragTarget|null = null; private dragTarget_: IDragTarget | null = null;
/** Whether the bubble would be deleted if dropped immediately. */ /** Whether the bubble would be deleted if dropped immediately. */
private wouldDeleteBubble_ = false; private wouldDeleteBubble_ = false;
private readonly startXY_: Coordinate; private readonly startXY_: Coordinate;
private dragSurface_: BlockDragSurfaceSvg|null; private dragSurface_: BlockDragSurfaceSvg | null;
/** /**
* @param bubble The item on the bubble canvas to drag. * @param bubble The item on the bubble canvas to drag.
@@ -116,11 +115,13 @@ export class BubbleDragger {
* @param dragTarget The drag target that the bubblee is currently over. * @param dragTarget The drag target that the bubblee is currently over.
* @returns Whether dropping the bubble immediately would delete the block. * @returns Whether dropping the bubble immediately would delete the block.
*/ */
private shouldDelete_(dragTarget: IDragTarget|null): boolean { private shouldDelete_(dragTarget: IDragTarget | null): boolean {
if (dragTarget) { if (dragTarget) {
const componentManager = this.workspace.getComponentManager(); const componentManager = this.workspace.getComponentManager();
const isDeleteArea = componentManager.hasCapability( const isDeleteArea = componentManager.hasCapability(
dragTarget.id, ComponentManager.Capability.DELETE_AREA); dragTarget.id,
ComponentManager.Capability.DELETE_AREA
);
if (isDeleteArea) { if (isDeleteArea) {
return (dragTarget as IDeleteArea).wouldDelete(this.bubble, false); return (dragTarget as IDeleteArea).wouldDelete(this.bubble, false);
} }
@@ -149,7 +150,7 @@ export class BubbleDragger {
this.dragBubble(e, currentDragDeltaXY); this.dragBubble(e, currentDragDeltaXY);
const preventMove = const preventMove =
this.dragTarget_ && this.dragTarget_.shouldPreventMove(this.bubble); this.dragTarget_ && this.dragTarget_.shouldPreventMove(this.bubble);
let newLoc; let newLoc;
if (preventMove) { if (preventMove) {
newLoc = this.startXY_; newLoc = this.startXY_;
@@ -187,7 +188,8 @@ export class BubbleDragger {
private fireMoveEvent_() { private fireMoveEvent_() {
if (this.bubble instanceof WorkspaceCommentSvg) { if (this.bubble instanceof WorkspaceCommentSvg) {
const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))( const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(
this.bubble) as CommentMove; this.bubble
) as CommentMove;
event.setOldCoordinate(this.startXY_); event.setOldCoordinate(this.startXY_);
event.recordNew(); event.recordNew();
eventUtils.fire(event); eventUtils.fire(event);
@@ -207,8 +209,9 @@ export class BubbleDragger {
*/ */
private pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate { private pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate {
const result = new Coordinate( const result = new Coordinate(
pixelCoord.x / this.workspace.scale, pixelCoord.x / this.workspace.scale,
pixelCoord.y / this.workspace.scale); pixelCoord.y / this.workspace.scale
);
if (this.workspace.isMutator) { if (this.workspace.isMutator) {
// If we're in a mutator, its scale is always 1, purely because of some // If we're in a mutator, its scale is always 1, purely because of some
// oddities in our rendering optimizations. The actual scale is the same // oddities in our rendering optimizations. The actual scale is the same

View File

@@ -21,7 +21,6 @@ import * as mathUtils from './utils/math.js';
import type {WorkspaceCommentSvg} from './workspace_comment_svg.js'; import type {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
/** /**
* Bumps the given object that has passed out of bounds. * Bumps the given object that has passed out of bounds.
* *
@@ -34,8 +33,10 @@ import type {WorkspaceSvg} from './workspace_svg.js';
* @returns True if object was bumped. * @returns True if object was bumped.
*/ */
function bumpObjectIntoBounds( function bumpObjectIntoBounds(
workspace: WorkspaceSvg, bounds: ContainerRegion, workspace: WorkspaceSvg,
object: IBoundedElement): boolean { bounds: ContainerRegion,
object: IBoundedElement
): boolean {
// Compute new top/left position for object. // Compute new top/left position for object.
const objectMetrics = object.getBoundingRectangle(); const objectMetrics = object.getBoundingRectangle();
const height = objectMetrics.bottom - objectMetrics.top; const height = objectMetrics.bottom - objectMetrics.top;
@@ -46,8 +47,11 @@ function bumpObjectIntoBounds(
const bottomClamp = boundsBottom - height; const bottomClamp = boundsBottom - height;
// If the object is taller than the workspace we want to // If the object is taller than the workspace we want to
// top-align the block // top-align the block
const newYPosition = const newYPosition = mathUtils.clamp(
mathUtils.clamp(topClamp, objectMetrics.top, bottomClamp); topClamp,
objectMetrics.top,
bottomClamp
);
const deltaY = newYPosition - objectMetrics.top; const deltaY = newYPosition - objectMetrics.top;
// Note: Even in RTL mode the "anchor" of the object is the // Note: Even in RTL mode the "anchor" of the object is the
@@ -66,8 +70,11 @@ function bumpObjectIntoBounds(
// the right clamp to match. // the right clamp to match.
rightClamp = Math.max(leftClamp, rightClamp); rightClamp = Math.max(leftClamp, rightClamp);
} }
const newXPosition = const newXPosition = mathUtils.clamp(
mathUtils.clamp(leftClamp, objectMetrics.left, rightClamp); leftClamp,
objectMetrics.left,
rightClamp
);
const deltaX = newXPosition - objectMetrics.left; const deltaX = newXPosition - objectMetrics.left;
if (deltaX || deltaY) { if (deltaX || deltaY) {
@@ -84,8 +91,9 @@ export const bumpIntoBounds = bumpObjectIntoBounds;
* @param workspace The workspace to handle. * @param workspace The workspace to handle.
* @returns The event handler. * @returns The event handler.
*/ */
export function bumpIntoBoundsHandler(workspace: WorkspaceSvg): export function bumpIntoBoundsHandler(
(p1: Abstract) => void { workspace: WorkspaceSvg
): (p1: Abstract) => void {
return (e) => { return (e) => {
const metricsManager = workspace.getMetricsManager(); const metricsManager = workspace.getMetricsManager();
if (!metricsManager.hasFixedEdges() || workspace.isDragging()) { if (!metricsManager.hasFixedEdges() || workspace.isDragging()) {
@@ -96,8 +104,10 @@ export function bumpIntoBoundsHandler(workspace: WorkspaceSvg):
const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true); const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true);
// Triggered by move/create event // Triggered by move/create event
const object = const object = extractObjectFromEvent(
extractObjectFromEvent(workspace, e as eventUtils.BumpEvent); workspace,
e as eventUtils.BumpEvent
);
if (!object) { if (!object) {
return; return;
} }
@@ -106,18 +116,25 @@ export function bumpIntoBoundsHandler(workspace: WorkspaceSvg):
eventUtils.setGroup(e.group); eventUtils.setGroup(e.group);
const wasBumped = bumpObjectIntoBounds( const wasBumped = bumpObjectIntoBounds(
workspace, scrollMetricsInWsCoords, (object as IBoundedElement)); workspace,
scrollMetricsInWsCoords,
object as IBoundedElement
);
if (wasBumped && !e.group) { if (wasBumped && !e.group) {
console.warn( console.warn(
'Moved object in bounds but there was no' + 'Moved object in bounds but there was no' +
' event group. This may break undo.'); ' event group. This may break undo.'
);
} }
eventUtils.setGroup(existingGroup); eventUtils.setGroup(existingGroup);
} else if (e.type === eventUtils.VIEWPORT_CHANGE) { } else if (e.type === eventUtils.VIEWPORT_CHANGE) {
const viewportEvent = (e as ViewportChange); const viewportEvent = e as ViewportChange;
if (viewportEvent.scale && viewportEvent.oldScale && if (
viewportEvent.scale > viewportEvent.oldScale) { viewportEvent.scale &&
viewportEvent.oldScale &&
viewportEvent.scale > viewportEvent.oldScale
) {
bumpTopObjectsIntoBounds(workspace); bumpTopObjectsIntoBounds(workspace);
} }
} }
@@ -134,8 +151,9 @@ export function bumpIntoBoundsHandler(workspace: WorkspaceSvg):
* object. * object.
*/ */
function extractObjectFromEvent( function extractObjectFromEvent(
workspace: WorkspaceSvg, e: eventUtils.BumpEvent): BlockSvg|null| workspace: WorkspaceSvg,
WorkspaceCommentSvg { e: eventUtils.BumpEvent
): BlockSvg | null | WorkspaceCommentSvg {
let object = null; let object = null;
switch (e.type) { switch (e.type) {
case eventUtils.BLOCK_CREATE: case eventUtils.BLOCK_CREATE:
@@ -147,10 +165,9 @@ function extractObjectFromEvent(
break; break;
case eventUtils.COMMENT_CREATE: case eventUtils.COMMENT_CREATE:
case eventUtils.COMMENT_MOVE: case eventUtils.COMMENT_MOVE:
object = object = workspace.getCommentById(
workspace.getCommentById((e as CommentCreate | CommentMove).commentId! (e as CommentCreate | CommentMove).commentId!
) as WorkspaceCommentSvg | ) as WorkspaceCommentSvg | null;
null;
break; break;
} }
return object; return object;
@@ -169,7 +186,7 @@ export function bumpTopObjectsIntoBounds(workspace: WorkspaceSvg) {
const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true); const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true);
const topBlocks = workspace.getTopBoundedElements(); const topBlocks = workspace.getTopBoundedElements();
for (let i = 0, block; block = topBlocks[i]; i++) { for (let i = 0, block; (block = topBlocks[i]); i++) {
bumpObjectIntoBounds(workspace, scrollMetricsInWsCoords, block); bumpObjectIntoBounds(workspace, scrollMetricsInWsCoords, block);
} }
} }

View File

@@ -9,9 +9,8 @@ goog.declareModuleId('Blockly.clipboard');
import type {CopyData, ICopyable} from './interfaces/i_copyable.js'; import type {CopyData, ICopyable} from './interfaces/i_copyable.js';
/** Metadata about the object that is currently on the clipboard. */ /** Metadata about the object that is currently on the clipboard. */
let copyData: CopyData|null = null; let copyData: CopyData | null = null;
/** /**
* Copy a block or workspace comment onto the local clipboard. * Copy a block or workspace comment onto the local clipboard.
@@ -36,7 +35,7 @@ function copyInternal(toCopy: ICopyable) {
* @returns The pasted thing if the paste was successful, null otherwise. * @returns The pasted thing if the paste was successful, null otherwise.
* @internal * @internal
*/ */
export function paste(): ICopyable|null { export function paste(): ICopyable | null {
if (!copyData) { if (!copyData) {
return null; return null;
} }
@@ -46,8 +45,10 @@ export function paste(): ICopyable|null {
if (workspace.isFlyout) { if (workspace.isFlyout) {
workspace = workspace.targetWorkspace!; workspace = workspace.targetWorkspace!;
} }
if (copyData.typeCounts && if (
workspace.isCapacityAvailable(copyData.typeCounts)) { copyData.typeCounts &&
workspace.isCapacityAvailable(copyData.typeCounts)
) {
return workspace.paste(copyData.saveInfo); return workspace.paste(copyData.saveInfo);
} }
return null; return null;
@@ -61,18 +62,18 @@ export function paste(): ICopyable|null {
* duplication failed. * duplication failed.
* @internal * @internal
*/ */
export function duplicate(toDuplicate: ICopyable): ICopyable|null { export function duplicate(toDuplicate: ICopyable): ICopyable | null {
return TEST_ONLY.duplicateInternal(toDuplicate); return TEST_ONLY.duplicateInternal(toDuplicate);
} }
/** /**
* Private version of duplicate for stubbing in tests. * Private version of duplicate for stubbing in tests.
*/ */
function duplicateInternal(toDuplicate: ICopyable): ICopyable|null { function duplicateInternal(toDuplicate: ICopyable): ICopyable | null {
const oldCopyData = copyData; const oldCopyData = copyData;
copy(toDuplicate); copy(toDuplicate);
const pastedThing = const pastedThing =
toDuplicate.toCopyData()?.source?.paste(copyData!.saveInfo) ?? null; toDuplicate.toCopyData()?.source?.paste(copyData!.saveInfo) ?? null;
copyData = oldCopyData; copyData = oldCopyData;
return pastedThing; return pastedThing;
} }

View File

@@ -29,7 +29,6 @@ import * as dom from './utils/dom.js';
import type {Size} from './utils/size.js'; import type {Size} from './utils/size.js';
import {Svg} from './utils/svg.js'; import {Svg} from './utils/svg.js';
/** /**
* Class for a comment. * Class for a comment.
*/ */
@@ -40,7 +39,7 @@ export class Comment extends Icon {
* The model's text value at the start of an edit. * The model's text value at the start of an edit.
* Used to tell if an event should be fired at the end of an edit. * Used to tell if an event should be fired at the end of an edit.
*/ */
private cachedText: string|null = ''; private cachedText: string | null = '';
/** /**
* Array holding info needed to unbind events. * Array holding info needed to unbind events.
@@ -52,13 +51,13 @@ export class Comment extends Icon {
/** /**
* The SVG element that contains the text edit area, or null if not created. * The SVG element that contains the text edit area, or null if not created.
*/ */
private foreignObject: SVGForeignObjectElement|null = null; private foreignObject: SVGForeignObjectElement | null = null;
/** The editable text area, or null if not created. */ /** The editable text area, or null if not created. */
private textarea_: HTMLTextAreaElement|null = null; private textarea_: HTMLTextAreaElement | null = null;
/** The top-level node of the comment text, or null if not created. */ /** The top-level node of the comment text, or null if not created. */
private paragraphElement_: SVGTextElement|null = null; private paragraphElement_: SVGTextElement | null = null;
/** @param block The block associated with this comment. */ /** @param block The block associated with this comment. */
constructor(block: BlockSvg) { constructor(block: BlockSvg) {
@@ -81,28 +80,35 @@ export class Comment extends Icon {
protected override drawIcon_(group: Element) { protected override drawIcon_(group: Element) {
// Circle. // Circle.
dom.createSvgElement( dom.createSvgElement(
Svg.CIRCLE, Svg.CIRCLE,
{'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'}, group); {'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'},
group
);
// Can't use a real '?' text character since different browsers and // Can't use a real '?' text character since different browsers and
// operating systems render it differently. Body of question mark. // operating systems render it differently. Body of question mark.
dom.createSvgElement( dom.createSvgElement(
Svg.PATH, { Svg.PATH,
'class': 'blocklyIconSymbol', {
'd': 'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405' + 'class': 'blocklyIconSymbol',
'0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25' + 'd':
'-1.201,0.998 -1.201,1.528 -1.204,2.19z', 'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405' +
}, '0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25' +
group); '-1.201,0.998 -1.201,1.528 -1.204,2.19z',
},
group
);
// Dot of question mark. // Dot of question mark.
dom.createSvgElement( dom.createSvgElement(
Svg.RECT, { Svg.RECT,
'class': 'blocklyIconSymbol', {
'x': '6.8', 'class': 'blocklyIconSymbol',
'y': '10.78', 'x': '6.8',
'height': '2', 'y': '10.78',
'width': '2', 'height': '2',
}, 'width': '2',
group); },
group
);
} }
/** /**
@@ -123,16 +129,19 @@ export class Comment extends Icon {
* For non-editable mode see Warning.textToDom_. * For non-editable mode see Warning.textToDom_.
*/ */
this.foreignObject = dom.createSvgElement( this.foreignObject = dom.createSvgElement(Svg.FOREIGNOBJECT, {
Svg.FOREIGNOBJECT, 'x': Bubble.BORDER_WIDTH,
{'x': Bubble.BORDER_WIDTH, 'y': Bubble.BORDER_WIDTH}); 'y': Bubble.BORDER_WIDTH,
});
const body = document.createElementNS(dom.HTML_NS, 'body'); const body = document.createElementNS(dom.HTML_NS, 'body');
body.setAttribute('xmlns', dom.HTML_NS); body.setAttribute('xmlns', dom.HTML_NS);
body.className = 'blocklyMinimalBody'; body.className = 'blocklyMinimalBody';
this.textarea_ = document.createElementNS(dom.HTML_NS, 'textarea') as this.textarea_ = document.createElementNS(
HTMLTextAreaElement; dom.HTML_NS,
'textarea'
) as HTMLTextAreaElement;
const textarea = this.textarea_; const textarea = this.textarea_;
textarea.className = 'blocklyCommentTextarea'; textarea.className = 'blocklyCommentTextarea';
textarea.setAttribute('dir', this.getBlock().RTL ? 'RTL' : 'LTR'); textarea.setAttribute('dir', this.getBlock().RTL ? 'RTL' : 'LTR');
@@ -142,33 +151,62 @@ export class Comment extends Icon {
body.appendChild(textarea); body.appendChild(textarea);
this.foreignObject!.appendChild(body); this.foreignObject!.appendChild(body);
this.boundEvents.push(browserEvents.conditionalBind( this.boundEvents.push(
textarea, 'focus', this, this.startEdit, true)); browserEvents.conditionalBind(
textarea,
'focus',
this,
this.startEdit,
true
)
);
// Don't zoom with mousewheel. // Don't zoom with mousewheel.
this.boundEvents.push(browserEvents.conditionalBind( this.boundEvents.push(
textarea, 'wheel', this, function(e: Event) { browserEvents.conditionalBind(
textarea,
'wheel',
this,
function (e: Event) {
e.stopPropagation(); e.stopPropagation();
})); }
this.boundEvents.push(browserEvents.conditionalBind( )
textarea, 'change', this, );
this.boundEvents.push(
browserEvents.conditionalBind(
textarea,
'change',
this,
/** /**
* @param _e Unused event parameter. * @param _e Unused event parameter.
*/ */
function(this: Comment, _e: Event) { function (this: Comment, _e: Event) {
if (this.cachedText !== this.model.text) { if (this.cachedText !== this.model.text) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))( eventUtils.fire(
this.getBlock(), 'comment', null, this.cachedText, new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this.model.text)); this.getBlock(),
'comment',
null,
this.cachedText,
this.model.text
)
);
} }
})); }
this.boundEvents.push(browserEvents.conditionalBind( )
textarea, 'input', this, );
this.boundEvents.push(
browserEvents.conditionalBind(
textarea,
'input',
this,
/** /**
* @param _e Unused event parameter. * @param _e Unused event parameter.
*/ */
function(this: Comment, _e: Event) { function (this: Comment, _e: Event) {
this.model.text = textarea.value; this.model.text = textarea.value;
})); }
)
);
setTimeout(textarea.focus.bind(textarea), 0); setTimeout(textarea.focus.bind(textarea), 0);
@@ -225,8 +263,13 @@ export class Comment extends Icon {
if (visible === this.isVisible()) { if (visible === this.isVisible()) {
return; return;
} }
eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))( eventUtils.fire(
this.getBlock(), visible, 'comment')); new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
this.getBlock(),
visible,
'comment'
)
);
this.model.pinned = visible; this.model.pinned = visible;
if (visible) { if (visible) {
this.createBubble(); this.createBubble();
@@ -248,9 +291,13 @@ export class Comment extends Icon {
private createEditableBubble() { private createEditableBubble() {
const block = this.getBlock(); const block = this.getBlock();
this.bubble_ = new Bubble( this.bubble_ = new Bubble(
block.workspace, this.createEditor(), block.pathObject.svgPath, block.workspace,
(this.iconXY_ as Coordinate), this.model.size.width, this.createEditor(),
this.model.size.height); block.pathObject.svgPath,
this.iconXY_ as Coordinate,
this.model.size.width,
this.model.size.height
);
// Expose this comment's block's ID on its top-level SVG group. // Expose this comment's block's ID on its top-level SVG group.
this.bubble_.setSvgId(block.id); this.bubble_.setSvgId(block.id);
this.bubble_.registerResizeEvent(this.onBubbleResize.bind(this)); this.bubble_.registerResizeEvent(this.onBubbleResize.bind(this));
@@ -264,7 +311,10 @@ export class Comment extends Icon {
// TODO (#2917): It would be great if the comment could support line breaks. // TODO (#2917): It would be great if the comment could support line breaks.
this.paragraphElement_ = Bubble.textToDom(this.model.text ?? ''); this.paragraphElement_ = Bubble.textToDom(this.model.text ?? '');
this.bubble_ = Bubble.createNonEditableBubble( this.bubble_ = Bubble.createNonEditableBubble(
this.paragraphElement_, this.getBlock(), this.iconXY_ as Coordinate); this.paragraphElement_,
this.getBlock(),
this.iconXY_ as Coordinate
);
this.applyColour(); this.applyColour();
} }

View File

@@ -15,18 +15,16 @@ import type {ICopyable} from './interfaces/i_copyable.js';
import type {Workspace} from './workspace.js'; import type {Workspace} from './workspace.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
/** Database of all workspaces. */ /** Database of all workspaces. */
const WorkspaceDB_ = Object.create(null); const WorkspaceDB_ = Object.create(null);
/** /**
* Find the workspace with the specified ID. * Find the workspace with the specified ID.
* *
* @param id ID of workspace to find. * @param id ID of workspace to find.
* @returns The sought after workspace or null if not found. * @returns The sought after workspace or null if not found.
*/ */
export function getWorkspaceById(id: string): Workspace|null { export function getWorkspaceById(id: string): Workspace | null {
return WorkspaceDB_[id] || null; return WorkspaceDB_[id] || null;
} }
@@ -90,12 +88,12 @@ export function setMainWorkspace(workspace: Workspace) {
/** /**
* Currently selected copyable object. * Currently selected copyable object.
*/ */
let selected: ICopyable|null = null; let selected: ICopyable | null = null;
/** /**
* Returns the currently selected copyable object. * Returns the currently selected copyable object.
*/ */
export function getSelected(): ICopyable|null { export function getSelected(): ICopyable | null {
return selected; return selected;
} }
@@ -107,14 +105,14 @@ export function getSelected(): ICopyable|null {
* @param newSelection The newly selected block. * @param newSelection The newly selected block.
* @internal * @internal
*/ */
export function setSelected(newSelection: ICopyable|null) { export function setSelected(newSelection: ICopyable | null) {
selected = newSelection; selected = newSelection;
} }
/** /**
* Container element in which to render the WidgetDiv, DropDownDiv and Tooltip. * Container element in which to render the WidgetDiv, DropDownDiv and Tooltip.
*/ */
let parentContainer: Element|null; let parentContainer: Element | null;
/** /**
* Get the container element in which to render the WidgetDiv, DropDownDiv and * Get the container element in which to render the WidgetDiv, DropDownDiv and
@@ -122,7 +120,7 @@ let parentContainer: Element|null;
* *
* @returns The parent container. * @returns The parent container.
*/ */
export function getParentContainer(): Element|null { export function getParentContainer(): Element | null {
return parentContainer; return parentContainer;
} }
@@ -189,7 +187,9 @@ export const draggingConnections: Connection[] = [];
* @returns Map of types to type counts for descendants of the bock. * @returns Map of types to type counts for descendants of the bock.
*/ */
export function getBlockTypeCounts( export function getBlockTypeCounts(
block: Block, opt_stripFollowing?: boolean): {[key: string]: number} { block: Block,
opt_stripFollowing?: boolean
): {[key: string]: number} {
const typeCountsMap = Object.create(null); const typeCountsMap = Object.create(null);
const descendants = block.getDescendants(true); const descendants = block.getDescendants(true);
if (opt_stripFollowing) { if (opt_stripFollowing) {
@@ -199,7 +199,7 @@ export function getBlockTypeCounts(
descendants.splice(index, descendants.length - index); descendants.splice(index, descendants.length - index);
} }
} }
for (let i = 0, checkBlock; checkBlock = descendants[i]; i++) { for (let i = 0, checkBlock; (checkBlock = descendants[i]); i++) {
if (typeCountsMap[checkBlock.type]) { if (typeCountsMap[checkBlock.type]) {
typeCountsMap[checkBlock.type]++; typeCountsMap[checkBlock.type]++;
} else { } else {
@@ -218,7 +218,7 @@ export function getBlockTypeCounts(
* of jsonDef. * of jsonDef.
*/ */
function jsonInitFactory(jsonDef: AnyDuringMigration): () => void { function jsonInitFactory(jsonDef: AnyDuringMigration): () => void {
return function(this: Block) { return function (this: Block) {
this.jsonInit(jsonDef); this.jsonInit(jsonDef);
}; };
} }
@@ -249,7 +249,8 @@ function defineBlocksWithJsonArrayInternal(jsonArray: AnyDuringMigration[]) {
* definitions created. * definitions created.
*/ */
export function createBlockDefinitionsFromJsonArray( export function createBlockDefinitionsFromJsonArray(
jsonArray: AnyDuringMigration[]): {[key: string]: BlockDefinition} { jsonArray: AnyDuringMigration[]
): {[key: string]: BlockDefinition} {
const blocks: {[key: string]: BlockDefinition} = {}; const blocks: {[key: string]: BlockDefinition} = {};
for (let i = 0; i < jsonArray.length; i++) { for (let i = 0; i < jsonArray.length; i++) {
const elem = jsonArray[i]; const elem = jsonArray[i];
@@ -260,8 +261,9 @@ export function createBlockDefinitionsFromJsonArray(
const type = elem['type']; const type = elem['type'];
if (!type) { if (!type) {
console.warn( console.warn(
`Block definition #${i} in JSON array is missing a type attribute. ` + `Block definition #${i} in JSON array is missing a type attribute. ` +
'Skipping.'); 'Skipping.'
);
continue; continue;
} }
blocks[type] = {init: jsonInitFactory(elem)}; blocks[type] = {init: jsonInitFactory(elem)};

View File

@@ -19,7 +19,6 @@ import type {IDragTarget} from './interfaces/i_drag_target.js';
import type {IPositionable} from './interfaces/i_positionable.js'; import type {IPositionable} from './interfaces/i_positionable.js';
import * as arrayUtils from './utils/array.js'; import * as arrayUtils from './utils/array.js';
class Capability<_T> { class Capability<_T> {
static POSITIONABLE = new Capability<IPositionable>('positionable'); static POSITIONABLE = new Capability<IPositionable>('positionable');
static DRAG_TARGET = new Capability<IDragTarget>('drag_target'); static DRAG_TARGET = new Capability<IDragTarget>('drag_target');
@@ -67,8 +66,12 @@ export class ComponentManager {
const id = componentInfo.component.id; const id = componentInfo.component.id;
if (!opt_allowOverrides && this.componentData.has(id)) { if (!opt_allowOverrides && this.componentData.has(id)) {
throw Error( throw Error(
'Plugin "' + id + '" with capabilities "' + 'Plugin "' +
this.componentData.get(id)?.capabilities + '" already added.'); id +
'" with capabilities "' +
this.componentData.get(id)?.capabilities +
'" already added.'
);
} }
this.componentData.set(id, componentInfo); this.componentData.set(id, componentInfo);
const stringCapabilities = []; const stringCapabilities = [];
@@ -107,15 +110,20 @@ export class ComponentManager {
* @param id The ID of the component to add the capability to. * @param id The ID of the component to add the capability to.
* @param capability The capability to add. * @param capability The capability to add.
*/ */
addCapability<T>(id: string, capability: string|Capability<T>) { addCapability<T>(id: string, capability: string | Capability<T>) {
if (!this.getComponent(id)) { if (!this.getComponent(id)) {
throw Error( throw Error(
'Cannot add capability, "' + capability + '". Plugin "' + id + 'Cannot add capability, "' +
'" has not been added to the ComponentManager'); capability +
'". Plugin "' +
id +
'" has not been added to the ComponentManager'
);
} }
if (this.hasCapability(id, capability)) { if (this.hasCapability(id, capability)) {
console.warn( console.warn(
'Plugin "' + id + 'already has capability "' + capability + '"'); 'Plugin "' + id + 'already has capability "' + capability + '"'
);
return; return;
} }
capability = `${capability}`.toLowerCase(); capability = `${capability}`.toLowerCase();
@@ -129,16 +137,24 @@ export class ComponentManager {
* @param id The ID of the component to remove the capability from. * @param id The ID of the component to remove the capability from.
* @param capability The capability to remove. * @param capability The capability to remove.
*/ */
removeCapability<T>(id: string, capability: string|Capability<T>) { removeCapability<T>(id: string, capability: string | Capability<T>) {
if (!this.getComponent(id)) { if (!this.getComponent(id)) {
throw Error( throw Error(
'Cannot remove capability, "' + capability + '". Plugin "' + id + 'Cannot remove capability, "' +
'" has not been added to the ComponentManager'); capability +
'". Plugin "' +
id +
'" has not been added to the ComponentManager'
);
} }
if (!this.hasCapability(id, capability)) { if (!this.hasCapability(id, capability)) {
console.warn( console.warn(
'Plugin "' + id + 'doesn\'t have capability "' + capability + 'Plugin "' +
'" to remove'); id +
'doesn\'t have capability "' +
capability +
'" to remove'
);
return; return;
} }
capability = `${capability}`.toLowerCase(); capability = `${capability}`.toLowerCase();
@@ -153,10 +169,12 @@ export class ComponentManager {
* @param capability The capability to check for. * @param capability The capability to check for.
* @returns Whether the component has the capability. * @returns Whether the component has the capability.
*/ */
hasCapability<T>(id: string, capability: string|Capability<T>): boolean { hasCapability<T>(id: string, capability: string | Capability<T>): boolean {
capability = `${capability}`.toLowerCase(); capability = `${capability}`.toLowerCase();
return this.componentData.has(id) && return (
this.componentData.get(id)!.capabilities.indexOf(capability) !== -1; this.componentData.has(id) &&
this.componentData.get(id)!.capabilities.indexOf(capability) !== -1
);
} }
/** /**
@@ -165,7 +183,7 @@ export class ComponentManager {
* @param id The ID of the component to get. * @param id The ID of the component to get.
* @returns The component with the given name or undefined if not found. * @returns The component with the given name or undefined if not found.
*/ */
getComponent(id: string): IComponent|undefined { getComponent(id: string): IComponent | undefined {
return this.componentData.get(id)?.component; return this.componentData.get(id)?.component;
} }
@@ -177,7 +195,9 @@ export class ComponentManager {
* @returns The components that match the specified capability. * @returns The components that match the specified capability.
*/ */
getComponents<T extends IComponent>( getComponents<T extends IComponent>(
capability: string|Capability<T>, sorted: boolean): T[] { capability: string | Capability<T>,
sorted: boolean
): T[] {
capability = `${capability}`.toLowerCase(); capability = `${capability}`.toLowerCase();
const componentIds = this.capabilityToComponentIds.get(capability); const componentIds = this.capabilityToComponentIds.get(capability);
if (!componentIds) { if (!componentIds) {
@@ -189,10 +209,10 @@ export class ComponentManager {
componentIds.forEach((id) => { componentIds.forEach((id) => {
componentDataList.push(this.componentData.get(id)!); componentDataList.push(this.componentData.get(id)!);
}); });
componentDataList.sort(function(a, b) { componentDataList.sort(function (a, b) {
return a.weight - b.weight; return a.weight - b.weight;
}); });
componentDataList.forEach(function(componentDatum) { componentDataList.forEach(function (componentDatum) {
components.push(componentDatum.component as T); components.push(componentDatum.component as T);
}); });
} else { } else {
@@ -208,7 +228,7 @@ export namespace ComponentManager {
/** An object storing component information. */ /** An object storing component information. */
export interface ComponentDatum { export interface ComponentDatum {
component: IComponent; component: IComponent;
capabilities: Array<string|Capability<IComponent>>; capabilities: Array<string | Capability<IComponent>>;
weight: number; weight: number;
} }
} }

View File

@@ -7,7 +7,6 @@
import * as goog from '../closure/goog/goog.js'; import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.config'); goog.declareModuleId('Blockly.config');
/** /**
* All the values that we expect developers to be able to change * All the values that we expect developers to be able to change
* before injecting Blockly. * before injecting Blockly.

View File

@@ -22,7 +22,6 @@ import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
import * as blocks from './serialization/blocks.js'; import * as blocks from './serialization/blocks.js';
import * as Xml from './xml.js'; import * as Xml from './xml.js';
/** /**
* Class for a connection between blocks. * Class for a connection between blocks.
*/ */
@@ -41,7 +40,7 @@ export class Connection implements IASTNodeLocationWithBlock {
protected sourceBlock_: Block; protected sourceBlock_: Block;
/** Connection this connection connects to. Null if not connected. */ /** Connection this connection connects to. Null if not connected. */
targetConnection: Connection|null = null; targetConnection: Connection | null = null;
/** /**
* Has this connection been disposed of? * Has this connection been disposed of?
@@ -51,10 +50,10 @@ export class Connection implements IASTNodeLocationWithBlock {
disposed = false; disposed = false;
/** List of compatible value types. Null if all types are compatible. */ /** List of compatible value types. Null if all types are compatible. */
private check: string[]|null = null; private check: string[] | null = null;
/** DOM representation of a shadow block, or null if none. */ /** DOM representation of a shadow block, or null if none. */
private shadowDom: Element|null = null; private shadowDom: Element | null = null;
/** /**
* Horizontal location of this connection. * Horizontal location of this connection.
@@ -70,7 +69,7 @@ export class Connection implements IASTNodeLocationWithBlock {
*/ */
y = 0; y = 0;
private shadowState: blocks.State|null = null; private shadowState: blocks.State | null = null;
/** /**
* @param source The block establishing this connection. * @param source The block establishing this connection.
@@ -113,8 +112,9 @@ export class Connection implements IASTNodeLocationWithBlock {
// Connect the new connection to the parent. // Connect the new connection to the parent.
let event; let event;
if (eventUtils.isEnabled()) { if (eventUtils.isEnabled()) {
event = event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(
new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock) as BlockMove; childBlock
) as BlockMove;
event.setReason(['connect']); event.setReason(['connect']);
} }
connectReciprocally(this, childConnection); connectReciprocally(this, childConnection);
@@ -126,11 +126,15 @@ export class Connection implements IASTNodeLocationWithBlock {
// Deal with the orphan if it exists. // Deal with the orphan if it exists.
if (orphan) { if (orphan) {
const orphanConnection = this.type === INPUT ? orphan.outputConnection : const orphanConnection =
orphan.previousConnection; this.type === INPUT
? orphan.outputConnection
: orphan.previousConnection;
if (!orphanConnection) return; if (!orphanConnection) return;
const connection = Connection.getConnectionForOrphanedConnection( const connection = Connection.getConnectionForOrphanedConnection(
childBlock, orphanConnection); childBlock,
orphanConnection
);
if (connection) { if (connection) {
orphanConnection.connect(connection); orphanConnection.connect(connection);
} else { } else {
@@ -176,8 +180,10 @@ export class Connection implements IASTNodeLocationWithBlock {
* @returns True if connection faces down or right. * @returns True if connection faces down or right.
*/ */
isSuperior(): boolean { isSuperior(): boolean {
return this.type === ConnectionType.INPUT_VALUE || return (
this.type === ConnectionType.NEXT_STATEMENT; this.type === ConnectionType.INPUT_VALUE ||
this.type === ConnectionType.NEXT_STATEMENT
);
} }
/** /**
@@ -259,7 +265,7 @@ export class Connection implements IASTNodeLocationWithBlock {
*/ */
protected disconnectInternal(setParent = true) { protected disconnectInternal(setParent = true) {
const {parentConnection, childConnection} = const {parentConnection, childConnection} =
this.getParentAndChildConnections(); this.getParentAndChildConnections();
if (!parentConnection || !childConnection) { if (!parentConnection || !childConnection) {
throw Error('Source connection not connected.'); throw Error('Source connection not connected.');
} }
@@ -272,7 +278,8 @@ export class Connection implements IASTNodeLocationWithBlock {
let event; let event;
if (eventUtils.isEnabled()) { if (eventUtils.isEnabled()) {
event = new (eventUtils.get(eventUtils.BLOCK_MOVE))( event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(
childConnection.getSourceBlock()) as BlockMove; childConnection.getSourceBlock()
) as BlockMove;
event.setReason(['disconnect']); event.setReason(['disconnect']);
} }
const otherConnection = this.targetConnection; const otherConnection = this.targetConnection;
@@ -301,8 +308,10 @@ export class Connection implements IASTNodeLocationWithBlock {
* @returns The parent connection and child connection, given this connection * @returns The parent connection and child connection, given this connection
* and the connection it is connected to. * and the connection it is connected to.
*/ */
protected getParentAndChildConnections(): protected getParentAndChildConnections(): {
{parentConnection?: Connection, childConnection?: Connection} { parentConnection?: Connection;
childConnection?: Connection;
} {
if (!this.targetConnection) return {}; if (!this.targetConnection) return {};
if (this.isSuperior()) { if (this.isSuperior()) {
return { return {
@@ -329,7 +338,7 @@ export class Connection implements IASTNodeLocationWithBlock {
* *
* @returns The connected block or null if none is connected. * @returns The connected block or null if none is connected.
*/ */
targetBlock(): Block|null { targetBlock(): Block | null {
if (this.isConnected()) { if (this.isConnected()) {
return this.targetConnection?.getSourceBlock() ?? null; return this.targetConnection?.getSourceBlock() ?? null;
} }
@@ -341,10 +350,15 @@ export class Connection implements IASTNodeLocationWithBlock {
*/ */
protected onCheckChanged_() { protected onCheckChanged_() {
// The new value type may not be compatible with the existing connection. // The new value type may not be compatible with the existing connection.
if (this.isConnected() && if (
(!this.targetConnection || this.isConnected() &&
!this.getConnectionChecker().canConnect( (!this.targetConnection ||
this, this.targetConnection, false))) { !this.getConnectionChecker().canConnect(
this,
this.targetConnection,
false
))
) {
const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_; const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
child!.unplug(); child!.unplug();
} }
@@ -357,7 +371,7 @@ export class Connection implements IASTNodeLocationWithBlock {
* types are compatible. * types are compatible.
* @returns The connection being modified (to allow chaining). * @returns The connection being modified (to allow chaining).
*/ */
setCheck(check: string|string[]|null): Connection { setCheck(check: string | string[] | null): Connection {
if (check) { if (check) {
if (!Array.isArray(check)) { if (!Array.isArray(check)) {
check = [check]; check = [check];
@@ -376,7 +390,7 @@ export class Connection implements IASTNodeLocationWithBlock {
* @returns List of compatible value types. * @returns List of compatible value types.
* Null if all types are compatible. * Null if all types are compatible.
*/ */
getCheck(): string[]|null { getCheck(): string[] | null {
return this.check; return this.check;
} }
@@ -385,7 +399,7 @@ export class Connection implements IASTNodeLocationWithBlock {
* *
* @param shadowDom DOM representation of a block or null. * @param shadowDom DOM representation of a block or null.
*/ */
setShadowDom(shadowDom: Element|null) { setShadowDom(shadowDom: Element | null) {
this.setShadowStateInternal({shadowDom}); this.setShadowStateInternal({shadowDom});
} }
@@ -398,10 +412,10 @@ export class Connection implements IASTNodeLocationWithBlock {
* just returned. * just returned.
* @returns Shadow DOM representation of a block or null. * @returns Shadow DOM representation of a block or null.
*/ */
getShadowDom(returnCurrent?: boolean): Element|null { getShadowDom(returnCurrent?: boolean): Element | null {
return returnCurrent && this.targetBlock()!.isShadow() ? return returnCurrent && this.targetBlock()!.isShadow()
Xml.blockToDom((this.targetBlock() as Block)) as Element : ? (Xml.blockToDom(this.targetBlock() as Block) as Element)
this.shadowDom; : this.shadowDom;
} }
/** /**
@@ -409,7 +423,7 @@ export class Connection implements IASTNodeLocationWithBlock {
* *
* @param shadowState An state represetation of the block or null. * @param shadowState An state represetation of the block or null.
*/ */
setShadowState(shadowState: blocks.State|null) { setShadowState(shadowState: blocks.State | null) {
this.setShadowStateInternal({shadowState}); this.setShadowStateInternal({shadowState});
} }
@@ -423,7 +437,7 @@ export class Connection implements IASTNodeLocationWithBlock {
* returned. * returned.
* @returns Serialized object representation of the block, or null. * @returns Serialized object representation of the block, or null.
*/ */
getShadowState(returnCurrent?: boolean): blocks.State|null { getShadowState(returnCurrent?: boolean): blocks.State | null {
if (returnCurrent && this.targetBlock() && this.targetBlock()!.isShadow()) { if (returnCurrent && this.targetBlock() && this.targetBlock()!.isShadow()) {
return blocks.save(this.targetBlock() as Block); return blocks.save(this.targetBlock() as Block);
} }
@@ -454,7 +468,7 @@ export class Connection implements IASTNodeLocationWithBlock {
* exists. * exists.
* @internal * @internal
*/ */
getParentInput(): Input|null { getParentInput(): Input | null {
let parentInput = null; let parentInput = null;
const inputs = this.sourceBlock_.inputList; const inputs = this.sourceBlock_.inputList;
for (let i = 0; i < inputs.length; i++) { for (let i = 0; i < inputs.length; i++) {
@@ -486,7 +500,7 @@ export class Connection implements IASTNodeLocationWithBlock {
msg = 'Next Connection of '; msg = 'Next Connection of ';
} else { } else {
let parentInput = null; let parentInput = null;
for (let i = 0, input; input = block.inputList[i]; i++) { for (let i = 0, input; (input = block.inputList[i]); i++) {
if (input.connection === this) { if (input.connection === this) {
parentInput = input; parentInput = input;
break; break;
@@ -508,8 +522,10 @@ export class Connection implements IASTNodeLocationWithBlock {
* *
* @returns The state of both the shadowDom_ and shadowState_ properties. * @returns The state of both the shadowDom_ and shadowState_ properties.
*/ */
private stashShadowState(): private stashShadowState(): {
{shadowDom: Element|null, shadowState: blocks.State|null} { shadowDom: Element | null;
shadowState: blocks.State | null;
} {
const shadowDom = this.getShadowDom(true); const shadowDom = this.getShadowDom(true);
const shadowState = this.getShadowState(true); const shadowState = this.getShadowState(true);
// Set to null so it doesn't respawn. // Set to null so it doesn't respawn.
@@ -524,9 +540,12 @@ export class Connection implements IASTNodeLocationWithBlock {
* @param param0 The state to reapply to the shadowDom_ and shadowState_ * @param param0 The state to reapply to the shadowDom_ and shadowState_
* properties. * properties.
*/ */
private applyShadowState({shadowDom, shadowState}: { private applyShadowState({
shadowDom: Element|null, shadowDom,
shadowState: blocks.State|null shadowState,
}: {
shadowDom: Element | null;
shadowState: blocks.State | null;
}) { }) {
this.shadowDom = shadowDom; this.shadowDom = shadowDom;
this.shadowState = shadowState; this.shadowState = shadowState;
@@ -537,9 +556,12 @@ export class Connection implements IASTNodeLocationWithBlock {
* *
* @param param0 The state to set the shadow of this connection to. * @param param0 The state to set the shadow of this connection to.
*/ */
private setShadowStateInternal({shadowDom = null, shadowState = null}: { private setShadowStateInternal({
shadowDom?: Element|null, shadowDom = null,
shadowState?: blocks.State|null shadowState = null,
}: {
shadowDom?: Element | null;
shadowState?: blocks.State | null;
} = {}) { } = {}) {
// One or both of these should always be null. // One or both of these should always be null.
// If neither is null, the shadowState will get priority. // If neither is null, the shadowState will get priority.
@@ -577,11 +599,11 @@ export class Connection implements IASTNodeLocationWithBlock {
* @returns The shadow block that was created, or null if both the * @returns The shadow block that was created, or null if both the
* shadowState_ and shadowDom_ are null. * shadowState_ and shadowDom_ are null.
*/ */
private createShadowBlock(attemptToConnect: boolean): Block|null { private createShadowBlock(attemptToConnect: boolean): Block | null {
const parentBlock = this.getSourceBlock(); const parentBlock = this.getSourceBlock();
const shadowState = this.getShadowState(); const shadowState = this.getShadowState();
const shadowDom = this.getShadowDom(); const shadowDom = this.getShadowDom();
if (parentBlock.isDeadOrDying() || !shadowState && !shadowDom) { if (parentBlock.isDeadOrDying() || (!shadowState && !shadowDom)) {
return null; return null;
} }
@@ -614,7 +636,8 @@ export class Connection implements IASTNodeLocationWithBlock {
} }
} else { } else {
throw new Error( throw new Error(
'Cannot connect a shadow block to a previous/output connection'); 'Cannot connect a shadow block to a previous/output connection'
);
} }
} }
return blockShadow; return blockShadow;
@@ -628,7 +651,7 @@ export class Connection implements IASTNodeLocationWithBlock {
* *
* @param shadow The shadow to serialize, or null. * @param shadow The shadow to serialize, or null.
*/ */
private serializeShadow(shadow: Block|null) { private serializeShadow(shadow: Block | null) {
if (!shadow) { if (!shadow) {
return; return;
} }
@@ -646,10 +669,14 @@ export class Connection implements IASTNodeLocationWithBlock {
* @returns The suitable connection point on the chain of blocks, or null. * @returns The suitable connection point on the chain of blocks, or null.
*/ */
static getConnectionForOrphanedConnection( static getConnectionForOrphanedConnection(
startBlock: Block, orphanConnection: Connection): Connection|null { startBlock: Block,
orphanConnection: Connection
): Connection | null {
if (orphanConnection.type === ConnectionType.OUTPUT_VALUE) { if (orphanConnection.type === ConnectionType.OUTPUT_VALUE) {
return getConnectionForOrphanedOutput( return getConnectionForOrphanedOutput(
startBlock, orphanConnection.getSourceBlock()); startBlock,
orphanConnection.getSourceBlock()
);
} }
// Otherwise we're dealing with a stack. // Otherwise we're dealing with a stack.
const connection = startBlock.lastConnectionInStack(true); const connection = startBlock.lastConnectionInStack(true);
@@ -684,17 +711,19 @@ function connectReciprocally(first: Connection, second: Connection) {
* @param orphanBlock The inferior block. * @param orphanBlock The inferior block.
* @returns The suitable connection point on 'block', or null. * @returns The suitable connection point on 'block', or null.
*/ */
function getSingleConnection(block: Block, orphanBlock: Block): Connection| function getSingleConnection(
null { block: Block,
orphanBlock: Block
): Connection | null {
let foundConnection = null; let foundConnection = null;
const output = orphanBlock.outputConnection; const output = orphanBlock.outputConnection;
const typeChecker = output?.getConnectionChecker(); const typeChecker = output?.getConnectionChecker();
for (let i = 0, input; input = block.inputList[i]; i++) { for (let i = 0, input; (input = block.inputList[i]); i++) {
const connection = input.connection; const connection = input.connection;
if (connection && typeChecker?.canConnect(output, connection, false)) { if (connection && typeChecker?.canConnect(output, connection, false)) {
if (foundConnection) { if (foundConnection) {
return null; // More than one connection. return null; // More than one connection.
} }
foundConnection = connection; foundConnection = connection;
} }
@@ -714,10 +743,12 @@ function getSingleConnection(block: Block, orphanBlock: Block): Connection|
* @returns The suitable connection point on the chain of blocks, or null. * @returns The suitable connection point on the chain of blocks, or null.
*/ */
function getConnectionForOrphanedOutput( function getConnectionForOrphanedOutput(
startBlock: Block, orphanBlock: Block): Connection|null { startBlock: Block,
let newBlock: Block|null = startBlock; orphanBlock: Block
): Connection | null {
let newBlock: Block | null = startBlock;
let connection; let connection;
while (connection = getSingleConnection(newBlock, orphanBlock)) { while ((connection = getSingleConnection(newBlock, orphanBlock))) {
newBlock = connection.targetBlock(); newBlock = connection.targetBlock();
if (!newBlock || newBlock.isShadow()) { if (!newBlock || newBlock.isShadow()) {
return connection; return connection;

View File

@@ -21,7 +21,6 @@ import * as internalConstants from './internal_constants.js';
import * as registry from './registry.js'; import * as registry from './registry.js';
import type {RenderedConnection} from './rendered_connection.js'; import type {RenderedConnection} from './rendered_connection.js';
/** /**
* Class for connection type checking logic. * Class for connection type checking logic.
*/ */
@@ -38,10 +37,15 @@ export class ConnectionChecker implements IConnectionChecker {
* @returns Whether the connection is legal. * @returns Whether the connection is legal.
*/ */
canConnect( canConnect(
a: Connection|null, b: Connection|null, isDragging: boolean, a: Connection | null,
opt_distance?: number): boolean { b: Connection | null,
return this.canConnectWithReason(a, b, isDragging, opt_distance) === isDragging: boolean,
Connection.CAN_CONNECT; opt_distance?: number
): boolean {
return (
this.canConnectWithReason(a, b, isDragging, opt_distance) ===
Connection.CAN_CONNECT
);
} }
/** /**
@@ -57,8 +61,11 @@ export class ConnectionChecker implements IConnectionChecker {
* otherwise. * otherwise.
*/ */
canConnectWithReason( canConnectWithReason(
a: Connection|null, b: Connection|null, isDragging: boolean, a: Connection | null,
opt_distance?: number): number { b: Connection | null,
isDragging: boolean,
opt_distance?: number
): number {
const safety = this.doSafetyChecks(a, b); const safety = this.doSafetyChecks(a, b);
if (safety !== Connection.CAN_CONNECT) { if (safety !== Connection.CAN_CONNECT) {
return safety; return safety;
@@ -71,10 +78,14 @@ export class ConnectionChecker implements IConnectionChecker {
return Connection.REASON_CHECKS_FAILED; return Connection.REASON_CHECKS_FAILED;
} }
if (isDragging && if (
!this.doDragChecks( isDragging &&
a as RenderedConnection, b as RenderedConnection, !this.doDragChecks(
opt_distance || 0)) { a as RenderedConnection,
b as RenderedConnection,
opt_distance || 0
)
) {
return Connection.REASON_DRAG_CHECKS_FAILED; return Connection.REASON_DRAG_CHECKS_FAILED;
} }
@@ -89,8 +100,11 @@ export class ConnectionChecker implements IConnectionChecker {
* @param b The second of the two connections being checked. * @param b The second of the two connections being checked.
* @returns A developer-readable error string. * @returns A developer-readable error string.
*/ */
getErrorMessage(errorCode: number, a: Connection|null, b: Connection|null): getErrorMessage(
string { errorCode: number,
a: Connection | null,
b: Connection | null
): string {
switch (errorCode) { switch (errorCode) {
case Connection.REASON_SELF_CONNECTION: case Connection.REASON_SELF_CONNECTION:
return 'Attempted to connect a block to itself.'; return 'Attempted to connect a block to itself.';
@@ -105,8 +119,12 @@ export class ConnectionChecker implements IConnectionChecker {
const connOne = a!; const connOne = a!;
const connTwo = b!; const connTwo = b!;
let msg = 'Connection checks failed. '; let msg = 'Connection checks failed. ';
msg += connOne + ' expected ' + connOne.getCheck() + ', found ' + msg +=
connTwo.getCheck(); connOne +
' expected ' +
connOne.getCheck() +
', found ' +
connTwo.getCheck();
return msg; return msg;
} }
case Connection.REASON_SHADOW_PARENT: case Connection.REASON_SHADOW_PARENT:
@@ -128,7 +146,7 @@ export class ConnectionChecker implements IConnectionChecker {
* @param b The second of the connections to check. * @param b The second of the connections to check.
* @returns An enum with the reason this connection is safe or unsafe. * @returns An enum with the reason this connection is safe or unsafe.
*/ */
doSafetyChecks(a: Connection|null, b: Connection|null): number { doSafetyChecks(a: Connection | null, b: Connection | null): number {
if (!a || !b) { if (!a || !b) {
return Connection.REASON_TARGET_NULL; return Connection.REASON_TARGET_NULL;
} }
@@ -150,22 +168,25 @@ export class ConnectionChecker implements IConnectionChecker {
if (superiorBlock === inferiorBlock) { if (superiorBlock === inferiorBlock) {
return Connection.REASON_SELF_CONNECTION; return Connection.REASON_SELF_CONNECTION;
} else if ( } else if (
inferiorConnection.type !== inferiorConnection.type !==
internalConstants.OPPOSITE_TYPE[superiorConnection.type]) { internalConstants.OPPOSITE_TYPE[superiorConnection.type]
) {
return Connection.REASON_WRONG_TYPE; return Connection.REASON_WRONG_TYPE;
} else if (superiorBlock.workspace !== inferiorBlock.workspace) { } else if (superiorBlock.workspace !== inferiorBlock.workspace) {
return Connection.REASON_DIFFERENT_WORKSPACES; return Connection.REASON_DIFFERENT_WORKSPACES;
} else if (superiorBlock.isShadow() && !inferiorBlock.isShadow()) { } else if (superiorBlock.isShadow() && !inferiorBlock.isShadow()) {
return Connection.REASON_SHADOW_PARENT; return Connection.REASON_SHADOW_PARENT;
} else if ( } else if (
inferiorConnection.type === ConnectionType.OUTPUT_VALUE && inferiorConnection.type === ConnectionType.OUTPUT_VALUE &&
inferiorBlock.previousConnection && inferiorBlock.previousConnection &&
inferiorBlock.previousConnection.isConnected()) { inferiorBlock.previousConnection.isConnected()
) {
return Connection.REASON_PREVIOUS_AND_OUTPUT; return Connection.REASON_PREVIOUS_AND_OUTPUT;
} else if ( } else if (
inferiorConnection.type === ConnectionType.PREVIOUS_STATEMENT && inferiorConnection.type === ConnectionType.PREVIOUS_STATEMENT &&
inferiorBlock.outputConnection && inferiorBlock.outputConnection &&
inferiorBlock.outputConnection.isConnected()) { inferiorBlock.outputConnection.isConnected()
) {
return Connection.REASON_PREVIOUS_AND_OUTPUT; return Connection.REASON_PREVIOUS_AND_OUTPUT;
} }
return Connection.CAN_CONNECT; return Connection.CAN_CONNECT;
@@ -206,8 +227,11 @@ export class ConnectionChecker implements IConnectionChecker {
* @param distance The maximum allowable distance between connections. * @param distance The maximum allowable distance between connections.
* @returns True if the connection is allowed during a drag. * @returns True if the connection is allowed during a drag.
*/ */
doDragChecks(a: RenderedConnection, b: RenderedConnection, distance: number): doDragChecks(
boolean { a: RenderedConnection,
b: RenderedConnection,
distance: number
): boolean {
if (a.distanceFrom(b) > distance) { if (a.distanceFrom(b) > distance) {
return false; return false;
} }
@@ -223,8 +247,10 @@ export class ConnectionChecker implements IConnectionChecker {
case ConnectionType.OUTPUT_VALUE: { case ConnectionType.OUTPUT_VALUE: {
// Don't offer to connect an already connected left (male) value plug to // Don't offer to connect an already connected left (male) value plug to
// an available right (female) value plug. // an available right (female) value plug.
if (b.isConnected() && !b.targetBlock()!.isInsertionMarker() || if (
a.isConnected()) { (b.isConnected() && !b.targetBlock()!.isInsertionMarker()) ||
a.isConnected()
) {
return false; return false;
} }
break; break;
@@ -233,8 +259,11 @@ export class ConnectionChecker implements IConnectionChecker {
// Offering to connect the left (male) of a value block to an already // Offering to connect the left (male) of a value block to an already
// connected value pair is ok, we'll splice it in. // connected value pair is ok, we'll splice it in.
// However, don't offer to splice into an immovable block. // However, don't offer to splice into an immovable block.
if (b.isConnected() && !b.targetBlock()!.isMovable() && if (
!b.targetBlock()!.isShadow()) { b.isConnected() &&
!b.targetBlock()!.isMovable() &&
!b.targetBlock()!.isShadow()
) {
return false; return false;
} }
break; break;
@@ -244,15 +273,22 @@ export class ConnectionChecker implements IConnectionChecker {
// the stack. But covering up a shadow block or stack of shadow blocks // the stack. But covering up a shadow block or stack of shadow blocks
// is fine. Similarly, replacing a terminal statement with another // is fine. Similarly, replacing a terminal statement with another
// terminal statement is allowed. // terminal statement is allowed.
if (b.isConnected() && !a.getSourceBlock().nextConnection && if (
!b.targetBlock()!.isShadow() && b.targetBlock()!.nextConnection) { b.isConnected() &&
!a.getSourceBlock().nextConnection &&
!b.targetBlock()!.isShadow() &&
b.targetBlock()!.nextConnection
) {
return false; return false;
} }
// Don't offer to splice into a stack where the connected block is // Don't offer to splice into a stack where the connected block is
// immovable, unless the block is a shadow block. // immovable, unless the block is a shadow block.
if (b.targetBlock() && !b.targetBlock()!.isMovable() && if (
!b.targetBlock()!.isShadow()) { b.targetBlock() &&
!b.targetBlock()!.isMovable() &&
!b.targetBlock()!.isShadow()
) {
return false; return false;
} }
break; break;
@@ -307,4 +343,7 @@ export class ConnectionChecker implements IConnectionChecker {
} }
registry.register( registry.register(
registry.Type.CONNECTION_CHECKER, registry.DEFAULT, ConnectionChecker); registry.Type.CONNECTION_CHECKER,
registry.DEFAULT,
ConnectionChecker
);

View File

@@ -19,7 +19,6 @@ import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
import type {RenderedConnection} from './rendered_connection.js'; import type {RenderedConnection} from './rendered_connection.js';
import type {Coordinate} from './utils/coordinate.js'; import type {Coordinate} from './utils/coordinate.js';
/** /**
* Database of connections. * Database of connections.
* Connections are stored in order of their vertical component. This way * Connections are stored in order of their vertical component. This way
@@ -58,8 +57,10 @@ export class ConnectionDB {
* @returns The index of the connection, or -1 if the connection was not * @returns The index of the connection, or -1 if the connection was not
* found. * found.
*/ */
private findIndexOfConnection(conn: RenderedConnection, yPos: number): private findIndexOfConnection(
number { conn: RenderedConnection,
yPos: number
): number {
if (!this.connections.length) { if (!this.connections.length) {
return -1; return -1;
} }
@@ -81,8 +82,10 @@ export class ConnectionDB {
} }
pointer = bestGuess; pointer = bestGuess;
while (pointer < this.connections.length && while (
this.connections[pointer].y === yPos) { pointer < this.connections.length &&
this.connections[pointer].y === yPos
) {
if (this.connections[pointer] === conn) { if (this.connections[pointer] === conn) {
return pointer; return pointer;
} }
@@ -140,8 +143,10 @@ export class ConnectionDB {
* @param maxRadius The maximum radius to another connection. * @param maxRadius The maximum radius to another connection.
* @returns List of connections. * @returns List of connections.
*/ */
getNeighbours(connection: RenderedConnection, maxRadius: number): getNeighbours(
RenderedConnection[] { connection: RenderedConnection,
maxRadius: number
): RenderedConnection[] {
const db = this.connections; const db = this.connections;
const currentX = connection.x; const currentX = connection.x;
const currentY = connection.y; const currentY = connection.y;
@@ -218,8 +223,10 @@ export class ConnectionDB {
* connection or null, and 'radius' which is the distance. * connection or null, and 'radius' which is the distance.
*/ */
searchForClosest( searchForClosest(
conn: RenderedConnection, maxRadius: number, conn: RenderedConnection,
dxy: Coordinate): {connection: RenderedConnection|null, radius: number} { maxRadius: number,
dxy: Coordinate
): {connection: RenderedConnection | null; radius: number} {
if (!this.connections.length) { if (!this.connections.length) {
// Don't bother. // Don't bother.
return {connection: null, radius: maxRadius}; return {connection: null, radius: maxRadius};
@@ -253,8 +260,10 @@ export class ConnectionDB {
} }
let pointerMax = closestIndex; let pointerMax = closestIndex;
while (pointerMax < this.connections.length && while (
this.isInYRange(pointerMax, conn.y, maxRadius)) { pointerMax < this.connections.length &&
this.isInYRange(pointerMax, conn.y, maxRadius)
) {
temp = this.connections[pointerMax]; temp = this.connections[pointerMax];
if (this.connectionChecker.canConnect(conn, temp, true, bestRadius)) { if (this.connectionChecker.canConnect(conn, temp, true, bestRadius)) {
bestConnection = temp; bestConnection = temp;

View File

@@ -7,7 +7,6 @@
import * as goog from '../closure/goog/goog.js'; import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ConnectionType'); goog.declareModuleId('Blockly.ConnectionType');
/** /**
* Enum for the type of a connection or input. * Enum for the type of a connection or input.
*/ */
@@ -19,5 +18,5 @@ export enum ConnectionType {
// A down-facing block stack. E.g. 'if-do' or 'else'. // A down-facing block stack. E.g. 'if-do' or 'else'.
NEXT_STATEMENT, NEXT_STATEMENT,
// An up-facing block stack. E.g. 'break out of loop'. // An up-facing block stack. E.g. 'break out of loop'.
PREVIOUS_STATEMENT PREVIOUS_STATEMENT,
} }

View File

@@ -7,7 +7,6 @@
import * as goog from '../closure/goog/goog.js'; import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.constants'); goog.declareModuleId('Blockly.constants');
/** /**
* The language-neutral ID given to the collapsed input. * The language-neutral ID given to the collapsed input.
*/ */

View File

@@ -13,7 +13,10 @@ import * as browserEvents from './browser_events.js';
import * as clipboard from './clipboard.js'; import * as clipboard from './clipboard.js';
import {config} from './config.js'; import {config} from './config.js';
import * as dom from './utils/dom.js'; import * as dom from './utils/dom.js';
import type {ContextMenuOption, LegacyContextMenuOption} from './contextmenu_registry.js'; import type {
ContextMenuOption,
LegacyContextMenuOption,
} from './contextmenu_registry.js';
import * as eventUtils from './events/utils.js'; import * as eventUtils from './events/utils.js';
import {Menu} from './menu.js'; import {Menu} from './menu.js';
import {MenuItem} from './menuitem.js'; import {MenuItem} from './menuitem.js';
@@ -27,11 +30,10 @@ import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
import * as Xml from './xml.js'; import * as Xml from './xml.js';
/** /**
* Which block is the context menu attached to? * Which block is the context menu attached to?
*/ */
let currentBlock: Block|null = null; let currentBlock: Block | null = null;
const dummyOwner = {}; const dummyOwner = {};
@@ -40,7 +42,7 @@ const dummyOwner = {};
* *
* @returns The block the context menu is attached to. * @returns The block the context menu is attached to.
*/ */
export function getCurrentBlock(): Block|null { export function getCurrentBlock(): Block | null {
return currentBlock; return currentBlock;
} }
@@ -49,14 +51,14 @@ export function getCurrentBlock(): Block|null {
* *
* @param block The block the context menu is attached to. * @param block The block the context menu is attached to.
*/ */
export function setCurrentBlock(block: Block|null) { export function setCurrentBlock(block: Block | null) {
currentBlock = block; currentBlock = block;
} }
/** /**
* Menu object. * Menu object.
*/ */
let menu_: Menu|null = null; let menu_: Menu | null = null;
/** /**
* Construct the menu based on the list of options and show the menu. * Construct the menu based on the list of options and show the menu.
@@ -66,8 +68,10 @@ let menu_: Menu|null = null;
* @param rtl True if RTL, false if LTR. * @param rtl True if RTL, false if LTR.
*/ */
export function show( export function show(
e: Event, options: (ContextMenuOption|LegacyContextMenuOption)[], e: Event,
rtl: boolean) { options: (ContextMenuOption | LegacyContextMenuOption)[],
rtl: boolean
) {
WidgetDiv.show(dummyOwner, rtl, dispose); WidgetDiv.show(dummyOwner, rtl, dispose);
if (!options.length) { if (!options.length) {
hide(); hide();
@@ -79,10 +83,10 @@ export function show(
position_(menu, e, rtl); position_(menu, e, rtl);
// 1ms delay is required for focusing on context menus because some other // 1ms delay is required for focusing on context menus because some other
// mouse event is still waiting in the queue and clears focus. // mouse event is still waiting in the queue and clears focus.
setTimeout(function() { setTimeout(function () {
menu.focus(); menu.focus();
}, 1); }, 1);
currentBlock = null; // May be set by Blockly.Block. currentBlock = null; // May be set by Blockly.Block.
} }
/** /**
@@ -93,8 +97,9 @@ export function show(
* @returns The menu that will be shown on right click. * @returns The menu that will be shown on right click.
*/ */
function populate_( function populate_(
options: (ContextMenuOption|LegacyContextMenuOption)[], options: (ContextMenuOption | LegacyContextMenuOption)[],
rtl: boolean): Menu { rtl: boolean
): Menu {
/* Here's what one option object looks like: /* Here's what one option object looks like:
{text: 'Make It So', {text: 'Make It So',
enabled: true, enabled: true,
@@ -110,7 +115,7 @@ function populate_(
menu.addChild(menuItem); menu.addChild(menuItem);
menuItem.setEnabled(option.enabled); menuItem.setEnabled(option.enabled);
if (option.enabled) { if (option.enabled) {
const actionHandler = function() { const actionHandler = function () {
hide(); hide();
requestAnimationFrame(() => { requestAnimationFrame(() => {
setTimeout(() => { setTimeout(() => {
@@ -143,10 +148,11 @@ function position_(menu: Menu, e: Event, rtl: boolean) {
// This one is just a point, but we'll pretend that it's a rect so we can use // This one is just a point, but we'll pretend that it's a rect so we can use
// some helper functions. // some helper functions.
const anchorBBox = new Rect( const anchorBBox = new Rect(
mouseEvent.clientY + viewportBBox.top, mouseEvent.clientY + viewportBBox.top,
mouseEvent.clientY + viewportBBox.top, mouseEvent.clientY + viewportBBox.top,
mouseEvent.clientX + viewportBBox.left, mouseEvent.clientX + viewportBBox.left,
mouseEvent.clientX + viewportBBox.left); mouseEvent.clientX + viewportBBox.left
);
createWidget_(menu); createWidget_(menu);
const menuSize = menu.getSize(); const menuSize = menu.getSize();
@@ -179,7 +185,11 @@ function createWidget_(menu: Menu) {
dom.addClass(menuDom, 'blocklyContextMenu'); dom.addClass(menuDom, 'blocklyContextMenu');
// Prevent system context menu when right-clicking a Blockly context menu. // Prevent system context menu when right-clicking a Blockly context menu.
browserEvents.conditionalBind( browserEvents.conditionalBind(
(menuDom as EventTarget), 'contextmenu', null, haltPropagation); menuDom as EventTarget,
'contextmenu',
null,
haltPropagation
);
// Focus only after the initial render to avoid issue #1329. // Focus only after the initial render to avoid issue #1329.
menu.focus(); menu.focus();
} }
@@ -256,12 +266,13 @@ export function callbackFactory(block: Block, xml: Element): () => void {
* containing text, enabled, and a callback. * containing text, enabled, and a callback.
* @internal * @internal
*/ */
export function commentDeleteOption(comment: WorkspaceCommentSvg): export function commentDeleteOption(
LegacyContextMenuOption { comment: WorkspaceCommentSvg
): LegacyContextMenuOption {
const deleteOption = { const deleteOption = {
text: Msg['REMOVE_COMMENT'], text: Msg['REMOVE_COMMENT'],
enabled: true, enabled: true,
callback: function() { callback: function () {
eventUtils.setGroup(true); eventUtils.setGroup(true);
comment.dispose(); comment.dispose();
eventUtils.setGroup(false); eventUtils.setGroup(false);
@@ -279,12 +290,13 @@ export function commentDeleteOption(comment: WorkspaceCommentSvg):
* containing text, enabled, and a callback. * containing text, enabled, and a callback.
* @internal * @internal
*/ */
export function commentDuplicateOption(comment: WorkspaceCommentSvg): export function commentDuplicateOption(
LegacyContextMenuOption { comment: WorkspaceCommentSvg
): LegacyContextMenuOption {
const duplicateOption = { const duplicateOption = {
text: Msg['DUPLICATE_COMMENT'], text: Msg['DUPLICATE_COMMENT'],
enabled: true, enabled: true,
callback: function() { callback: function () {
clipboard.duplicate(comment); clipboard.duplicate(comment);
}, },
}; };
@@ -303,15 +315,20 @@ export function commentDuplicateOption(comment: WorkspaceCommentSvg):
* @internal * @internal
*/ */
export function workspaceCommentOption( export function workspaceCommentOption(
ws: WorkspaceSvg, e: Event): ContextMenuOption { ws: WorkspaceSvg,
e: Event
): ContextMenuOption {
/** /**
* Helper function to create and position a comment correctly based on the * Helper function to create and position a comment correctly based on the
* location of the mouse event. * location of the mouse event.
*/ */
function addWsComment() { function addWsComment() {
const comment = new WorkspaceCommentSvg( const comment = new WorkspaceCommentSvg(
ws, Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'], ws,
WorkspaceCommentSvg.DEFAULT_SIZE, WorkspaceCommentSvg.DEFAULT_SIZE); Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'],
WorkspaceCommentSvg.DEFAULT_SIZE,
WorkspaceCommentSvg.DEFAULT_SIZE
);
const injectionDiv = ws.getInjectionDiv(); const injectionDiv = ws.getInjectionDiv();
// Bounding rect coordinates are in client coordinates, meaning that they // Bounding rect coordinates are in client coordinates, meaning that they
@@ -322,8 +339,9 @@ export function workspaceCommentOption(
// The client coordinates offset by the injection div's upper left corner. // The client coordinates offset by the injection div's upper left corner.
const mouseEvent = e as MouseEvent; const mouseEvent = e as MouseEvent;
const clientOffsetPixels = new Coordinate( const clientOffsetPixels = new Coordinate(
mouseEvent.clientX - boundingRect.left, mouseEvent.clientX - boundingRect.left,
mouseEvent.clientY - boundingRect.top); mouseEvent.clientY - boundingRect.top
);
// The offset in pixels between the main workspace's origin and the upper // The offset in pixels between the main workspace's origin and the upper
// left corner of the injection div. // left corner of the injection div.
@@ -331,8 +349,10 @@ export function workspaceCommentOption(
// The position of the new comment in pixels relative to the origin of the // The position of the new comment in pixels relative to the origin of the
// main workspace. // main workspace.
const finalOffset = const finalOffset = Coordinate.difference(
Coordinate.difference(clientOffsetPixels, mainOffsetPixels); clientOffsetPixels,
mainOffsetPixels
);
// The position of the new comment in main workspace coordinates. // The position of the new comment in main workspace coordinates.
finalOffset.scale(1 / ws.scale); finalOffset.scale(1 / ws.scale);
@@ -350,7 +370,7 @@ export function workspaceCommentOption(
enabled: true, enabled: true,
} as ContextMenuOption; } as ContextMenuOption;
wsCommentOption.text = Msg['ADD_COMMENT']; wsCommentOption.text = Msg['ADD_COMMENT'];
wsCommentOption.callback = function() { wsCommentOption.callback = function () {
addWsComment(); addWsComment();
}; };
return wsCommentOption; return wsCommentOption;

View File

@@ -9,7 +9,11 @@ goog.declareModuleId('Blockly.ContextMenuItems');
import type {BlockSvg} from './block_svg.js'; import type {BlockSvg} from './block_svg.js';
import * as clipboard from './clipboard.js'; import * as clipboard from './clipboard.js';
import {ContextMenuRegistry, RegistryItem, Scope} from './contextmenu_registry.js'; import {
ContextMenuRegistry,
RegistryItem,
Scope,
} from './contextmenu_registry.js';
import * as dialog from './dialog.js'; import * as dialog from './dialog.js';
import * as Events from './events/events.js'; import * as Events from './events/events.js';
import * as eventUtils from './events/utils.js'; import * as eventUtils from './events/utils.js';
@@ -17,7 +21,6 @@ import {Msg} from './msg.js';
import {StatementInput} from './renderers/zelos/zelos.js'; import {StatementInput} from './renderers/zelos/zelos.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
/** /**
* Option to undo previous action. * Option to undo previous action.
*/ */
@@ -111,7 +114,7 @@ function toggleOption_(shouldCollapse: boolean, topBlocks: BlockSvg[]) {
} }
Events.setGroup(true); Events.setGroup(true);
for (let i = 0; i < topBlocks.length; i++) { for (let i = 0; i < topBlocks.length; i++) {
let block: BlockSvg|null = topBlocks[i]; let block: BlockSvg | null = topBlocks[i];
while (block) { while (block) {
timeoutCounter++; timeoutCounter++;
setTimeout(timeoutFn.bind(null, block), ms); setTimeout(timeoutFn.bind(null, block), ms);
@@ -133,7 +136,7 @@ export function registerCollapse() {
if (scope.workspace!.options.collapse) { if (scope.workspace!.options.collapse) {
const topBlocks = scope.workspace!.getTopBlocks(false); const topBlocks = scope.workspace!.getTopBlocks(false);
for (let i = 0; i < topBlocks.length; i++) { for (let i = 0; i < topBlocks.length; i++) {
let block: BlockSvg|null = topBlocks[i]; let block: BlockSvg | null = topBlocks[i];
while (block) { while (block) {
if (!block.isCollapsed()) { if (!block.isCollapsed()) {
return 'enabled'; return 'enabled';
@@ -167,7 +170,7 @@ export function registerExpand() {
if (scope.workspace!.options.collapse) { if (scope.workspace!.options.collapse) {
const topBlocks = scope.workspace!.getTopBlocks(false); const topBlocks = scope.workspace!.getTopBlocks(false);
for (let i = 0; i < topBlocks.length; i++) { for (let i = 0; i < topBlocks.length; i++) {
let block: BlockSvg|null = topBlocks[i]; let block: BlockSvg | null = topBlocks[i];
while (block) { while (block) {
if (block.isCollapsed()) { if (block.isCollapsed()) {
return 'enabled'; return 'enabled';
@@ -280,13 +283,16 @@ export function registerDeleteAll() {
deleteNext_(deletableBlocks); deleteNext_(deletableBlocks);
} else { } else {
dialog.confirm( dialog.confirm(
Msg['DELETE_ALL_BLOCKS'].replace( Msg['DELETE_ALL_BLOCKS'].replace(
'%1', String(deletableBlocks.length)), '%1',
function(ok) { String(deletableBlocks.length)
if (ok) { ),
deleteNext_(deletableBlocks); function (ok) {
} if (ok) {
}); deleteNext_(deletableBlocks);
}
}
);
} }
}, },
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE, scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
@@ -350,8 +356,12 @@ export function registerComment() {
}, },
preconditionFn(scope: Scope) { preconditionFn(scope: Scope) {
const block = scope.block; const block = scope.block;
if (!block!.isInFlyout && block!.workspace.options.comments && if (
!block!.isCollapsed() && block!.isEditable()) { !block!.isInFlyout &&
block!.workspace.options.comments &&
!block!.isCollapsed() &&
block!.isEditable()
) {
return 'enabled'; return 'enabled';
} }
return 'hidden'; return 'hidden';
@@ -377,8 +387,9 @@ export function registerComment() {
export function registerInline() { export function registerInline() {
const inlineOption: RegistryItem = { const inlineOption: RegistryItem = {
displayText(scope: Scope) { displayText(scope: Scope) {
return scope.block!.getInputsInline() ? Msg['EXTERNAL_INPUTS'] : return scope.block!.getInputsInline()
Msg['INLINE_INPUTS']; ? Msg['EXTERNAL_INPUTS']
: Msg['INLINE_INPUTS'];
}, },
preconditionFn(scope: Scope) { preconditionFn(scope: Scope) {
const block = scope.block; const block = scope.block;
@@ -386,8 +397,10 @@ export function registerInline() {
for (let i = 1; i < block!.inputList.length; i++) { for (let i = 1; i < block!.inputList.length; i++) {
// Only display this option if there are two value or dummy inputs // Only display this option if there are two value or dummy inputs
// next to each other. // next to each other.
if (!(block!.inputList[i - 1] instanceof StatementInput) && if (
!(block!.inputList[i] instanceof StatementInput)) { !(block!.inputList[i - 1] instanceof StatementInput) &&
!(block!.inputList[i] instanceof StatementInput)
) {
return 'enabled'; return 'enabled';
} }
} }
@@ -410,13 +423,17 @@ export function registerInline() {
export function registerCollapseExpandBlock() { export function registerCollapseExpandBlock() {
const collapseExpandOption: RegistryItem = { const collapseExpandOption: RegistryItem = {
displayText(scope: Scope) { displayText(scope: Scope) {
return scope.block!.isCollapsed() ? Msg['EXPAND_BLOCK'] : return scope.block!.isCollapsed()
Msg['COLLAPSE_BLOCK']; ? Msg['EXPAND_BLOCK']
: Msg['COLLAPSE_BLOCK'];
}, },
preconditionFn(scope: Scope) { preconditionFn(scope: Scope) {
const block = scope.block; const block = scope.block;
if (!block!.isInFlyout && block!.isMovable() && if (
block!.workspace.options.collapse) { !block!.isInFlyout &&
block!.isMovable() &&
block!.workspace.options.collapse
) {
return 'enabled'; return 'enabled';
} }
return 'hidden'; return 'hidden';
@@ -437,13 +454,17 @@ export function registerCollapseExpandBlock() {
export function registerDisable() { export function registerDisable() {
const disableOption: RegistryItem = { const disableOption: RegistryItem = {
displayText(scope: Scope) { displayText(scope: Scope) {
return scope.block!.isEnabled() ? Msg['DISABLE_BLOCK'] : return scope.block!.isEnabled()
Msg['ENABLE_BLOCK']; ? Msg['DISABLE_BLOCK']
: Msg['ENABLE_BLOCK'];
}, },
preconditionFn(scope: Scope) { preconditionFn(scope: Scope) {
const block = scope.block; const block = scope.block;
if (!block!.isInFlyout && block!.workspace.options.disable && if (
block!.isEditable()) { !block!.isInFlyout &&
block!.workspace.options.disable &&
block!.isEditable()
) {
if (block!.getInheritedDisabled()) { if (block!.getInheritedDisabled()) {
return 'disabled'; return 'disabled';
} }
@@ -481,9 +502,9 @@ export function registerDelete() {
// Blocks in the current stack would survive this block's deletion. // Blocks in the current stack would survive this block's deletion.
descendantCount -= nextBlock.getDescendants(false).length; descendantCount -= nextBlock.getDescendants(false).length;
} }
return descendantCount === 1 ? return descendantCount === 1
Msg['DELETE_BLOCK'] : ? Msg['DELETE_BLOCK']
Msg['DELETE_X_BLOCKS'].replace('%1', `${descendantCount}`); : Msg['DELETE_X_BLOCKS'].replace('%1', `${descendantCount}`);
}, },
preconditionFn(scope: Scope) { preconditionFn(scope: Scope) {
if (!scope.block!.isInFlyout && scope.block!.isDeletable()) { if (!scope.block!.isInFlyout && scope.block!.isDeletable()) {
@@ -513,8 +534,10 @@ export function registerHelp() {
}, },
preconditionFn(scope: Scope) { preconditionFn(scope: Scope) {
const block = scope.block; const block = scope.block;
const url = typeof block!.helpUrl === 'function' ? block!.helpUrl() : const url =
block!.helpUrl; typeof block!.helpUrl === 'function'
? block!.helpUrl()
: block!.helpUrl;
if (url) { if (url) {
return 'enabled'; return 'enabled';
} }

View File

@@ -15,7 +15,6 @@ goog.declareModuleId('Blockly.ContextMenuRegistry');
import type {BlockSvg} from './block_svg.js'; import type {BlockSvg} from './block_svg.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
/** /**
* Class for the registry of context menu items. This is intended to be a * 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 * singleton. You should not create a new instance, and only access this class
@@ -66,7 +65,7 @@ export class ContextMenuRegistry {
* @param id The ID of the RegistryItem to get. * @param id The ID of the RegistryItem to get.
* @returns RegistryItem or null if not found * @returns RegistryItem or null if not found
*/ */
getItem(id: string): RegistryItem|null { getItem(id: string): RegistryItem | null {
return this.registry_.get(id) ?? null; return this.registry_.get(id) ?? null;
} }
@@ -81,16 +80,19 @@ export class ContextMenuRegistry {
* block being clicked on) * block being clicked on)
* @returns the list of ContextMenuOptions * @returns the list of ContextMenuOptions
*/ */
getContextMenuOptions(scopeType: ScopeType, scope: Scope): getContextMenuOptions(
ContextMenuOption[] { scopeType: ScopeType,
scope: Scope
): ContextMenuOption[] {
const menuOptions: ContextMenuOption[] = []; const menuOptions: ContextMenuOption[] = [];
for (const item of this.registry_.values()) { for (const item of this.registry_.values()) {
if (scopeType === item.scopeType) { if (scopeType === item.scopeType) {
const precondition = item.preconditionFn(scope); const precondition = item.preconditionFn(scope);
if (precondition !== 'hidden') { if (precondition !== 'hidden') {
const displayText = typeof item.displayText === 'function' ? const displayText =
item.displayText(scope) : typeof item.displayText === 'function'
item.displayText; ? item.displayText(scope)
: item.displayText;
const menuOption: ContextMenuOption = { const menuOption: ContextMenuOption = {
text: displayText, text: displayText,
enabled: precondition === 'enabled', enabled: precondition === 'enabled',
@@ -102,7 +104,7 @@ export class ContextMenuRegistry {
} }
} }
} }
menuOptions.sort(function(a, b) { menuOptions.sort(function (a, b) {
return a.weight - b.weight; return a.weight - b.weight;
}); });
return menuOptions; return menuOptions;
@@ -135,7 +137,7 @@ export namespace ContextMenuRegistry {
export interface RegistryItem { export interface RegistryItem {
callback: (p1: Scope) => void; callback: (p1: Scope) => void;
scopeType: ScopeType; scopeType: ScopeType;
displayText: ((p1: Scope) => string)|string; displayText: ((p1: Scope) => string) | string;
preconditionFn: (p1: Scope) => string; preconditionFn: (p1: Scope) => string;
weight: number; weight: number;
id: string; id: string;
@@ -175,4 +177,4 @@ export type Scope = ContextMenuRegistry.Scope;
export type RegistryItem = ContextMenuRegistry.RegistryItem; export type RegistryItem = ContextMenuRegistry.RegistryItem;
export type ContextMenuOption = ContextMenuRegistry.ContextMenuOption; export type ContextMenuOption = ContextMenuRegistry.ContextMenuOption;
export type LegacyContextMenuOption = export type LegacyContextMenuOption =
ContextMenuRegistry.LegacyContextMenuOption; ContextMenuRegistry.LegacyContextMenuOption;

View File

@@ -7,7 +7,6 @@
import * as goog from '../closure/goog/goog.js'; import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Css'); goog.declareModuleId('Blockly.Css');
/** Has CSS already been injected? */ /** Has CSS already been injected? */
let injected = false; let injected = false;

View File

@@ -18,7 +18,6 @@ import {DragTarget} from './drag_target.js';
import type {IDeleteArea} from './interfaces/i_delete_area.js'; import type {IDeleteArea} from './interfaces/i_delete_area.js';
import type {IDraggable} from './interfaces/i_draggable.js'; import type {IDraggable} from './interfaces/i_draggable.js';
/** /**
* Abstract class for a component that can delete a block or bubble that is * Abstract class for a component that can delete a block or bubble that is
* dropped on top of it. * dropped on top of it.
@@ -59,7 +58,7 @@ export class DeleteArea extends DragTarget implements IDeleteArea {
*/ */
wouldDelete(element: IDraggable, couldConnect: boolean): boolean { wouldDelete(element: IDraggable, couldConnect: boolean): boolean {
if (element instanceof BlockSvg) { if (element instanceof BlockSvg) {
const block = (element); const block = element;
const couldDeleteBlock = !block.getParent() && block.isDeletable(); const couldDeleteBlock = !block.getParent() && block.isDeletable();
this.updateWouldDelete_(couldDeleteBlock && !couldConnect); this.updateWouldDelete_(couldDeleteBlock && !couldConnect);
} else { } else {

View File

@@ -7,22 +7,28 @@
import * as goog from '../closure/goog/goog.js'; import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.dialog'); goog.declareModuleId('Blockly.dialog');
let alertImplementation = function (
let alertImplementation = function(message: string, opt_callback?: () => void) { message: string,
opt_callback?: () => void
) {
window.alert(message); window.alert(message);
if (opt_callback) { if (opt_callback) {
opt_callback(); opt_callback();
} }
}; };
let confirmImplementation = function( let confirmImplementation = function (
message: string, callback: (result: boolean) => void) { message: string,
callback: (result: boolean) => void
) {
callback(window.confirm(message)); callback(window.confirm(message));
}; };
let promptImplementation = function( let promptImplementation = function (
message: string, defaultValue: string, message: string,
callback: (result: string|null) => void) { defaultValue: string,
callback: (result: string | null) => void
) {
callback(window.prompt(message, defaultValue)); callback(window.prompt(message, defaultValue));
}; };
@@ -65,7 +71,6 @@ function confirmInternal(message: string, callback: (p1: boolean) => void) {
confirmImplementation(message, callback); confirmImplementation(message, callback);
} }
/** /**
* Sets the function to be run when Blockly.dialog.confirm() is called. * Sets the function to be run when Blockly.dialog.confirm() is called.
* *
@@ -73,7 +78,8 @@ function confirmInternal(message: string, callback: (p1: boolean) => void) {
* @see Blockly.dialog.confirm * @see Blockly.dialog.confirm
*/ */
export function setConfirm( export function setConfirm(
confirmFunction: (p1: string, p2: (p1: boolean) => void) => void) { confirmFunction: (p1: string, p2: (p1: boolean) => void) => void
) {
confirmImplementation = confirmFunction; confirmImplementation = confirmFunction;
} }
@@ -88,8 +94,10 @@ export function setConfirm(
* @param callback The callback for handling user response. * @param callback The callback for handling user response.
*/ */
export function prompt( export function prompt(
message: string, defaultValue: string, message: string,
callback: (p1: string|null) => void) { defaultValue: string,
callback: (p1: string | null) => void
) {
promptImplementation(message, defaultValue, callback); promptImplementation(message, defaultValue, callback);
} }
@@ -100,8 +108,12 @@ export function prompt(
* @see Blockly.dialog.prompt * @see Blockly.dialog.prompt
*/ */
export function setPrompt( export function setPrompt(
promptFunction: (p1: string, p2: string, p3: (p1: string|null) => void) => promptFunction: (
void) { p1: string,
p2: string,
p3: (p1: string | null) => void
) => void
) {
promptImplementation = promptFunction; promptImplementation = promptFunction;
} }

View File

@@ -17,7 +17,6 @@ import type {IDragTarget} from './interfaces/i_drag_target.js';
import type {IDraggable} from './interfaces/i_draggable.js'; import type {IDraggable} from './interfaces/i_draggable.js';
import type {Rect} from './utils/rect.js'; import type {Rect} from './utils/rect.js';
/** /**
* Abstract class for a component with custom behaviour when a block or bubble * Abstract class for a component with custom behaviour when a block or bubble
* is dragged over or dropped on top of it. * is dragged over or dropped on top of it.
@@ -76,7 +75,7 @@ export class DragTarget implements IDragTarget {
* @returns The component's bounding box. Null if drag target area should be * @returns The component's bounding box. Null if drag target area should be
* ignored. * ignored.
*/ */
getClientRect(): Rect|null { getClientRect(): Rect | null {
return null; return null;
} }

View File

@@ -23,7 +23,6 @@ import type {Size} from './utils/size.js';
import * as style from './utils/style.js'; import * as style from './utils/style.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
/** /**
* Arrow size in px. Should match the value in CSS * Arrow size in px. Should match the value in CSS
* (need to position pre-render). * (need to position pre-render).
@@ -52,10 +51,10 @@ export const ANIMATION_TIME = 0.25;
* Timer for animation out, to be cleared if we need to immediately hide * Timer for animation out, to be cleared if we need to immediately hide
* without disrupting new shows. * without disrupting new shows.
*/ */
let animateOutTimer: ReturnType<typeof setTimeout>|null = null; let animateOutTimer: ReturnType<typeof setTimeout> | null = null;
/** Callback for when the drop-down is hidden. */ /** Callback for when the drop-down is hidden. */
let onHide: Function|null = null; let onHide: Function | null = null;
/** A class name representing the current owner's workspace renderer. */ /** A class name representing the current owner's workspace renderer. */
let renderedClassName = ''; let renderedClassName = '';
@@ -76,13 +75,13 @@ let arrow: HTMLDivElement;
* Drop-downs will appear within the bounds of this element if possible. * Drop-downs will appear within the bounds of this element if possible.
* Set in setBoundsElement. * Set in setBoundsElement.
*/ */
let boundsElement: Element|null = null; let boundsElement: Element | null = null;
/** The object currently using the drop-down. */ /** The object currently using the drop-down. */
let owner: Field|null = null; let owner: Field | null = null;
/** Whether the dropdown was positioned to a field or the source block. */ /** Whether the dropdown was positioned to a field or the source block. */
let positionToField: boolean|null = null; let positionToField: boolean | null = null;
/** /**
* Dropdown bounds info object used to encapsulate sizing information about a * Dropdown bounds info object used to encapsulate sizing information about a
@@ -103,9 +102,9 @@ export interface PositionMetrics {
initialY: number; initialY: number;
finalX: number; finalX: number;
finalY: number; finalY: number;
arrowX: number|null; arrowX: number | null;
arrowY: number|null; arrowY: number | null;
arrowAtTop: boolean|null; arrowAtTop: boolean | null;
arrowVisible: boolean; arrowVisible: boolean;
} }
@@ -116,7 +115,7 @@ export interface PositionMetrics {
*/ */
export function createDom() { export function createDom() {
if (div) { if (div) {
return; // Already created. return; // Already created.
} }
div = document.createElement('div'); div = document.createElement('div');
div.className = 'blocklyDropDownDiv'; div.className = 'blocklyDropDownDiv';
@@ -133,15 +132,15 @@ export function createDom() {
div.style.opacity = '0'; div.style.opacity = '0';
// Transition animation for transform: translate() and opacity. // Transition animation for transform: translate() and opacity.
div.style.transition = 'transform ' + ANIMATION_TIME + 's, ' + div.style.transition =
'opacity ' + ANIMATION_TIME + 's'; 'transform ' + ANIMATION_TIME + 's, ' + 'opacity ' + ANIMATION_TIME + 's';
// Handle focusin/out events to add a visual indicator when // Handle focusin/out events to add a visual indicator when
// a child is focused or blurred. // a child is focused or blurred.
div.addEventListener('focusin', function() { div.addEventListener('focusin', function () {
dom.addClass(div, 'blocklyFocused'); dom.addClass(div, 'blocklyFocused');
}); });
div.addEventListener('focusout', function() { div.addEventListener('focusout', function () {
dom.removeClass(div, 'blocklyFocused'); dom.removeClass(div, 'blocklyFocused');
}); });
} }
@@ -152,14 +151,14 @@ export function createDom() {
* *
* @param boundsElem Element to bind drop-down to. * @param boundsElem Element to bind drop-down to.
*/ */
export function setBoundsElement(boundsElem: Element|null) { export function setBoundsElement(boundsElem: Element | null) {
boundsElement = boundsElem; boundsElement = boundsElem;
} }
/** /**
* @returns The field that currently owns this, or null. * @returns The field that currently owns this, or null.
*/ */
export function getOwner(): Field|null { export function getOwner(): Field | null {
return owner; return owner;
} }
@@ -202,11 +201,17 @@ export function setColour(backgroundColour: string, borderColour: string) {
* @returns True if the menu rendered below block; false if above. * @returns True if the menu rendered below block; false if above.
*/ */
export function showPositionedByBlock<T>( export function showPositionedByBlock<T>(
field: Field<T>, block: BlockSvg, opt_onHide?: Function, field: Field<T>,
opt_secondaryYOffset?: number): boolean { block: BlockSvg,
opt_onHide?: Function,
opt_secondaryYOffset?: number
): boolean {
return showPositionedByRect( return showPositionedByRect(
getScaledBboxOfBlock(block), field as Field, opt_onHide, getScaledBboxOfBlock(block),
opt_secondaryYOffset); field as Field,
opt_onHide,
opt_secondaryYOffset
);
} }
/** /**
@@ -221,12 +226,17 @@ export function showPositionedByBlock<T>(
* @returns True if the menu rendered below block; false if above. * @returns True if the menu rendered below block; false if above.
*/ */
export function showPositionedByField<T>( export function showPositionedByField<T>(
field: Field<T>, opt_onHide?: Function, field: Field<T>,
opt_secondaryYOffset?: number): boolean { opt_onHide?: Function,
opt_secondaryYOffset?: number
): boolean {
positionToField = true; positionToField = true;
return showPositionedByRect( return showPositionedByRect(
getScaledBboxOfField(field as Field), field as Field, opt_onHide, getScaledBboxOfField(field as Field),
opt_secondaryYOffset); field as Field,
opt_onHide,
opt_secondaryYOffset
);
} }
/** /**
* Get the scaled bounding box of a block. * Get the scaled bounding box of a block.
@@ -267,8 +277,11 @@ function getScaledBboxOfField(field: Field): Rect {
* @returns True if the menu rendered below block; false if above. * @returns True if the menu rendered below block; false if above.
*/ */
function showPositionedByRect( function showPositionedByRect(
bBox: Rect, field: Field, opt_onHide?: Function, bBox: Rect,
opt_secondaryYOffset?: number): boolean { field: Field,
opt_onHide?: Function,
opt_secondaryYOffset?: number
): boolean {
// If we can fit it, render below the block. // If we can fit it, render below the block.
const primaryX = bBox.left + (bBox.right - bBox.left) / 2; const primaryX = bBox.left + (bBox.right - bBox.left) / 2;
const primaryY = bBox.bottom; const primaryY = bBox.bottom;
@@ -286,8 +299,14 @@ function showPositionedByRect(
} }
setBoundsElement(workspace.getParentSvg().parentNode as Element | null); setBoundsElement(workspace.getParentSvg().parentNode as Element | null);
return show( return show(
field, sourceBlock.RTL, primaryX, primaryY, secondaryX, secondaryY, field,
opt_onHide); sourceBlock.RTL,
primaryX,
primaryY,
secondaryX,
secondaryY,
opt_onHide
);
} }
/** /**
@@ -310,8 +329,14 @@ function showPositionedByRect(
* @internal * @internal
*/ */
export function show<T>( export function show<T>(
newOwner: Field<T>, rtl: boolean, primaryX: number, primaryY: number, newOwner: Field<T>,
secondaryX: number, secondaryY: number, opt_onHide?: Function): boolean { rtl: boolean,
primaryX: number,
primaryY: number,
secondaryX: number,
secondaryY: number,
opt_onHide?: Function
): boolean {
owner = newOwner as Field; owner = newOwner as Field;
onHide = opt_onHide || null; onHide = opt_onHide || null;
// Set direction. // Set direction.
@@ -345,7 +370,7 @@ const internal = {
* @returns An object containing size information about the bounding element * @returns An object containing size information about the bounding element
* (bounding box and width/height). * (bounding box and width/height).
*/ */
getBoundsInfo: function(): BoundsInfo { getBoundsInfo: function (): BoundsInfo {
const boundPosition = style.getPageOffset(boundsElement as Element); const boundPosition = style.getPageOffset(boundsElement as Element);
const boundSize = style.getSize(boundsElement as Element); const boundSize = style.getSize(boundsElement as Element);
@@ -370,9 +395,12 @@ const internal = {
* @returns Various final metrics, including rendered positions for drop-down * @returns Various final metrics, including rendered positions for drop-down
* and arrow. * and arrow.
*/ */
getPositionMetrics: function( getPositionMetrics: function (
primaryX: number, primaryY: number, secondaryX: number, primaryX: number,
secondaryY: number): PositionMetrics { primaryY: number,
secondaryX: number,
secondaryY: number
): PositionMetrics {
const boundsInfo = internal.getBoundsInfo(); const boundsInfo = internal.getBoundsInfo();
const divSize = style.getSize(div as Element); const divSize = style.getSize(div as Element);
@@ -383,7 +411,11 @@ const internal = {
// Can we fit in-bounds above the target? // Can we fit in-bounds above the target?
if (secondaryY - divSize.height > boundsInfo.top) { if (secondaryY - divSize.height > boundsInfo.top) {
return getPositionAboveMetrics( return getPositionAboveMetrics(
secondaryX, secondaryY, boundsInfo, divSize); secondaryX,
secondaryY,
boundsInfo,
divSize
);
} }
// Can we fit outside the workspace bounds (but inside the window) // Can we fit outside the workspace bounds (but inside the window)
// below? // below?
@@ -394,7 +426,11 @@ const internal = {
// above? // above?
if (secondaryY - divSize.height > document.documentElement.clientTop) { if (secondaryY - divSize.height > document.documentElement.clientTop) {
return getPositionAboveMetrics( return getPositionAboveMetrics(
secondaryX, secondaryY, boundsInfo, divSize); secondaryX,
secondaryY,
boundsInfo,
divSize
);
} }
// Last resort, render at top of page. // Last resort, render at top of page.
@@ -415,10 +451,17 @@ const internal = {
* and arrow. * and arrow.
*/ */
function getPositionBelowMetrics( function getPositionBelowMetrics(
primaryX: number, primaryY: number, boundsInfo: BoundsInfo, primaryX: number,
divSize: Size): PositionMetrics { primaryY: number,
const xCoords = boundsInfo: BoundsInfo,
getPositionX(primaryX, boundsInfo.left, boundsInfo.right, divSize.width); divSize: Size
): PositionMetrics {
const xCoords = getPositionX(
primaryX,
boundsInfo.left,
boundsInfo.right,
divSize.width
);
const arrowY = -(ARROW_SIZE / 2 + BORDER_SIZE); const arrowY = -(ARROW_SIZE / 2 + BORDER_SIZE);
const finalY = primaryY + PADDING_Y; const finalY = primaryY + PADDING_Y;
@@ -426,7 +469,7 @@ function getPositionBelowMetrics(
return { return {
initialX: xCoords.divX, initialX: xCoords.divX,
initialY: primaryY, initialY: primaryY,
finalX: xCoords.divX, // X position remains constant during animation. finalX: xCoords.divX, // X position remains constant during animation.
finalY, finalY,
arrowX: xCoords.arrowX, arrowX: xCoords.arrowX,
arrowY, arrowY,
@@ -448,19 +491,26 @@ function getPositionBelowMetrics(
* and arrow. * and arrow.
*/ */
function getPositionAboveMetrics( function getPositionAboveMetrics(
secondaryX: number, secondaryY: number, boundsInfo: BoundsInfo, secondaryX: number,
divSize: Size): PositionMetrics { secondaryY: number,
boundsInfo: BoundsInfo,
divSize: Size
): PositionMetrics {
const xCoords = getPositionX( const xCoords = getPositionX(
secondaryX, boundsInfo.left, boundsInfo.right, divSize.width); secondaryX,
boundsInfo.left,
boundsInfo.right,
divSize.width
);
const arrowY = divSize.height - BORDER_SIZE * 2 - ARROW_SIZE / 2; const arrowY = divSize.height - BORDER_SIZE * 2 - ARROW_SIZE / 2;
const finalY = secondaryY - divSize.height - PADDING_Y; const finalY = secondaryY - divSize.height - PADDING_Y;
const initialY = secondaryY - divSize.height; // No padding on Y. const initialY = secondaryY - divSize.height; // No padding on Y.
return { return {
initialX: xCoords.divX, initialX: xCoords.divX,
initialY, initialY,
finalX: xCoords.divX, // X position remains constant during animation. finalX: xCoords.divX, // X position remains constant during animation.
finalY, finalY,
arrowX: xCoords.arrowX, arrowX: xCoords.arrowX,
arrowY, arrowY,
@@ -481,16 +531,23 @@ function getPositionAboveMetrics(
* and arrow. * and arrow.
*/ */
function getPositionTopOfPageMetrics( function getPositionTopOfPageMetrics(
sourceX: number, boundsInfo: BoundsInfo, divSize: Size): PositionMetrics { sourceX: number,
const xCoords = boundsInfo: BoundsInfo,
getPositionX(sourceX, boundsInfo.left, boundsInfo.right, divSize.width); divSize: Size
): PositionMetrics {
const xCoords = getPositionX(
sourceX,
boundsInfo.left,
boundsInfo.right,
divSize.width
);
// No need to provide arrow-specific information because it won't be visible. // No need to provide arrow-specific information because it won't be visible.
return { return {
initialX: xCoords.divX, initialX: xCoords.divX,
initialY: 0, initialY: 0,
finalX: xCoords.divX, // X position remains constant during animation. finalX: xCoords.divX, // X position remains constant during animation.
finalY: 0, // Y position remains constant during animation. finalY: 0, // Y position remains constant during animation.
arrowAtTop: null, arrowAtTop: null,
arrowX: null, arrowX: null,
arrowY: null, arrowY: null,
@@ -511,8 +568,11 @@ function getPositionTopOfPageMetrics(
* @internal * @internal
*/ */
export function getPositionX( export function getPositionX(
sourceX: number, boundsLeft: number, boundsRight: number, sourceX: number,
divWidth: number): {divX: number, arrowX: number} { boundsLeft: number,
boundsRight: number,
divWidth: number
): {divX: number; arrowX: number} {
let divX = sourceX; let divX = sourceX;
// Offset the topLeft coord so that the dropdowndiv is centered. // Offset the topLeft coord so that the dropdowndiv is centered.
divX -= divWidth / 2; divX -= divWidth / 2;
@@ -527,7 +587,10 @@ export function getPositionX(
const horizPadding = ARROW_HORIZONTAL_PADDING; const horizPadding = ARROW_HORIZONTAL_PADDING;
// Clamp the arrow position so that it stays attached to the dropdowndiv. // Clamp the arrow position so that it stays attached to the dropdowndiv.
relativeArrowX = math.clamp( relativeArrowX = math.clamp(
horizPadding, relativeArrowX, divWidth - horizPadding - ARROW_SIZE); horizPadding,
relativeArrowX,
divWidth - horizPadding - ARROW_SIZE
);
return {arrowX: relativeArrowX, divX}; return {arrowX: relativeArrowX, divX};
} }
@@ -550,7 +613,9 @@ export function isVisible(): boolean {
* @returns True if hidden. * @returns True if hidden.
*/ */
export function hideIfOwner<T>( export function hideIfOwner<T>(
divOwner: Field<T>, opt_withoutAnimation?: boolean): boolean { divOwner: Field<T>,
opt_withoutAnimation?: boolean
): boolean {
if (owner === divOwner) { if (owner === divOwner) {
if (opt_withoutAnimation) { if (opt_withoutAnimation) {
hideWithoutAnimation(); hideWithoutAnimation();
@@ -569,7 +634,7 @@ export function hide() {
div.style.transform = 'translate(0, 0)'; div.style.transform = 'translate(0, 0)';
div.style.opacity = '0'; div.style.opacity = '0';
// Finish animation - reset all values to default. // Finish animation - reset all values to default.
animateOutTimer = setTimeout(function() { animateOutTimer = setTimeout(function () {
hideWithoutAnimation(); hideWithoutAnimation();
}, ANIMATION_TIME * 1000); }, ANIMATION_TIME * 1000);
if (onHide) { if (onHide) {
@@ -625,20 +690,33 @@ export function hideWithoutAnimation() {
* @returns True if the menu rendered at the primary origin point. * @returns True if the menu rendered at the primary origin point.
*/ */
function positionInternal( function positionInternal(
primaryX: number, primaryY: number, secondaryX: number, primaryX: number,
secondaryY: number): boolean { primaryY: number,
const metrics = secondaryX: number,
internal.getPositionMetrics(primaryX, primaryY, secondaryX, secondaryY); secondaryY: number
): boolean {
const metrics = internal.getPositionMetrics(
primaryX,
primaryY,
secondaryX,
secondaryY
);
// Update arrow CSS. // Update arrow CSS.
if (metrics.arrowVisible) { if (metrics.arrowVisible) {
arrow.style.display = ''; arrow.style.display = '';
arrow.style.transform = 'translate(' + metrics.arrowX + 'px,' + arrow.style.transform =
metrics.arrowY + 'px) rotate(45deg)'; 'translate(' +
metrics.arrowX +
'px,' +
metrics.arrowY +
'px) rotate(45deg)';
arrow.setAttribute( arrow.setAttribute(
'class', 'class',
metrics.arrowAtTop ? 'blocklyDropDownArrow blocklyArrowTop' : metrics.arrowAtTop
'blocklyDropDownArrow blocklyArrowBottom'); ? 'blocklyDropDownArrow blocklyArrowTop'
: 'blocklyDropDownArrow blocklyArrowBottom'
);
} else { } else {
arrow.style.display = 'none'; arrow.style.display = 'none';
} }
@@ -679,8 +757,9 @@ export function repositionForWindowResize() {
// it. // it.
if (owner) { if (owner) {
const block = owner.getSourceBlock() as BlockSvg; const block = owner.getSourceBlock() as BlockSvg;
const bBox = positionToField ? getScaledBboxOfField(owner) : const bBox = positionToField
getScaledBboxOfBlock(block); ? getScaledBboxOfField(owner)
: getScaledBboxOfBlock(block);
// If we can fit it, render below the block. // If we can fit it, render below the block.
const primaryX = bBox.left + (bBox.right - bBox.left) / 2; const primaryX = bBox.left + (bBox.right - bBox.left) / 2;
const primaryY = bBox.bottom; const primaryY = bBox.bottom;

View File

@@ -7,7 +7,6 @@
import * as goog from '../../closure/goog/goog.js'; import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events'); goog.declareModuleId('Blockly.Events');
import {Abstract, AbstractEventJson} from './events_abstract.js'; import {Abstract, AbstractEventJson} from './events_abstract.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js'; import {BlockBase, BlockBaseJson} from './events_block_base.js';
import {BlockChange, BlockChangeJson} from './events_block_change.js'; import {BlockChange, BlockChangeJson} from './events_block_change.js';
@@ -25,7 +24,10 @@ import {CommentMove, CommentMoveJson} from './events_comment_move.js';
import {MarkerMove, MarkerMoveJson} from './events_marker_move.js'; import {MarkerMove, MarkerMoveJson} from './events_marker_move.js';
import {Selected, SelectedJson} from './events_selected.js'; import {Selected, SelectedJson} from './events_selected.js';
import {ThemeChange, ThemeChangeJson} from './events_theme_change.js'; import {ThemeChange, ThemeChangeJson} from './events_theme_change.js';
import {ToolboxItemSelect, ToolboxItemSelectJson} from './events_toolbox_item_select.js'; import {
ToolboxItemSelect,
ToolboxItemSelectJson,
} from './events_toolbox_item_select.js';
import {TrashcanOpen, TrashcanOpenJson} from './events_trashcan_open.js'; import {TrashcanOpen, TrashcanOpenJson} from './events_trashcan_open.js';
import {Ui} from './events_ui.js'; import {Ui} from './events_ui.js';
import {UiBase} from './events_ui_base.js'; import {UiBase} from './events_ui_base.js';
@@ -37,7 +39,6 @@ import {ViewportChange, ViewportChangeJson} from './events_viewport.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import {FinishedLoading, FinishedLoadingJson} from './workspace_events.js'; import {FinishedLoading, FinishedLoadingJson} from './workspace_events.js';
// Events. // Events.
export {Abstract}; export {Abstract};
export {AbstractEventJson}; export {AbstractEventJson};

View File

@@ -19,7 +19,6 @@ import type {Workspace} from '../workspace.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
/** /**
* Abstract class for an event. * Abstract class for an event.
*/ */
@@ -74,8 +73,11 @@ export abstract class Abstract {
*/ */
fromJson(json: AbstractEventJson) { fromJson(json: AbstractEventJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.Abstract.prototype.fromJson', 'version 9', 'version 10', 'Blockly.Events.Abstract.prototype.fromJson',
'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
this.isBlank = false; this.isBlank = false;
this.group = json['group'] || ''; this.group = json['group'] || '';
} }
@@ -89,8 +91,11 @@ export abstract class Abstract {
* supertypes of parameters to static methods in superclasses. * supertypes of parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: AbstractEventJson, workspace: Workspace, event: any): static fromJson(
Abstract { json: AbstractEventJson,
workspace: Workspace,
event: any
): Abstract {
event.isBlank = false; event.isBlank = false;
event.group = json['group'] || ''; event.group = json['group'] || '';
event.workspaceId = workspace.id; event.workspaceId = workspace.id;
@@ -130,8 +135,9 @@ export abstract class Abstract {
} }
if (!workspace) { if (!workspace) {
throw Error( throw Error(
'Workspace is null. Event must have been generated from real' + 'Workspace is null. Event must have been generated from real' +
' Blockly events.'); ' Blockly events.'
);
} }
return workspace; return workspace;
} }

View File

@@ -16,8 +16,10 @@ import type {Block} from '../block.js';
import * as deprecation from '../utils/deprecation.js'; import * as deprecation from '../utils/deprecation.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js'; import {
Abstract as AbstractEvent,
AbstractEventJson,
} from './events_abstract.js';
/** /**
* Abstract class for any event related to blocks. * Abstract class for any event related to blocks.
@@ -51,8 +53,9 @@ export class BlockBase extends AbstractEvent {
const json = super.toJson() as BlockBaseJson; const json = super.toJson() as BlockBaseJson;
if (!this.blockId) { if (!this.blockId) {
throw new Error( throw new Error(
'The block ID is undefined. Either pass a block to ' + 'The block ID is undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['blockId'] = this.blockId; json['blockId'] = this.blockId;
return json; return json;
@@ -65,8 +68,11 @@ export class BlockBase extends AbstractEvent {
*/ */
override fromJson(json: BlockBaseJson) { override fromJson(json: BlockBaseJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.BlockBase.prototype.fromJson', 'version 9', 'Blockly.Events.BlockBase.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.blockId = json['blockId']; this.blockId = json['blockId'];
} }
@@ -80,10 +86,16 @@ export class BlockBase extends AbstractEvent {
* static methods in superclasses. * static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: BlockBaseJson, workspace: Workspace, event?: any): static fromJson(
BlockBase { json: BlockBaseJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new BlockBase()) as BlockBase; event?: any
): BlockBase {
const newEvent = super.fromJson(
json,
workspace,
event ?? new BlockBase()
) as BlockBase;
newEvent.blockId = json['blockId']; newEvent.blockId = json['blockId'];
return newEvent; return newEvent;
} }

View File

@@ -23,7 +23,6 @@ import * as Xml from '../xml.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js'; import {BlockBase, BlockBaseJson} from './events_block_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
/** /**
* Notifies listeners when some element of a block has changed (e.g. * Notifies listeners when some element of a block has changed (e.g.
* field values, comments, etc). * field values, comments, etc).
@@ -53,12 +52,16 @@ export class BlockChange extends BlockBase {
* @param opt_newValue New value of element. * @param opt_newValue New value of element.
*/ */
constructor( constructor(
opt_block?: Block, opt_element?: string, opt_name?: string|null, opt_block?: Block,
opt_oldValue?: unknown, opt_newValue?: unknown) { opt_element?: string,
opt_name?: string | null,
opt_oldValue?: unknown,
opt_newValue?: unknown
) {
super(opt_block); super(opt_block);
if (!opt_block) { if (!opt_block) {
return; // Blank event to be populated by fromJson. return; // Blank event to be populated by fromJson.
} }
this.element = opt_element; this.element = opt_element;
this.name = opt_name || undefined; this.name = opt_name || undefined;
@@ -75,8 +78,9 @@ export class BlockChange extends BlockBase {
const json = super.toJson() as BlockChangeJson; const json = super.toJson() as BlockChangeJson;
if (!this.element) { if (!this.element) {
throw new Error( throw new Error(
'The changed element is undefined. Either pass an ' + 'The changed element is undefined. Either pass an ' +
'element to the constructor, or call fromJson'); 'element to the constructor, or call fromJson'
);
} }
json['element'] = this.element; json['element'] = this.element;
json['name'] = this.name; json['name'] = this.name;
@@ -92,8 +96,11 @@ export class BlockChange extends BlockBase {
*/ */
override fromJson(json: BlockChangeJson) { override fromJson(json: BlockChangeJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.BlockChange.prototype.fromJson', 'version 9', 'Blockly.Events.BlockChange.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.element = json['element']; this.element = json['element'];
this.name = json['name']; this.name = json['name'];
@@ -110,11 +117,16 @@ export class BlockChange extends BlockBase {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: BlockChangeJson, workspace: Workspace, event?: any): static fromJson(
BlockChange { json: BlockChangeJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new BlockChange()) as event?: any
BlockChange; ): BlockChange {
const newEvent = super.fromJson(
json,
workspace,
event ?? new BlockChange()
) as BlockChange;
newEvent.element = json['element']; newEvent.element = json['element'];
newEvent.name = json['name']; newEvent.name = json['name'];
newEvent.oldValue = json['oldValue']; newEvent.oldValue = json['oldValue'];
@@ -140,14 +152,16 @@ export class BlockChange extends BlockBase {
const workspace = this.getEventWorkspace_(); const workspace = this.getEventWorkspace_();
if (!this.blockId) { if (!this.blockId) {
throw new Error( throw new Error(
'The block ID is undefined. Either pass a block to ' + 'The block ID is undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
const block = workspace.getBlockById(this.blockId); const block = workspace.getBlockById(this.blockId);
if (!block) { if (!block) {
throw new Error( throw new Error(
'The associated block is undefined. Either pass a ' + 'The associated block is undefined. Either pass a ' +
'block to the constructor, or call fromJson'); 'block to the constructor, or call fromJson'
);
} }
// Assume the block is rendered so that then we can check. // Assume the block is rendered so that then we can check.
const blockSvg = block as BlockSvg; const blockSvg = block as BlockSvg;
@@ -162,12 +176,12 @@ export class BlockChange extends BlockBase {
if (field) { if (field) {
field.setValue(value); field.setValue(value);
} else { } else {
console.warn('Can\'t set non-existent field: ' + this.name); console.warn("Can't set non-existent field: " + this.name);
} }
break; break;
} }
case 'comment': case 'comment':
block.setCommentText(value as string || null); block.setCommentText((value as string) || null);
break; break;
case 'collapsed': case 'collapsed':
block.setCollapsed(!!value); block.setCollapsed(!!value);
@@ -181,13 +195,15 @@ export class BlockChange extends BlockBase {
case 'mutation': { case 'mutation': {
const oldState = BlockChange.getExtraBlockState_(block as BlockSvg); const oldState = BlockChange.getExtraBlockState_(block as BlockSvg);
if (block.loadExtraState) { if (block.loadExtraState) {
block.loadExtraState(JSON.parse(value as string || '{}')); block.loadExtraState(JSON.parse((value as string) || '{}'));
} else if (block.domToMutation) { } else if (block.domToMutation) {
block.domToMutation( block.domToMutation(
utilsXml.textToDom(value as string || '<mutation/>')); utilsXml.textToDom((value as string) || '<mutation/>')
);
} }
eventUtils.fire( eventUtils.fire(
new BlockChange(block, 'mutation', null, oldState, value)); new BlockChange(block, 'mutation', null, oldState, value)
);
break; break;
} }
default: default:

View File

@@ -23,7 +23,6 @@ import {BlockBase, BlockBaseJson} from './events_block_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import {Workspace} from '../workspace.js'; import {Workspace} from '../workspace.js';
/** /**
* Notifies listeners when a block (or connected stack of blocks) is * Notifies listeners when a block (or connected stack of blocks) is
* created. * created.
@@ -32,7 +31,7 @@ export class BlockCreate extends BlockBase {
override type = eventUtils.BLOCK_CREATE; override type = eventUtils.BLOCK_CREATE;
/** The XML representation of the created block(s). */ /** The XML representation of the created block(s). */
xml?: Element|DocumentFragment; xml?: Element | DocumentFragment;
/** The JSON respresentation of the created block(s). */ /** The JSON respresentation of the created block(s). */
json?: blocks.State; json?: blocks.State;
@@ -45,7 +44,7 @@ export class BlockCreate extends BlockBase {
super(opt_block); super(opt_block);
if (!opt_block) { if (!opt_block) {
return; // Blank event to be populated by fromJson. return; // Blank event to be populated by fromJson.
} }
if (opt_block.isShadow()) { if (opt_block.isShadow()) {
@@ -68,18 +67,21 @@ export class BlockCreate extends BlockBase {
const json = super.toJson() as BlockCreateJson; const json = super.toJson() as BlockCreateJson;
if (!this.xml) { if (!this.xml) {
throw new Error( throw new Error(
'The block XML is undefined. Either pass a block to ' + 'The block XML is undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.ids) { if (!this.ids) {
throw new Error( throw new Error(
'The block IDs are undefined. Either pass a block to ' + 'The block IDs are undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.json) { if (!this.json) {
throw new Error( throw new Error(
'The block JSON is undefined. Either pass a block to ' + 'The block JSON is undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['xml'] = Xml.domToText(this.xml); json['xml'] = Xml.domToText(this.xml);
json['ids'] = this.ids; json['ids'] = this.ids;
@@ -97,8 +99,11 @@ export class BlockCreate extends BlockBase {
*/ */
override fromJson(json: BlockCreateJson) { override fromJson(json: BlockCreateJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.BlockCreate.prototype.fromJson', 'version 9', 'Blockly.Events.BlockCreate.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.xml = utilsXml.textToDom(json['xml']); this.xml = utilsXml.textToDom(json['xml']);
this.ids = json['ids']; this.ids = json['ids'];
@@ -117,11 +122,16 @@ export class BlockCreate extends BlockBase {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: BlockCreateJson, workspace: Workspace, event?: any): static fromJson(
BlockCreate { json: BlockCreateJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new BlockCreate()) as event?: any
BlockCreate; ): BlockCreate {
const newEvent = super.fromJson(
json,
workspace,
event ?? new BlockCreate()
) as BlockCreate;
newEvent.xml = utilsXml.textToDom(json['xml']); newEvent.xml = utilsXml.textToDom(json['xml']);
newEvent.ids = json['ids']; newEvent.ids = json['ids'];
newEvent.json = json['json'] as blocks.State; newEvent.json = json['json'] as blocks.State;
@@ -140,13 +150,15 @@ export class BlockCreate extends BlockBase {
const workspace = this.getEventWorkspace_(); const workspace = this.getEventWorkspace_();
if (!this.json) { if (!this.json) {
throw new Error( throw new Error(
'The block JSON is undefined. Either pass a block to ' + 'The block JSON is undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.ids) { if (!this.ids) {
throw new Error( throw new Error(
'The block IDs are undefined. Either pass a block to ' + 'The block IDs are undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (forward) { if (forward) {
blocks.append(this.json, workspace); blocks.append(this.json, workspace);
@@ -158,7 +170,7 @@ export class BlockCreate extends BlockBase {
block.dispose(false); block.dispose(false);
} else if (id === this.blockId) { } else if (id === this.blockId) {
// Only complain about root-level block. // Only complain about root-level block.
console.warn('Can\'t uncreate non-existent block: ' + id); console.warn("Can't uncreate non-existent block: " + id);
} }
} }
} }

View File

@@ -23,14 +23,13 @@ import {BlockBase, BlockBaseJson} from './events_block_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import {Workspace} from '../workspace.js'; import {Workspace} from '../workspace.js';
/** /**
* Notifies listeners when a block (or connected stack of blocks) is * Notifies listeners when a block (or connected stack of blocks) is
* deleted. * deleted.
*/ */
export class BlockDelete extends BlockBase { export class BlockDelete extends BlockBase {
/** The XML representation of the deleted block(s). */ /** The XML representation of the deleted block(s). */
oldXml?: Element|DocumentFragment; oldXml?: Element | DocumentFragment;
/** The JSON respresentation of the deleted block(s). */ /** The JSON respresentation of the deleted block(s). */
oldJson?: blocks.State; oldJson?: blocks.State;
@@ -48,7 +47,7 @@ export class BlockDelete extends BlockBase {
super(opt_block); super(opt_block);
if (!opt_block) { if (!opt_block) {
return; // Blank event to be populated by fromJson. return; // Blank event to be populated by fromJson.
} }
if (opt_block.getParent()) { if (opt_block.getParent()) {
@@ -62,8 +61,9 @@ export class BlockDelete extends BlockBase {
this.oldXml = Xml.blockToDomWithXY(opt_block); this.oldXml = Xml.blockToDomWithXY(opt_block);
this.ids = eventUtils.getDescendantIds(opt_block); this.ids = eventUtils.getDescendantIds(opt_block);
this.wasShadow = opt_block.isShadow(); this.wasShadow = opt_block.isShadow();
this.oldJson = this.oldJson = blocks.save(opt_block, {
blocks.save(opt_block, {addCoordinates: true}) as blocks.State; addCoordinates: true,
}) as blocks.State;
} }
/** /**
@@ -75,23 +75,27 @@ export class BlockDelete extends BlockBase {
const json = super.toJson() as BlockDeleteJson; const json = super.toJson() as BlockDeleteJson;
if (!this.oldXml) { if (!this.oldXml) {
throw new Error( throw new Error(
'The old block XML is undefined. Either pass a block ' + 'The old block XML is undefined. Either pass a block ' +
'to the constructor, or call fromJson'); 'to the constructor, or call fromJson'
);
} }
if (!this.ids) { if (!this.ids) {
throw new Error( throw new Error(
'The block IDs are undefined. Either pass a block to ' + 'The block IDs are undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (this.wasShadow === undefined) { if (this.wasShadow === undefined) {
throw new Error( throw new Error(
'Whether the block was a shadow is undefined. Either ' + 'Whether the block was a shadow is undefined. Either ' +
'pass a block to the constructor, or call fromJson'); 'pass a block to the constructor, or call fromJson'
);
} }
if (!this.oldJson) { if (!this.oldJson) {
throw new Error( throw new Error(
'The old block JSON is undefined. Either pass a block ' + 'The old block JSON is undefined. Either pass a block ' +
'to the constructor, or call fromJson'); 'to the constructor, or call fromJson'
);
} }
json['oldXml'] = Xml.domToText(this.oldXml); json['oldXml'] = Xml.domToText(this.oldXml);
json['ids'] = this.ids; json['ids'] = this.ids;
@@ -110,13 +114,16 @@ export class BlockDelete extends BlockBase {
*/ */
override fromJson(json: BlockDeleteJson) { override fromJson(json: BlockDeleteJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.BlockDelete.prototype.fromJson', 'version 9', 'Blockly.Events.BlockDelete.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.oldXml = utilsXml.textToDom(json['oldXml']); this.oldXml = utilsXml.textToDom(json['oldXml']);
this.ids = json['ids']; this.ids = json['ids'];
this.wasShadow = this.wasShadow =
json['wasShadow'] || this.oldXml.tagName.toLowerCase() === 'shadow'; json['wasShadow'] || this.oldXml.tagName.toLowerCase() === 'shadow';
this.oldJson = json['oldJson']; this.oldJson = json['oldJson'];
if (json['recordUndo'] !== undefined) { if (json['recordUndo'] !== undefined) {
this.recordUndo = json['recordUndo']; this.recordUndo = json['recordUndo'];
@@ -132,15 +139,20 @@ export class BlockDelete extends BlockBase {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: BlockDeleteJson, workspace: Workspace, event?: any): static fromJson(
BlockDelete { json: BlockDeleteJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new BlockDelete()) as event?: any
BlockDelete; ): BlockDelete {
const newEvent = super.fromJson(
json,
workspace,
event ?? new BlockDelete()
) as BlockDelete;
newEvent.oldXml = utilsXml.textToDom(json['oldXml']); newEvent.oldXml = utilsXml.textToDom(json['oldXml']);
newEvent.ids = json['ids']; newEvent.ids = json['ids'];
newEvent.wasShadow = newEvent.wasShadow =
json['wasShadow'] || newEvent.oldXml.tagName.toLowerCase() === 'shadow'; json['wasShadow'] || newEvent.oldXml.tagName.toLowerCase() === 'shadow';
newEvent.oldJson = json['oldJson']; newEvent.oldJson = json['oldJson'];
if (json['recordUndo'] !== undefined) { if (json['recordUndo'] !== undefined) {
newEvent.recordUndo = json['recordUndo']; newEvent.recordUndo = json['recordUndo'];
@@ -157,13 +169,15 @@ export class BlockDelete extends BlockBase {
const workspace = this.getEventWorkspace_(); const workspace = this.getEventWorkspace_();
if (!this.ids) { if (!this.ids) {
throw new Error( throw new Error(
'The block IDs are undefined. Either pass a block to ' + 'The block IDs are undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.oldJson) { if (!this.oldJson) {
throw new Error( throw new Error(
'The old block JSON is undefined. Either pass a block ' + 'The old block JSON is undefined. Either pass a block ' +
'to the constructor, or call fromJson'); 'to the constructor, or call fromJson'
);
} }
if (forward) { if (forward) {
for (let i = 0; i < this.ids.length; i++) { for (let i = 0; i < this.ids.length; i++) {
@@ -173,7 +187,7 @@ export class BlockDelete extends BlockBase {
block.dispose(false); block.dispose(false);
} else if (id === this.blockId) { } else if (id === this.blockId) {
// Only complain about root-level block. // Only complain about root-level block.
console.warn('Can\'t delete non-existent block: ' + id); console.warn("Can't delete non-existent block: " + id);
} }
} }
} else { } else {

View File

@@ -20,7 +20,6 @@ import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import {Workspace} from '../workspace.js'; import {Workspace} from '../workspace.js';
/** /**
* Notifies listeners when a block is being manually dragged/dropped. * Notifies listeners when a block is being manually dragged/dropped.
*/ */
@@ -66,13 +65,15 @@ export class BlockDrag extends UiBase {
const json = super.toJson() as BlockDragJson; const json = super.toJson() as BlockDragJson;
if (this.isStart === undefined) { if (this.isStart === undefined) {
throw new Error( throw new Error(
'Whether this event is the start of a drag is undefined. ' + 'Whether this event is the start of a drag is undefined. ' +
'Either pass the value to the constructor, or call fromJson'); 'Either pass the value to the constructor, or call fromJson'
);
} }
if (this.blockId === undefined) { if (this.blockId === undefined) {
throw new Error( throw new Error(
'The block ID is undefined. Either pass a block to ' + 'The block ID is undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['isStart'] = this.isStart; json['isStart'] = this.isStart;
json['blockId'] = this.blockId; json['blockId'] = this.blockId;
@@ -89,8 +90,11 @@ export class BlockDrag extends UiBase {
*/ */
override fromJson(json: BlockDragJson) { override fromJson(json: BlockDragJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.BlockDrag.prototype.fromJson', 'version 9', 'Blockly.Events.BlockDrag.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.isStart = json['isStart']; this.isStart = json['isStart'];
this.blockId = json['blockId']; this.blockId = json['blockId'];
@@ -106,10 +110,16 @@ export class BlockDrag extends UiBase {
* static methods in superclasses.. * static methods in superclasses..
* @internal * @internal
*/ */
static fromJson(json: BlockDragJson, workspace: Workspace, event?: any): static fromJson(
BlockDrag { json: BlockDragJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new BlockDrag()) as BlockDrag; event?: any
): BlockDrag {
const newEvent = super.fromJson(
json,
workspace,
event ?? new BlockDrag()
) as BlockDrag;
newEvent.isStart = json['isStart']; newEvent.isStart = json['isStart'];
newEvent.blockId = json['blockId']; newEvent.blockId = json['blockId'];
newEvent.blocks = json['blocks']; newEvent.blocks = json['blocks'];

View File

@@ -22,7 +22,6 @@ import {BlockBase, BlockBaseJson} from './events_block_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
interface BlockLocation { interface BlockLocation {
parentId?: string; parentId?: string;
inputName?: string; inputName?: string;
@@ -109,14 +108,16 @@ export class BlockMove extends BlockBase {
json['oldParentId'] = this.oldParentId; json['oldParentId'] = this.oldParentId;
json['oldInputName'] = this.oldInputName; json['oldInputName'] = this.oldInputName;
if (this.oldCoordinate) { if (this.oldCoordinate) {
json['oldCoordinate'] = `${Math.round(this.oldCoordinate.x)}, ` + json['oldCoordinate'] =
`${Math.round(this.oldCoordinate.y)}`; `${Math.round(this.oldCoordinate.x)}, ` +
`${Math.round(this.oldCoordinate.y)}`;
} }
json['newParentId'] = this.newParentId; json['newParentId'] = this.newParentId;
json['newInputName'] = this.newInputName; json['newInputName'] = this.newInputName;
if (this.newCoordinate) { if (this.newCoordinate) {
json['newCoordinate'] = `${Math.round(this.newCoordinate.x)}, ` + json['newCoordinate'] =
`${Math.round(this.newCoordinate.y)}`; `${Math.round(this.newCoordinate.x)}, ` +
`${Math.round(this.newCoordinate.y)}`;
} }
if (this.reason) { if (this.reason) {
json['reason'] = this.reason; json['reason'] = this.reason;
@@ -134,8 +135,11 @@ export class BlockMove extends BlockBase {
*/ */
override fromJson(json: BlockMoveJson) { override fromJson(json: BlockMoveJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.BlockMove.prototype.fromJson', 'version 9', 'Blockly.Events.BlockMove.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.oldParentId = json['oldParentId']; this.oldParentId = json['oldParentId'];
this.oldInputName = json['oldInputName']; this.oldInputName = json['oldInputName'];
@@ -166,10 +170,16 @@ export class BlockMove extends BlockBase {
* static methods in superclasses. * static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: BlockMoveJson, workspace: Workspace, event?: any): static fromJson(
BlockMove { json: BlockMoveJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new BlockMove()) as BlockMove; event?: any
): BlockMove {
const newEvent = super.fromJson(
json,
workspace,
event ?? new BlockMove()
) as BlockMove;
newEvent.oldParentId = json['oldParentId']; newEvent.oldParentId = json['oldParentId'];
newEvent.oldInputName = json['oldInputName']; newEvent.oldInputName = json['oldInputName'];
if (json['oldCoordinate']) { if (json['oldCoordinate']) {
@@ -218,14 +228,15 @@ export class BlockMove extends BlockBase {
const workspace = this.getEventWorkspace_(); const workspace = this.getEventWorkspace_();
if (!this.blockId) { if (!this.blockId) {
throw new Error( throw new Error(
'The block ID is undefined. Either pass a block to ' + 'The block ID is undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
const block = workspace.getBlockById(this.blockId); const block = workspace.getBlockById(this.blockId);
if (!block) { if (!block) {
throw new Error( throw new Error(
'The block associated with the block move event ' + 'The block associated with the block move event ' + 'could not be found'
'could not be found'); );
} }
const location = {} as BlockLocation; const location = {} as BlockLocation;
const parent = block.getParent(); const parent = block.getParent();
@@ -247,9 +258,11 @@ export class BlockMove extends BlockBase {
* @returns False if something changed. * @returns False if something changed.
*/ */
override isNull(): boolean { override isNull(): boolean {
return this.oldParentId === this.newParentId && return (
this.oldInputName === this.newInputName && this.oldParentId === this.newParentId &&
Coordinate.equals(this.oldCoordinate, this.newCoordinate); this.oldInputName === this.newInputName &&
Coordinate.equals(this.oldCoordinate, this.newCoordinate)
);
} }
/** /**
@@ -261,22 +274,23 @@ export class BlockMove extends BlockBase {
const workspace = this.getEventWorkspace_(); const workspace = this.getEventWorkspace_();
if (!this.blockId) { if (!this.blockId) {
throw new Error( throw new Error(
'The block ID is undefined. Either pass a block to ' + 'The block ID is undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
const block = workspace.getBlockById(this.blockId); const block = workspace.getBlockById(this.blockId);
if (!block) { if (!block) {
console.warn('Can\'t move non-existent block: ' + this.blockId); console.warn("Can't move non-existent block: " + this.blockId);
return; return;
} }
const parentId = forward ? this.newParentId : this.oldParentId; const parentId = forward ? this.newParentId : this.oldParentId;
const inputName = forward ? this.newInputName : this.oldInputName; const inputName = forward ? this.newInputName : this.oldInputName;
const coordinate = forward ? this.newCoordinate : this.oldCoordinate; const coordinate = forward ? this.newCoordinate : this.oldCoordinate;
let parentBlock: Block|null; let parentBlock: Block | null;
if (parentId) { if (parentId) {
parentBlock = workspace.getBlockById(parentId); parentBlock = workspace.getBlockById(parentId);
if (!parentBlock) { if (!parentBlock) {
console.warn('Can\'t connect to non-existent block: ' + parentId); console.warn("Can't connect to non-existent block: " + parentId);
return; return;
} }
} }
@@ -288,8 +302,10 @@ export class BlockMove extends BlockBase {
block.moveBy(coordinate.x - xy.x, coordinate.y - xy.y, this.reason); block.moveBy(coordinate.x - xy.x, coordinate.y - xy.y, this.reason);
} else { } else {
let blockConnection = block.outputConnection; let blockConnection = block.outputConnection;
if (!blockConnection || if (
block.previousConnection && block.previousConnection.isConnected()) { !blockConnection ||
(block.previousConnection && block.previousConnection.isConnected())
) {
blockConnection = block.previousConnection; blockConnection = block.previousConnection;
} }
let parentConnection; let parentConnection;
@@ -305,7 +321,7 @@ export class BlockMove extends BlockBase {
if (parentConnection && blockConnection) { if (parentConnection && blockConnection) {
blockConnection.connect(parentConnection); blockConnection.connect(parentConnection);
} else { } else {
console.warn('Can\'t connect to non-existent input: ' + inputName); console.warn("Can't connect to non-existent input: " + inputName);
} }
} }
} }

View File

@@ -20,7 +20,6 @@ import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Class for a bubble open event. * Class for a bubble open event.
*/ */
@@ -44,7 +43,10 @@ export class BubbleOpen extends UiBase {
* 'warning'. Undefined for a blank event. * 'warning'. Undefined for a blank event.
*/ */
constructor( constructor(
opt_block?: BlockSvg, opt_isOpen?: boolean, opt_bubbleType?: BubbleType) { opt_block?: BlockSvg,
opt_isOpen?: boolean,
opt_bubbleType?: BubbleType
) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined; const workspaceId = opt_block ? opt_block.workspace.id : undefined;
super(workspaceId); super(workspaceId);
if (!opt_block) return; if (!opt_block) return;
@@ -63,13 +65,15 @@ export class BubbleOpen extends UiBase {
const json = super.toJson() as BubbleOpenJson; const json = super.toJson() as BubbleOpenJson;
if (this.isOpen === undefined) { if (this.isOpen === undefined) {
throw new Error( throw new Error(
'Whether this event is for opening the bubble is undefined. ' + 'Whether this event is for opening the bubble is undefined. ' +
'Either pass the value to the constructor, or call fromJson'); 'Either pass the value to the constructor, or call fromJson'
);
} }
if (!this.bubbleType) { if (!this.bubbleType) {
throw new Error( throw new Error(
'The type of bubble is undefined. Either pass the ' + 'The type of bubble is undefined. Either pass the ' +
'value to the constructor, or call fromJson'); 'value to the constructor, or call fromJson'
);
} }
json['isOpen'] = this.isOpen; json['isOpen'] = this.isOpen;
json['bubbleType'] = this.bubbleType; json['bubbleType'] = this.bubbleType;
@@ -84,8 +88,11 @@ export class BubbleOpen extends UiBase {
*/ */
override fromJson(json: BubbleOpenJson) { override fromJson(json: BubbleOpenJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.BubbleOpen.prototype.fromJson', 'version 9', 'Blockly.Events.BubbleOpen.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.isOpen = json['isOpen']; this.isOpen = json['isOpen'];
this.bubbleType = json['bubbleType']; this.bubbleType = json['bubbleType'];
@@ -101,11 +108,16 @@ export class BubbleOpen extends UiBase {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: BubbleOpenJson, workspace: Workspace, event?: any): static fromJson(
BubbleOpen { json: BubbleOpenJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new BubbleOpen()) as event?: any
BubbleOpen; ): BubbleOpen {
const newEvent = super.fromJson(
json,
workspace,
event ?? new BubbleOpen()
) as BubbleOpen;
newEvent.isOpen = json['isOpen']; newEvent.isOpen = json['isOpen'];
newEvent.bubbleType = json['bubbleType']; newEvent.bubbleType = json['bubbleType'];
newEvent.blockId = json['blockId']; newEvent.blockId = json['blockId'];

View File

@@ -21,7 +21,6 @@ import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import {Workspace} from '../workspace.js'; import {Workspace} from '../workspace.js';
/** /**
* Notifies listeners that ome blockly element was clicked. * Notifies listeners that ome blockly element was clicked.
*/ */
@@ -46,8 +45,10 @@ export class Click extends UiBase {
* Undefined for a blank event. * Undefined for a blank event.
*/ */
constructor( constructor(
opt_block?: Block|null, opt_workspaceId?: string|null, opt_block?: Block | null,
opt_targetType?: ClickTarget) { opt_workspaceId?: string | null,
opt_targetType?: ClickTarget
) {
let workspaceId = opt_block ? opt_block.workspace.id : opt_workspaceId; let workspaceId = opt_block ? opt_block.workspace.id : opt_workspaceId;
if (workspaceId === null) { if (workspaceId === null) {
workspaceId = undefined; workspaceId = undefined;
@@ -67,8 +68,9 @@ export class Click extends UiBase {
const json = super.toJson() as ClickJson; const json = super.toJson() as ClickJson;
if (!this.targetType) { if (!this.targetType) {
throw new Error( throw new Error(
'The click target type is undefined. Either pass a block to ' + 'The click target type is undefined. Either pass a block to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['targetType'] = this.targetType; json['targetType'] = this.targetType;
json['blockId'] = this.blockId; json['blockId'] = this.blockId;
@@ -82,8 +84,11 @@ export class Click extends UiBase {
*/ */
override fromJson(json: ClickJson) { override fromJson(json: ClickJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.Click.prototype.fromJson', 'version 9', 'version 10', 'Blockly.Events.Click.prototype.fromJson',
'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.targetType = json['targetType']; this.targetType = json['targetType'];
this.blockId = json['blockId']; this.blockId = json['blockId'];
@@ -99,8 +104,11 @@ export class Click extends UiBase {
* @internal * @internal
*/ */
static fromJson(json: ClickJson, workspace: Workspace, event?: any): Click { static fromJson(json: ClickJson, workspace: Workspace, event?: any): Click {
const newEvent = const newEvent = super.fromJson(
super.fromJson(json, workspace, event ?? new Click()) as Click; json,
workspace,
event ?? new Click()
) as Click;
newEvent.targetType = json['targetType']; newEvent.targetType = json['targetType'];
newEvent.blockId = json['blockId']; newEvent.blockId = json['blockId'];
return newEvent; return newEvent;

View File

@@ -17,13 +17,15 @@ import * as utilsXml from '../utils/xml.js';
import type {WorkspaceComment} from '../workspace_comment.js'; import type {WorkspaceComment} from '../workspace_comment.js';
import * as Xml from '../xml.js'; import * as Xml from '../xml.js';
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js'; import {
Abstract as AbstractEvent,
AbstractEventJson,
} from './events_abstract.js';
import type {CommentCreate} from './events_comment_create.js'; import type {CommentCreate} from './events_comment_create.js';
import type {CommentDelete} from './events_comment_delete.js'; import type {CommentDelete} from './events_comment_delete.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Abstract class for a comment event. * Abstract class for a comment event.
*/ */
@@ -59,8 +61,9 @@ export class CommentBase extends AbstractEvent {
const json = super.toJson() as CommentBaseJson; const json = super.toJson() as CommentBaseJson;
if (!this.commentId) { if (!this.commentId) {
throw new Error( throw new Error(
'The comment ID is undefined. Either pass a comment to ' + 'The comment ID is undefined. Either pass a comment to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['commentId'] = this.commentId; json['commentId'] = this.commentId;
return json; return json;
@@ -73,8 +76,11 @@ export class CommentBase extends AbstractEvent {
*/ */
override fromJson(json: CommentBaseJson) { override fromJson(json: CommentBaseJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.CommentBase.prototype.fromJson', 'version 9', 'Blockly.Events.CommentBase.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.commentId = json['commentId']; this.commentId = json['commentId'];
} }
@@ -88,11 +94,16 @@ export class CommentBase extends AbstractEvent {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: CommentBaseJson, workspace: Workspace, event?: any): static fromJson(
CommentBase { json: CommentBaseJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new CommentBase()) as event?: any
CommentBase; ): CommentBase {
const newEvent = super.fromJson(
json,
workspace,
event ?? new CommentBase()
) as CommentBase;
newEvent.commentId = json['commentId']; newEvent.commentId = json['commentId'];
return newEvent; return newEvent;
} }
@@ -104,7 +115,9 @@ export class CommentBase extends AbstractEvent {
* @param create if True then Create, if False then Delete * @param create if True then Create, if False then Delete
*/ */
static CommentCreateDeleteHelper( static CommentCreateDeleteHelper(
event: CommentCreate|CommentDelete, create: boolean) { event: CommentCreate | CommentDelete,
create: boolean
) {
const workspace = event.getEventWorkspace_(); const workspace = event.getEventWorkspace_();
if (create) { if (create) {
const xmlElement = utilsXml.createElement('xml'); const xmlElement = utilsXml.createElement('xml');
@@ -116,16 +129,16 @@ export class CommentBase extends AbstractEvent {
} else { } else {
if (!event.commentId) { if (!event.commentId) {
throw new Error( throw new Error(
'The comment ID is undefined. Either pass a comment to ' + 'The comment ID is undefined. Either pass a comment to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
const comment = workspace.getCommentById(event.commentId); const comment = workspace.getCommentById(event.commentId);
if (comment) { if (comment) {
comment.dispose(); comment.dispose();
} else { } else {
// Only complain about root-level block. // Only complain about root-level block.
console.warn( console.warn("Can't uncreate non-existent comment: " + event.commentId);
'Can\'t uncreate non-existent comment: ' + event.commentId);
} }
} }
} }

View File

@@ -20,7 +20,6 @@ import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Notifies listeners that the contents of a workspace comment has changed. * Notifies listeners that the contents of a workspace comment has changed.
*/ */
@@ -41,18 +40,20 @@ export class CommentChange extends CommentBase {
* @param opt_newContents New contents of the comment. * @param opt_newContents New contents of the comment.
*/ */
constructor( constructor(
opt_comment?: WorkspaceComment, opt_oldContents?: string, opt_comment?: WorkspaceComment,
opt_newContents?: string) { opt_oldContents?: string,
opt_newContents?: string
) {
super(opt_comment); super(opt_comment);
if (!opt_comment) { if (!opt_comment) {
return; // Blank event to be populated by fromJson. return; // Blank event to be populated by fromJson.
} }
this.oldContents_ = this.oldContents_ =
typeof opt_oldContents === 'undefined' ? '' : opt_oldContents; typeof opt_oldContents === 'undefined' ? '' : opt_oldContents;
this.newContents_ = this.newContents_ =
typeof opt_newContents === 'undefined' ? '' : opt_newContents; typeof opt_newContents === 'undefined' ? '' : opt_newContents;
} }
/** /**
@@ -64,13 +65,15 @@ export class CommentChange extends CommentBase {
const json = super.toJson() as CommentChangeJson; const json = super.toJson() as CommentChangeJson;
if (!this.oldContents_) { if (!this.oldContents_) {
throw new Error( throw new Error(
'The old contents is undefined. Either pass a value to ' + 'The old contents is undefined. Either pass a value to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.newContents_) { if (!this.newContents_) {
throw new Error( throw new Error(
'The new contents is undefined. Either pass a value to ' + 'The new contents is undefined. Either pass a value to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['oldContents'] = this.oldContents_; json['oldContents'] = this.oldContents_;
json['newContents'] = this.newContents_; json['newContents'] = this.newContents_;
@@ -84,8 +87,11 @@ export class CommentChange extends CommentBase {
*/ */
override fromJson(json: CommentChangeJson) { override fromJson(json: CommentChangeJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.CommentChange.prototype.fromJson', 'version 9', 'Blockly.Events.CommentChange.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.oldContents_ = json['oldContents']; this.oldContents_ = json['oldContents'];
this.newContents_ = json['newContents']; this.newContents_ = json['newContents'];
@@ -100,11 +106,16 @@ export class CommentChange extends CommentBase {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: CommentChangeJson, workspace: Workspace, event?: any): static fromJson(
CommentChange { json: CommentChangeJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new CommentChange()) as event?: any
CommentChange; ): CommentChange {
const newEvent = super.fromJson(
json,
workspace,
event ?? new CommentChange()
) as CommentChange;
newEvent.oldContents_ = json['oldContents']; newEvent.oldContents_ = json['oldContents'];
newEvent.newContents_ = json['newContents']; newEvent.newContents_ = json['newContents'];
return newEvent; return newEvent;
@@ -128,24 +139,27 @@ export class CommentChange extends CommentBase {
const workspace = this.getEventWorkspace_(); const workspace = this.getEventWorkspace_();
if (!this.commentId) { if (!this.commentId) {
throw new Error( throw new Error(
'The comment ID is undefined. Either pass a comment to ' + 'The comment ID is undefined. Either pass a comment to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
const comment = workspace.getCommentById(this.commentId); const comment = workspace.getCommentById(this.commentId);
if (!comment) { if (!comment) {
console.warn('Can\'t change non-existent comment: ' + this.commentId); console.warn("Can't change non-existent comment: " + this.commentId);
return; return;
} }
const contents = forward ? this.newContents_ : this.oldContents_; const contents = forward ? this.newContents_ : this.oldContents_;
if (!contents) { if (!contents) {
if (forward) { if (forward) {
throw new Error( throw new Error(
'The new contents is undefined. Either pass a value to ' + 'The new contents is undefined. Either pass a value to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
throw new Error( throw new Error(
'The old contents is undefined. Either pass a value to ' + 'The old contents is undefined. Either pass a value to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
comment.setContent(contents); comment.setContent(contents);
} }
@@ -157,4 +171,7 @@ export interface CommentChangeJson extends CommentBaseJson {
} }
registry.register( registry.register(
registry.Type.EVENT, eventUtils.COMMENT_CHANGE, CommentChange); registry.Type.EVENT,
eventUtils.COMMENT_CHANGE,
CommentChange
);

View File

@@ -22,7 +22,6 @@ import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Notifies listeners that a workspace comment was created. * Notifies listeners that a workspace comment was created.
*/ */
@@ -30,7 +29,7 @@ export class CommentCreate extends CommentBase {
override type = eventUtils.COMMENT_CREATE; override type = eventUtils.COMMENT_CREATE;
/** The XML representation of the created workspace comment. */ /** The XML representation of the created workspace comment. */
xml?: Element|DocumentFragment; xml?: Element | DocumentFragment;
/** /**
* @param opt_comment The created comment. * @param opt_comment The created comment.
@@ -56,8 +55,9 @@ export class CommentCreate extends CommentBase {
const json = super.toJson() as CommentCreateJson; const json = super.toJson() as CommentCreateJson;
if (!this.xml) { if (!this.xml) {
throw new Error( throw new Error(
'The comment XML is undefined. Either pass a comment to ' + 'The comment XML is undefined. Either pass a comment to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['xml'] = Xml.domToText(this.xml); json['xml'] = Xml.domToText(this.xml);
return json; return json;
@@ -70,8 +70,11 @@ export class CommentCreate extends CommentBase {
*/ */
override fromJson(json: CommentCreateJson) { override fromJson(json: CommentCreateJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.CommentCreate.prototype.fromJson', 'version 9', 'Blockly.Events.CommentCreate.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.xml = utilsXml.textToDom(json['xml']); this.xml = utilsXml.textToDom(json['xml']);
} }
@@ -85,11 +88,16 @@ export class CommentCreate extends CommentBase {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: CommentCreateJson, workspace: Workspace, event?: any): static fromJson(
CommentCreate { json: CommentCreateJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new CommentCreate()) as event?: any
CommentCreate; ): CommentCreate {
const newEvent = super.fromJson(
json,
workspace,
event ?? new CommentCreate()
) as CommentCreate;
newEvent.xml = utilsXml.textToDom(json['xml']); newEvent.xml = utilsXml.textToDom(json['xml']);
return newEvent; return newEvent;
} }
@@ -109,4 +117,7 @@ export interface CommentCreateJson extends CommentBaseJson {
} }
registry.register( registry.register(
registry.Type.EVENT, eventUtils.COMMENT_CREATE, CommentCreate); registry.Type.EVENT,
eventUtils.COMMENT_CREATE,
CommentCreate
);

View File

@@ -21,7 +21,6 @@ import * as utilsXml from '../utils/xml.js';
import * as Xml from '../xml.js'; import * as Xml from '../xml.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Notifies listeners that a workspace comment has been deleted. * Notifies listeners that a workspace comment has been deleted.
*/ */
@@ -39,7 +38,7 @@ export class CommentDelete extends CommentBase {
super(opt_comment); super(opt_comment);
if (!opt_comment) { if (!opt_comment) {
return; // Blank event to be populated by fromJson. return; // Blank event to be populated by fromJson.
} }
this.xml = opt_comment.toXmlWithXY(); this.xml = opt_comment.toXmlWithXY();
@@ -63,8 +62,9 @@ export class CommentDelete extends CommentBase {
const json = super.toJson() as CommentDeleteJson; const json = super.toJson() as CommentDeleteJson;
if (!this.xml) { if (!this.xml) {
throw new Error( throw new Error(
'The comment XML is undefined. Either pass a comment to ' + 'The comment XML is undefined. Either pass a comment to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['xml'] = Xml.domToText(this.xml); json['xml'] = Xml.domToText(this.xml);
return json; return json;
@@ -79,11 +79,16 @@ export class CommentDelete extends CommentBase {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: CommentDeleteJson, workspace: Workspace, event?: any): static fromJson(
CommentDelete { json: CommentDeleteJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new CommentDelete()) as event?: any
CommentDelete; ): CommentDelete {
const newEvent = super.fromJson(
json,
workspace,
event ?? new CommentDelete()
) as CommentDelete;
newEvent.xml = utilsXml.textToDom(json['xml']); newEvent.xml = utilsXml.textToDom(json['xml']);
return newEvent; return newEvent;
} }
@@ -94,4 +99,7 @@ export interface CommentDeleteJson extends CommentBaseJson {
} }
registry.register( registry.register(
registry.Type.EVENT, eventUtils.COMMENT_DELETE, CommentDelete); registry.Type.EVENT,
eventUtils.COMMENT_DELETE,
CommentDelete
);

View File

@@ -21,7 +21,6 @@ import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Notifies listeners that a workspace comment has moved. * Notifies listeners that a workspace comment has moved.
*/ */
@@ -46,7 +45,7 @@ export class CommentMove extends CommentBase {
super(opt_comment); super(opt_comment);
if (!opt_comment) { if (!opt_comment) {
return; // Blank event to be populated by fromJson. return; // Blank event to be populated by fromJson.
} }
this.comment_ = opt_comment; this.comment_ = opt_comment;
@@ -60,13 +59,15 @@ export class CommentMove extends CommentBase {
recordNew() { recordNew() {
if (this.newCoordinate_) { if (this.newCoordinate_) {
throw Error( throw Error(
'Tried to record the new position of a comment on the ' + 'Tried to record the new position of a comment on the ' +
'same event twice.'); 'same event twice.'
);
} }
if (!this.comment_) { if (!this.comment_) {
throw new Error( throw new Error(
'The comment is undefined. Pass a comment to ' + 'The comment is undefined. Pass a comment to ' +
'the constructor if you want to use the record functionality'); 'the constructor if you want to use the record functionality'
);
} }
this.newCoordinate_ = this.comment_.getRelativeToSurfaceXY(); this.newCoordinate_ = this.comment_.getRelativeToSurfaceXY();
} }
@@ -91,18 +92,23 @@ export class CommentMove extends CommentBase {
const json = super.toJson() as CommentMoveJson; const json = super.toJson() as CommentMoveJson;
if (!this.oldCoordinate_) { if (!this.oldCoordinate_) {
throw new Error( throw new Error(
'The old comment position is undefined. Either pass a comment to ' + 'The old comment position is undefined. Either pass a comment to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.newCoordinate_) { if (!this.newCoordinate_) {
throw new Error( throw new Error(
'The new comment position is undefined. Either call recordNew, or ' + 'The new comment position is undefined. Either call recordNew, or ' +
'call fromJson'); 'call fromJson'
);
} }
json['oldCoordinate'] = `${Math.round(this.oldCoordinate_.x)}, ` + json['oldCoordinate'] =
`${Math.round(this.oldCoordinate_.y)}`; `${Math.round(this.oldCoordinate_.x)}, ` +
json['newCoordinate'] = Math.round(this.newCoordinate_.x) + ',' + `${Math.round(this.oldCoordinate_.y)}`;
Math.round(this.newCoordinate_.y); json['newCoordinate'] =
Math.round(this.newCoordinate_.x) +
',' +
Math.round(this.newCoordinate_.y);
return json; return json;
} }
@@ -113,8 +119,11 @@ export class CommentMove extends CommentBase {
*/ */
override fromJson(json: CommentMoveJson) { override fromJson(json: CommentMoveJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.CommentMove.prototype.fromJson', 'version 9', 'Blockly.Events.CommentMove.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
let xy = json['oldCoordinate'].split(','); let xy = json['oldCoordinate'].split(',');
this.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1])); this.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
@@ -131,11 +140,16 @@ export class CommentMove extends CommentBase {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: CommentMoveJson, workspace: Workspace, event?: any): static fromJson(
CommentMove { json: CommentMoveJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new CommentMove()) as event?: any
CommentMove; ): CommentMove {
const newEvent = super.fromJson(
json,
workspace,
event ?? new CommentMove()
) as CommentMove;
let xy = json['oldCoordinate'].split(','); let xy = json['oldCoordinate'].split(',');
newEvent.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1])); newEvent.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
xy = json['newCoordinate'].split(','); xy = json['newCoordinate'].split(',');
@@ -161,21 +175,23 @@ export class CommentMove extends CommentBase {
const workspace = this.getEventWorkspace_(); const workspace = this.getEventWorkspace_();
if (!this.commentId) { if (!this.commentId) {
throw new Error( throw new Error(
'The comment ID is undefined. Either pass a comment to ' + 'The comment ID is undefined. Either pass a comment to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
const comment = workspace.getCommentById(this.commentId); const comment = workspace.getCommentById(this.commentId);
if (!comment) { if (!comment) {
console.warn('Can\'t move non-existent comment: ' + this.commentId); console.warn("Can't move non-existent comment: " + this.commentId);
return; return;
} }
const target = forward ? this.newCoordinate_ : this.oldCoordinate_; const target = forward ? this.newCoordinate_ : this.oldCoordinate_;
if (!target) { if (!target) {
throw new Error( throw new Error(
'Either oldCoordinate_ or newCoordinate_ is undefined. ' + 'Either oldCoordinate_ or newCoordinate_ is undefined. ' +
'Either pass a comment to the constructor and call recordNew, ' + 'Either pass a comment to the constructor and call recordNew, ' +
'or call fromJson'); 'or call fromJson'
);
} }
// TODO: Check if the comment is being dragged, and give up if so. // TODO: Check if the comment is being dragged, and give up if so.
const current = comment.getRelativeToSurfaceXY(); const current = comment.getRelativeToSurfaceXY();

View File

@@ -22,7 +22,6 @@ import {AbstractEventJson} from './events_abstract.js';
import {UiBase} from './events_ui_base.js'; import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
/** /**
* Notifies listeners that a marker (used for keyboard navigation) has * Notifies listeners that a marker (used for keyboard navigation) has
* moved. * moved.
@@ -57,8 +56,11 @@ export class MarkerMove extends UiBase {
* Undefined for a blank event. * Undefined for a blank event.
*/ */
constructor( constructor(
opt_block?: Block|null, isCursor?: boolean, opt_oldNode?: ASTNode|null, opt_block?: Block | null,
opt_newNode?: ASTNode) { isCursor?: boolean,
opt_oldNode?: ASTNode | null,
opt_newNode?: ASTNode
) {
let workspaceId = opt_block ? opt_block.workspace.id : undefined; let workspaceId = opt_block ? opt_block.workspace.id : undefined;
if (opt_newNode && opt_newNode.getType() === ASTNode.types.WORKSPACE) { if (opt_newNode && opt_newNode.getType() === ASTNode.types.WORKSPACE) {
workspaceId = (opt_newNode.getLocation() as Workspace).id; workspaceId = (opt_newNode.getLocation() as Workspace).id;
@@ -80,13 +82,15 @@ export class MarkerMove extends UiBase {
const json = super.toJson() as MarkerMoveJson; const json = super.toJson() as MarkerMoveJson;
if (this.isCursor === undefined) { if (this.isCursor === undefined) {
throw new Error( throw new Error(
'Whether this is a cursor event or not is undefined. Either pass ' + 'Whether this is a cursor event or not is undefined. Either pass ' +
'a value to the constructor, or call fromJson'); 'a value to the constructor, or call fromJson'
);
} }
if (!this.newNode) { if (!this.newNode) {
throw new Error( throw new Error(
'The new node is undefined. Either pass a node to ' + 'The new node is undefined. Either pass a node to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['isCursor'] = this.isCursor; json['isCursor'] = this.isCursor;
json['blockId'] = this.blockId; json['blockId'] = this.blockId;
@@ -102,8 +106,11 @@ export class MarkerMove extends UiBase {
*/ */
override fromJson(json: MarkerMoveJson) { override fromJson(json: MarkerMoveJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.MarkerMove.prototype.fromJson', 'version 9', 'Blockly.Events.MarkerMove.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.isCursor = json['isCursor']; this.isCursor = json['isCursor'];
this.blockId = json['blockId']; this.blockId = json['blockId'];
@@ -120,11 +127,16 @@ export class MarkerMove extends UiBase {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: MarkerMoveJson, workspace: Workspace, event?: any): static fromJson(
MarkerMove { json: MarkerMoveJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new MarkerMove()) as event?: any
MarkerMove; ): MarkerMove {
const newEvent = super.fromJson(
json,
workspace,
event ?? new MarkerMove()
) as MarkerMove;
newEvent.isCursor = json['isCursor']; newEvent.isCursor = json['isCursor'];
newEvent.blockId = json['blockId']; newEvent.blockId = json['blockId'];
newEvent.oldNode = json['oldNode']; newEvent.oldNode = json['oldNode'];

View File

@@ -20,7 +20,6 @@ import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Class for a selected event. * Class for a selected event.
* Notifies listeners that a new element has been selected. * Notifies listeners that a new element has been selected.
@@ -46,8 +45,10 @@ export class Selected extends UiBase {
* Null if no element previously selected. Undefined for a blank event. * Null if no element previously selected. Undefined for a blank event.
*/ */
constructor( constructor(
opt_oldElementId?: string|null, opt_newElementId?: string|null, opt_oldElementId?: string | null,
opt_workspaceId?: string) { opt_newElementId?: string | null,
opt_workspaceId?: string
) {
super(opt_workspaceId); super(opt_workspaceId);
this.oldElementId = opt_oldElementId ?? undefined; this.oldElementId = opt_oldElementId ?? undefined;
@@ -73,8 +74,11 @@ export class Selected extends UiBase {
*/ */
override fromJson(json: SelectedJson) { override fromJson(json: SelectedJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.Selected.prototype.fromJson', 'version 9', 'version 10', 'Blockly.Events.Selected.prototype.fromJson',
'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.oldElementId = json['oldElementId']; this.oldElementId = json['oldElementId'];
this.newElementId = json['newElementId']; this.newElementId = json['newElementId'];
@@ -89,10 +93,16 @@ export class Selected extends UiBase {
* static methods in superclasses. * static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: SelectedJson, workspace: Workspace, event?: any): static fromJson(
Selected { json: SelectedJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new Selected()) as Selected; event?: any
): Selected {
const newEvent = super.fromJson(
json,
workspace,
event ?? new Selected()
) as Selected;
newEvent.oldElementId = json['oldElementId']; newEvent.oldElementId = json['oldElementId'];
newEvent.newElementId = json['newElementId']; newEvent.newElementId = json['newElementId'];
return newEvent; return newEvent;

View File

@@ -19,7 +19,6 @@ import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Notifies listeners that the workspace theme has changed. * Notifies listeners that the workspace theme has changed.
*/ */
@@ -48,8 +47,9 @@ export class ThemeChange extends UiBase {
const json = super.toJson() as ThemeChangeJson; const json = super.toJson() as ThemeChangeJson;
if (!this.themeName) { if (!this.themeName) {
throw new Error( throw new Error(
'The theme name is undefined. Either pass a theme name to ' + 'The theme name is undefined. Either pass a theme name to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['themeName'] = this.themeName; json['themeName'] = this.themeName;
return json; return json;
@@ -62,8 +62,11 @@ export class ThemeChange extends UiBase {
*/ */
override fromJson(json: ThemeChangeJson) { override fromJson(json: ThemeChangeJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.ThemeChange.prototype.fromJson', 'version 9', 'Blockly.Events.ThemeChange.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.themeName = json['themeName']; this.themeName = json['themeName'];
} }
@@ -77,11 +80,16 @@ export class ThemeChange extends UiBase {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: ThemeChangeJson, workspace: Workspace, event?: any): static fromJson(
ThemeChange { json: ThemeChangeJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new ThemeChange()) as event?: any
ThemeChange; ): ThemeChange {
const newEvent = super.fromJson(
json,
workspace,
event ?? new ThemeChange()
) as ThemeChange;
newEvent.themeName = json['themeName']; newEvent.themeName = json['themeName'];
return newEvent; return newEvent;
} }

View File

@@ -19,7 +19,6 @@ import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Notifies listeners that a toolbox item has been selected. * Notifies listeners that a toolbox item has been selected.
*/ */
@@ -41,8 +40,10 @@ export class ToolboxItemSelect extends UiBase {
* Undefined for a blank event. * Undefined for a blank event.
*/ */
constructor( constructor(
opt_oldItem?: string|null, opt_newItem?: string|null, opt_oldItem?: string | null,
opt_workspaceId?: string) { opt_newItem?: string | null,
opt_workspaceId?: string
) {
super(opt_workspaceId); super(opt_workspaceId);
this.oldItem = opt_oldItem ?? undefined; this.oldItem = opt_oldItem ?? undefined;
this.newItem = opt_newItem ?? undefined; this.newItem = opt_newItem ?? undefined;
@@ -67,8 +68,11 @@ export class ToolboxItemSelect extends UiBase {
*/ */
override fromJson(json: ToolboxItemSelectJson) { override fromJson(json: ToolboxItemSelectJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.ToolboxItemSelect.prototype.fromJson', 'version 9', 'Blockly.Events.ToolboxItemSelect.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.oldItem = json['oldItem']; this.oldItem = json['oldItem'];
this.newItem = json['newItem']; this.newItem = json['newItem'];
@@ -84,11 +88,15 @@ export class ToolboxItemSelect extends UiBase {
* @internal * @internal
*/ */
static fromJson( static fromJson(
json: ToolboxItemSelectJson, workspace: Workspace, json: ToolboxItemSelectJson,
event?: any): ToolboxItemSelect { workspace: Workspace,
const newEvent = event?: any
super.fromJson(json, workspace, event ?? new ToolboxItemSelect()) as ): ToolboxItemSelect {
ToolboxItemSelect; const newEvent = super.fromJson(
json,
workspace,
event ?? new ToolboxItemSelect()
) as ToolboxItemSelect;
newEvent.oldItem = json['oldItem']; newEvent.oldItem = json['oldItem'];
newEvent.newItem = json['newItem']; newEvent.newItem = json['newItem'];
return newEvent; return newEvent;
@@ -101,4 +109,7 @@ export interface ToolboxItemSelectJson extends AbstractEventJson {
} }
registry.register( registry.register(
registry.Type.EVENT, eventUtils.TOOLBOX_ITEM_SELECT, ToolboxItemSelect); registry.Type.EVENT,
eventUtils.TOOLBOX_ITEM_SELECT,
ToolboxItemSelect
);

View File

@@ -20,7 +20,6 @@ import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Notifies listeners when the trashcan is opening or closing. * Notifies listeners when the trashcan is opening or closing.
*/ */
@@ -52,8 +51,9 @@ export class TrashcanOpen extends UiBase {
const json = super.toJson() as TrashcanOpenJson; const json = super.toJson() as TrashcanOpenJson;
if (this.isOpen === undefined) { if (this.isOpen === undefined) {
throw new Error( throw new Error(
'Whether this is already open or not is undefined. Either pass ' + 'Whether this is already open or not is undefined. Either pass ' +
'a value to the constructor, or call fromJson'); 'a value to the constructor, or call fromJson'
);
} }
json['isOpen'] = this.isOpen; json['isOpen'] = this.isOpen;
return json; return json;
@@ -66,8 +66,11 @@ export class TrashcanOpen extends UiBase {
*/ */
override fromJson(json: TrashcanOpenJson) { override fromJson(json: TrashcanOpenJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.TrashcanOpen.prototype.fromJson', 'version 9', 'Blockly.Events.TrashcanOpen.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.isOpen = json['isOpen']; this.isOpen = json['isOpen'];
} }
@@ -81,11 +84,16 @@ export class TrashcanOpen extends UiBase {
* parameters to static methods in superclasses. * parameters to static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: TrashcanOpenJson, workspace: Workspace, event?: any): static fromJson(
TrashcanOpen { json: TrashcanOpenJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new TrashcanOpen()) as event?: any
TrashcanOpen; ): TrashcanOpen {
const newEvent = super.fromJson(
json,
workspace,
event ?? new TrashcanOpen()
) as TrashcanOpen;
newEvent.isOpen = json['isOpen']; newEvent.isOpen = json['isOpen'];
return newEvent; return newEvent;
} }

View File

@@ -18,7 +18,6 @@ import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js'; import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
/** /**
* Class for a UI event. * Class for a UI event.
* *
@@ -39,8 +38,11 @@ export class Ui extends UiBase {
* @param opt_newValue New value of element. * @param opt_newValue New value of element.
*/ */
constructor( constructor(
opt_block?: Block|null, opt_element?: string, opt_block?: Block | null,
opt_oldValue?: AnyDuringMigration, opt_newValue?: AnyDuringMigration) { opt_element?: string,
opt_oldValue?: AnyDuringMigration,
opt_newValue?: AnyDuringMigration
) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined; const workspaceId = opt_block ? opt_block.workspace.id : undefined;
super(workspaceId); super(workspaceId);

View File

@@ -15,7 +15,6 @@ goog.declareModuleId('Blockly.Events.UiBase');
import {Abstract as AbstractEvent} from './events_abstract.js'; import {Abstract as AbstractEvent} from './events_abstract.js';
/** /**
* Base class for a UI event. * Base class for a UI event.
* UI events are events that don't need to be sent over the wire for multi-user * UI events are events that don't need to be sent over the wire for multi-user

View File

@@ -15,10 +15,12 @@ goog.declareModuleId('Blockly.Events.VarBase');
import * as deprecation from '../utils/deprecation.js'; import * as deprecation from '../utils/deprecation.js';
import type {VariableModel} from '../variable_model.js'; import type {VariableModel} from '../variable_model.js';
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js'; import {
Abstract as AbstractEvent,
AbstractEventJson,
} from './events_abstract.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Abstract class for a variable event. * Abstract class for a variable event.
*/ */
@@ -49,8 +51,9 @@ export class VarBase extends AbstractEvent {
const json = super.toJson() as VarBaseJson; const json = super.toJson() as VarBaseJson;
if (!this.varId) { if (!this.varId) {
throw new Error( throw new Error(
'The var ID is undefined. Either pass a variable to ' + 'The var ID is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['varId'] = this.varId; json['varId'] = this.varId;
return json; return json;
@@ -63,8 +66,11 @@ export class VarBase extends AbstractEvent {
*/ */
override fromJson(json: VarBaseJson) { override fromJson(json: VarBaseJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.VarBase.prototype.fromJson', 'version 9', 'version 10', 'Blockly.Events.VarBase.prototype.fromJson',
'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.varId = json['varId']; this.varId = json['varId'];
} }
@@ -78,10 +84,16 @@ export class VarBase extends AbstractEvent {
* static methods in superclasses. * static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: VarBaseJson, workspace: Workspace, event?: any): static fromJson(
VarBase { json: VarBaseJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new VarBase()) as VarBase; event?: any
): VarBase {
const newEvent = super.fromJson(
json,
workspace,
event ?? new VarBase()
) as VarBase;
newEvent.varId = json['varId']; newEvent.varId = json['varId'];
return newEvent; return newEvent;
} }

View File

@@ -20,7 +20,6 @@ import {VarBase, VarBaseJson} from './events_var_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Notifies listeners that a variable model has been created. * Notifies listeners that a variable model has been created.
*/ */
@@ -40,7 +39,7 @@ export class VarCreate extends VarBase {
super(opt_variable); super(opt_variable);
if (!opt_variable) { if (!opt_variable) {
return; // Blank event to be populated by fromJson. return; // Blank event to be populated by fromJson.
} }
this.varType = opt_variable.type; this.varType = opt_variable.type;
this.varName = opt_variable.name; this.varName = opt_variable.name;
@@ -55,13 +54,15 @@ export class VarCreate extends VarBase {
const json = super.toJson() as VarCreateJson; const json = super.toJson() as VarCreateJson;
if (this.varType === undefined) { if (this.varType === undefined) {
throw new Error( throw new Error(
'The var type is undefined. Either pass a variable to ' + 'The var type is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.varName) { if (!this.varName) {
throw new Error( throw new Error(
'The var name is undefined. Either pass a variable to ' + 'The var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['varType'] = this.varType; json['varType'] = this.varType;
json['varName'] = this.varName; json['varName'] = this.varName;
@@ -75,8 +76,11 @@ export class VarCreate extends VarBase {
*/ */
override fromJson(json: VarCreateJson) { override fromJson(json: VarCreateJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.VarCreate.prototype.fromJson', 'version 9', 'Blockly.Events.VarCreate.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.varType = json['varType']; this.varType = json['varType'];
this.varName = json['varName']; this.varName = json['varName'];
@@ -91,10 +95,16 @@ export class VarCreate extends VarBase {
* static methods in superclasses. * static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: VarCreateJson, workspace: Workspace, event?: any): static fromJson(
VarCreate { json: VarCreateJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new VarCreate()) as VarCreate; event?: any
): VarCreate {
const newEvent = super.fromJson(
json,
workspace,
event ?? new VarCreate()
) as VarCreate;
newEvent.varType = json['varType']; newEvent.varType = json['varType'];
newEvent.varName = json['varName']; newEvent.varName = json['varName'];
return newEvent; return newEvent;
@@ -109,13 +119,15 @@ export class VarCreate extends VarBase {
const workspace = this.getEventWorkspace_(); const workspace = this.getEventWorkspace_();
if (!this.varId) { if (!this.varId) {
throw new Error( throw new Error(
'The var ID is undefined. Either pass a variable to ' + 'The var ID is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.varName) { if (!this.varName) {
throw new Error( throw new Error(
'The var name is undefined. Either pass a variable to ' + 'The var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (forward) { if (forward) {
workspace.createVariable(this.varName, this.varType, this.varId); workspace.createVariable(this.varName, this.varType, this.varId);

View File

@@ -15,7 +15,6 @@ import {VarBase, VarBaseJson} from './events_var_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Notifies listeners that a variable model has been deleted. * Notifies listeners that a variable model has been deleted.
* *
@@ -35,7 +34,7 @@ export class VarDelete extends VarBase {
super(opt_variable); super(opt_variable);
if (!opt_variable) { if (!opt_variable) {
return; // Blank event to be populated by fromJson. return; // Blank event to be populated by fromJson.
} }
this.varType = opt_variable.type; this.varType = opt_variable.type;
this.varName = opt_variable.name; this.varName = opt_variable.name;
@@ -50,13 +49,15 @@ export class VarDelete extends VarBase {
const json = super.toJson() as VarDeleteJson; const json = super.toJson() as VarDeleteJson;
if (this.varType === undefined) { if (this.varType === undefined) {
throw new Error( throw new Error(
'The var type is undefined. Either pass a variable to ' + 'The var type is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.varName) { if (!this.varName) {
throw new Error( throw new Error(
'The var name is undefined. Either pass a variable to ' + 'The var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['varType'] = this.varType; json['varType'] = this.varType;
json['varName'] = this.varName; json['varName'] = this.varName;
@@ -70,8 +71,11 @@ export class VarDelete extends VarBase {
*/ */
override fromJson(json: VarDeleteJson) { override fromJson(json: VarDeleteJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.VarDelete.prototype.fromJson', 'version 9', 'Blockly.Events.VarDelete.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.varType = json['varType']; this.varType = json['varType'];
this.varName = json['varName']; this.varName = json['varName'];
@@ -86,10 +90,16 @@ export class VarDelete extends VarBase {
* static methods in superclasses. * static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: VarDeleteJson, workspace: Workspace, event?: any): static fromJson(
VarDelete { json: VarDeleteJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new VarDelete()) as VarDelete; event?: any
): VarDelete {
const newEvent = super.fromJson(
json,
workspace,
event ?? new VarDelete()
) as VarDelete;
newEvent.varType = json['varType']; newEvent.varType = json['varType'];
newEvent.varName = json['varName']; newEvent.varName = json['varName'];
return newEvent; return newEvent;
@@ -104,13 +114,15 @@ export class VarDelete extends VarBase {
const workspace = this.getEventWorkspace_(); const workspace = this.getEventWorkspace_();
if (!this.varId) { if (!this.varId) {
throw new Error( throw new Error(
'The var ID is undefined. Either pass a variable to ' + 'The var ID is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.varName) { if (!this.varName) {
throw new Error( throw new Error(
'The var name is undefined. Either pass a variable to ' + 'The var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (forward) { if (forward) {
workspace.deleteVariableById(this.varId); workspace.deleteVariableById(this.varId);

View File

@@ -15,7 +15,6 @@ import {VarBase, VarBaseJson} from './events_var_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Notifies listeners that a variable model was renamed. * Notifies listeners that a variable model was renamed.
* *
@@ -38,7 +37,7 @@ export class VarRename extends VarBase {
super(opt_variable); super(opt_variable);
if (!opt_variable) { if (!opt_variable) {
return; // Blank event to be populated by fromJson. return; // Blank event to be populated by fromJson.
} }
this.oldName = opt_variable.name; this.oldName = opt_variable.name;
this.newName = typeof newName === 'undefined' ? '' : newName; this.newName = typeof newName === 'undefined' ? '' : newName;
@@ -53,13 +52,15 @@ export class VarRename extends VarBase {
const json = super.toJson() as VarRenameJson; const json = super.toJson() as VarRenameJson;
if (!this.oldName) { if (!this.oldName) {
throw new Error( throw new Error(
'The old var name is undefined. Either pass a variable to ' + 'The old var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.newName) { if (!this.newName) {
throw new Error( throw new Error(
'The new var name is undefined. Either pass a value to ' + 'The new var name is undefined. Either pass a value to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['oldName'] = this.oldName; json['oldName'] = this.oldName;
json['newName'] = this.newName; json['newName'] = this.newName;
@@ -73,8 +74,11 @@ export class VarRename extends VarBase {
*/ */
override fromJson(json: VarRenameJson) { override fromJson(json: VarRenameJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.VarRename.prototype.fromJson', 'version 9', 'Blockly.Events.VarRename.prototype.fromJson',
'version 10', 'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.oldName = json['oldName']; this.oldName = json['oldName'];
this.newName = json['newName']; this.newName = json['newName'];
@@ -89,10 +93,16 @@ export class VarRename extends VarBase {
* static methods in superclasses. * static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: VarRenameJson, workspace: Workspace, event?: any): static fromJson(
VarRename { json: VarRenameJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new VarRename()) as VarRename; event?: any
): VarRename {
const newEvent = super.fromJson(
json,
workspace,
event ?? new VarRename()
) as VarRename;
newEvent.oldName = json['oldName']; newEvent.oldName = json['oldName'];
newEvent.newName = json['newName']; newEvent.newName = json['newName'];
return newEvent; return newEvent;
@@ -107,18 +117,21 @@ export class VarRename extends VarBase {
const workspace = this.getEventWorkspace_(); const workspace = this.getEventWorkspace_();
if (!this.varId) { if (!this.varId) {
throw new Error( throw new Error(
'The var ID is undefined. Either pass a variable to ' + 'The var ID is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.oldName) { if (!this.oldName) {
throw new Error( throw new Error(
'The old var name is undefined. Either pass a variable to ' + 'The old var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (!this.newName) { if (!this.newName) {
throw new Error( throw new Error(
'The new var name is undefined. Either pass a value to ' + 'The new var name is undefined. Either pass a value to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (forward) { if (forward) {
workspace.renameVariableById(this.varId, this.newName); workspace.renameVariableById(this.varId, this.newName);

View File

@@ -19,7 +19,6 @@ import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
/** /**
* Notifies listeners that the workspace surface's position or scale has * Notifies listeners that the workspace surface's position or scale has
* changed. * changed.
@@ -59,8 +58,12 @@ export class ViewportChange extends UiBase {
* event. * event.
*/ */
constructor( constructor(
opt_top?: number, opt_left?: number, opt_scale?: number, opt_top?: number,
opt_workspaceId?: string, opt_oldScale?: number) { opt_left?: number,
opt_scale?: number,
opt_workspaceId?: string,
opt_oldScale?: number
) {
super(opt_workspaceId); super(opt_workspaceId);
this.viewTop = opt_top; this.viewTop = opt_top;
@@ -78,23 +81,27 @@ export class ViewportChange extends UiBase {
const json = super.toJson() as ViewportChangeJson; const json = super.toJson() as ViewportChangeJson;
if (this.viewTop === undefined) { if (this.viewTop === undefined) {
throw new Error( throw new Error(
'The view top is undefined. Either pass a value to ' + 'The view top is undefined. Either pass a value to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (this.viewLeft === undefined) { if (this.viewLeft === undefined) {
throw new Error( throw new Error(
'The view left is undefined. Either pass a value to ' + 'The view left is undefined. Either pass a value to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (this.scale === undefined) { if (this.scale === undefined) {
throw new Error( throw new Error(
'The scale is undefined. Either pass a value to ' + 'The scale is undefined. Either pass a value to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
if (this.oldScale === undefined) { if (this.oldScale === undefined) {
throw new Error( throw new Error(
'The old scale is undefined. Either pass a value to ' + 'The old scale is undefined. Either pass a value to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['viewTop'] = this.viewTop; json['viewTop'] = this.viewTop;
json['viewLeft'] = this.viewLeft; json['viewLeft'] = this.viewLeft;
@@ -110,8 +117,11 @@ export class ViewportChange extends UiBase {
*/ */
override fromJson(json: ViewportChangeJson) { override fromJson(json: ViewportChangeJson) {
deprecation.warn( deprecation.warn(
'Blockly.Events.Viewport.prototype.fromJson', 'version 9', 'version 10', 'Blockly.Events.Viewport.prototype.fromJson',
'Blockly.Events.fromJson'); 'version 9',
'version 10',
'Blockly.Events.fromJson'
);
super.fromJson(json); super.fromJson(json);
this.viewTop = json['viewTop']; this.viewTop = json['viewTop'];
this.viewLeft = json['viewLeft']; this.viewLeft = json['viewLeft'];
@@ -128,11 +138,16 @@ export class ViewportChange extends UiBase {
* static methods in superclasses. * static methods in superclasses.
* @internal * @internal
*/ */
static fromJson(json: ViewportChangeJson, workspace: Workspace, event?: any): static fromJson(
ViewportChange { json: ViewportChangeJson,
const newEvent = workspace: Workspace,
super.fromJson(json, workspace, event ?? new ViewportChange()) as event?: any
ViewportChange; ): ViewportChange {
const newEvent = super.fromJson(
json,
workspace,
event ?? new ViewportChange()
) as ViewportChange;
newEvent.viewTop = json['viewTop']; newEvent.viewTop = json['viewTop'];
newEvent.viewLeft = json['viewLeft']; newEvent.viewLeft = json['viewLeft'];
newEvent.scale = json['scale']; newEvent.scale = json['scale'];
@@ -149,4 +164,7 @@ export interface ViewportChangeJson extends AbstractEventJson {
} }
registry.register( registry.register(
registry.Type.EVENT, eventUtils.VIEWPORT_CHANGE, ViewportChange); registry.Type.EVENT,
eventUtils.VIEWPORT_CHANGE,
ViewportChange
);

View File

@@ -22,7 +22,6 @@ import type {CommentCreate} from './events_comment_create.js';
import type {CommentMove} from './events_comment_move.js'; import type {CommentMove} from './events_comment_move.js';
import type {ViewportChange} from './events_viewport.js'; import type {ViewportChange} from './events_viewport.js';
/** Group ID for new events. Grouped events are indivisible. */ /** Group ID for new events. Grouped events are indivisible. */
let group = ''; let group = '';
@@ -187,7 +186,7 @@ export const FINISHED_LOADING = 'finished_loading';
* Not to be confused with bumping so that disconnected connections do not * Not to be confused with bumping so that disconnected connections do not
* appear connected. * appear connected.
*/ */
export type BumpEvent = BlockCreate|BlockMove|CommentCreate|CommentMove; export type BumpEvent = BlockCreate | BlockMove | CommentCreate | CommentMove;
/** /**
* List of events that cause objects to be bumped back into the visible * List of events that cause objects to be bumped back into the visible
@@ -196,8 +195,12 @@ export type BumpEvent = BlockCreate|BlockMove|CommentCreate|CommentMove;
* Not to be confused with bumping so that disconnected connections do not * Not to be confused with bumping so that disconnected connections do not
* appear connected. * appear connected.
*/ */
export const BUMP_EVENTS: string[] = export const BUMP_EVENTS: string[] = [
[BLOCK_CREATE, BLOCK_MOVE, COMMENT_CREATE, COMMENT_MOVE]; BLOCK_CREATE,
BLOCK_MOVE,
COMMENT_CREATE,
COMMENT_MOVE,
];
/** List of events queued for firing. */ /** List of events queued for firing. */
const FIRE_QUEUE: Abstract[] = []; const FIRE_QUEUE: Abstract[] = [];
@@ -235,12 +238,11 @@ function fireInternal(event: Abstract) {
FIRE_QUEUE.push(event); FIRE_QUEUE.push(event);
} }
/** Fire all queued events. */ /** Fire all queued events. */
function fireNow() { function fireNow() {
const queue = filter(FIRE_QUEUE, true); const queue = filter(FIRE_QUEUE, true);
FIRE_QUEUE.length = 0; FIRE_QUEUE.length = 0;
for (let i = 0, event; event = queue[i]; i++) { for (let i = 0, event; (event = queue[i]); i++) {
if (!event.workspaceId) { if (!event.workspaceId) {
continue; continue;
} }
@@ -268,7 +270,7 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
const mergedQueue = []; const mergedQueue = [];
const hash = Object.create(null); const hash = Object.create(null);
// Merge duplicates. // Merge duplicates.
for (let i = 0, event; event = queue[i]; i++) { for (let i = 0, event; (event = queue[i]); i++) {
if (!event.isNull()) { if (!event.isNull()) {
// Treat all UI events as the same type in hash table. // Treat all UI events as the same type in hash table.
const eventType = event.isUiEvent ? UI : event.type; const eventType = event.isUiEvent ? UI : event.type;
@@ -293,8 +295,9 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
if (moveEvent.reason) { if (moveEvent.reason) {
if (lastEvent.reason) { if (lastEvent.reason) {
// Concatenate reasons without duplicates. // Concatenate reasons without duplicates.
const reasonSet = const reasonSet = new Set(
new Set(moveEvent.reason.concat(lastEvent.reason)); moveEvent.reason.concat(lastEvent.reason)
);
lastEvent.reason = Array.from(reasonSet); lastEvent.reason = Array.from(reasonSet);
} else { } else {
lastEvent.reason = moveEvent.reason; lastEvent.reason = moveEvent.reason;
@@ -302,9 +305,10 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
} }
lastEntry.index = i; lastEntry.index = i;
} else if ( } else if (
event.type === CHANGE && event.type === CHANGE &&
(event as BlockChange).element === lastEvent.element && (event as BlockChange).element === lastEvent.element &&
(event as BlockChange).name === lastEvent.name) { (event as BlockChange).name === lastEvent.name
) {
const changeEvent = event as BlockChange; const changeEvent = event as BlockChange;
// Merge change events. // Merge change events.
lastEvent.newValue = changeEvent.newValue; lastEvent.newValue = changeEvent.newValue;
@@ -326,7 +330,7 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
} }
} }
// Filter out any events that have become null due to merging. // Filter out any events that have become null due to merging.
queue = mergedQueue.filter(function(e) { queue = mergedQueue.filter(function (e) {
return !e.isNull(); return !e.isNull();
}); });
if (!forward) { if (!forward) {
@@ -335,11 +339,13 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
} }
// Move mutation events to the top of the queue. // Move mutation events to the top of the queue.
// Intentionally skip first event. // Intentionally skip first event.
for (let i = 1, event; event = queue[i]; i++) { for (let i = 1, event; (event = queue[i]); i++) {
// AnyDuringMigration because: Property 'element' does not exist on type // AnyDuringMigration because: Property 'element' does not exist on type
// 'Abstract'. // 'Abstract'.
if (event.type === CHANGE && if (
(event as AnyDuringMigration).element === 'mutation') { event.type === CHANGE &&
(event as AnyDuringMigration).element === 'mutation'
) {
queue.unshift(queue.splice(i, 1)[0]); queue.unshift(queue.splice(i, 1)[0]);
} }
} }
@@ -351,7 +357,7 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
* in the undo stack. Called by Workspace.clearUndo. * in the undo stack. Called by Workspace.clearUndo.
*/ */
export function clearPendingUndo() { export function clearPendingUndo() {
for (let i = 0, event; event = FIRE_QUEUE[i]; i++) { for (let i = 0, event; (event = FIRE_QUEUE[i]); i++) {
event.recordUndo = false; event.recordUndo = false;
} }
} }
@@ -395,14 +401,14 @@ export function getGroup(): string {
* @param state True to start new group, false to end group. * @param state True to start new group, false to end group.
* String to set group explicitly. * String to set group explicitly.
*/ */
export function setGroup(state: boolean|string) { export function setGroup(state: boolean | string) {
TEST_ONLY.setGroupInternal(state); TEST_ONLY.setGroupInternal(state);
} }
/** /**
* Private version of setGroup for stubbing in tests. * Private version of setGroup for stubbing in tests.
*/ */
function setGroupInternal(state: boolean|string) { function setGroupInternal(state: boolean | string) {
if (typeof state === 'boolean') { if (typeof state === 'boolean') {
group = state ? idGenerator.genUid() : ''; group = state ? idGenerator.genUid() : '';
} else { } else {
@@ -420,7 +426,7 @@ function setGroupInternal(state: boolean|string) {
export function getDescendantIds(block: Block): string[] { export function getDescendantIds(block: Block): string[] {
const ids = []; const ids = [];
const descendants = block.getDescendants(false); const descendants = block.getDescendants(false);
for (let i = 0, descendant; descendant = descendants[i]; i++) { for (let i = 0, descendant; (descendant = descendants[i]); i++) {
ids[i] = descendant.id; ids[i] = descendant.id;
} }
return ids; return ids;
@@ -435,7 +441,9 @@ export function getDescendantIds(block: Block): string[] {
* @throws {Error} if an event type is not found in the registry. * @throws {Error} if an event type is not found in the registry.
*/ */
export function fromJson( export function fromJson(
json: AnyDuringMigration, workspace: Workspace): Abstract { json: AnyDuringMigration,
workspace: Workspace
): Abstract {
const eventClass = get(json['type']); const eventClass = get(json['type']);
if (!eventClass) throw Error('Unknown event type.'); if (!eventClass) throw Error('Unknown event type.');
@@ -457,11 +465,14 @@ export function fromJson(
* Returns false if no static fromJson method exists on the contructor, or if * Returns false if no static fromJson method exists on the contructor, or if
* the static fromJson method is inheritted. * the static fromJson method is inheritted.
*/ */
function eventClassHasStaticFromJson(eventClass: new (...p: any[]) => Abstract): function eventClassHasStaticFromJson(
boolean { eventClass: new (...p: any[]) => Abstract
): boolean {
const untypedEventClass = eventClass as any; const untypedEventClass = eventClass as any;
return Object.getOwnPropertyDescriptors(untypedEventClass).fromJson && return (
typeof untypedEventClass.fromJson === 'function'; Object.getOwnPropertyDescriptors(untypedEventClass).fromJson &&
typeof untypedEventClass.fromJson === 'function'
);
} }
/** /**
@@ -470,8 +481,9 @@ function eventClassHasStaticFromJson(eventClass: new (...p: any[]) => Abstract):
* @param eventType The type of the event to get. * @param eventType The type of the event to get.
* @returns The event class with the given type. * @returns The event class with the given type.
*/ */
export function get(eventType: string): export function get(
(new (...p1: AnyDuringMigration[]) => Abstract) { eventType: string
): new (...p1: AnyDuringMigration[]) => Abstract {
const event = registry.getClass(registry.Type.EVENT, eventType); const event = registry.getClass(registry.Type.EVENT, eventType);
if (!event) { if (!event) {
throw new Error(`Event type ${eventType} not found in registry.`); throw new Error(`Event type ${eventType} not found in registry.`);
@@ -493,8 +505,9 @@ export function disableOrphans(event: Abstract) {
if (!blockEvent.workspaceId) { if (!blockEvent.workspaceId) {
return; return;
} }
const eventWorkspace = const eventWorkspace = common.getWorkspaceById(
common.getWorkspaceById(blockEvent.workspaceId) as WorkspaceSvg; blockEvent.workspaceId
) as WorkspaceSvg;
if (!blockEvent.blockId) { if (!blockEvent.blockId) {
throw new Error('Encountered a blockEvent without a proper blockId'); throw new Error('Encountered a blockEvent without a proper blockId');
} }
@@ -507,12 +520,13 @@ export function disableOrphans(event: Abstract) {
const parent = block.getParent(); const parent = block.getParent();
if (parent && parent.isEnabled()) { if (parent && parent.isEnabled()) {
const children = block.getDescendants(false); const children = block.getDescendants(false);
for (let i = 0, child; child = children[i]; i++) { for (let i = 0, child; (child = children[i]); i++) {
child.setEnabled(true); child.setEnabled(true);
} }
} else if ( } else if (
(block.outputConnection || block.previousConnection) && (block.outputConnection || block.previousConnection) &&
!eventWorkspace.isDragging()) { !eventWorkspace.isDragging()
) {
do { do {
block.setEnabled(false); block.setEnabled(false);
block = block.getNextBlock(); block = block.getNextBlock();

View File

@@ -14,10 +14,12 @@ goog.declareModuleId('Blockly.Events.FinishedLoading');
import * as registry from '../registry.js'; import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js'; import {
Abstract as AbstractEvent,
AbstractEventJson,
} from './events_abstract.js';
import * as eventUtils from './utils.js'; import * as eventUtils from './utils.js';
/** /**
* Notifies listeners when the workspace has finished deserializing from * Notifies listeners when the workspace has finished deserializing from
* JSON/XML. * JSON/XML.
@@ -49,8 +51,9 @@ export class FinishedLoading extends AbstractEvent {
const json = super.toJson() as FinishedLoadingJson; const json = super.toJson() as FinishedLoadingJson;
if (!this.workspaceId) { if (!this.workspaceId) {
throw new Error( throw new Error(
'The workspace ID is undefined. Either pass a workspace to ' + 'The workspace ID is undefined. Either pass a workspace to ' +
'the constructor, or call fromJson'); 'the constructor, or call fromJson'
);
} }
json['workspaceId'] = this.workspaceId; json['workspaceId'] = this.workspaceId;
return json; return json;
@@ -72,4 +75,7 @@ export interface FinishedLoadingJson extends AbstractEventJson {
} }
registry.register( registry.register(
registry.Type.EVENT, eventUtils.FINISHED_LOADING, FinishedLoading); registry.Type.EVENT,
eventUtils.FINISHED_LOADING,
FinishedLoading
);

View File

@@ -13,7 +13,6 @@ import {FieldDropdown} from './field_dropdown.js';
import {Mutator} from './mutator.js'; import {Mutator} from './mutator.js';
import * as parsing from './utils/parsing.js'; import * as parsing from './utils/parsing.js';
/** The set of all registered extensions, keyed by extension name/id. */ /** The set of all registered extensions, keyed by extension name/id. */
const allExtensions = Object.create(null); const allExtensions = Object.create(null);
export const TEST_ONLY = {allExtensions}; export const TEST_ONLY = {allExtensions};
@@ -54,7 +53,7 @@ export function registerMixin(name: string, mixinObj: AnyDuringMigration) {
if (!mixinObj || typeof mixinObj !== 'object') { if (!mixinObj || typeof mixinObj !== 'object') {
throw Error('Error: Mixin "' + name + '" must be a object'); throw Error('Error: Mixin "' + name + '" must be a object');
} }
register(name, function(this: Block) { register(name, function (this: Block) {
this.mixin(mixinObj); this.mixin(mixinObj);
}); });
} }
@@ -73,8 +72,11 @@ export function registerMixin(name: string, mixinObj: AnyDuringMigration) {
* @throws {Error} if the mutation is invalid or can't be applied to the block. * @throws {Error} if the mutation is invalid or can't be applied to the block.
*/ */
export function registerMutator( export function registerMutator(
name: string, mixinObj: AnyDuringMigration, name: string,
opt_helperFn?: () => AnyDuringMigration, opt_blockList?: string[]) { mixinObj: AnyDuringMigration,
opt_helperFn?: () => AnyDuringMigration,
opt_blockList?: string[]
) {
const errorPrefix = 'Error when registering mutator "' + name + '": '; const errorPrefix = 'Error when registering mutator "' + name + '": ';
checkHasMutatorProperties(errorPrefix, mixinObj); checkHasMutatorProperties(errorPrefix, mixinObj);
@@ -85,7 +87,7 @@ export function registerMutator(
} }
// Sanity checks passed. // Sanity checks passed.
register(name, function(this: Block) { register(name, function (this: Block) {
if (hasMutatorDialog) { if (hasMutatorDialog) {
this.setMutator(new Mutator(opt_blockList || [], this as BlockSvg)); this.setMutator(new Mutator(opt_blockList || [], this as BlockSvg));
} }
@@ -108,7 +110,8 @@ export function unregister(name: string) {
delete allExtensions[name]; delete allExtensions[name];
} else { } else {
console.warn( console.warn(
'No extension mapping for name "' + name + '" found to unregister'); 'No extension mapping for name "' + name + '" found to unregister'
);
} }
} }
@@ -151,11 +154,15 @@ export function apply(name: string, block: Block, isMutator: boolean) {
const errorPrefix = 'Error after applying mutator "' + name + '": '; const errorPrefix = 'Error after applying mutator "' + name + '": ';
checkHasMutatorProperties(errorPrefix, block); checkHasMutatorProperties(errorPrefix, block);
} else { } else {
if (!mutatorPropertiesMatch( if (
mutatorProperties as AnyDuringMigration[], block)) { !mutatorPropertiesMatch(mutatorProperties as AnyDuringMigration[], block)
) {
throw Error( throw Error(
'Error when applying extension "' + name + '": ' + 'Error when applying extension "' +
'mutation properties changed when applying a non-mutator extension.'); name +
'": ' +
'mutation properties changed when applying a non-mutator extension.'
);
} }
} }
} }
@@ -173,9 +180,12 @@ function checkNoMutatorProperties(mutationName: string, block: Block) {
const properties = getMutatorProperties(block); const properties = getMutatorProperties(block);
if (properties.length) { if (properties.length) {
throw Error( throw Error(
'Error: tried to apply mutation "' + mutationName + 'Error: tried to apply mutation "' +
mutationName +
'" to a block that already has mutator functions.' + '" to a block that already has mutator functions.' +
' Block id: ' + block.id); ' Block id: ' +
block.id
);
} }
} }
@@ -191,10 +201,14 @@ function checkNoMutatorProperties(mutationName: string, block: Block) {
* actually a function. * actually a function.
*/ */
function checkXmlHooks( function checkXmlHooks(
object: AnyDuringMigration, errorPrefix: string): boolean { object: AnyDuringMigration,
errorPrefix: string
): boolean {
return checkHasFunctionPair( return checkHasFunctionPair(
object.mutationToDom, object.domToMutation, object.mutationToDom,
errorPrefix + ' mutationToDom/domToMutation'); object.domToMutation,
errorPrefix + ' mutationToDom/domToMutation'
);
} }
/** /**
* Checks if the given object has both the 'saveExtraState' and 'loadExtraState' * Checks if the given object has both the 'saveExtraState' and 'loadExtraState'
@@ -208,10 +222,14 @@ function checkXmlHooks(
* actually a function. * actually a function.
*/ */
function checkJsonHooks( function checkJsonHooks(
object: AnyDuringMigration, errorPrefix: string): boolean { object: AnyDuringMigration,
errorPrefix: string
): boolean {
return checkHasFunctionPair( return checkHasFunctionPair(
object.saveExtraState, object.loadExtraState, object.saveExtraState,
errorPrefix + ' saveExtraState/loadExtraState'); object.loadExtraState,
errorPrefix + ' saveExtraState/loadExtraState'
);
} }
/** /**
@@ -225,9 +243,14 @@ function checkJsonHooks(
* actually a function. * actually a function.
*/ */
function checkMutatorDialog( function checkMutatorDialog(
object: AnyDuringMigration, errorPrefix: string): boolean { object: AnyDuringMigration,
errorPrefix: string
): boolean {
return checkHasFunctionPair( return checkHasFunctionPair(
object.compose, object.decompose, errorPrefix + ' compose/decompose'); object.compose,
object.decompose,
errorPrefix + ' compose/decompose'
);
} }
/** /**
@@ -243,8 +266,10 @@ function checkMutatorDialog(
* actually a function. * actually a function.
*/ */
function checkHasFunctionPair( function checkHasFunctionPair(
func1: AnyDuringMigration, func2: AnyDuringMigration, func1: AnyDuringMigration,
errorPrefix: string): boolean { func2: AnyDuringMigration,
errorPrefix: string
): boolean {
if (func1 && func2) { if (func1 && func2) {
if (typeof func1 !== 'function' || typeof func2 !== 'function') { if (typeof func1 !== 'function' || typeof func2 !== 'function') {
throw Error(errorPrefix + ' must be a function'); throw Error(errorPrefix + ' must be a function');
@@ -263,13 +288,16 @@ function checkHasFunctionPair(
* @param object The object to inspect. * @param object The object to inspect.
*/ */
function checkHasMutatorProperties( function checkHasMutatorProperties(
errorPrefix: string, object: AnyDuringMigration) { errorPrefix: string,
object: AnyDuringMigration
) {
const hasXmlHooks = checkXmlHooks(object, errorPrefix); const hasXmlHooks = checkXmlHooks(object, errorPrefix);
const hasJsonHooks = checkJsonHooks(object, errorPrefix); const hasJsonHooks = checkJsonHooks(object, errorPrefix);
if (!hasXmlHooks && !hasJsonHooks) { if (!hasXmlHooks && !hasJsonHooks) {
throw Error( throw Error(
errorPrefix + errorPrefix +
'Mutations must contain either XML hooks, or JSON hooks, or both'); 'Mutations must contain either XML hooks, or JSON hooks, or both'
);
} }
// A block with a mutator isn't required to have a mutation dialog, but // A block with a mutator isn't required to have a mutation dialog, but
// it should still have both or neither of compose and decompose. // it should still have both or neither of compose and decompose.
@@ -318,7 +346,9 @@ function getMutatorProperties(block: Block): AnyDuringMigration[] {
* @returns True if the property lists match. * @returns True if the property lists match.
*/ */
function mutatorPropertiesMatch( function mutatorPropertiesMatch(
oldProperties: AnyDuringMigration[], block: Block): boolean { oldProperties: AnyDuringMigration[],
block: Block
): boolean {
const newProperties = getMutatorProperties(block); const newProperties = getMutatorProperties(block);
if (newProperties.length !== oldProperties.length) { if (newProperties.length !== oldProperties.length) {
return false; return false;
@@ -343,10 +373,10 @@ export function runAfterPageLoad(fn: () => void) {
throw Error('runAfterPageLoad() requires browser document.'); throw Error('runAfterPageLoad() requires browser document.');
} }
if (document.readyState === 'complete') { if (document.readyState === 'complete') {
fn(); // Page has already loaded. Call immediately. fn(); // Page has already loaded. Call immediately.
} else { } else {
// Poll readyState. // Poll readyState.
const readyStateCheckInterval = setInterval(function() { const readyStateCheckInterval = setInterval(function () {
if (document.readyState === 'complete') { if (document.readyState === 'complete') {
clearInterval(readyStateCheckInterval); clearInterval(readyStateCheckInterval);
fn(); fn();
@@ -375,7 +405,9 @@ export function runAfterPageLoad(fn: () => void) {
* @returns The extension function. * @returns The extension function.
*/ */
export function buildTooltipForDropdown( export function buildTooltipForDropdown(
dropdownName: string, lookupTable: {[key: string]: string}): Function { dropdownName: string,
lookupTable: {[key: string]: string}
): Function {
// List of block types already validated, to minimize duplicate warnings. // List of block types already validated, to minimize duplicate warnings.
const blockTypesChecked: AnyDuringMigration[] = []; const blockTypesChecked: AnyDuringMigration[] = [];
@@ -383,8 +415,9 @@ export function buildTooltipForDropdown(
// Wait for load, in case Blockly.Msg is not yet populated. // Wait for load, in case Blockly.Msg is not yet populated.
// runAfterPageLoad() does not run in a Node.js environment due to lack // runAfterPageLoad() does not run in a Node.js environment due to lack
// of document object, in which case skip the validation. // of document object, in which case skip the validation.
if (typeof document === 'object') { // Relies on document.readyState if (typeof document === 'object') {
runAfterPageLoad(function() { // Relies on document.readyState
runAfterPageLoad(function () {
for (const key in lookupTable) { for (const key in lookupTable) {
// Will print warnings if reference is missing. // Will print warnings if reference is missing.
parsing.checkMessageReferences(lookupTable[key]); parsing.checkMessageReferences(lookupTable[key]);
@@ -399,24 +432,29 @@ export function buildTooltipForDropdown(
blockTypesChecked.push(this.type); blockTypesChecked.push(this.type);
} }
this.setTooltip(function(this: Block) { this.setTooltip(
const value = String(this.getFieldValue(dropdownName)); function (this: Block) {
let tooltip = lookupTable[value]; const value = String(this.getFieldValue(dropdownName));
if (tooltip === null) { let tooltip = lookupTable[value];
if (blockTypesChecked.indexOf(this.type) === -1) { if (tooltip === null) {
// Warn for missing values on generated tooltips. if (blockTypesChecked.indexOf(this.type) === -1) {
let warning = 'No tooltip mapping for value ' + value + ' of field ' + // Warn for missing values on generated tooltips.
let warning =
'No tooltip mapping for value ' +
value +
' of field ' +
dropdownName; dropdownName;
if (this.type !== null) { if (this.type !== null) {
warning += ' of block type ' + this.type; warning += ' of block type ' + this.type;
}
console.warn(warning + '.');
} }
console.warn(warning + '.'); } else {
tooltip = parsing.replaceMessageReferences(tooltip);
} }
} else { return tooltip;
tooltip = parsing.replaceMessageReferences(tooltip); }.bind(this)
} );
return tooltip;
}.bind(this));
} }
return extensionFn; return extensionFn;
} }
@@ -430,17 +468,25 @@ export function buildTooltipForDropdown(
* @param lookupTable The string lookup table * @param lookupTable The string lookup table
*/ */
function checkDropdownOptionsInTable( function checkDropdownOptionsInTable(
block: Block, dropdownName: string, lookupTable: {[key: string]: string}) { block: Block,
dropdownName: string,
lookupTable: {[key: string]: string}
) {
// Validate all dropdown options have values. // Validate all dropdown options have values.
const dropdown = block.getField(dropdownName); const dropdown = block.getField(dropdownName);
if (dropdown instanceof FieldDropdown && !dropdown.isOptionListDynamic()) { if (dropdown instanceof FieldDropdown && !dropdown.isOptionListDynamic()) {
const options = dropdown.getOptions(); const options = dropdown.getOptions();
for (let i = 0; i < options.length; i++) { for (let i = 0; i < options.length; i++) {
const optionKey = options[i][1]; // label, then value const optionKey = options[i][1]; // label, then value
if (lookupTable[optionKey] === null) { if (lookupTable[optionKey] === null) {
console.warn( console.warn(
'No tooltip mapping for value ' + optionKey + ' of field ' + 'No tooltip mapping for value ' +
dropdownName + ' of block type ' + block.type); optionKey +
' of field ' +
dropdownName +
' of block type ' +
block.type
);
} }
} }
} }
@@ -457,13 +503,16 @@ function checkDropdownOptionsInTable(
* @returns The extension function. * @returns The extension function.
*/ */
export function buildTooltipWithFieldText( export function buildTooltipWithFieldText(
msgTemplate: string, fieldName: string): Function { msgTemplate: string,
fieldName: string
): Function {
// Check the tooltip string messages for invalid references. // Check the tooltip string messages for invalid references.
// Wait for load, in case Blockly.Msg is not yet populated. // Wait for load, in case Blockly.Msg is not yet populated.
// runAfterPageLoad() does not run in a Node.js environment due to lack // runAfterPageLoad() does not run in a Node.js environment due to lack
// of document object, in which case skip the validation. // of document object, in which case skip the validation.
if (typeof document === 'object') { // Relies on document.readyState if (typeof document === 'object') {
runAfterPageLoad(function() { // Relies on document.readyState
runAfterPageLoad(function () {
// Will print warnings if reference is missing. // Will print warnings if reference is missing.
parsing.checkMessageReferences(msgTemplate); parsing.checkMessageReferences(msgTemplate);
}); });
@@ -471,11 +520,14 @@ export function buildTooltipWithFieldText(
/** The actual extension. */ /** The actual extension. */
function extensionFn(this: Block) { function extensionFn(this: Block) {
this.setTooltip(function(this: Block) { this.setTooltip(
const field = this.getField(fieldName); function (this: Block) {
return parsing.replaceMessageReferences(msgTemplate) const field = this.getField(fieldName);
return parsing
.replaceMessageReferences(msgTemplate)
.replace('%1', field ? field.getText() : ''); .replace('%1', field ? field.getText() : '');
}.bind(this)); }.bind(this)
);
} }
return extensionFn; return extensionFn;
} }
@@ -488,10 +540,14 @@ export function buildTooltipWithFieldText(
*/ */
function extensionParentTooltip(this: Block) { function extensionParentTooltip(this: Block) {
const tooltipWhenNotConnected = this.tooltip; const tooltipWhenNotConnected = this.tooltip;
this.setTooltip(function(this: Block) { this.setTooltip(
const parent = this.getParent(); function (this: Block) {
return parent && parent.getInputsInline() && parent.tooltip || const parent = this.getParent();
tooltipWhenNotConnected; return (
}.bind(this)); (parent && parent.getInputsInline() && parent.tooltip) ||
tooltipWhenNotConnected
);
}.bind(this)
);
} }
register('parent_tooltip_when_inline', extensionParentTooltip); register('parent_tooltip_when_inline', extensionParentTooltip);

View File

@@ -59,17 +59,21 @@ import {ISerializable} from './interfaces/i_serializable.js';
* *
* - `undefined` to set `newValue` as is. * - `undefined` to set `newValue` as is.
*/ */
export type FieldValidator<T = any> = (newValue: T) => T|null|undefined; export type FieldValidator<T = any> = (newValue: T) => T | null | undefined;
/** /**
* Abstract class for an editable field. * Abstract class for an editable field.
* *
* @typeParam T - The value stored on the field. * @typeParam T - The value stored on the field.
*/ */
export abstract class Field<T = any> implements IASTNodeLocationSvg, export abstract class Field<T = any>
IASTNodeLocationWithBlock, implements
IKeyboardAccessible, IASTNodeLocationSvg,
IRegistrable, ISerializable { IASTNodeLocationWithBlock,
IKeyboardAccessible,
IRegistrable,
ISerializable
{
/** /**
* To overwrite the default value which is set in **Field**, directly update * To overwrite the default value which is set in **Field**, directly update
* the prototype. * the prototype.
@@ -79,7 +83,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* FieldImage.prototype.DEFAULT_VALUE = null; * FieldImage.prototype.DEFAULT_VALUE = null;
* ``` * ```
*/ */
DEFAULT_VALUE: T|null = null; DEFAULT_VALUE: T | null = null;
/** Non-breaking space. */ /** Non-breaking space. */
static readonly NBSP = '\u00A0'; static readonly NBSP = '\u00A0';
@@ -96,47 +100,47 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* Static labels are usually unnamed. * Static labels are usually unnamed.
*/ */
name?: string = undefined; name?: string = undefined;
protected value_: T|null; protected value_: T | null;
/** Validation function called when user edits an editable field. */ /** Validation function called when user edits an editable field. */
protected validator_: FieldValidator<T>|null = null; protected validator_: FieldValidator<T> | null = null;
/** /**
* Used to cache the field's tooltip value if setTooltip is called when the * Used to cache the field's tooltip value if setTooltip is called when the
* field is not yet initialized. Is *not* guaranteed to be accurate. * field is not yet initialized. Is *not* guaranteed to be accurate.
*/ */
private tooltip_: Tooltip.TipInfo|null = null; private tooltip_: Tooltip.TipInfo | null = null;
protected size_: Size; protected size_: Size;
/** /**
* Holds the cursors svg element when the cursor is attached to the field. * Holds the cursors svg element when the cursor is attached to the field.
* This is null if there is no cursor on the field. * This is null if there is no cursor on the field.
*/ */
private cursorSvg_: SVGElement|null = null; private cursorSvg_: SVGElement | null = null;
/** /**
* Holds the markers svg element when the marker is attached to the field. * Holds the markers svg element when the marker is attached to the field.
* This is null if there is no marker on the field. * This is null if there is no marker on the field.
*/ */
private markerSvg_: SVGElement|null = null; private markerSvg_: SVGElement | null = null;
/** The rendered field's SVG group element. */ /** The rendered field's SVG group element. */
protected fieldGroup_: SVGGElement|null = null; protected fieldGroup_: SVGGElement | null = null;
/** The rendered field's SVG border element. */ /** The rendered field's SVG border element. */
protected borderRect_: SVGRectElement|null = null; protected borderRect_: SVGRectElement | null = null;
/** The rendered field's SVG text element. */ /** The rendered field's SVG text element. */
protected textElement_: SVGTextElement|null = null; protected textElement_: SVGTextElement | null = null;
/** The rendered field's text content element. */ /** The rendered field's text content element. */
protected textContent_: Text|null = null; protected textContent_: Text | null = null;
/** Mouse down event listener data. */ /** Mouse down event listener data. */
private mouseDownWrapper_: browserEvents.Data|null = null; private mouseDownWrapper_: browserEvents.Data | null = null;
/** Constants associated with the source block's renderer. */ /** Constants associated with the source block's renderer. */
protected constants_: ConstantProvider|null = null; protected constants_: ConstantProvider | null = null;
/** /**
* Has this field been disposed of? * Has this field been disposed of?
@@ -149,7 +153,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
maxDisplayLength = 50; maxDisplayLength = 50;
/** Block this field is attached to. Starts as null, then set in init. */ /** Block this field is attached to. Starts as null, then set in init. */
protected sourceBlock_: Block|null = null; protected sourceBlock_: Block | null = null;
/** Does this block need to be re-rendered? */ /** Does this block need to be re-rendered? */
protected isDirty_ = true; protected isDirty_ = true;
@@ -163,21 +167,21 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
protected enabled_ = true; protected enabled_ = true;
/** The element the click handler is bound to. */ /** The element the click handler is bound to. */
protected clickTarget_: Element|null = null; protected clickTarget_: Element | null = null;
/** /**
* The prefix field. * The prefix field.
* *
* @internal * @internal
*/ */
prefixField: string|null = null; prefixField: string | null = null;
/** /**
* The suffix field. * The suffix field.
* *
* @internal * @internal
*/ */
suffixField: string|null = null; suffixField: string | null = null;
/** /**
* Editable fields usually show some sort of UI indicating they are * Editable fields usually show some sort of UI indicating they are
@@ -208,15 +212,18 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* this parameter supports. * this parameter supports.
*/ */
constructor( constructor(
value: T|typeof Field.SKIP_SETUP, validator?: FieldValidator<T>|null, value: T | typeof Field.SKIP_SETUP,
config?: FieldConfig) { validator?: FieldValidator<T> | null,
config?: FieldConfig
) {
/** /**
* A generic value possessed by the field. * A generic value possessed by the field.
* Should generally be non-null, only null when the field is created. * Should generally be non-null, only null when the field is created.
*/ */
this.value_ = 'DEFAULT_VALUE' in new.target.prototype ? this.value_ =
new.target.prototype.DEFAULT_VALUE : 'DEFAULT_VALUE' in new.target.prototype
this.DEFAULT_VALUE; ? new.target.prototype.DEFAULT_VALUE
: this.DEFAULT_VALUE;
/** The size of the area rendered by the field. */ /** The size of the area rendered by the field. */
this.size_ = new Size(0, 0); this.size_ = new Size(0, 0);
@@ -263,13 +270,16 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* *
* @returns The renderer constant provider. * @returns The renderer constant provider.
*/ */
getConstants(): ConstantProvider|null { getConstants(): ConstantProvider | null {
if (!this.constants_ && this.sourceBlock_ && if (
!this.sourceBlock_.isDeadOrDying() && !this.constants_ &&
this.sourceBlock_.workspace.rendered) { this.sourceBlock_ &&
!this.sourceBlock_.isDeadOrDying() &&
this.sourceBlock_.workspace.rendered
) {
this.constants_ = (this.sourceBlock_.workspace as WorkspaceSvg) this.constants_ = (this.sourceBlock_.workspace as WorkspaceSvg)
.getRenderer() .getRenderer()
.getConstants(); .getConstants();
} }
return this.constants_; return this.constants_;
} }
@@ -280,7 +290,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* @returns The block containing this field. * @returns The block containing this field.
* @throws An error if the source block is not defined. * @throws An error if the source block is not defined.
*/ */
getSourceBlock(): Block|null { getSourceBlock(): Block | null {
return this.sourceBlock_; return this.sourceBlock_;
} }
@@ -334,16 +344,18 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
*/ */
protected createBorderRect_() { protected createBorderRect_() {
this.borderRect_ = dom.createSvgElement( this.borderRect_ = dom.createSvgElement(
Svg.RECT, { Svg.RECT,
'rx': this.getConstants()!.FIELD_BORDER_RECT_RADIUS, {
'ry': this.getConstants()!.FIELD_BORDER_RECT_RADIUS, 'rx': this.getConstants()!.FIELD_BORDER_RECT_RADIUS,
'x': 0, 'ry': this.getConstants()!.FIELD_BORDER_RECT_RADIUS,
'y': 0, 'x': 0,
'height': this.size_.height, 'y': 0,
'width': this.size_.width, 'height': this.size_.height,
'class': 'blocklyFieldRect', 'width': this.size_.width,
}, 'class': 'blocklyFieldRect',
this.fieldGroup_); },
this.fieldGroup_
);
} }
/** /**
@@ -353,10 +365,12 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
*/ */
protected createTextElement_() { protected createTextElement_() {
this.textElement_ = dom.createSvgElement( this.textElement_ = dom.createSvgElement(
Svg.TEXT, { Svg.TEXT,
'class': 'blocklyText', {
}, 'class': 'blocklyText',
this.fieldGroup_); },
this.fieldGroup_
);
if (this.getConstants()!.FIELD_TEXT_BASELINE_CENTER) { if (this.getConstants()!.FIELD_TEXT_BASELINE_CENTER) {
this.textElement_.setAttribute('dominant-baseline', 'central'); this.textElement_.setAttribute('dominant-baseline', 'central');
} }
@@ -373,7 +387,11 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
if (!clickTarget) throw new Error('A click target has not been set.'); if (!clickTarget) throw new Error('A click target has not been set.');
Tooltip.bindMouseEvents(clickTarget); Tooltip.bindMouseEvents(clickTarget);
this.mouseDownWrapper_ = browserEvents.conditionalBind( this.mouseDownWrapper_ = browserEvents.conditionalBind(
clickTarget, 'pointerdown', this, this.onMouseDown_); clickTarget,
'pointerdown',
this,
this.onMouseDown_
);
} }
/** /**
@@ -443,14 +461,18 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* Used to see if `this` has overridden any relevant hooks. * Used to see if `this` has overridden any relevant hooks.
* @returns The stringified version of the XML state, or null. * @returns The stringified version of the XML state, or null.
*/ */
protected saveLegacyState(callingClass: FieldProto): string|null { protected saveLegacyState(callingClass: FieldProto): string | null {
if (callingClass.prototype.saveState === this.saveState && if (
callingClass.prototype.toXml !== this.toXml) { callingClass.prototype.saveState === this.saveState &&
callingClass.prototype.toXml !== this.toXml
) {
const elem = utilsXml.createElement('field'); const elem = utilsXml.createElement('field');
elem.setAttribute('name', this.name || ''); elem.setAttribute('name', this.name || '');
const text = utilsXml.domToText(this.toXml(elem)); const text = utilsXml.domToText(this.toXml(elem));
return text.replace( return text.replace(
' xmlns="https://developers.google.com/blockly/xml"', ''); ' xmlns="https://developers.google.com/blockly/xml"',
''
);
} }
// Either they called this on purpose from their saveState, or they have // Either they called this on purpose from their saveState, or they have
// no implementations of either hook. Just do our thing. // no implementations of either hook. Just do our thing.
@@ -466,10 +488,14 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* @param state The state to apply to the field. * @param state The state to apply to the field.
* @returns Whether the state was applied or not. * @returns Whether the state was applied or not.
*/ */
loadLegacyState(callingClass: FieldProto, state: AnyDuringMigration): loadLegacyState(
boolean { callingClass: FieldProto,
if (callingClass.prototype.loadState === this.loadState && state: AnyDuringMigration
callingClass.prototype.fromXml !== this.fromXml) { ): boolean {
if (
callingClass.prototype.loadState === this.loadState &&
callingClass.prototype.fromXml !== this.fromXml
) {
this.fromXml(utilsXml.textToDom(state as string)); this.fromXml(utilsXml.textToDom(state as string));
return true; return true;
} }
@@ -539,9 +565,12 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* @returns Whether this field is clickable. * @returns Whether this field is clickable.
*/ */
isClickable(): boolean { isClickable(): boolean {
return this.enabled_ && !!this.sourceBlock_ && return (
this.sourceBlock_.isEditable() && this.enabled_ &&
this.showEditor_ !== Field.prototype.showEditor_; !!this.sourceBlock_ &&
this.sourceBlock_.isEditable() &&
this.showEditor_ !== Field.prototype.showEditor_
);
} }
/** /**
@@ -553,8 +582,12 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* editable block. * editable block.
*/ */
isCurrentlyEditable(): boolean { isCurrentlyEditable(): boolean {
return this.enabled_ && this.EDITABLE && !!this.sourceBlock_ && return (
this.sourceBlock_.isEditable(); this.enabled_ &&
this.EDITABLE &&
!!this.sourceBlock_ &&
this.sourceBlock_.isEditable()
);
} }
/** /**
@@ -570,9 +603,10 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
isSerializable = true; isSerializable = true;
} else if (this.EDITABLE) { } else if (this.EDITABLE) {
console.warn( console.warn(
'Detected an editable field that was not serializable.' + 'Detected an editable field that was not serializable.' +
' Please define SERIALIZABLE property as true on all editable custom' + ' Please define SERIALIZABLE property as true on all editable custom' +
' fields. Proceeding with serialization.'); ' fields. Proceeding with serialization.'
);
isSerializable = true; isSerializable = true;
} }
} }
@@ -630,7 +664,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* *
* @returns Validation function, or null. * @returns Validation function, or null.
*/ */
getValidator(): FieldValidator<T>|null { getValidator(): FieldValidator<T> | null {
return this.validator_; return this.validator_;
} }
@@ -640,7 +674,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* *
* @returns The group element. * @returns The group element.
*/ */
getSvgRoot(): SVGGElement|null { getSvgRoot(): SVGGElement | null {
return this.fieldGroup_; return this.fieldGroup_;
} }
@@ -764,17 +798,23 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
*/ */
protected updateSize_(margin?: number) { protected updateSize_(margin?: number) {
const constants = this.getConstants(); const constants = this.getConstants();
const xOffset = margin !== undefined ? margin : const xOffset =
this.borderRect_ ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : margin !== undefined
0; ? margin
: this.borderRect_
? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING
: 0;
let totalWidth = xOffset * 2; let totalWidth = xOffset * 2;
let totalHeight = constants!.FIELD_TEXT_HEIGHT; let totalHeight = constants!.FIELD_TEXT_HEIGHT;
let contentWidth = 0; let contentWidth = 0;
if (this.textElement_) { if (this.textElement_) {
contentWidth = dom.getFastTextWidth( contentWidth = dom.getFastTextWidth(
this.textElement_, constants!.FIELD_TEXT_FONTSIZE, this.textElement_,
constants!.FIELD_TEXT_FONTWEIGHT, constants!.FIELD_TEXT_FONTFAMILY); constants!.FIELD_TEXT_FONTSIZE,
constants!.FIELD_TEXT_FONTWEIGHT,
constants!.FIELD_TEXT_FONTFAMILY
);
totalWidth += contentWidth; totalWidth += contentWidth;
} }
if (this.borderRect_) { if (this.borderRect_) {
@@ -803,18 +843,23 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
const halfHeight = this.size_.height / 2; const halfHeight = this.size_.height / 2;
this.textElement_.setAttribute( this.textElement_.setAttribute(
'x', 'x',
String( String(
this.getSourceBlock()?.RTL ? this.getSourceBlock()?.RTL
this.size_.width - contentWidth - xOffset : ? this.size_.width - contentWidth - xOffset
xOffset)); : xOffset
)
);
this.textElement_.setAttribute( this.textElement_.setAttribute(
'y', 'y',
String( String(
constants!.FIELD_TEXT_BASELINE_CENTER ? constants!.FIELD_TEXT_BASELINE_CENTER
halfHeight : ? halfHeight
halfHeight - constants!.FIELD_TEXT_HEIGHT / 2 + : halfHeight -
constants!.FIELD_TEXT_BASELINE)); constants!.FIELD_TEXT_HEIGHT / 2 +
constants!.FIELD_TEXT_BASELINE
)
);
} }
/** Position a field's border rect after a size change. */ /** Position a field's border rect after a size change. */
@@ -825,9 +870,13 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
this.borderRect_.setAttribute('width', String(this.size_.width)); this.borderRect_.setAttribute('width', String(this.size_.width));
this.borderRect_.setAttribute('height', String(this.size_.height)); this.borderRect_.setAttribute('height', String(this.size_.height));
this.borderRect_.setAttribute( this.borderRect_.setAttribute(
'rx', String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS)); 'rx',
String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS)
);
this.borderRect_.setAttribute( this.borderRect_.setAttribute(
'ry', String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS)); 'ry',
String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS)
);
} }
/** /**
@@ -852,8 +901,9 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
// Don't issue a warning if the field is actually zero width. // Don't issue a warning if the field is actually zero width.
if (this.size_.width !== 0) { if (this.size_.width !== 0) {
console.warn( console.warn(
'Deprecated use of setting size_.width to 0 to rerender a' + 'Deprecated use of setting size_.width to 0 to rerender a' +
' field. Set field.isDirty_ to true instead.'); ' field. Set field.isDirty_ to true instead.'
);
} }
} }
return this.size_; return this.size_;
@@ -953,7 +1003,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* *
* @returns Current text or null. * @returns Current text or null.
*/ */
protected getText_(): string|null { protected getText_(): string | null {
return null; return null;
} }
@@ -1031,8 +1081,15 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
this.doValueUpdate_(localValue); this.doValueUpdate_(localValue);
if (source && eventUtils.isEnabled()) { if (source && eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))( eventUtils.fire(
source, 'field', this.name || null, oldValue, localValue)); new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
source,
'field',
this.name || null,
oldValue,
localValue
)
);
} }
if (this.isDirty_) { if (this.isDirty_) {
this.forceRerender(); this.forceRerender();
@@ -1048,7 +1105,9 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* @returns New value, or an Error object. * @returns New value, or an Error object.
*/ */
private processValidation_( private processValidation_(
newValue: AnyDuringMigration, validatedValue: T|null|undefined): T|Error { newValue: AnyDuringMigration,
validatedValue: T | null | undefined
): T | Error {
if (validatedValue === null) { if (validatedValue === null) {
this.doValueInvalid_(newValue); this.doValueInvalid_(newValue);
if (this.isDirty_) { if (this.isDirty_) {
@@ -1056,7 +1115,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
} }
return Error(); return Error();
} }
return validatedValue === undefined ? newValue as T : validatedValue; return validatedValue === undefined ? (newValue as T) : validatedValue;
} }
/** /**
@@ -1064,7 +1123,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* *
* @returns Current value. * @returns Current value.
*/ */
getValue(): T|null { getValue(): T | null {
return this.value_; return this.value_;
} }
@@ -1088,10 +1147,11 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* *
* - `undefined` to set `newValue` as is. * - `undefined` to set `newValue` as is.
*/ */
protected doClassValidation_(newValue: T): T|null|undefined; protected doClassValidation_(newValue: T): T | null | undefined;
protected doClassValidation_(newValue?: AnyDuringMigration): T|null; protected doClassValidation_(newValue?: AnyDuringMigration): T | null;
protected doClassValidation_(newValue?: T|AnyDuringMigration): T|null protected doClassValidation_(
|undefined { newValue?: T | AnyDuringMigration
): T | null | undefined {
if (newValue === null || newValue === undefined) { if (newValue === null || newValue === undefined) {
return null; return null;
} }
@@ -1143,8 +1203,9 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* display the tooltip of the parent block. To not display a tooltip pass * display the tooltip of the parent block. To not display a tooltip pass
* the empty string. * the empty string.
*/ */
setTooltip(newTip: Tooltip.TipInfo|null) { setTooltip(newTip: Tooltip.TipInfo | null) {
if (!newTip && newTip !== '') { // If null or undefined. if (!newTip && newTip !== '') {
// If null or undefined.
newTip = this.sourceBlock_; newTip = this.sourceBlock_;
} }
const clickTarget = this.getClickTarget_(); const clickTarget = this.getClickTarget_();
@@ -1177,7 +1238,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* *
* @returns Element to bind click handler to. * @returns Element to bind click handler to.
*/ */
protected getClickTarget_(): Element|null { protected getClickTarget_(): Element | null {
return this.clickTarget_ || this.getSvgRoot(); return this.clickTarget_ || this.getSvgRoot();
} }
@@ -1351,7 +1412,8 @@ export class UnattachedFieldError extends Error {
/** @internal */ /** @internal */
constructor() { constructor() {
super( super(
'The field has not yet been attached to its input. ' + 'The field has not yet been attached to its input. ' +
'Call appendField to attach it.'); 'Call appendField to attach it.'
);
} }
} }

View File

@@ -18,7 +18,11 @@ import * as Css from './css.js';
import * as dropDownDiv from './dropdowndiv.js'; import * as dropDownDiv from './dropdowndiv.js';
import {Field, UnattachedFieldError} from './field.js'; import {Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js'; import * as fieldRegistry from './field_registry.js';
import {FieldInput, FieldInputConfig, FieldInputValidator} from './field_input.js'; import {
FieldInput,
FieldInputConfig,
FieldInputValidator,
} from './field_input.js';
import * as dom from './utils/dom.js'; import * as dom from './utils/dom.js';
import * as math from './utils/math.js'; import * as math from './utils/math.js';
import {Svg} from './utils/svg.js'; import {Svg} from './utils/svg.js';
@@ -92,13 +96,13 @@ export class FieldAngle extends FieldInput<number> {
private boundEvents: browserEvents.Data[] = []; private boundEvents: browserEvents.Data[] = [];
/** Dynamic red line pointing at the value's angle. */ /** Dynamic red line pointing at the value's angle. */
private line: SVGLineElement|null = null; private line: SVGLineElement | null = null;
/** Dynamic pink area extending from 0 to the value's angle. */ /** Dynamic pink area extending from 0 to the value's angle. */
private gauge: SVGPathElement|null = null; private gauge: SVGPathElement | null = null;
/** The degree symbol for this field. */ /** The degree symbol for this field. */
protected symbol_: SVGTSpanElement|null = null; protected symbol_: SVGTSpanElement | null = null;
/** /**
* @param 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.
@@ -114,8 +118,10 @@ export class FieldAngle extends FieldInput<number> {
* for a list of properties this parameter supports. * for a list of properties this parameter supports.
*/ */
constructor( constructor(
value?: string|number|typeof Field.SKIP_SETUP, value?: string | number | typeof Field.SKIP_SETUP,
validator?: FieldAngleValidator, config?: FieldAngleConfig) { validator?: FieldAngleValidator,
config?: FieldAngleConfig
) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
if (value === Field.SKIP_SETUP) return; if (value === Field.SKIP_SETUP) return;
@@ -192,8 +198,9 @@ export class FieldAngle extends FieldInput<number> {
if (this.sourceBlock_ instanceof BlockSvg) { if (this.sourceBlock_ instanceof BlockSvg) {
dropDownDiv.setColour( dropDownDiv.setColour(
this.sourceBlock_.style.colourPrimary, this.sourceBlock_.style.colourPrimary,
this.sourceBlock_.style.colourTertiary); this.sourceBlock_.style.colourTertiary
);
} }
dropDownDiv.showPositionedByField(this, this.dropdownDispose.bind(this)); dropDownDiv.showPositionedByField(this, this.dropdownDispose.bind(this));
@@ -217,50 +224,80 @@ export class FieldAngle extends FieldInput<number> {
'style': 'touch-action: none', 'style': 'touch-action: none',
}); });
const circle = dom.createSvgElement( const circle = dom.createSvgElement(
Svg.CIRCLE, { Svg.CIRCLE,
'cx': FieldAngle.HALF, {
'cy': FieldAngle.HALF, 'cx': FieldAngle.HALF,
'r': FieldAngle.RADIUS, 'cy': FieldAngle.HALF,
'class': 'blocklyAngleCircle', 'r': FieldAngle.RADIUS,
}, 'class': 'blocklyAngleCircle',
svg); },
this.gauge = svg
dom.createSvgElement(Svg.PATH, {'class': 'blocklyAngleGauge'}, svg); );
this.gauge = dom.createSvgElement(
Svg.PATH,
{'class': 'blocklyAngleGauge'},
svg
);
this.line = dom.createSvgElement( this.line = dom.createSvgElement(
Svg.LINE, { Svg.LINE,
'x1': FieldAngle.HALF, {
'y1': FieldAngle.HALF, 'x1': FieldAngle.HALF,
'class': 'blocklyAngleLine', 'y1': FieldAngle.HALF,
}, 'class': 'blocklyAngleLine',
svg); },
svg
);
// Draw markers around the edge. // Draw markers around the edge.
for (let angle = 0; angle < 360; angle += 15) { for (let angle = 0; angle < 360; angle += 15) {
dom.createSvgElement( dom.createSvgElement(
Svg.LINE, { Svg.LINE,
'x1': FieldAngle.HALF + FieldAngle.RADIUS, {
'y1': FieldAngle.HALF, 'x1': FieldAngle.HALF + FieldAngle.RADIUS,
'x2': FieldAngle.HALF + FieldAngle.RADIUS - 'y1': FieldAngle.HALF,
(angle % 45 === 0 ? 10 : 5), 'x2':
'y2': FieldAngle.HALF, FieldAngle.HALF + FieldAngle.RADIUS - (angle % 45 === 0 ? 10 : 5),
'class': 'blocklyAngleMarks', 'y2': FieldAngle.HALF,
'transform': 'rotate(' + angle + ',' + FieldAngle.HALF + ',' + 'class': 'blocklyAngleMarks',
FieldAngle.HALF + ')', 'transform':
}, 'rotate(' +
svg); angle +
',' +
FieldAngle.HALF +
',' +
FieldAngle.HALF +
')',
},
svg
);
} }
// The angle picker is different from other fields in that it updates on // 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 // mousemove even if it's not in the middle of a drag. In future we may
// change this behaviour. // change this behaviour.
this.boundEvents.push( this.boundEvents.push(
browserEvents.conditionalBind(svg, 'click', this, this.hide)); browserEvents.conditionalBind(svg, 'click', this, this.hide)
);
// On touch devices, the picker's value is only updated with a drag. Add // 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 // a click handler on the drag surface to update the value if the surface
// is clicked. // is clicked.
this.boundEvents.push(browserEvents.conditionalBind( this.boundEvents.push(
circle, 'pointerdown', this, this.onMouseMove_, true)); browserEvents.conditionalBind(
this.boundEvents.push(browserEvents.conditionalBind( circle,
circle, 'pointermove', this, this.onMouseMove_, true)); 'pointerdown',
this,
this.onMouseMove_,
true
)
);
this.boundEvents.push(
browserEvents.conditionalBind(
circle,
'pointermove',
this,
this.onMouseMove_,
true
)
);
return svg; return svg;
} }
@@ -353,14 +390,31 @@ export class FieldAngle extends FieldInput<number> {
x2 += Math.cos(angleRadians) * FieldAngle.RADIUS; x2 += Math.cos(angleRadians) * FieldAngle.RADIUS;
y2 -= Math.sin(angleRadians) * FieldAngle.RADIUS; y2 -= Math.sin(angleRadians) * FieldAngle.RADIUS;
// Don't ask how the flag calculations work. They just do. // Don't ask how the flag calculations work. They just do.
let largeFlag = let largeFlag = Math.abs(
Math.abs(Math.floor((angleRadians - angle1) / Math.PI) % 2); Math.floor((angleRadians - angle1) / Math.PI) % 2
);
if (clockwiseFlag) { if (clockwiseFlag) {
largeFlag = 1 - largeFlag; largeFlag = 1 - largeFlag;
} }
path.push( path.push(
' l ', x1, ',', y1, ' A ', FieldAngle.RADIUS, ',', FieldAngle.RADIUS, ' l ',
' 0 ', largeFlag, ' ', clockwiseFlag, ' ', x2, ',', y2, ' z'); x1,
',',
y1,
' A ',
FieldAngle.RADIUS,
',',
FieldAngle.RADIUS,
' 0 ',
largeFlag,
' ',
clockwiseFlag,
' ',
x2,
',',
y2,
' z'
);
} }
this.gauge.setAttribute('d', path.join('')); this.gauge.setAttribute('d', path.join(''));
this.line.setAttribute('x2', `${x2}`); this.line.setAttribute('x2', `${x2}`);
@@ -412,7 +466,7 @@ export class FieldAngle extends FieldInput<number> {
* @param newValue The input value. * @param newValue The input value.
* @returns A valid angle, or null if invalid. * @returns A valid angle, or null if invalid.
*/ */
protected override doClassValidation_(newValue?: any): number|null { protected override doClassValidation_(newValue?: any): number | null {
const value = Number(newValue); const value = Number(newValue);
if (isNaN(value) || !isFinite(value)) { if (isNaN(value) || !isFinite(value)) {
return null; return null;
@@ -456,7 +510,6 @@ fieldRegistry.register('field_angle', FieldAngle);
FieldAngle.prototype.DEFAULT_VALUE = 0; FieldAngle.prototype.DEFAULT_VALUE = 0;
/** /**
* CSS for angle field. * CSS for angle field.
*/ */

View File

@@ -19,8 +19,8 @@ import * as dom from './utils/dom.js';
import {Field, FieldConfig, FieldValidator} from './field.js'; import {Field, FieldConfig, FieldValidator} from './field.js';
import * as fieldRegistry from './field_registry.js'; import * as fieldRegistry from './field_registry.js';
type BoolString = 'TRUE'|'FALSE'; type BoolString = 'TRUE' | 'FALSE';
type CheckboxBool = BoolString|boolean; type CheckboxBool = BoolString | boolean;
/** /**
* Class for a checkbox field. * Class for a checkbox field.
@@ -45,7 +45,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
* NOTE: The default value is set in `Field`, so maintain that value instead * NOTE: The default value is set in `Field`, so maintain that value instead
* of overwriting it here or in the constructor. * of overwriting it here or in the constructor.
*/ */
override value_: boolean|null = this.value_; override value_: boolean | null = this.value_;
/** /**
* @param value The initial value of the field. Should either be 'TRUE', * @param value The initial value of the field. Should either be 'TRUE',
@@ -62,8 +62,10 @@ export class FieldCheckbox extends Field<CheckboxBool> {
* for a list of properties this parameter supports. * for a list of properties this parameter supports.
*/ */
constructor( constructor(
value?: CheckboxBool|typeof Field.SKIP_SETUP, value?: CheckboxBool | typeof Field.SKIP_SETUP,
validator?: FieldCheckboxValidator, config?: FieldCheckboxConfig) { validator?: FieldCheckboxValidator,
config?: FieldCheckboxConfig
) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
/** /**
@@ -136,7 +138,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
* @param character The character to use for the check mark, or null to use * @param character The character to use for the check mark, or null to use
* the default. * the default.
*/ */
setCheckCharacter(character: string|null) { setCheckCharacter(character: string | null) {
this.checkChar = character || FieldCheckbox.CHECK_CHAR; this.checkChar = character || FieldCheckbox.CHECK_CHAR;
this.forceRerender(); this.forceRerender();
} }
@@ -152,8 +154,9 @@ export class FieldCheckbox extends Field<CheckboxBool> {
* @param newValue The input value. * @param newValue The input value.
* @returns A valid value ('TRUE' or 'FALSE), or null if invalid. * @returns A valid value ('TRUE' or 'FALSE), or null if invalid.
*/ */
protected override doClassValidation_(newValue?: AnyDuringMigration): protected override doClassValidation_(
BoolString|null { newValue?: AnyDuringMigration
): BoolString | null {
if (newValue === true || newValue === 'TRUE') { if (newValue === true || newValue === 'TRUE') {
return 'TRUE'; return 'TRUE';
} }
@@ -191,7 +194,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
* *
* @returns The boolean value of this field. * @returns The boolean value of this field.
*/ */
getValueBoolean(): boolean|null { getValueBoolean(): boolean | null {
return this.value_; return this.value_;
} }
@@ -213,7 +216,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
* @param value The value to convert. * @param value The value to convert.
* @returns The converted value. * @returns The converted value.
*/ */
private convertValueToBool_(value: CheckboxBool|null): boolean { private convertValueToBool_(value: CheckboxBool | null): boolean {
if (typeof value === 'string') return value === 'TRUE'; if (typeof value === 'string') return value === 'TRUE';
return !!value; return !!value;
} }

View File

@@ -36,6 +36,7 @@ export class FieldColour extends Field<string> {
* Copied from goog.ui.ColorPicker.SIMPLE_GRID_COLORS * Copied from goog.ui.ColorPicker.SIMPLE_GRID_COLORS
* All colour pickers use this unless overridden with setColours. * All colour pickers use this unless overridden with setColours.
*/ */
// prettier-ignore
static COLOURS: string[] = [ static COLOURS: string[] = [
// grays // grays
'#ffffff', '#cccccc', '#c0c0c0', '#999999', '#ffffff', '#cccccc', '#c0c0c0', '#999999',
@@ -74,10 +75,10 @@ export class FieldColour extends Field<string> {
static COLUMNS = 7; static COLUMNS = 7;
/** The field's colour picker element. */ /** The field's colour picker element. */
private picker: HTMLElement|null = null; private picker: HTMLElement | null = null;
/** Index of the currently highlighted element. */ /** Index of the currently highlighted element. */
private highlightedIndex: number|null = null; private highlightedIndex: number | null = null;
/** /**
* Array holding info needed to unbind events. * Array holding info needed to unbind events.
@@ -103,13 +104,13 @@ export class FieldColour extends Field<string> {
protected override isDirty_ = false; protected override isDirty_ = false;
/** Array of colours used by this field. If null, use the global list. */ /** Array of colours used by this field. If null, use the global list. */
private colours: string[]|null = null; private colours: string[] | null = null;
/** /**
* Array of colour tooltips used by this field. If null, use the global * Array of colour tooltips used by this field. If null, use the global
* list. * list.
*/ */
private titles: string[]|null = null; private titles: string[] | null = null;
/** /**
* Number of colour columns used by this field. If 0, use the global * Number of colour columns used by this field. If 0, use the global
@@ -132,8 +133,10 @@ export class FieldColour extends Field<string> {
* for a list of properties this parameter supports. * for a list of properties this parameter supports.
*/ */
constructor( constructor(
value?: string|typeof Field.SKIP_SETUP, validator?: FieldColourValidator, value?: string | typeof Field.SKIP_SETUP,
config?: FieldColourConfig) { validator?: FieldColourValidator,
config?: FieldColourConfig
) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
if (value === Field.SKIP_SETUP) return; if (value === Field.SKIP_SETUP) return;
@@ -165,8 +168,9 @@ export class FieldColour extends Field<string> {
*/ */
override initView() { override initView() {
this.size_ = new Size( this.size_ = new Size(
this.getConstants()!.FIELD_COLOUR_DEFAULT_WIDTH, this.getConstants()!.FIELD_COLOUR_DEFAULT_WIDTH,
this.getConstants()!.FIELD_COLOUR_DEFAULT_HEIGHT); this.getConstants()!.FIELD_COLOUR_DEFAULT_HEIGHT
);
if (!this.getConstants()!.FIELD_COLOUR_FULL_BLOCK) { if (!this.getConstants()!.FIELD_COLOUR_FULL_BLOCK) {
this.createBorderRect_(); this.createBorderRect_();
this.getBorderRect().style['fillOpacity'] = '1'; this.getBorderRect().style['fillOpacity'] = '1';
@@ -185,7 +189,9 @@ export class FieldColour extends Field<string> {
} }
} else if (this.sourceBlock_ instanceof BlockSvg) { } else if (this.sourceBlock_ instanceof BlockSvg) {
this.sourceBlock_.pathObject.svgPath.setAttribute( this.sourceBlock_.pathObject.svgPath.setAttribute(
'fill', this.getValue() as string); 'fill',
this.getValue() as string
);
this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff'); this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff');
} }
} }
@@ -196,7 +202,7 @@ export class FieldColour extends Field<string> {
* @param newValue The input value. * @param newValue The input value.
* @returns A valid colour, or null if invalid. * @returns A valid colour, or null if invalid.
*/ */
protected override doClassValidation_(newValue?: any): string|null { protected override doClassValidation_(newValue?: any): string | null {
if (typeof newValue !== 'string') { if (typeof newValue !== 'string') {
return null; return null;
} }
@@ -214,8 +220,10 @@ export class FieldColour extends Field<string> {
if (this.borderRect_) { if (this.borderRect_) {
this.borderRect_.style.fill = newValue; this.borderRect_.style.fill = newValue;
} else if ( } else if (
this.sourceBlock_ && this.sourceBlock_.rendered && this.sourceBlock_ &&
this.sourceBlock_ instanceof BlockSvg) { this.sourceBlock_.rendered &&
this.sourceBlock_ instanceof BlockSvg
) {
this.sourceBlock_.pathObject.svgPath.setAttribute('fill', newValue); this.sourceBlock_.pathObject.svgPath.setAttribute('fill', newValue);
this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff'); this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff');
} }
@@ -297,7 +305,7 @@ export class FieldColour extends Field<string> {
*/ */
private onKeyDown(e: KeyboardEvent) { private onKeyDown(e: KeyboardEvent) {
let handled = true; let handled = true;
let highlighted: HTMLElement|null; let highlighted: HTMLElement | null;
switch (e.key) { switch (e.key) {
case 'ArrowUp': case 'ArrowUp':
this.moveHighlightBy(0, -1); this.moveHighlightBy(0, -1);
@@ -423,7 +431,7 @@ export class FieldColour extends Field<string> {
* *
* @returns Highlighted item (null if none). * @returns Highlighted item (null if none).
*/ */
private getHighlighted(): HTMLElement|null { private getHighlighted(): HTMLElement | null {
if (!this.highlightedIndex) { if (!this.highlightedIndex) {
return null; return null;
} }
@@ -476,7 +484,10 @@ export class FieldColour extends Field<string> {
aria.setRole(table, aria.Role.GRID); aria.setRole(table, aria.Role.GRID);
aria.setState(table, aria.State.EXPANDED, true); aria.setState(table, aria.State.EXPANDED, true);
aria.setState( aria.setState(
table, aria.State.ROWCOUNT, Math.floor(colours.length / columns)); table,
aria.State.ROWCOUNT,
Math.floor(colours.length / columns)
);
aria.setState(table, aria.State.COLCOUNT, columns); aria.setState(table, aria.State.COLCOUNT, columns);
let row: Element; let row: Element;
for (let i = 0; i < colours.length; i++) { for (let i = 0; i < colours.length; i++) {
@@ -485,7 +496,7 @@ export class FieldColour extends Field<string> {
aria.setRole(row, aria.Role.ROW); aria.setRole(row, aria.Role.ROW);
table.appendChild(row); table.appendChild(row);
} }
const cell = (document.createElement('td')); const cell = document.createElement('td');
row!.appendChild(cell); row!.appendChild(cell);
// This becomes the value, if clicked. // This becomes the value, if clicked.
cell.setAttribute('data-colour', colours[i]); cell.setAttribute('data-colour', colours[i]);
@@ -503,16 +514,51 @@ export class FieldColour extends Field<string> {
} }
// Configure event handler on the table to listen for any event in a cell. // Configure event handler on the table to listen for any event in a cell.
this.boundEvents.push(browserEvents.conditionalBind( this.boundEvents.push(
table, 'pointerdown', this, this.onClick, true)); browserEvents.conditionalBind(
this.boundEvents.push(browserEvents.conditionalBind( table,
table, 'pointermove', this, this.onMouseMove, true)); 'pointerdown',
this.boundEvents.push(browserEvents.conditionalBind( this,
table, 'pointerenter', this, this.onMouseEnter, true)); this.onClick,
this.boundEvents.push(browserEvents.conditionalBind( true
table, 'pointerleave', this, this.onMouseLeave, true)); )
this.boundEvents.push(browserEvents.conditionalBind( );
table, 'keydown', this, this.onKeyDown, false)); 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;
} }
@@ -547,7 +593,6 @@ FieldColour.prototype.DEFAULT_VALUE = FieldColour.COLOURS[0];
fieldRegistry.register('field_colour', FieldColour); fieldRegistry.register('field_colour', FieldColour);
/** /**
* CSS for colour picker. * CSS for colour picker.
*/ */

View File

@@ -16,7 +16,12 @@ goog.declareModuleId('Blockly.FieldDropdown');
import type {BlockSvg} from './block_svg.js'; import type {BlockSvg} from './block_svg.js';
import * as dropDownDiv from './dropdowndiv.js'; import * as dropDownDiv from './dropdowndiv.js';
import {Field, FieldConfig, FieldValidator, UnattachedFieldError} from './field.js'; import {
Field,
FieldConfig,
FieldValidator,
UnattachedFieldError,
} from './field.js';
import * as fieldRegistry from './field_registry.js'; import * as fieldRegistry from './field_registry.js';
import {Menu} from './menu.js'; import {Menu} from './menu.js';
import {MenuItem} from './menuitem.js'; import {MenuItem} from './menuitem.js';
@@ -44,21 +49,21 @@ export class FieldDropdown extends Field<string> {
static ARROW_CHAR = '▾'; static ARROW_CHAR = '▾';
/** A reference to the currently selected menu item. */ /** A reference to the currently selected menu item. */
private selectedMenuItem: MenuItem|null = null; private selectedMenuItem: MenuItem | null = null;
/** The dropdown menu. */ /** The dropdown menu. */
protected menu_: Menu|null = null; protected menu_: Menu | null = null;
/** /**
* SVG image element if currently selected option is an image, or null. * SVG image element if currently selected option is an image, or null.
*/ */
private imageElement: SVGImageElement|null = null; private imageElement: SVGImageElement | null = null;
/** Tspan based arrow element. */ /** Tspan based arrow element. */
private arrow: SVGTSpanElement|null = null; private arrow: SVGTSpanElement | null = null;
/** SVG based arrow element. */ /** SVG based arrow element. */
private svgArrow: SVGElement|null = null; private svgArrow: SVGElement | null = null;
/** /**
* Serializable fields are saved by the serializer, non-serializable fields * Serializable fields are saved by the serializer, non-serializable fields
@@ -72,24 +77,24 @@ export class FieldDropdown extends Field<string> {
protected menuGenerator_?: MenuGenerator; protected menuGenerator_?: MenuGenerator;
/** A cache of the most recently generated options. */ /** A cache of the most recently generated options. */
private generatedOptions: MenuOption[]|null = null; private generatedOptions: MenuOption[] | null = null;
/** /**
* The prefix field label, of common words set after options are trimmed. * The prefix field label, of common words set after options are trimmed.
* *
* @internal * @internal
*/ */
override prefixField: string|null = null; override prefixField: string | null = null;
/** /**
* The suffix field label, of common words set after options are trimmed. * The suffix field label, of common words set after options are trimmed.
* *
* @internal * @internal
*/ */
override suffixField: string|null = null; override suffixField: string | null = null;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix. // TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
private selectedOption!: MenuOption; private selectedOption!: MenuOption;
override clickTarget_: SVGElement|null = null; override clickTarget_: SVGElement | null = null;
/** /**
* @param menuGenerator A non-empty array of options for a dropdown list, or a * @param menuGenerator A non-empty array of options for a dropdown list, or a
@@ -108,15 +113,15 @@ export class FieldDropdown extends Field<string> {
* @throws {TypeError} If `menuGenerator` options are incorrectly structured. * @throws {TypeError} If `menuGenerator` options are incorrectly structured.
*/ */
constructor( constructor(
menuGenerator: MenuGenerator, menuGenerator: MenuGenerator,
validator?: FieldDropdownValidator, validator?: FieldDropdownValidator,
config?: FieldDropdownConfig, config?: FieldDropdownConfig
); );
constructor(menuGenerator: typeof Field.SKIP_SETUP); constructor(menuGenerator: typeof Field.SKIP_SETUP);
constructor( constructor(
menuGenerator: MenuGenerator|typeof Field.SKIP_SETUP, menuGenerator: MenuGenerator | typeof Field.SKIP_SETUP,
validator?: FieldDropdownValidator, validator?: FieldDropdownValidator,
config?: FieldDropdownConfig, config?: FieldDropdownConfig
) { ) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
@@ -210,17 +215,23 @@ export class FieldDropdown extends Field<string> {
* @returns True if the dropdown field should add a border rect. * @returns True if the dropdown field should add a border rect.
*/ */
protected shouldAddBorderRect_(): boolean { protected shouldAddBorderRect_(): boolean {
return !this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW || return (
this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW && !this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
!this.getSourceBlock()?.isShadow(); (this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW &&
!this.getSourceBlock()?.isShadow())
);
} }
/** Create a tspan based arrow. */ /** Create a tspan based arrow. */
protected createTextArrow_() { protected createTextArrow_() {
this.arrow = dom.createSvgElement(Svg.TSPAN, {}, this.textElement_); this.arrow = dom.createSvgElement(Svg.TSPAN, {}, this.textElement_);
this.arrow!.appendChild(document.createTextNode( this.arrow!.appendChild(
this.getSourceBlock()?.RTL ? FieldDropdown.ARROW_CHAR + ' ' : document.createTextNode(
' ' + FieldDropdown.ARROW_CHAR)); this.getSourceBlock()?.RTL
? FieldDropdown.ARROW_CHAR + ' '
: ' ' + FieldDropdown.ARROW_CHAR
)
);
if (this.getSourceBlock()?.RTL) { if (this.getSourceBlock()?.RTL) {
this.getTextElement().insertBefore(this.arrow, this.textContent_); this.getTextElement().insertBefore(this.arrow, this.textContent_);
} else { } else {
@@ -231,14 +242,18 @@ export class FieldDropdown extends Field<string> {
/** Create an SVG based arrow. */ /** Create an SVG based arrow. */
protected createSVGArrow_() { protected createSVGArrow_() {
this.svgArrow = dom.createSvgElement( this.svgArrow = dom.createSvgElement(
Svg.IMAGE, { Svg.IMAGE,
'height': this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px', {
'width': this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px', 'height': this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',
}, 'width': this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',
this.fieldGroup_); },
this.fieldGroup_
);
this.svgArrow!.setAttributeNS( this.svgArrow!.setAttributeNS(
dom.XLINK_NS, 'xlink:href', dom.XLINK_NS,
this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_DATAURI); 'xlink:href',
this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_DATAURI
);
} }
/** /**
@@ -266,11 +281,12 @@ export class FieldDropdown extends Field<string> {
dom.addClass(menuElement, 'blocklyDropdownMenu'); dom.addClass(menuElement, 'blocklyDropdownMenu');
if (this.getConstants()!.FIELD_DROPDOWN_COLOURED_DIV) { if (this.getConstants()!.FIELD_DROPDOWN_COLOURED_DIV) {
const primaryColour = const primaryColour = block.isShadow()
block.isShadow() ? block.getParent()!.getColour() : block.getColour(); ? block.getParent()!.getColour()
const borderColour = block.isShadow() ? : block.getColour();
(block.getParent() as BlockSvg).style.colourTertiary : const borderColour = block.isShadow()
(this.sourceBlock_ as BlockSvg).style.colourTertiary; ? (block.getParent() as BlockSvg).style.colourTertiary
: (this.sourceBlock_ as BlockSvg).style.colourTertiary;
dropDownDiv.setColour(primaryColour, borderColour); dropDownDiv.setColour(primaryColour, borderColour);
} }
@@ -284,8 +300,10 @@ export class FieldDropdown extends Field<string> {
if (this.selectedMenuItem) { if (this.selectedMenuItem) {
this.menu_!.setHighlighted(this.selectedMenuItem); this.menu_!.setHighlighted(this.selectedMenuItem);
style.scrollIntoContainerView( style.scrollIntoContainerView(
this.selectedMenuItem.getElement()!, dropDownDiv.getContentDiv(), this.selectedMenuItem.getElement()!,
true); dropDownDiv.getContentDiv(),
true
);
} }
this.applyColour(); this.applyColour();
@@ -397,16 +415,21 @@ export class FieldDropdown extends Field<string> {
* @param newValue The input value. * @param newValue The input value.
* @returns A valid language-neutral option, or null if invalid. * @returns A valid language-neutral option, or null if invalid.
*/ */
protected override doClassValidation_(newValue?: string): string|null { protected override doClassValidation_(newValue?: string): string | null {
const options = this.getOptions(true); const options = this.getOptions(true);
const isValueValid = options.some((option) => option[1] === newValue); const isValueValid = options.some((option) => option[1] === newValue);
if (!isValueValid) { if (!isValueValid) {
if (this.sourceBlock_) { if (this.sourceBlock_) {
console.warn( console.warn(
'Cannot set the dropdown\'s value to an unavailable option.' + "Cannot set the dropdown's value to an unavailable option." +
' Block type: ' + this.sourceBlock_.type + ' Block type: ' +
', Field name: ' + this.name + ', Value: ' + newValue); this.sourceBlock_.type +
', Field name: ' +
this.name +
', Value: ' +
newValue
);
} }
return null; return null;
} }
@@ -422,7 +445,7 @@ export class FieldDropdown extends Field<string> {
protected override doValueUpdate_(newValue: string) { protected override doValueUpdate_(newValue: string) {
super.doValueUpdate_(newValue); super.doValueUpdate_(newValue);
const options = this.getOptions(true); const options = this.getOptions(true);
for (let i = 0, option; option = options[i]; i++) { for (let i = 0, option; (option = options[i]); i++) {
if (option[1] === this.value_) { if (option[1] === this.value_) {
this.selectedOption = option; this.selectedOption = option;
} }
@@ -481,7 +504,10 @@ export class FieldDropdown extends Field<string> {
} }
this.imageElement!.style.display = ''; this.imageElement!.style.display = '';
this.imageElement!.setAttributeNS( this.imageElement!.setAttributeNS(
dom.XLINK_NS, 'xlink:href', imageJson.src); dom.XLINK_NS,
'xlink:href',
imageJson.src
);
this.imageElement!.setAttribute('height', String(imageJson.height)); this.imageElement!.setAttribute('height', String(imageJson.height));
this.imageElement!.setAttribute('width', String(imageJson.width)); this.imageElement!.setAttribute('width', String(imageJson.width));
@@ -491,21 +517,25 @@ export class FieldDropdown extends Field<string> {
// Height and width include the border rect. // Height and width include the border rect.
const hasBorder = !!this.borderRect_; const hasBorder = !!this.borderRect_;
const height = Math.max( const height = Math.max(
hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0, hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
imageHeight + IMAGE_Y_PADDING); imageHeight + IMAGE_Y_PADDING
const xPadding = );
hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0; const xPadding = hasBorder
? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING
: 0;
let arrowWidth = 0; let arrowWidth = 0;
if (this.svgArrow) { if (this.svgArrow) {
arrowWidth = this.positionSVGArrow( arrowWidth = this.positionSVGArrow(
imageWidth + xPadding, imageWidth + xPadding,
height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2); height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2
);
} else { } else {
arrowWidth = dom.getFastTextWidth( arrowWidth = dom.getFastTextWidth(
this.arrow as SVGTSpanElement, this.arrow as SVGTSpanElement,
this.getConstants()!.FIELD_TEXT_FONTSIZE, this.getConstants()!.FIELD_TEXT_FONTSIZE,
this.getConstants()!.FIELD_TEXT_FONTWEIGHT, this.getConstants()!.FIELD_TEXT_FONTWEIGHT,
this.getConstants()!.FIELD_TEXT_FONTFAMILY); this.getConstants()!.FIELD_TEXT_FONTFAMILY
);
} }
this.size_.width = imageWidth + arrowWidth + xPadding * 2; this.size_.width = imageWidth + arrowWidth + xPadding * 2;
this.size_.height = height; this.size_.height = height;
@@ -535,19 +565,24 @@ export class FieldDropdown extends Field<string> {
// Height and width include the border rect. // Height and width include the border rect.
const hasBorder = !!this.borderRect_; const hasBorder = !!this.borderRect_;
const height = Math.max( const height = Math.max(
hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0, hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
this.getConstants()!.FIELD_TEXT_HEIGHT); this.getConstants()!.FIELD_TEXT_HEIGHT
);
const textWidth = dom.getFastTextWidth( const textWidth = dom.getFastTextWidth(
this.getTextElement(), this.getConstants()!.FIELD_TEXT_FONTSIZE, this.getTextElement(),
this.getConstants()!.FIELD_TEXT_FONTWEIGHT, this.getConstants()!.FIELD_TEXT_FONTSIZE,
this.getConstants()!.FIELD_TEXT_FONTFAMILY); this.getConstants()!.FIELD_TEXT_FONTWEIGHT,
const xPadding = this.getConstants()!.FIELD_TEXT_FONTFAMILY
hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0; );
const xPadding = hasBorder
? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING
: 0;
let arrowWidth = 0; let arrowWidth = 0;
if (this.svgArrow) { if (this.svgArrow) {
arrowWidth = this.positionSVGArrow( arrowWidth = this.positionSVGArrow(
textWidth + xPadding, textWidth + xPadding,
height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2); height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2
);
} }
this.size_.width = textWidth + arrowWidth + xPadding * 2; this.size_.width = textWidth + arrowWidth + xPadding * 2;
this.size_.height = height; this.size_.height = height;
@@ -571,13 +606,16 @@ export class FieldDropdown extends Field<string> {
throw new UnattachedFieldError(); throw new UnattachedFieldError();
} }
const hasBorder = !!this.borderRect_; const hasBorder = !!this.borderRect_;
const xPadding = const xPadding = hasBorder
hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0; ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING
: 0;
const textPadding = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_PADDING; const textPadding = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_PADDING;
const svgArrowSize = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE; const svgArrowSize = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE;
const arrowX = block.RTL ? xPadding : x + textPadding; const arrowX = block.RTL ? xPadding : x + textPadding;
this.svgArrow.setAttribute( this.svgArrow.setAttribute(
'transform', 'translate(' + arrowX + ',' + y + ')'); 'transform',
'translate(' + arrowX + ',' + y + ')'
);
return svgArrowSize + textPadding; return svgArrowSize + textPadding;
} }
@@ -588,7 +626,7 @@ export class FieldDropdown extends Field<string> {
* *
* @returns Selected option text. * @returns Selected option text.
*/ */
protected override getText_(): string|null { protected override getText_(): string | null {
if (!this.selectedOption) { if (!this.selectedOption) {
return null; return null;
} }
@@ -610,9 +648,10 @@ export class FieldDropdown extends Field<string> {
static fromJson(options: FieldDropdownFromJsonConfig): FieldDropdown { static fromJson(options: FieldDropdownFromJsonConfig): FieldDropdown {
if (!options.options) { if (!options.options) {
throw new Error( throw new Error(
'options are required for the dropdown field. The ' + 'options are required for the dropdown field. The ' +
'options property must be assigned an array of ' + 'options property must be assigned an array of ' +
'[humanReadableValue, languageNeutralValue] tuples.'); '[humanReadableValue, languageNeutralValue] tuples.'
);
} }
// `this` might be a subclass of FieldDropdown if that class doesn't // `this` might be a subclass of FieldDropdown if that class doesn't
// override the static fromJson method. // override the static fromJson method.
@@ -647,7 +686,7 @@ export type MenuGeneratorFunction = (this: FieldDropdown) => MenuOption[];
* Either an array of menu options or a function that generates an array of * Either an array of menu options or a function that generates an array of
* menu options for FieldDropdown or its descendants. * menu options for FieldDropdown or its descendants.
*/ */
export type MenuGenerator = MenuOption[]|MenuGeneratorFunction; export type MenuGenerator = MenuOption[] | MenuGeneratorFunction;
/** /**
* Config options for the dropdown field. * Config options for the dropdown field.
@@ -691,8 +730,11 @@ const IMAGE_Y_PADDING: number = IMAGE_Y_OFFSET * 2;
* Factor out common words in statically defined options. * Factor out common words in statically defined options.
* Create prefix and/or suffix labels. * Create prefix and/or suffix labels.
*/ */
function trimOptions(options: MenuOption[]): function trimOptions(options: MenuOption[]): {
{options: MenuOption[]; prefix?: string; suffix?: string;} { options: MenuOption[];
prefix?: string;
suffix?: string;
} {
let hasImages = false; let hasImages = false;
const trimmedOptions = options.map(([label, value]): MenuOption => { const trimmedOptions = options.map(([label, value]): MenuOption => {
if (typeof label === 'string') { if (typeof label === 'string') {
@@ -702,9 +744,10 @@ function trimOptions(options: MenuOption[]):
hasImages = true; hasImages = true;
// Copy the image properties so they're not influenced by the original. // Copy the image properties so they're not influenced by the original.
// NOTE: No need to deep copy since image properties are only 1 level deep. // NOTE: No need to deep copy since image properties are only 1 level deep.
const imageLabel = label.alt !== null ? const imageLabel =
{...label, alt: parsing.replaceMessageReferences(label.alt)} : label.alt !== null
{...label}; ? {...label, alt: parsing.replaceMessageReferences(label.alt)}
: {...label};
return [imageLabel, value]; return [imageLabel, value];
}); });
@@ -717,16 +760,20 @@ function trimOptions(options: MenuOption[]):
const prefixLength = utilsString.commonWordPrefix(stringLabels, shortest); const prefixLength = utilsString.commonWordPrefix(stringLabels, shortest);
const suffixLength = utilsString.commonWordSuffix(stringLabels, shortest); const suffixLength = utilsString.commonWordSuffix(stringLabels, shortest);
if ((!prefixLength && !suffixLength) || if (
(shortest <= prefixLength + suffixLength)) { (!prefixLength && !suffixLength) ||
shortest <= prefixLength + suffixLength
) {
// One or more strings will entirely vanish if we proceed. Abort. // One or more strings will entirely vanish if we proceed. Abort.
return {options: stringOptions}; return {options: stringOptions};
} }
const prefix = const prefix = prefixLength
prefixLength ? stringLabels[0].substring(0, prefixLength - 1) : undefined; ? stringLabels[0].substring(0, prefixLength - 1)
const suffix = : undefined;
suffixLength ? stringLabels[0].substr(1 - suffixLength) : undefined; const suffix = suffixLength
? stringLabels[0].substr(1 - suffixLength)
: undefined;
return { return {
options: applyTrim(stringOptions, prefixLength, suffixLength), options: applyTrim(stringOptions, prefixLength, suffixLength),
prefix, prefix,
@@ -745,12 +792,13 @@ function trimOptions(options: MenuOption[]):
* @returns A new array with all of the option text trimmed. * @returns A new array with all of the option text trimmed.
*/ */
function applyTrim( function applyTrim(
options: [string, string][], prefixLength: number, options: [string, string][],
suffixLength: number): MenuOption[] { prefixLength: number,
return options.map( suffixLength: number
([text, value]) => ): MenuOption[] {
[text.substring(prefixLength, text.length - suffixLength), return options.map(([text, value]) => [
value, text.substring(prefixLength, text.length - suffixLength),
value,
]); ]);
} }
@@ -773,23 +821,38 @@ function validateOptions(options: MenuOption[]) {
if (!Array.isArray(tuple)) { if (!Array.isArray(tuple)) {
foundError = true; foundError = true;
console.error( console.error(
'Invalid option[' + i + ']: Each FieldDropdown option must be an ' + 'Invalid option[' +
'array. Found: ', i +
tuple); ']: Each FieldDropdown option must be an ' +
'array. Found: ',
tuple
);
} else if (typeof tuple[1] !== 'string') { } else if (typeof tuple[1] !== 'string') {
foundError = true; foundError = true;
console.error( console.error(
'Invalid option[' + i + ']: Each FieldDropdown option id must be ' + 'Invalid option[' +
'a string. Found ' + tuple[1] + ' in: ', i +
tuple); ']: Each FieldDropdown option id must be ' +
'a string. Found ' +
tuple[1] +
' in: ',
tuple
);
} else if ( } else if (
tuple[0] && typeof tuple[0] !== 'string' && tuple[0] &&
typeof tuple[0].src !== 'string') { typeof tuple[0] !== 'string' &&
typeof tuple[0].src !== 'string'
) {
foundError = true; foundError = true;
console.error( console.error(
'Invalid option[' + i + ']: Each FieldDropdown option must have a ' + 'Invalid option[' +
'string label or image description. Found' + tuple[0] + ' in: ', i +
tuple); ']: Each FieldDropdown option must have a ' +
'string label or image description. Found' +
tuple[0] +
' in: ',
tuple
);
} }
} }
if (foundError) { if (foundError) {

View File

@@ -32,10 +32,10 @@ export class FieldImage extends Field<string> {
private readonly imageHeight: number; private readonly imageHeight: number;
/** The function to be called when this field is clicked. */ /** The function to be called when this field is clicked. */
private clickHandler: ((p1: FieldImage) => void)|null = null; private clickHandler: ((p1: FieldImage) => void) | null = null;
/** The rendered field's image element. */ /** The rendered field's image element. */
private imageElement: SVGImageElement|null = null; private imageElement: SVGImageElement | null = null;
/** /**
* Editable fields usually show some sort of UI indicating they are * Editable fields usually show some sort of UI indicating they are
@@ -73,22 +73,27 @@ export class FieldImage extends Field<string> {
* for a list of properties this parameter supports. * for a list of properties this parameter supports.
*/ */
constructor( constructor(
src: string|typeof Field.SKIP_SETUP, width: string|number, src: string | typeof Field.SKIP_SETUP,
height: string|number, alt?: string, onClick?: (p1: FieldImage) => void, width: string | number,
flipRtl?: boolean, config?: FieldImageConfig) { height: string | number,
alt?: string,
onClick?: (p1: FieldImage) => void,
flipRtl?: boolean,
config?: FieldImageConfig
) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
const imageHeight = Number(parsing.replaceMessageReferences(height)); const imageHeight = Number(parsing.replaceMessageReferences(height));
const imageWidth = Number(parsing.replaceMessageReferences(width)); const imageWidth = Number(parsing.replaceMessageReferences(width));
if (isNaN(imageHeight) || isNaN(imageWidth)) { if (isNaN(imageHeight) || isNaN(imageWidth)) {
throw Error( throw Error(
'Height and width values of an image field must cast to' + 'Height and width values of an image field must cast to' + ' numbers.'
' numbers.'); );
} }
if (imageHeight <= 0 || imageWidth <= 0) { if (imageHeight <= 0 || imageWidth <= 0) {
throw Error( throw Error(
'Height and width values of an image field must be greater' + 'Height and width values of an image field must be greater' + ' than 0.'
' than 0.'); );
} }
/** The size of the area rendered by the field. */ /** The size of the area rendered by the field. */
@@ -134,14 +139,19 @@ export class FieldImage extends Field<string> {
*/ */
override initView() { override initView() {
this.imageElement = dom.createSvgElement( this.imageElement = dom.createSvgElement(
Svg.IMAGE, { Svg.IMAGE,
'height': this.imageHeight + 'px', {
'width': this.size_.width + 'px', 'height': this.imageHeight + 'px',
'alt': this.altText, 'width': this.size_.width + 'px',
}, 'alt': this.altText,
this.fieldGroup_); },
this.fieldGroup_
);
this.imageElement.setAttributeNS( this.imageElement.setAttributeNS(
dom.XLINK_NS, 'xlink:href', this.value_ as string); dom.XLINK_NS,
'xlink:href',
this.value_ as string
);
if (this.clickHandler) { if (this.clickHandler) {
this.imageElement.style.cursor = 'pointer'; this.imageElement.style.cursor = 'pointer';
@@ -157,7 +167,7 @@ export class FieldImage extends Field<string> {
* @param newValue The input value. * @param newValue The input value.
* @returns A string, or null if invalid. * @returns A string, or null if invalid.
*/ */
protected override doClassValidation_(newValue?: any): string|null { protected override doClassValidation_(newValue?: any): string | null {
if (typeof newValue !== 'string') { if (typeof newValue !== 'string') {
return null; return null;
} }
@@ -191,7 +201,7 @@ export class FieldImage extends Field<string> {
* *
* @param alt New alt text. * @param alt New alt text.
*/ */
setAlt(alt: string|null) { setAlt(alt: string | null) {
if (alt === this.altText) { if (alt === this.altText) {
return; return;
} }
@@ -217,7 +227,7 @@ export class FieldImage extends Field<string> {
* @param func The function that is called when the image is clicked, or null * @param func The function that is called when the image is clicked, or null
* to remove. * to remove.
*/ */
setOnClickHandler(func: ((p1: FieldImage) => void)|null) { setOnClickHandler(func: ((p1: FieldImage) => void) | null) {
this.clickHandler = func; this.clickHandler = func;
} }
@@ -228,7 +238,7 @@ export class FieldImage extends Field<string> {
* *
* @returns The image alt text. * @returns The image alt text.
*/ */
protected override getText_(): string|null { protected override getText_(): string | null {
return this.altText; return this.altText;
} }
@@ -245,14 +255,21 @@ export class FieldImage extends Field<string> {
static fromJson(options: FieldImageFromJsonConfig): FieldImage { static fromJson(options: FieldImageFromJsonConfig): FieldImage {
if (!options.src || !options.width || !options.height) { if (!options.src || !options.width || !options.height) {
throw new Error( throw new Error(
'src, width, and height values for an image field are' + 'src, width, and height values for an image field are' +
'required. The width and height must be non-zero.'); 'required. The width and height must be non-zero.'
);
} }
// `this` might be a subclass of FieldImage if that class doesn't override // `this` might be a subclass of FieldImage if that class doesn't override
// the static fromJson method. // the static fromJson method.
return new this( return new this(
options.src, options.width, options.height, undefined, undefined, options.src,
undefined, options); options.width,
options.height,
undefined,
undefined,
undefined,
options
);
} }
} }

View File

@@ -22,7 +22,12 @@ import * as dialog from './dialog.js';
import * as dom from './utils/dom.js'; import * as dom from './utils/dom.js';
import * as dropDownDiv from './dropdowndiv.js'; import * as dropDownDiv from './dropdowndiv.js';
import * as eventUtils from './events/utils.js'; import * as eventUtils from './events/utils.js';
import {Field, FieldConfig, FieldValidator, UnattachedFieldError} from './field.js'; import {
Field,
FieldConfig,
FieldValidator,
UnattachedFieldError,
} from './field.js';
import {Msg} from './msg.js'; import {Msg} from './msg.js';
import * as aria from './utils/aria.js'; import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js'; import {Coordinate} from './utils/coordinate.js';
@@ -36,7 +41,7 @@ import * as renderManagement from './render_management.js';
* *
* @internal * @internal
*/ */
type InputTypes = string|number; type InputTypes = string | number;
/** /**
* Abstract class for an editable input field. * Abstract class for an editable input field.
@@ -44,7 +49,9 @@ type InputTypes = string|number;
* @typeParam T - The value stored on the field. * @typeParam T - The value stored on the field.
* @internal * @internal
*/ */
export abstract class FieldInput<T extends InputTypes> extends Field<string|T> { export abstract class FieldInput<T extends InputTypes> extends Field<
string | T
> {
/** /**
* Pixel size of input border radius. * Pixel size of input border radius.
* Should match blocklyText's border-radius in CSS. * Should match blocklyText's border-radius in CSS.
@@ -55,7 +62,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
protected spellcheck_ = true; protected spellcheck_ = true;
/** The HTML input element. */ /** The HTML input element. */
protected htmlInput_: HTMLInputElement|null = null; protected htmlInput_: HTMLInputElement | null = null;
/** True if the field's value is currently being edited via the UI. */ /** True if the field's value is currently being edited via the UI. */
protected isBeingEdited_ = false; protected isBeingEdited_ = false;
@@ -66,19 +73,19 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
protected isTextValid_ = false; protected isTextValid_ = false;
/** Key down event data. */ /** Key down event data. */
private onKeyDownWrapper_: browserEvents.Data|null = null; private onKeyDownWrapper_: browserEvents.Data | null = null;
/** Key input event data. */ /** Key input event data. */
private onKeyInputWrapper_: browserEvents.Data|null = null; private onKeyInputWrapper_: browserEvents.Data | null = null;
/** /**
* Whether the field should consider the whole parent block to be its click * Whether the field should consider the whole parent block to be its click
* target. * target.
*/ */
fullBlockClickTarget_: boolean|null = false; fullBlockClickTarget_: boolean | null = false;
/** The workspace that this field belongs to. */ /** The workspace that this field belongs to. */
protected workspace_: WorkspaceSvg|null = null; protected workspace_: WorkspaceSvg | null = null;
/** /**
* Serializable fields are saved by the serializer, non-serializable fields * Serializable fields are saved by the serializer, non-serializable fields
@@ -104,8 +111,10 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
* for a list of properties this parameter supports. * for a list of properties this parameter supports.
*/ */
constructor( constructor(
value?: string|typeof Field.SKIP_SETUP, value?: string | typeof Field.SKIP_SETUP,
validator?: FieldInputValidator<T>|null, config?: FieldInputConfig) { validator?: FieldInputValidator<T> | null,
config?: FieldInputConfig
) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
if (value === Field.SKIP_SETUP) return; if (value === Field.SKIP_SETUP) return;
@@ -137,7 +146,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
let nFields = 0; let nFields = 0;
let nConnections = 0; let nConnections = 0;
// Count the number of fields, excluding text fields // Count the number of fields, excluding text fields
for (let i = 0, input; input = block.inputList[i]; i++) { for (let i = 0, input; (input = block.inputList[i]); i++) {
for (let j = 0; input.fieldRow[j]; j++) { for (let j = 0; input.fieldRow[j]; j++) {
nFields++; nFields++;
} }
@@ -148,7 +157,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
// The special case is when this is the only non-label field on the block // The special case is when this is the only non-label field on the block
// and it has an output but no inputs. // and it has an output but no inputs.
this.fullBlockClickTarget_ = this.fullBlockClickTarget_ =
nFields <= 1 && block.outputConnection && !nConnections; nFields <= 1 && block.outputConnection && !nConnections;
} else { } else {
this.fullBlockClickTarget_ = false; this.fullBlockClickTarget_ = false;
} }
@@ -178,9 +187,15 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
// Revert value when the text becomes invalid. // Revert value when the text becomes invalid.
this.value_ = this.htmlInput_!.getAttribute('data-untyped-default-value'); this.value_ = this.htmlInput_!.getAttribute('data-untyped-default-value');
if (this.sourceBlock_ && eventUtils.isEnabled()) { if (this.sourceBlock_ && eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))( eventUtils.fire(
this.sourceBlock_, 'field', this.name || null, oldValue, new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this.value_)); this.sourceBlock_,
'field',
this.name || null,
oldValue,
this.value_
)
);
} }
} }
} }
@@ -193,7 +208,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
* @param newValue The value to be saved. The default validator guarantees * @param newValue The value to be saved. The default validator guarantees
* that this is a string. * that this is a string.
*/ */
protected override doValueUpdate_(newValue: string|T) { protected override doValueUpdate_(newValue: string | T) {
this.isDirty_ = true; this.isDirty_ = true;
this.isTextValid_ = true; this.isTextValid_ = true;
this.value_ = newValue; this.value_ = newValue;
@@ -211,7 +226,9 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
this.borderRect_.setAttribute('stroke', source.style.colourTertiary); this.borderRect_.setAttribute('stroke', source.style.colourTertiary);
} else { } else {
source.pathObject.svgPath.setAttribute( source.pathObject.svgPath.setAttribute(
'fill', this.getConstants()!.FIELD_BORDER_RECT_COLOUR); 'fill',
this.getConstants()!.FIELD_BORDER_RECT_COLOUR
);
} }
} }
@@ -250,7 +267,9 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
// AnyDuringMigration because: Argument of type 'boolean' is not // AnyDuringMigration because: Argument of type 'boolean' is not
// assignable to parameter of type 'string'. // assignable to parameter of type 'string'.
this.htmlInput_.setAttribute( this.htmlInput_.setAttribute(
'spellcheck', this.spellcheck_ as AnyDuringMigration); 'spellcheck',
this.spellcheck_ as AnyDuringMigration
);
} }
} }
@@ -267,8 +286,11 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
*/ */
protected override showEditor_(_e?: Event, quietInput = false) { protected override showEditor_(_e?: Event, quietInput = false) {
this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace; this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace;
if (!quietInput && this.workspace_.options.modalInputs && if (
(userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD)) { !quietInput &&
this.workspace_.options.modalInputs &&
(userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD)
) {
this.showPromptEditor_(); this.showPromptEditor_();
} else { } else {
this.showInlineEditor_(quietInput); this.showInlineEditor_(quietInput);
@@ -282,12 +304,15 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
*/ */
private showPromptEditor_() { private showPromptEditor_() {
dialog.prompt( dialog.prompt(
Msg['CHANGE_VALUE_TITLE'], this.getText(), (text: string|null) => { Msg['CHANGE_VALUE_TITLE'],
// Text is null if user pressed cancel button. this.getText(),
if (text !== null) { (text: string | null) => {
this.setValue(this.getValueFromEditorText_(text)); // Text is null if user pressed cancel button.
} if (text !== null) {
}); this.setValue(this.getValueFromEditorText_(text));
}
}
);
} }
/** /**
@@ -329,12 +354,14 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
if (!clickTarget) throw new Error('A click target has not been set.'); if (!clickTarget) throw new Error('A click target has not been set.');
dom.addClass(clickTarget, 'editing'); dom.addClass(clickTarget, 'editing');
const htmlInput = (document.createElement('input')); const htmlInput = document.createElement('input');
htmlInput.className = 'blocklyHtmlInput'; htmlInput.className = 'blocklyHtmlInput';
// AnyDuringMigration because: Argument of type 'boolean' is not assignable // AnyDuringMigration because: Argument of type 'boolean' is not assignable
// to parameter of type 'string'. // to parameter of type 'string'.
htmlInput.setAttribute( htmlInput.setAttribute(
'spellcheck', this.spellcheck_ as AnyDuringMigration); 'spellcheck',
this.spellcheck_ as AnyDuringMigration
);
const scale = this.workspace_!.getScale(); const scale = this.workspace_!.getScale();
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt'; const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
div!.style.fontSize = fontSize; div!.style.fontSize = fontSize;
@@ -347,15 +374,15 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
// Override border radius. // Override border radius.
borderRadius = (bBox.bottom - bBox.top) / 2 + 'px'; borderRadius = (bBox.bottom - bBox.top) / 2 + 'px';
// Pull stroke colour from the existing shadow block // Pull stroke colour from the existing shadow block
const strokeColour = block.getParent() ? const strokeColour = block.getParent()
(block.getParent() as BlockSvg).style.colourTertiary : ? (block.getParent() as BlockSvg).style.colourTertiary
(this.sourceBlock_ as BlockSvg).style.colourTertiary; : (this.sourceBlock_ as BlockSvg).style.colourTertiary;
htmlInput.style.border = 1 * scale + 'px solid ' + strokeColour; htmlInput.style.border = 1 * scale + 'px solid ' + strokeColour;
div!.style.borderRadius = borderRadius; div!.style.borderRadius = borderRadius;
div!.style.transition = 'box-shadow 0.25s ease 0s'; div!.style.transition = 'box-shadow 0.25s ease 0s';
if (this.getConstants()!.FIELD_TEXTINPUT_BOX_SHADOW) { if (this.getConstants()!.FIELD_TEXTINPUT_BOX_SHADOW) {
div!.style.boxShadow = 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; htmlInput.style.borderRadius = borderRadius;
@@ -417,10 +444,18 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
protected bindInputEvents_(htmlInput: HTMLElement) { protected bindInputEvents_(htmlInput: HTMLElement) {
// Trap Enter without IME and Esc to hide. // Trap Enter without IME and Esc to hide.
this.onKeyDownWrapper_ = browserEvents.conditionalBind( this.onKeyDownWrapper_ = browserEvents.conditionalBind(
htmlInput, 'keydown', this, this.onHtmlInputKeyDown_); htmlInput,
'keydown',
this,
this.onHtmlInputKeyDown_
);
// Resize after every input change. // Resize after every input change.
this.onKeyInputWrapper_ = browserEvents.conditionalBind( this.onKeyInputWrapper_ = browserEvents.conditionalBind(
htmlInput, 'input', this, this.onHtmlInputChange_); htmlInput,
'input',
this,
this.onHtmlInputChange_
);
} }
/** Unbind handlers for user input and workspace size changes. */ /** Unbind handlers for user input and workspace size changes. */
@@ -446,7 +481,8 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
dropDownDiv.hideWithoutAnimation(); dropDownDiv.hideWithoutAnimation();
} else if (e.key === 'Escape') { } else if (e.key === 'Escape') {
this.setValue( this.setValue(
this.htmlInput_!.getAttribute('data-untyped-default-value')); this.htmlInput_!.getAttribute('data-untyped-default-value')
);
WidgetDiv.hide(); WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation(); dropDownDiv.hideWithoutAnimation();
} else if (e.key === 'Tab') { } else if (e.key === 'Tab') {
@@ -525,8 +561,10 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
if (!(block instanceof BlockSvg)) return false; if (!(block instanceof BlockSvg)) return false;
bumpObjects.bumpIntoBounds( bumpObjects.bumpIntoBounds(
this.workspace_!, this.workspace_!,
this.workspace_!.getMetricsManager().getViewMetrics(true), block); this.workspace_!.getMetricsManager().getViewMetrics(true),
block
);
this.resizeEditor_(); this.resizeEditor_();
@@ -550,7 +588,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
* *
* @returns The HTML value if we're editing, otherwise null. * @returns The HTML value if we're editing, otherwise null.
*/ */
protected override getText_(): string|null { protected override getText_(): string | null {
if (this.isBeingEdited_ && this.htmlInput_) { if (this.isBeingEdited_ && this.htmlInput_) {
// We are currently editing, return the HTML input value instead. // We are currently editing, return the HTML input value instead.
return this.htmlInput_.value; return this.htmlInput_.value;
@@ -611,5 +649,6 @@ export interface FieldInputConfig extends FieldConfig {
* - `undefined` to set `newValue` as is. * - `undefined` to set `newValue` as is.
* @internal * @internal
*/ */
export type FieldInputValidator<T extends InputTypes> = export type FieldInputValidator<T extends InputTypes> = FieldValidator<
FieldValidator<string|T>; string | T
>;

View File

@@ -23,7 +23,7 @@ import * as parsing from './utils/parsing.js';
*/ */
export class FieldLabel extends Field<string> { 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; private class: string | null = null;
/** /**
* Editable fields usually show some sort of UI indicating they are * Editable fields usually show some sort of UI indicating they are
@@ -47,8 +47,10 @@ export class FieldLabel extends Field<string> {
* for a list of properties this parameter supports. * for a list of properties this parameter supports.
*/ */
constructor( constructor(
value?: string|typeof Field.SKIP_SETUP, textClass?: string, value?: string | typeof Field.SKIP_SETUP,
config?: FieldLabelConfig) { textClass?: string,
config?: FieldLabelConfig
) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
if (value === Field.SKIP_SETUP) return; if (value === Field.SKIP_SETUP) return;
@@ -83,8 +85,9 @@ export class FieldLabel extends Field<string> {
* @param newValue The input value. * @param newValue The input value.
* @returns A valid string, or null if invalid. * @returns A valid string, or null if invalid.
*/ */
protected override doClassValidation_(newValue?: AnyDuringMigration): string protected override doClassValidation_(
|null { newValue?: AnyDuringMigration
): string | null {
if (newValue === null || newValue === undefined) { if (newValue === null || newValue === undefined) {
return null; return null;
} }
@@ -96,7 +99,7 @@ export class FieldLabel extends Field<string> {
* *
* @param cssClass The new CSS class name, or null to remove. * @param cssClass The new CSS class name, or null to remove.
*/ */
setClass(cssClass: string|null) { setClass(cssClass: string | null) {
if (this.textElement_) { if (this.textElement_) {
if (this.class) { if (this.class) {
dom.removeClass(this.textElement_, this.class); dom.removeClass(this.textElement_, this.class);
@@ -139,7 +142,6 @@ export interface FieldLabelConfig extends FieldConfig {
} }
// clang-format on // clang-format on
/** /**
* fromJson config options for the label field. * fromJson config options for the label field.
*/ */

View File

@@ -14,7 +14,11 @@
import * as goog from '../closure/goog/goog.js'; import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldLabelSerializable'); goog.declareModuleId('Blockly.FieldLabelSerializable');
import {FieldLabel, FieldLabelConfig, FieldLabelFromJsonConfig} from './field_label.js'; import {
FieldLabel,
FieldLabelConfig,
FieldLabelFromJsonConfig,
} from './field_label.js';
import * as fieldRegistry from './field_registry.js'; import * as fieldRegistry from './field_registry.js';
import * as parsing from './utils/parsing.js'; import * as parsing from './utils/parsing.js';
@@ -57,8 +61,9 @@ export class FieldLabelSerializable extends FieldLabel {
* @nocollapse * @nocollapse
* @internal * @internal
*/ */
static override fromJson(options: FieldLabelFromJsonConfig): static override fromJson(
FieldLabelSerializable { options: FieldLabelFromJsonConfig
): FieldLabelSerializable {
const text = parsing.replaceMessageReferences(options.text); const text = parsing.replaceMessageReferences(options.text);
// `this` might be a subclass of FieldLabelSerializable if that class // `this` might be a subclass of FieldLabelSerializable if that class
// doesn't override the static fromJson method. // doesn't override the static fromJson method.

View File

@@ -15,7 +15,11 @@ goog.declareModuleId('Blockly.FieldMultilineInput');
import * as Css from './css.js'; import * as Css from './css.js';
import {Field, UnattachedFieldError} from './field.js'; import {Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js'; import * as fieldRegistry from './field_registry.js';
import {FieldTextInput, FieldTextInputConfig, FieldTextInputValidator} from './field_textinput.js'; import {
FieldTextInput,
FieldTextInputConfig,
FieldTextInputValidator,
} from './field_textinput.js';
import * as aria from './utils/aria.js'; import * as aria from './utils/aria.js';
import * as dom from './utils/dom.js'; import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js'; import * as parsing from './utils/parsing.js';
@@ -31,7 +35,7 @@ export class FieldMultilineInput extends FieldTextInput {
* The SVG group element that will contain a text element for each text row * The SVG group element that will contain a text element for each text row
* when initialized. * when initialized.
*/ */
textGroup: SVGGElement|null = null; textGroup: SVGGElement | null = null;
/** /**
* Defines the maximum number of lines of field. * Defines the maximum number of lines of field.
@@ -58,9 +62,10 @@ export class FieldMultilineInput extends FieldTextInput {
* for a list of properties this parameter supports. * for a list of properties this parameter supports.
*/ */
constructor( constructor(
value?: string|typeof Field.SKIP_SETUP, value?: string | typeof Field.SKIP_SETUP,
validator?: FieldMultilineInputValidator, validator?: FieldMultilineInputValidator,
config?: FieldMultilineInputConfig) { config?: FieldMultilineInputConfig
) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
if (value === Field.SKIP_SETUP) return; if (value === Field.SKIP_SETUP) return;
@@ -96,8 +101,10 @@ export class FieldMultilineInput extends FieldTextInput {
// needed so the plain-text representation of the XML produced by // needed so the plain-text representation of the XML produced by
// `Blockly.Xml.domToText` will appear on a single line (this is a // `Blockly.Xml.domToText` will appear on a single line (this is a
// limitation of the plain-text format). // limitation of the plain-text format).
fieldElement.textContent = fieldElement.textContent = (this.getValue() as string).replace(
(this.getValue() as string).replace(/\n/g, '&#10;'); /\n/g,
'&#10;'
);
return fieldElement; return fieldElement;
} }
@@ -151,10 +158,12 @@ export class FieldMultilineInput extends FieldTextInput {
override initView() { override initView() {
this.createBorderRect_(); this.createBorderRect_();
this.textGroup = dom.createSvgElement( this.textGroup = dom.createSvgElement(
Svg.G, { Svg.G,
'class': 'blocklyEditableText', {
}, 'class': 'blocklyEditableText',
this.fieldGroup_); },
this.fieldGroup_
);
} }
/** /**
@@ -175,8 +184,9 @@ export class FieldMultilineInput extends FieldTextInput {
} }
const lines = textLines.split('\n'); const lines = textLines.split('\n');
textLines = ''; textLines = '';
const displayLinesNumber = const displayLinesNumber = this.isOverflowedY_
this.isOverflowedY_ ? this.maxLines_ : lines.length; ? this.maxLines_
: lines.length;
for (let i = 0; i < displayLinesNumber; i++) { for (let i = 0; i < displayLinesNumber; i++) {
let text = lines[i]; let text = lines[i];
if (text.length > this.maxDisplayLength) { if (text.length > this.maxDisplayLength) {
@@ -226,7 +236,7 @@ export class FieldMultilineInput extends FieldTextInput {
// Remove all text group children. // Remove all text group children.
let currentChild; let currentChild;
const textGroup = this.textGroup; const textGroup = this.textGroup;
while (currentChild = textGroup!.firstChild) { while ((currentChild = textGroup!.firstChild)) {
textGroup!.removeChild(currentChild); textGroup!.removeChild(currentChild);
} }
@@ -234,16 +244,19 @@ export class FieldMultilineInput extends FieldTextInput {
const lines = this.getDisplayText_().split('\n'); const lines = this.getDisplayText_().split('\n');
let y = 0; let y = 0;
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const lineHeight = this.getConstants()!.FIELD_TEXT_HEIGHT + const lineHeight =
this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING; this.getConstants()!.FIELD_TEXT_HEIGHT +
this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING;
const span = dom.createSvgElement( const span = dom.createSvgElement(
Svg.TEXT, { Svg.TEXT,
'class': 'blocklyText blocklyMultilineText', {
'x': this.getConstants()!.FIELD_BORDER_RECT_X_PADDING, 'class': 'blocklyText blocklyMultilineText',
'y': y + this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING, 'x': this.getConstants()!.FIELD_BORDER_RECT_X_PADDING,
'dy': this.getConstants()!.FIELD_TEXT_BASELINE, 'y': y + this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING,
}, 'dy': this.getConstants()!.FIELD_TEXT_BASELINE,
textGroup); },
textGroup
);
span.appendChild(document.createTextNode(lines[i])); span.appendChild(document.createTextNode(lines[i]));
y += lineHeight; y += lineHeight;
} }
@@ -289,13 +302,18 @@ export class FieldMultilineInput extends FieldTextInput {
let totalHeight = 0; let totalHeight = 0;
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const tspan = nodes[i] as SVGTextElement; const tspan = nodes[i] as SVGTextElement;
const textWidth = const textWidth = dom.getFastTextWidth(
dom.getFastTextWidth(tspan, fontSize, fontWeight, fontFamily); tspan,
fontSize,
fontWeight,
fontFamily
);
if (textWidth > totalWidth) { if (textWidth > totalWidth) {
totalWidth = textWidth; totalWidth = textWidth;
} }
totalHeight += this.getConstants()!.FIELD_TEXT_HEIGHT + totalHeight +=
(i > 0 ? this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING : 0); this.getConstants()!.FIELD_TEXT_HEIGHT +
(i > 0 ? this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING : 0);
} }
if (this.isBeingEdited_) { if (this.isBeingEdited_) {
// The default width is based on the longest line in the display text, // The default width is based on the longest line in the display text,
@@ -304,24 +322,31 @@ export class FieldMultilineInput extends FieldTextInput {
// Otherwise we would get wrong editor width when there are more // Otherwise we would get wrong editor width when there are more
// lines than this.maxLines_. // lines than this.maxLines_.
const actualEditorLines = String(this.value_).split('\n'); const actualEditorLines = String(this.value_).split('\n');
const dummyTextElement = dom.createSvgElement( const dummyTextElement = dom.createSvgElement(Svg.TEXT, {
Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'}); 'class': 'blocklyText blocklyMultilineText',
});
for (let i = 0; i < actualEditorLines.length; i++) { for (let i = 0; i < actualEditorLines.length; i++) {
if (actualEditorLines[i].length > this.maxDisplayLength) { if (actualEditorLines[i].length > this.maxDisplayLength) {
actualEditorLines[i] = actualEditorLines[i] = actualEditorLines[i].substring(
actualEditorLines[i].substring(0, this.maxDisplayLength); 0,
this.maxDisplayLength
);
} }
dummyTextElement.textContent = actualEditorLines[i]; dummyTextElement.textContent = actualEditorLines[i];
const lineWidth = dom.getFastTextWidth( const lineWidth = dom.getFastTextWidth(
dummyTextElement, fontSize, fontWeight, fontFamily); dummyTextElement,
fontSize,
fontWeight,
fontFamily
);
if (lineWidth > totalWidth) { if (lineWidth > totalWidth) {
totalWidth = lineWidth; totalWidth = lineWidth;
} }
} }
const scrollbarWidth = const scrollbarWidth =
this.htmlInput_!.offsetWidth - this.htmlInput_!.clientWidth; this.htmlInput_!.offsetWidth - this.htmlInput_!.clientWidth;
totalWidth += scrollbarWidth; totalWidth += scrollbarWidth;
} }
if (this.borderRect_) { if (this.borderRect_) {
@@ -360,7 +385,7 @@ export class FieldMultilineInput extends FieldTextInput {
const div = WidgetDiv.getDiv(); const div = WidgetDiv.getDiv();
const scale = this.workspace_!.getScale(); const scale = this.workspace_!.getScale();
const htmlInput = (document.createElement('textarea')); const htmlInput = document.createElement('textarea');
htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput'; htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
htmlInput.setAttribute('spellcheck', String(this.spellcheck_)); htmlInput.setAttribute('spellcheck', String(this.spellcheck_));
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt'; const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
@@ -370,11 +395,12 @@ export class FieldMultilineInput extends FieldTextInput {
htmlInput.style.borderRadius = borderRadius; htmlInput.style.borderRadius = borderRadius;
const paddingX = this.getConstants()!.FIELD_BORDER_RECT_X_PADDING * scale; const paddingX = this.getConstants()!.FIELD_BORDER_RECT_X_PADDING * scale;
const paddingY = const paddingY =
this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING * scale / 2; (this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING * scale) / 2;
htmlInput.style.padding = paddingY + 'px ' + paddingX + 'px ' + paddingY + htmlInput.style.padding =
'px ' + paddingX + 'px'; paddingY + 'px ' + paddingX + 'px ' + paddingY + 'px ' + paddingX + 'px';
const lineHeight = this.getConstants()!.FIELD_TEXT_HEIGHT + const lineHeight =
this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING; this.getConstants()!.FIELD_TEXT_HEIGHT +
this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING;
htmlInput.style.lineHeight = lineHeight * scale + 'px'; htmlInput.style.lineHeight = lineHeight * scale + 'px';
div!.appendChild(htmlInput); div!.appendChild(htmlInput);
@@ -401,8 +427,11 @@ export class FieldMultilineInput extends FieldTextInput {
* scrolling functionality is enabled. * scrolling functionality is enabled.
*/ */
setMaxLines(maxLines: number) { setMaxLines(maxLines: number) {
if (typeof maxLines === 'number' && maxLines > 0 && if (
maxLines !== this.maxLines_) { typeof maxLines === 'number' &&
maxLines > 0 &&
maxLines !== this.maxLines_
) {
this.maxLines_ = maxLines; this.maxLines_ = maxLines;
this.forceRerender(); this.forceRerender();
} }
@@ -438,8 +467,9 @@ export class FieldMultilineInput extends FieldTextInput {
* @nocollapse * @nocollapse
* @internal * @internal
*/ */
static override fromJson(options: FieldMultilineInputFromJsonConfig): static override fromJson(
FieldMultilineInput { options: FieldMultilineInputFromJsonConfig
): FieldMultilineInput {
const text = parsing.replaceMessageReferences(options.text); const text = parsing.replaceMessageReferences(options.text);
// `this` might be a subclass of FieldMultilineInput if that class doesn't // `this` might be a subclass of FieldMultilineInput if that class doesn't
// override the static fromJson method. // override the static fromJson method.
@@ -449,7 +479,6 @@ export class FieldMultilineInput extends FieldTextInput {
fieldRegistry.register('field_multilinetext', FieldMultilineInput); fieldRegistry.register('field_multilinetext', FieldMultilineInput);
/** /**
* CSS for multiline field. * CSS for multiline field.
*/ */
@@ -477,8 +506,8 @@ export interface FieldMultilineInputConfig extends FieldTextInputConfig {
/** /**
* fromJson config options for the multiline input field. * fromJson config options for the multiline input field.
*/ */
export interface FieldMultilineInputFromJsonConfig extends export interface FieldMultilineInputFromJsonConfig
FieldMultilineInputConfig { extends FieldMultilineInputConfig {
text?: string; text?: string;
} }

View File

@@ -14,7 +14,11 @@ goog.declareModuleId('Blockly.FieldNumber');
import {Field} from './field.js'; import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js'; import * as fieldRegistry from './field_registry.js';
import {FieldInput, FieldInputConfig, FieldInputValidator} from './field_input.js'; import {
FieldInput,
FieldInputConfig,
FieldInputValidator,
} from './field_input.js';
import * as aria from './utils/aria.js'; import * as aria from './utils/aria.js';
/** /**
@@ -34,7 +38,7 @@ export class FieldNumber extends FieldInput<number> {
* The number of decimal places to allow, or null to allow any number of * The number of decimal places to allow, or null to allow any number of
* decimal digits. * decimal digits.
*/ */
private decimalPlaces: number|null = null; private decimalPlaces: number | null = null;
/** Don't spellcheck numbers. Our validator does a better job. */ /** Don't spellcheck numbers. Our validator does a better job. */
protected override spellcheck_ = false; protected override spellcheck_ = false;
@@ -59,9 +63,13 @@ export class FieldNumber extends FieldInput<number> {
* for a list of properties this parameter supports. * for a list of properties this parameter supports.
*/ */
constructor( constructor(
value?: string|number|typeof Field.SKIP_SETUP, min?: string|number|null, value?: string | number | typeof Field.SKIP_SETUP,
max?: string|number|null, precision?: string|number|null, min?: string | number | null,
validator?: FieldNumberValidator|null, config?: FieldNumberConfig) { max?: string | number | null,
precision?: string | number | null,
validator?: FieldNumberValidator | null,
config?: FieldNumberConfig
) {
// Pass SENTINEL so that we can define properties before value validation. // Pass SENTINEL so that we can define properties before value validation.
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
@@ -103,8 +111,10 @@ export class FieldNumber extends FieldInput<number> {
* @param precision Precision for value. * @param precision Precision for value.
*/ */
setConstraints( setConstraints(
min: number|string|undefined|null, max: number|string|undefined|null, min: number | string | undefined | null,
precision: number|string|undefined|null) { max: number | string | undefined | null,
precision: number | string | undefined | null
) {
this.setMinInternal(min); this.setMinInternal(min);
this.setMaxInternal(max); this.setMaxInternal(max);
this.setPrecisionInternal(precision); this.setPrecisionInternal(precision);
@@ -117,7 +127,7 @@ export class FieldNumber extends FieldInput<number> {
* *
* @param min Minimum value. * @param min Minimum value.
*/ */
setMin(min: number|string|undefined|null) { setMin(min: number | string | undefined | null) {
this.setMinInternal(min); this.setMinInternal(min);
this.setValue(this.getValue()); this.setValue(this.getValue());
} }
@@ -128,7 +138,7 @@ export class FieldNumber extends FieldInput<number> {
* *
* @param min Minimum value. * @param min Minimum value.
*/ */
private setMinInternal(min: number|string|undefined|null) { private setMinInternal(min: number | string | undefined | null) {
if (min == null) { if (min == null) {
this.min_ = -Infinity; this.min_ = -Infinity;
} else { } else {
@@ -155,7 +165,7 @@ export class FieldNumber extends FieldInput<number> {
* *
* @param max Maximum value. * @param max Maximum value.
*/ */
setMax(max: number|string|undefined|null) { setMax(max: number | string | undefined | null) {
this.setMaxInternal(max); this.setMaxInternal(max);
this.setValue(this.getValue()); this.setValue(this.getValue());
} }
@@ -166,7 +176,7 @@ export class FieldNumber extends FieldInput<number> {
* *
* @param max Maximum value. * @param max Maximum value.
*/ */
private setMaxInternal(max: number|string|undefined|null) { private setMaxInternal(max: number | string | undefined | null) {
if (max == null) { if (max == null) {
this.max_ = Infinity; this.max_ = Infinity;
} else { } else {
@@ -193,7 +203,7 @@ export class FieldNumber extends FieldInput<number> {
* *
* @param precision The number to which the field's value is rounded. * @param precision The number to which the field's value is rounded.
*/ */
setPrecision(precision: number|string|undefined|null) { setPrecision(precision: number | string | undefined | null) {
this.setPrecisionInternal(precision); this.setPrecisionInternal(precision);
this.setValue(this.getValue()); this.setValue(this.getValue());
} }
@@ -204,14 +214,15 @@ export class FieldNumber extends FieldInput<number> {
* *
* @param precision The number to which the field's value is rounded. * @param precision The number to which the field's value is rounded.
*/ */
private setPrecisionInternal(precision: number|string|undefined|null) { private setPrecisionInternal(precision: number | string | undefined | null) {
this.precision_ = Number(precision) || 0; this.precision_ = Number(precision) || 0;
let precisionString = String(this.precision_); let precisionString = String(this.precision_);
if (precisionString.indexOf('e') !== -1) { if (precisionString.indexOf('e') !== -1) {
// String() is fast. But it turns .0000001 into '1e-7'. // String() is fast. But it turns .0000001 into '1e-7'.
// Use the much slower toLocaleString to access all the digits. // Use the much slower toLocaleString to access all the digits.
precisionString = precisionString = this.precision_.toLocaleString('en-US', {
this.precision_.toLocaleString('en-US', {maximumFractionDigits: 20}); maximumFractionDigits: 20,
});
} }
const decimalIndex = precisionString.indexOf('.'); const decimalIndex = precisionString.indexOf('.');
if (decimalIndex === -1) { if (decimalIndex === -1) {
@@ -241,8 +252,9 @@ export class FieldNumber extends FieldInput<number> {
* @param newValue The input value. * @param newValue The input value.
* @returns A valid number, or null if invalid. * @returns A valid number, or null if invalid.
*/ */
protected override doClassValidation_(newValue?: AnyDuringMigration): number protected override doClassValidation_(
|null { newValue?: AnyDuringMigration
): number | null {
if (newValue === null) { if (newValue === null) {
return null; return null;
} }
@@ -251,7 +263,7 @@ export class FieldNumber extends FieldInput<number> {
newValue = `${newValue}`; newValue = `${newValue}`;
// TODO: Handle cases like 'ten', '1.203,14', etc. // TODO: Handle cases like 'ten', '1.203,14', etc.
// 'O' is sometimes mistaken for '0' by inexperienced users. // 'O' is sometimes mistaken for '0' by inexperienced users.
newValue = newValue.replace(/O/ig, '0'); newValue = newValue.replace(/O/gi, '0');
// Strip out thousands separators. // Strip out thousands separators.
newValue = newValue.replace(/,/g, ''); newValue = newValue.replace(/,/g, '');
// Ignore case of 'Infinity'. // Ignore case of 'Infinity'.
@@ -309,7 +321,13 @@ export class FieldNumber extends FieldInput<number> {
// `this` might be a subclass of FieldNumber if that class doesn't override // `this` might be a subclass of FieldNumber if that class doesn't override
// the static fromJson method. // the static fromJson method.
return new this( return new this(
options.value, undefined, undefined, undefined, undefined, options); options.value,
undefined,
undefined,
undefined,
undefined,
options
);
} }
} }

View File

@@ -50,7 +50,7 @@ export function unregister(type: string) {
* given type name * given type name
* @internal * @internal
*/ */
export function fromJson<T>(options: RegistryOptions): Field<T>|null { export function fromJson<T>(options: RegistryOptions): Field<T> | null {
return TEST_ONLY.fromJsonInternal(options); return TEST_ONLY.fromJsonInternal(options);
} }
@@ -59,14 +59,16 @@ export function fromJson<T>(options: RegistryOptions): Field<T>|null {
* *
* @param options * @param options
*/ */
function fromJsonInternal<T>(options: RegistryOptions): Field<T>|null { function fromJsonInternal<T>(options: RegistryOptions): Field<T> | null {
const fieldObject = registry.getObject(registry.Type.FIELD, options.type); const fieldObject = registry.getObject(registry.Type.FIELD, options.type);
if (!fieldObject) { if (!fieldObject) {
console.warn( console.warn(
'Blockly could not create a field of type ' + options['type'] + 'Blockly could not create a field of type ' +
options['type'] +
'. The field is probably not being registered. This could be because' + '. The field is probably not being registered. This could be because' +
' the file is not loaded, the field does not register itself (Issue' + ' the file is not loaded, the field does not register itself (Issue' +
' #1584), or the registration is not being reached.'); ' #1584), or the registration is not being reached.'
);
return null; return null;
} else if (typeof (fieldObject as any).fromJson !== 'function') { } else if (typeof (fieldObject as any).fromJson !== 'function') {
throw new TypeError('returned Field was not a IRegistrableField'); throw new TypeError('returned Field was not a IRegistrableField');

View File

@@ -16,7 +16,11 @@ goog.declareModuleId('Blockly.FieldTextInput');
import './events/events_block_change.js'; import './events/events_block_change.js';
import {Field} from './field.js'; import {Field} from './field.js';
import {FieldInput, FieldInputConfig, FieldInputValidator} from './field_input.js'; import {
FieldInput,
FieldInputConfig,
FieldInputValidator,
} from './field_input.js';
import * as fieldRegistry from './field_registry.js'; import * as fieldRegistry from './field_registry.js';
import * as parsing from './utils/parsing.js'; import * as parsing from './utils/parsing.js';
@@ -39,8 +43,10 @@ export class FieldTextInput extends FieldInput<string> {
* for a list of properties this parameter supports. * for a list of properties this parameter supports.
*/ */
constructor( constructor(
value?: string|typeof Field.SKIP_SETUP, value?: string | typeof Field.SKIP_SETUP,
validator?: FieldTextInputValidator|null, config?: FieldTextInputConfig) { validator?: FieldTextInputValidator | null,
config?: FieldTextInputConfig
) {
super(value, validator, config); super(value, validator, config);
} }
@@ -50,8 +56,9 @@ export class FieldTextInput extends FieldInput<string> {
* @param newValue The input value. * @param newValue The input value.
* @returns A valid string, or null if invalid. * @returns A valid string, or null if invalid.
*/ */
protected override doClassValidation_(newValue?: AnyDuringMigration): string protected override doClassValidation_(
|null { newValue?: AnyDuringMigration
): string | null {
if (newValue === undefined) { if (newValue === undefined) {
return null; return null;
} }

View File

@@ -17,7 +17,12 @@ import './events/events_block_change.js';
import type {Block} from './block.js'; import type {Block} from './block.js';
import {Field, FieldConfig, UnattachedFieldError} from './field.js'; import {Field, FieldConfig, UnattachedFieldError} from './field.js';
import {FieldDropdown, FieldDropdownValidator, MenuGenerator, MenuOption} from './field_dropdown.js'; import {
FieldDropdown,
FieldDropdownValidator,
MenuGenerator,
MenuOption,
} from './field_dropdown.js';
import * as fieldRegistry from './field_registry.js'; import * as fieldRegistry from './field_registry.js';
import * as internalConstants from './internal_constants.js'; import * as internalConstants from './internal_constants.js';
import type {Menu} from './menu.js'; import type {Menu} from './menu.js';
@@ -33,7 +38,7 @@ import * as Xml from './xml.js';
* Class for a variable's dropdown field. * Class for a variable's dropdown field.
*/ */
export class FieldVariable extends FieldDropdown { export class FieldVariable extends FieldDropdown {
protected override menuGenerator_: MenuGenerator|undefined; protected override menuGenerator_: MenuGenerator | undefined;
defaultVariableName: string; defaultVariableName: string;
/** The type of the default variable for this field. */ /** The type of the default variable for this field. */
@@ -43,11 +48,11 @@ export class FieldVariable extends FieldDropdown {
* All of the types of variables that will be available in this field's * All of the types of variables that will be available in this field's
* dropdown. * dropdown.
*/ */
variableTypes: string[]|null = []; variableTypes: string[] | null = [];
protected override size_: Size; protected override size_: Size;
/** The variable model associated with this field. */ /** The variable model associated with this field. */
private variable: VariableModel|null = null; private variable: VariableModel | null = null;
/** /**
* Serializable fields are saved by the serializer, non-serializable fields * Serializable fields are saved by the serializer, non-serializable fields
@@ -75,9 +80,12 @@ export class FieldVariable extends FieldDropdown {
* for a list of properties this parameter supports. * for a list of properties this parameter supports.
*/ */
constructor( constructor(
varName: string|null|typeof Field.SKIP_SETUP, varName: string | null | typeof Field.SKIP_SETUP,
validator?: FieldVariableValidator, variableTypes?: string[], validator?: FieldVariableValidator,
defaultType?: string, config?: FieldVariableConfig) { variableTypes?: string[],
defaultType?: string,
config?: FieldVariableConfig
) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
/** /**
@@ -131,10 +139,14 @@ export class FieldVariable extends FieldDropdown {
throw new UnattachedFieldError(); throw new UnattachedFieldError();
} }
if (this.variable) { if (this.variable) {
return; // Initialization already happened. return; // Initialization already happened.
} }
const variable = Variables.getOrCreateVariablePackage( const variable = Variables.getOrCreateVariablePackage(
block.workspace, null, this.defaultVariableName, this.defaultType); block.workspace,
null,
this.defaultVariableName,
this.defaultType
);
// Don't call setValue because we don't want to cause a rerender. // Don't call setValue because we don't want to cause a rerender.
this.doValueUpdate_(variable.getId()); this.doValueUpdate_(variable.getId());
} }
@@ -144,9 +156,11 @@ export class FieldVariable extends FieldDropdown {
if (!block) { if (!block) {
throw new UnattachedFieldError(); throw new UnattachedFieldError();
} }
return super.shouldAddBorderRect_() && return (
(!this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW || super.shouldAddBorderRect_() &&
block.type !== 'variables_get'); (!this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
block.type !== 'variables_get')
);
} }
/** /**
@@ -164,21 +178,32 @@ export class FieldVariable extends FieldDropdown {
const variableName = fieldElement.textContent; const variableName = fieldElement.textContent;
// 'variabletype' should be lowercase, but until July 2019 it was sometimes // 'variabletype' should be lowercase, but until July 2019 it was sometimes
// recorded as 'variableType'. Thus we need to check for both. // recorded as 'variableType'. Thus we need to check for both.
const variableType = fieldElement.getAttribute('variabletype') || const variableType =
fieldElement.getAttribute('variableType') || ''; fieldElement.getAttribute('variabletype') ||
fieldElement.getAttribute('variableType') ||
'';
// AnyDuringMigration because: Argument of type 'string | null' is not // AnyDuringMigration because: Argument of type 'string | null' is not
// assignable to parameter of type 'string | undefined'. // assignable to parameter of type 'string | undefined'.
const variable = Variables.getOrCreateVariablePackage( const variable = Variables.getOrCreateVariablePackage(
block.workspace, id, variableName as AnyDuringMigration, variableType); block.workspace,
id,
variableName as AnyDuringMigration,
variableType
);
// This should never happen :) // This should never happen :)
if (variableType !== null && variableType !== variable.type) { if (variableType !== null && variableType !== variable.type) {
throw Error( throw Error(
'Serialized variable type with id \'' + variable.getId() + "Serialized variable type with id '" +
'\' had type ' + variable.type + ', and ' + variable.getId() +
"' had type " +
variable.type +
', and ' +
'does not match variable field that references it: ' + 'does not match variable field that references it: ' +
Xml.domToText(fieldElement) + '.'); Xml.domToText(fieldElement) +
'.'
);
} }
this.setValue(variable.getId()); this.setValue(variable.getId());
@@ -243,8 +268,11 @@ export class FieldVariable extends FieldDropdown {
} }
// This is necessary so that blocks in the flyout can have custom var names. // This is necessary so that blocks in the flyout can have custom var names.
const variable = Variables.getOrCreateVariablePackage( const variable = Variables.getOrCreateVariablePackage(
block.workspace, state['id'] || null, state['name'], block.workspace,
state['type'] || ''); state['id'] || null,
state['name'],
state['type'] || ''
);
this.setValue(variable.getId()); this.setValue(variable.getId());
} }
@@ -265,7 +293,7 @@ export class FieldVariable extends FieldDropdown {
* *
* @returns Current variable's ID. * @returns Current variable's ID.
*/ */
override getValue(): string|null { override getValue(): string | null {
return this.variable ? this.variable.getId() : null; return this.variable ? this.variable.getId() : null;
} }
@@ -287,7 +315,7 @@ export class FieldVariable extends FieldDropdown {
* @returns The selected variable, or null if none was selected. * @returns The selected variable, or null if none was selected.
* @internal * @internal
*/ */
getVariable(): VariableModel|null { getVariable(): VariableModel | null {
return this.variable; return this.variable;
} }
@@ -299,7 +327,7 @@ export class FieldVariable extends FieldDropdown {
* *
* @returns Validation function, or null. * @returns Validation function, or null.
*/ */
override getValidator(): FieldVariableValidator|null { override getValidator(): FieldVariableValidator | null {
// Validators shouldn't operate on the initial setValue call. // Validators shouldn't operate on the initial setValue call.
// Normally this is achieved by calling setValidator after setValue, but // Normally this is achieved by calling setValidator after setValue, but
// this is not a possibility with variable fields. // this is not a possibility with variable fields.
@@ -315,8 +343,9 @@ export class FieldVariable extends FieldDropdown {
* @param newValue The ID of the new variable to set. * @param newValue The ID of the new variable to set.
* @returns The validated ID, or null if invalid. * @returns The validated ID, or null if invalid.
*/ */
protected override doClassValidation_(newValue?: AnyDuringMigration): string protected override doClassValidation_(
|null { newValue?: AnyDuringMigration
): string | null {
if (newValue === null) { if (newValue === null) {
return null; return null;
} }
@@ -328,15 +357,14 @@ export class FieldVariable extends FieldDropdown {
const variable = Variables.getVariable(block.workspace, newId); const variable = Variables.getVariable(block.workspace, newId);
if (!variable) { if (!variable) {
console.warn( console.warn(
'Variable id doesn\'t point to a real variable! ' + "Variable id doesn't point to a real variable! " + 'ID was ' + newId
'ID was ' + newId); );
return null; return null;
} }
// Type Checks. // Type Checks.
const type = variable.type; const type = variable.type;
if (!this.typeIsAllowed(type)) { if (!this.typeIsAllowed(type)) {
console.warn( console.warn("Variable type doesn't match this field! Type was " + type);
'Variable type doesn\'t match this field! Type was ' + type);
return null; return null;
} }
return newId; return newId;
@@ -368,7 +396,7 @@ export class FieldVariable extends FieldDropdown {
private typeIsAllowed(type: string): boolean { private typeIsAllowed(type: string): boolean {
const typeList = this.getVariableTypes(); const typeList = this.getVariableTypes();
if (!typeList) { if (!typeList) {
return true; // If it's null, all types are valid. return true; // If it's null, all types are valid.
} }
for (let i = 0; i < typeList.length; i++) { for (let i = 0; i < typeList.length; i++) {
if (type === typeList[i]) { if (type === typeList[i]) {
@@ -397,7 +425,8 @@ export class FieldVariable extends FieldDropdown {
// Throw an error if variableTypes is an empty list. // Throw an error if variableTypes is an empty list.
const name = this.getText(); const name = this.getText();
throw Error( throw Error(
'\'variableTypes\' of field variable ' + name + ' was an empty list'); "'variableTypes' of field variable " + name + ' was an empty list'
);
} }
return variableTypes; return variableTypes;
} }
@@ -412,7 +441,7 @@ export class FieldVariable extends FieldDropdown {
* @param defaultType The type of the variable to create if this field's * @param defaultType The type of the variable to create if this field's
* value is not explicitly set. Defaults to ''. * value is not explicitly set. Defaults to ''.
*/ */
private setTypes(variableTypes: string[]|null = null, defaultType = '') { private setTypes(variableTypes: string[] | null = null, defaultType = '') {
// If you expected that the default type would be the same as the only entry // If you expected that the default type would be the same as the only entry
// in the variable types array, tell the Blockly team by commenting on // in the variable types array, tell the Blockly team by commenting on
// #1499. // #1499.
@@ -428,13 +457,17 @@ export class FieldVariable extends FieldDropdown {
} }
if (!isInArray) { if (!isInArray) {
throw Error( throw Error(
'Invalid default type \'' + defaultType + '\' in ' + "Invalid default type '" +
'the definition of a FieldVariable'); defaultType +
"' in " +
'the definition of a FieldVariable'
);
} }
} else if (variableTypes !== null) { } else if (variableTypes !== null) {
throw Error( throw Error(
'\'variableTypes\' was not an array in the definition of ' + "'variableTypes' was not an array in the definition of " +
'a FieldVariable'); 'a FieldVariable'
);
} }
// Only update the field once all checks pass. // Only update the field once all checks pass.
this.defaultType = defaultType; this.defaultType = defaultType;
@@ -467,7 +500,9 @@ export class FieldVariable extends FieldDropdown {
if (id === internalConstants.RENAME_VARIABLE_ID) { if (id === internalConstants.RENAME_VARIABLE_ID) {
// Rename variable. // Rename variable.
Variables.renameVariable( Variables.renameVariable(
this.sourceBlock_.workspace, this.variable as VariableModel); this.sourceBlock_.workspace,
this.variable as VariableModel
);
return; return;
} else if (id === internalConstants.DELETE_VARIABLE_ID) { } else if (id === internalConstants.DELETE_VARIABLE_ID) {
// Delete variable. // Delete variable.
@@ -500,8 +535,9 @@ export class FieldVariable extends FieldDropdown {
* @nocollapse * @nocollapse
* @internal * @internal
*/ */
static override fromJson(options: FieldVariableFromJsonConfig): static override fromJson(
FieldVariable { options: FieldVariableFromJsonConfig
): FieldVariable {
const varName = parsing.replaceMessageReferences(options.variable); const varName = parsing.replaceMessageReferences(options.variable);
// `this` might be a subclass of FieldVariable if that class doesn't // `this` might be a subclass of FieldVariable if that class doesn't
// override the static fromJson method. // override the static fromJson method.
@@ -517,8 +553,9 @@ export class FieldVariable extends FieldDropdown {
static dropdownCreate(this: FieldVariable): MenuOption[] { static dropdownCreate(this: FieldVariable): MenuOption[] {
if (!this.variable) { if (!this.variable) {
throw Error( throw Error(
'Tried to call dropdownCreate on a variable field with no' + 'Tried to call dropdownCreate on a variable field with no' +
' variable selected.'); ' variable selected.'
);
} }
const name = this.getText(); const name = this.getText();
let variableModelList: VariableModel[] = []; let variableModelList: VariableModel[] = [];
@@ -529,7 +566,7 @@ export class FieldVariable extends FieldDropdown {
for (let i = 0; i < variableTypes.length; i++) { for (let i = 0; i < variableTypes.length; i++) {
const variableType = variableTypes[i]; const variableType = variableTypes[i];
const variables = const variables =
this.sourceBlock_.workspace.getVariablesOfType(variableType); this.sourceBlock_.workspace.getVariablesOfType(variableType);
variableModelList = variableModelList.concat(variables); variableModelList = variableModelList.concat(variables);
} }
} }
@@ -540,8 +577,10 @@ export class FieldVariable extends FieldDropdown {
// Set the UUID as the internal representation of the variable. // Set the UUID as the internal representation of the variable.
options[i] = [variableModelList[i].name, variableModelList[i].getId()]; options[i] = [variableModelList[i].name, variableModelList[i].getId()];
} }
options.push( options.push([
[Msg['RENAME_VARIABLE'], internalConstants.RENAME_VARIABLE_ID]); Msg['RENAME_VARIABLE'],
internalConstants.RENAME_VARIABLE_ID,
]);
if (Msg['DELETE_VARIABLE']) { if (Msg['DELETE_VARIABLE']) {
options.push([ options.push([
Msg['DELETE_VARIABLE'].replace('%1', name), Msg['DELETE_VARIABLE'].replace('%1', name),

View File

@@ -36,7 +36,6 @@ import {WorkspaceSvg} from './workspace_svg.js';
import * as utilsXml from './utils/xml.js'; import * as utilsXml from './utils/xml.js';
import * as Xml from './xml.js'; import * as Xml from './xml.js';
enum FlyoutItemType { enum FlyoutItemType {
BLOCK = 'block', BLOCK = 'block',
BUTTON = 'button', BUTTON = 'button',
@@ -69,7 +68,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* between 0 and 1 specifying the degree of scrolling and a * between 0 and 1 specifying the degree of scrolling and a
* similar x property. * similar x property.
*/ */
protected abstract setMetrics_(xyRatio: {x?: number, y?: number}): void; protected abstract setMetrics_(xyRatio: {x?: number; y?: number}): void;
/** /**
* Lay out the blocks in the flyout. * Lay out the blocks in the flyout.
@@ -137,14 +136,14 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* Function that will be registered as a change listener on the workspace * Function that will be registered as a change listener on the workspace
* to reflow when blocks in the flyout workspace change. * to reflow when blocks in the flyout workspace change.
*/ */
private reflowWrapper: Function|null = null; private reflowWrapper: Function | null = null;
/** /**
* Function that disables blocks in the flyout based on max block counts * Function that disables blocks in the flyout based on max block counts
* allowed in the target workspace. Registered as a change listener on the * allowed in the target workspace. Registered as a change listener on the
* target workspace. * target workspace.
*/ */
private filterWrapper: Function|null = null; private filterWrapper: Function | null = null;
/** /**
* List of background mats that lurk behind each block to catch clicks * List of background mats that lurk behind each block to catch clicks
@@ -247,12 +246,12 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* The path around the background of the flyout, which will be filled with a * The path around the background of the flyout, which will be filled with a
* background colour. * background colour.
*/ */
protected svgBackground_: SVGPathElement|null = null; protected svgBackground_: SVGPathElement | null = null;
/** /**
* The root SVG group for the button or label. * The root SVG group for the button or label.
*/ */
protected svgGroup_: SVGGElement|null = null; protected svgGroup_: SVGGElement | null = null;
/** /**
* @param workspaceOptions Dictionary of options for the * @param workspaceOptions Dictionary of options for the
* workspace. * workspace.
@@ -263,7 +262,8 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
this.workspace_ = new WorkspaceSvg(workspaceOptions); this.workspace_ = new WorkspaceSvg(workspaceOptions);
this.workspace_.setMetricsManager( this.workspace_.setMetricsManager(
new FlyoutMetricsManager(this.workspace_, this)); new FlyoutMetricsManager(this.workspace_, this)
);
this.workspace_.internalIsFlyout = true; this.workspace_.internalIsFlyout = true;
// Keep the workspace visibility consistent with the flyout's visibility. // Keep the workspace visibility consistent with the flyout's visibility.
@@ -326,7 +326,9 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* put the flyout in. This should be <svg> or <g>. * put the flyout in. This should be <svg> or <g>.
* @returns The flyout's SVG group. * @returns The flyout's SVG group.
*/ */
createDom(tagName: string|Svg<SVGSVGElement>|Svg<SVGGElement>): SVGElement { createDom(
tagName: string | Svg<SVGSVGElement> | Svg<SVGGElement>
): SVGElement {
/* /*
<svg | g> <svg | g>
<path class="blocklyFlyoutBackground"/> <path class="blocklyFlyoutBackground"/>
@@ -335,15 +337,22 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
*/ */
// Setting style to display:none to start. The toolbox and flyout // Setting style to display:none to start. The toolbox and flyout
// hide/show code will set up proper visibility and size later. // hide/show code will set up proper visibility and size later.
this.svgGroup_ = dom.createSvgElement( this.svgGroup_ = dom.createSvgElement(tagName, {
tagName, {'class': 'blocklyFlyout', 'style': 'display: none'}); 'class': 'blocklyFlyout',
'style': 'display: none',
});
this.svgBackground_ = dom.createSvgElement( this.svgBackground_ = dom.createSvgElement(
Svg.PATH, {'class': 'blocklyFlyoutBackground'}, this.svgGroup_); Svg.PATH,
{'class': 'blocklyFlyoutBackground'},
this.svgGroup_
);
this.svgGroup_.appendChild(this.workspace_.createDom()); this.svgGroup_.appendChild(this.workspace_.createDom());
this.workspace_.getThemeManager().subscribe( this.workspace_
this.svgBackground_, 'flyoutBackgroundColour', 'fill'); .getThemeManager()
this.workspace_.getThemeManager().subscribe( .subscribe(this.svgBackground_, 'flyoutBackgroundColour', 'fill');
this.svgBackground_, 'flyoutOpacity', 'fill-opacity'); this.workspace_
.getThemeManager()
.subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity');
return this.svgGroup_; return this.svgGroup_;
} }
@@ -358,26 +367,42 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
this.workspace_.targetWorkspace = targetWorkspace; this.workspace_.targetWorkspace = targetWorkspace;
this.workspace_.scrollbar = new ScrollbarPair( this.workspace_.scrollbar = new ScrollbarPair(
this.workspace_, this.horizontalLayout, !this.horizontalLayout, this.workspace_,
'blocklyFlyoutScrollbar', this.SCROLLBAR_MARGIN); this.horizontalLayout,
!this.horizontalLayout,
'blocklyFlyoutScrollbar',
this.SCROLLBAR_MARGIN
);
this.hide(); this.hide();
this.boundEvents.push(browserEvents.conditionalBind( this.boundEvents.push(
(this.svgGroup_ as SVGGElement), 'wheel', this, this.wheel_)); browserEvents.conditionalBind(
this.svgGroup_ as SVGGElement,
'wheel',
this,
this.wheel_
)
);
if (!this.autoClose) { if (!this.autoClose) {
this.filterWrapper = this.filterForCapacity.bind(this); this.filterWrapper = this.filterForCapacity.bind(this);
this.targetWorkspace.addChangeListener(this.filterWrapper); this.targetWorkspace.addChangeListener(this.filterWrapper);
} }
// Dragging the flyout up and down. // Dragging the flyout up and down.
this.boundEvents.push(browserEvents.conditionalBind( this.boundEvents.push(
(this.svgBackground_ as SVGPathElement), 'pointerdown', this, browserEvents.conditionalBind(
this.onMouseDown)); this.svgBackground_ as SVGPathElement,
'pointerdown',
this,
this.onMouseDown
)
);
// A flyout connected to a workspace doesn't have its own current gesture. // A flyout connected to a workspace doesn't have its own current gesture.
this.workspace_.getGesture = this.workspace_.getGesture = this.targetWorkspace.getGesture.bind(
this.targetWorkspace.getGesture.bind(this.targetWorkspace); this.targetWorkspace
);
// Get variables from the main workspace rather than the target workspace. // Get variables from the main workspace rather than the target workspace.
this.workspace_.setVariableMap(this.targetWorkspace.getVariableMap()); this.workspace_.setVariableMap(this.targetWorkspace.getVariableMap());
@@ -546,11 +571,15 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
// reposition in resize, we need to call setPosition. See issue #4692. // reposition in resize, we need to call setPosition. See issue #4692.
if (scrollbar.hScroll) { if (scrollbar.hScroll) {
scrollbar.hScroll.setPosition( scrollbar.hScroll.setPosition(
scrollbar.hScroll.position.x, scrollbar.hScroll.position.y); scrollbar.hScroll.position.x,
scrollbar.hScroll.position.y
);
} }
if (scrollbar.vScroll) { if (scrollbar.vScroll) {
scrollbar.vScroll.setPosition( scrollbar.vScroll.setPosition(
scrollbar.vScroll.position.x, scrollbar.vScroll.position.y); scrollbar.vScroll.position.x,
scrollbar.vScroll.position.y
);
} }
} }
} }
@@ -583,7 +612,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* in the flyout. This is either an array of Nodes, a NodeList, a * in the flyout. This is either an array of Nodes, a NodeList, a
* toolbox definition, or a string with the name of the dynamic category. * toolbox definition, or a string with the name of the dynamic category.
*/ */
show(flyoutDef: toolbox.FlyoutDefinition|string) { show(flyoutDef: toolbox.FlyoutDefinition | string) {
this.workspace_.setResizesEnabled(false); this.workspace_.setResizesEnabled(false);
this.hide(); this.hide();
this.clearOldBlocks(); this.clearOldBlocks();
@@ -626,40 +655,42 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* of objects to show in the flyout. * of objects to show in the flyout.
* @returns The list of contents and gaps needed to lay out the flyout. * @returns The list of contents and gaps needed to lay out the flyout.
*/ */
private createFlyoutInfo(parsedContent: toolbox.FlyoutItemInfoArray): private createFlyoutInfo(parsedContent: toolbox.FlyoutItemInfoArray): {
{contents: FlyoutItem[], gaps: number[]} { contents: FlyoutItem[];
gaps: number[];
} {
const contents: FlyoutItem[] = []; const contents: FlyoutItem[] = [];
const gaps: number[] = []; const gaps: number[] = [];
this.permanentlyDisabled.length = 0; this.permanentlyDisabled.length = 0;
const defaultGap = this.horizontalLayout ? this.GAP_X : this.GAP_Y; const defaultGap = this.horizontalLayout ? this.GAP_X : this.GAP_Y;
for (const info of parsedContent) { for (const info of parsedContent) {
if ('custom' in info) { if ('custom' in info) {
const customInfo = (info as toolbox.DynamicCategoryInfo); const customInfo = info as toolbox.DynamicCategoryInfo;
const categoryName = customInfo['custom']; const categoryName = customInfo['custom'];
const flyoutDef = this.getDynamicCategoryContents(categoryName); const flyoutDef = this.getDynamicCategoryContents(categoryName);
const parsedDynamicContent = const parsedDynamicContent =
toolbox.convertFlyoutDefToJsonArray(flyoutDef); toolbox.convertFlyoutDefToJsonArray(flyoutDef);
const {contents: dynamicContents, gaps: dynamicGaps} = const {contents: dynamicContents, gaps: dynamicGaps} =
this.createFlyoutInfo(parsedDynamicContent); this.createFlyoutInfo(parsedDynamicContent);
contents.push(...dynamicContents); contents.push(...dynamicContents);
gaps.push(...dynamicGaps); gaps.push(...dynamicGaps);
} }
switch (info['kind'].toUpperCase()) { switch (info['kind'].toUpperCase()) {
case 'BLOCK': { case 'BLOCK': {
const blockInfo = (info as toolbox.BlockInfo); const blockInfo = info as toolbox.BlockInfo;
const block = this.createFlyoutBlock(blockInfo); const block = this.createFlyoutBlock(blockInfo);
contents.push({type: FlyoutItemType.BLOCK, block: block}); contents.push({type: FlyoutItemType.BLOCK, block: block});
this.addBlockGap(blockInfo, gaps, defaultGap); this.addBlockGap(blockInfo, gaps, defaultGap);
break; break;
} }
case 'SEP': { case 'SEP': {
const sepInfo = (info as toolbox.SeparatorInfo); const sepInfo = info as toolbox.SeparatorInfo;
this.addSeparatorGap(sepInfo, gaps, defaultGap); this.addSeparatorGap(sepInfo, gaps, defaultGap);
break; break;
} }
case 'LABEL': { case 'LABEL': {
const labelInfo = (info as toolbox.LabelInfo); const labelInfo = info as toolbox.LabelInfo;
// A label is a button with different styling. // A label is a button with different styling.
const label = this.createButton(labelInfo, /** isLabel */ true); const label = this.createButton(labelInfo, /** isLabel */ true);
contents.push({type: FlyoutItemType.BUTTON, button: label}); contents.push({type: FlyoutItemType.BUTTON, button: label});
@@ -667,7 +698,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
break; break;
} }
case 'BUTTON': { case 'BUTTON': {
const buttonInfo = (info as toolbox.ButtonInfo); const buttonInfo = info as toolbox.ButtonInfo;
const button = this.createButton(buttonInfo, /** isLabel */ false); const button = this.createButton(buttonInfo, /** isLabel */ false);
contents.push({type: FlyoutItemType.BUTTON, button: button}); contents.push({type: FlyoutItemType.BUTTON, button: button});
gaps.push(defaultGap); gaps.push(defaultGap);
@@ -686,17 +717,18 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* @returns The definition of the * @returns The definition of the
* flyout in one of its many forms. * flyout in one of its many forms.
*/ */
private getDynamicCategoryContents(categoryName: string): private getDynamicCategoryContents(
toolbox.FlyoutDefinition { categoryName: string
): toolbox.FlyoutDefinition {
// Look up the correct category generation function and call that to get a // Look up the correct category generation function and call that to get a
// valid XML list. // valid XML list.
const fnToApply = const fnToApply =
this.workspace_.targetWorkspace!.getToolboxCategoryCallback( this.workspace_.targetWorkspace!.getToolboxCategoryCallback(categoryName);
categoryName);
if (typeof fnToApply !== 'function') { if (typeof fnToApply !== 'function') {
throw TypeError( throw TypeError(
'Couldn\'t find a callback function when opening' + "Couldn't find a callback function when opening" +
' a toolbox category.'); ' a toolbox category.'
);
} }
return fnToApply(this.workspace_.targetWorkspace!); return fnToApply(this.workspace_.targetWorkspace!);
} }
@@ -709,11 +741,16 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* @returns The object used to display the button in the * @returns The object used to display the button in the
* flyout. * flyout.
*/ */
private createButton(btnInfo: toolbox.ButtonOrLabelInfo, isLabel: boolean): private createButton(
FlyoutButton { btnInfo: toolbox.ButtonOrLabelInfo,
isLabel: boolean
): FlyoutButton {
const curButton = new FlyoutButton( const curButton = new FlyoutButton(
this.workspace_, (this.targetWorkspace as WorkspaceSvg), btnInfo, this.workspace_,
isLabel); this.targetWorkspace as WorkspaceSvg,
btnInfo,
isLabel
);
return curButton; return curButton;
} }
@@ -727,9 +764,11 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
private createFlyoutBlock(blockInfo: toolbox.BlockInfo): BlockSvg { private createFlyoutBlock(blockInfo: toolbox.BlockInfo): BlockSvg {
let block; let block;
if (blockInfo['blockxml']) { if (blockInfo['blockxml']) {
const xml = (typeof blockInfo['blockxml'] === 'string' ? const xml = (
utilsXml.textToDom(blockInfo['blockxml']) : typeof blockInfo['blockxml'] === 'string'
blockInfo['blockxml']) as Element; ? utilsXml.textToDom(blockInfo['blockxml'])
: blockInfo['blockxml']
) as Element;
block = this.getRecycledBlock(xml.getAttribute('type')!); block = this.getRecycledBlock(xml.getAttribute('type')!);
if (!block) { if (!block) {
block = Xml.domToBlock(xml, this.workspace_); block = Xml.domToBlock(xml, this.workspace_);
@@ -738,10 +777,10 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
block = this.getRecycledBlock(blockInfo['type']!); block = this.getRecycledBlock(blockInfo['type']!);
if (!block) { if (!block) {
if (blockInfo['enabled'] === undefined) { if (blockInfo['enabled'] === undefined) {
blockInfo['enabled'] = blockInfo['disabled'] !== 'true' && blockInfo['enabled'] =
blockInfo['disabled'] !== true; blockInfo['disabled'] !== 'true' && blockInfo['disabled'] !== true;
} }
block = blocks.append((blockInfo as blocks.State), this.workspace_); block = blocks.append(blockInfo as blocks.State, this.workspace_);
} }
} }
@@ -750,7 +789,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
// Do not enable these blocks as a result of capacity filtering. // Do not enable these blocks as a result of capacity filtering.
this.permanentlyDisabled.push(block); this.permanentlyDisabled.push(block);
} }
return (block as BlockSvg); return block as BlockSvg;
} }
/** /**
@@ -761,7 +800,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* @returns The recycled block, or undefined if * @returns The recycled block, or undefined if
* one could not be recycled. * one could not be recycled.
*/ */
private getRecycledBlock(blockType: string): BlockSvg|undefined { private getRecycledBlock(blockType: string): BlockSvg | undefined {
let index = -1; let index = -1;
for (let i = 0; i < this.recycledBlocks.length; i++) { for (let i = 0; i < this.recycledBlocks.length; i++) {
if (this.recycledBlocks[i].type === blockType) { if (this.recycledBlocks[i].type === blockType) {
@@ -781,14 +820,19 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* next. * next.
*/ */
private addBlockGap( private addBlockGap(
blockInfo: toolbox.BlockInfo, gaps: number[], defaultGap: number) { blockInfo: toolbox.BlockInfo,
gaps: number[],
defaultGap: number
) {
let gap; let gap;
if (blockInfo['gap']) { if (blockInfo['gap']) {
gap = parseInt(String(blockInfo['gap'])); gap = parseInt(String(blockInfo['gap']));
} else if (blockInfo['blockxml']) { } else if (blockInfo['blockxml']) {
const xml = (typeof blockInfo['blockxml'] === 'string' ? const xml = (
utilsXml.textToDom(blockInfo['blockxml']) : typeof blockInfo['blockxml'] === 'string'
blockInfo['blockxml']) as Element; ? utilsXml.textToDom(blockInfo['blockxml'])
: blockInfo['blockxml']
) as Element;
gap = parseInt(xml.getAttribute('gap')!); gap = parseInt(xml.getAttribute('gap')!);
} }
gaps.push(!gap || isNaN(gap) ? defaultGap : gap); gaps.push(!gap || isNaN(gap) ? defaultGap : gap);
@@ -804,7 +848,10 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* element. * element.
*/ */
private addSeparatorGap( private addSeparatorGap(
sepInfo: toolbox.SeparatorInfo, gaps: number[], defaultGap: number) { sepInfo: toolbox.SeparatorInfo,
gaps: number[],
defaultGap: number
) {
// Change the gap between two toolbox elements. // Change the gap between two toolbox elements.
// <sep gap="36"></sep> // <sep gap="36"></sep>
// The default gap is 24, can be set larger or smaller. // The default gap is 24, can be set larger or smaller.
@@ -824,7 +871,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
private clearOldBlocks() { private clearOldBlocks() {
// Delete any blocks from a previous showing. // Delete any blocks from a previous showing.
const oldBlocks = this.workspace_.getTopBlocks(false); const oldBlocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; block = oldBlocks[i]; i++) { for (let i = 0, block; (block = oldBlocks[i]); i++) {
if (this.blockIsRecyclable_(block)) { if (this.blockIsRecyclable_(block)) {
this.recycleBlock(block); this.recycleBlock(block);
} else { } else {
@@ -841,7 +888,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
} }
this.mats.length = 0; this.mats.length = 0;
// Delete any buttons from a previous showing. // Delete any buttons from a previous showing.
for (let i = 0, button; button = this.buttons_[i]; i++) { for (let i = 0, button; (button = this.buttons_[i]); i++) {
button.dispose(); button.dispose();
} }
this.buttons_.length = 0; this.buttons_.length = 0;
@@ -893,19 +940,38 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* as a mat for that block. * as a mat for that block.
*/ */
protected addBlockListeners_( protected addBlockListeners_(
root: SVGElement, block: BlockSvg, rect: SVGElement) { root: SVGElement,
this.listeners.push(browserEvents.conditionalBind( block: BlockSvg,
root, 'pointerdown', null, this.blockMouseDown(block))); rect: SVGElement
this.listeners.push(browserEvents.conditionalBind( ) {
rect, 'pointerdown', null, this.blockMouseDown(block)));
this.listeners.push( this.listeners.push(
browserEvents.bind(root, 'pointerenter', block, block.addSelect)); browserEvents.conditionalBind(
root,
'pointerdown',
null,
this.blockMouseDown(block)
)
);
this.listeners.push( this.listeners.push(
browserEvents.bind(root, 'pointerleave', block, block.removeSelect)); browserEvents.conditionalBind(
rect,
'pointerdown',
null,
this.blockMouseDown(block)
)
);
this.listeners.push( this.listeners.push(
browserEvents.bind(rect, 'pointerenter', block, block.addSelect)); browserEvents.bind(root, 'pointerenter', block, block.addSelect)
);
this.listeners.push( this.listeners.push(
browserEvents.bind(rect, 'pointerleave', block, block.removeSelect)); browserEvents.bind(root, 'pointerleave', block, block.removeSelect)
);
this.listeners.push(
browserEvents.bind(rect, 'pointerenter', block, block.addSelect)
);
this.listeners.push(
browserEvents.bind(rect, 'pointerleave', block, block.removeSelect)
);
} }
/** /**
@@ -972,7 +1038,9 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
this.targetWorkspace.hideChaff(); this.targetWorkspace.hideChaff();
const newVariables = Variables.getAddedVariables( const newVariables = Variables.getAddedVariables(
this.targetWorkspace, variablesBeforeCreation); this.targetWorkspace,
variablesBeforeCreation
);
if (eventUtils.isEnabled()) { if (eventUtils.isEnabled()) {
eventUtils.setGroup(true); eventUtils.setGroup(true);
@@ -980,7 +1048,8 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
for (let i = 0; i < newVariables.length; i++) { for (let i = 0; i < newVariables.length; i++) {
const thisVariable = newVariables[i]; const thisVariable = newVariables[i];
eventUtils.fire( eventUtils.fire(
new (eventUtils.get(eventUtils.VAR_CREATE))(thisVariable)); new (eventUtils.get(eventUtils.VAR_CREATE))(thisVariable)
);
} }
// Block events come after var events, in case they refer to newly created // Block events come after var events, in case they refer to newly created
@@ -1009,8 +1078,14 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
button.show(); button.show();
// Clicking on a flyout button or label is a lot like clicking on the // Clicking on a flyout button or label is a lot like clicking on the
// flyout background. // flyout background.
this.listeners.push(browserEvents.conditionalBind( this.listeners.push(
buttonSvg, 'pointerdown', this, this.onMouseDown)); browserEvents.conditionalBind(
buttonSvg,
'pointerdown',
this,
this.onMouseDown
)
);
this.buttons_.push(button); this.buttons_.push(button);
} }
@@ -1029,8 +1104,12 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* the block. * the block.
*/ */
protected createRect_( protected createRect_(
block: BlockSvg, x: number, y: number, block: BlockSvg,
blockHW: {height: number, width: number}, index: number): SVGElement { x: number,
y: number,
blockHW: {height: number; width: number},
index: number
): SVGElement {
// Create an invisible rectangle under the block to act as a button. Just // Create an invisible rectangle under the block to act as a button. Just
// using the block as a button is poor, since blocks have holes in them. // using the block as a button is poor, since blocks have holes in them.
const rect = dom.createSvgElement(Svg.RECT, { const rect = dom.createSvgElement(Svg.RECT, {
@@ -1065,7 +1144,9 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
const blockXY = block.getRelativeToSurfaceXY(); const blockXY = block.getRelativeToSurfaceXY();
rect.setAttribute('y', String(blockXY.y)); rect.setAttribute('y', String(blockXY.y));
rect.setAttribute( rect.setAttribute(
'x', String(this.RTL ? blockXY.x - blockHW.width : blockXY.x)); 'x',
String(this.RTL ? blockXY.x - blockHW.width : blockXY.x)
);
} }
/** /**
@@ -1076,10 +1157,11 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
*/ */
private filterForCapacity() { private filterForCapacity() {
const blocks = this.workspace_.getTopBlocks(false); const blocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; block = blocks[i]; i++) { for (let i = 0, block; (block = blocks[i]); i++) {
if (this.permanentlyDisabled.indexOf(block) === -1) { if (this.permanentlyDisabled.indexOf(block) === -1) {
const enable = this.targetWorkspace.isCapacityAvailable( const enable = this.targetWorkspace.isCapacityAvailable(
common.getBlockTypeCounts(block)); common.getBlockTypeCounts(block)
);
while (block) { while (block) {
block.setEnabled(enable); block.setEnabled(enable);
block = block.getNextBlock(); block = block.getNextBlock();
@@ -1107,8 +1189,9 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* @internal * @internal
*/ */
isScrollable(): boolean { isScrollable(): boolean {
return this.workspace_.scrollbar ? this.workspace_.scrollbar.isVisible() : return this.workspace_.scrollbar
false; ? this.workspace_.scrollbar.isVisible()
: false;
} }
/** /**
@@ -1125,10 +1208,10 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
} }
// Clone the block. // Clone the block.
const json = (blocks.save(oldBlock) as blocks.State); const json = blocks.save(oldBlock) as blocks.State;
// Normallly this resizes leading to weird jumps. Save it for terminateDrag. // Normallly this resizes leading to weird jumps. Save it for terminateDrag.
targetWorkspace.setResizesEnabled(false); targetWorkspace.setResizesEnabled(false);
const block = (blocks.append(json, targetWorkspace) as BlockSvg); const block = blocks.append(json, targetWorkspace) as BlockSvg;
this.positionNewBlock(oldBlock, block); this.positionNewBlock(oldBlock, block);
@@ -1160,13 +1243,17 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
// The position of the old block in pixels relative to the upper left corner // The position of the old block in pixels relative to the upper left corner
// of the injection div. // of the injection div.
const oldBlockOffsetPixels = const oldBlockOffsetPixels = Coordinate.sum(
Coordinate.sum(flyoutOffsetPixels, oldBlockPos); flyoutOffsetPixels,
oldBlockPos
);
// The position of the old block in pixels relative to the origin of the // The position of the old block in pixels relative to the origin of the
// main workspace. // main workspace.
const finalOffset = const finalOffset = Coordinate.difference(
Coordinate.difference(oldBlockOffsetPixels, mainOffsetPixels); oldBlockOffsetPixels,
mainOffsetPixels
);
// The position of the old block in main workspace coordinates. // The position of the old block in main workspace coordinates.
finalOffset.scale(1 / targetWorkspace.scale); finalOffset.scale(1 / targetWorkspace.scale);
@@ -1180,6 +1267,6 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
*/ */
export interface FlyoutItem { export interface FlyoutItem {
type: FlyoutItemType; type: FlyoutItemType;
button?: FlyoutButton|undefined; button?: FlyoutButton | undefined;
block?: BlockSvg|undefined; block?: BlockSvg | undefined;
} }

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