mirror of
https://github.com/google/blockly.git
synced 2026-01-04 23:50:12 +01:00
@@ -1,3 +0,0 @@
|
||||
Language: JavaScript
|
||||
BasedOnStyle: Google
|
||||
ColumnLimit: 80
|
||||
@@ -1,7 +1,11 @@
|
||||
*_compressed*.js
|
||||
# Build Artifacts
|
||||
/msg/*
|
||||
/build/*
|
||||
/dist/*
|
||||
/typings/*
|
||||
/docs/*
|
||||
|
||||
# Tests other than mocha unit tests
|
||||
/tests/blocks/*
|
||||
/tests/themes/*
|
||||
/tests/compile/*
|
||||
@@ -11,10 +15,14 @@
|
||||
/tests/screenshot/*
|
||||
/tests/test_runner.js
|
||||
/tests/workspace_svg/*
|
||||
|
||||
# Demos, scripts, misc
|
||||
/node_modules/*
|
||||
/generators/*
|
||||
/demos/*
|
||||
/appengine/*
|
||||
/externs/*
|
||||
/closure/*
|
||||
/scripts/gulpfiles/*
|
||||
/typings/*
|
||||
CHANGELOG.md
|
||||
PULL_REQUEST_TEMPLATE.md
|
||||
81
.eslintrc.js
81
.eslintrc.js
@@ -1,21 +1,6 @@
|
||||
const rules = {
|
||||
'curly': ['error'],
|
||||
'eol-last': ['error'],
|
||||
'keyword-spacing': ['error'],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'max-len': [
|
||||
'error',
|
||||
{
|
||||
'code': 100,
|
||||
'tabWidth': 4,
|
||||
'ignoreStrings': true,
|
||||
'ignoreRegExpLiterals': true,
|
||||
'ignoreUrls': true,
|
||||
},
|
||||
],
|
||||
'no-trailing-spaces': ['error', {'skipBlankLines': true}],
|
||||
'no-unused-vars': [
|
||||
'warn',
|
||||
'error',
|
||||
{
|
||||
'args': 'after-used',
|
||||
// Ignore vars starting with an underscore.
|
||||
@@ -29,20 +14,12 @@ const rules = {
|
||||
// Blockly uses single quotes except for JSON blobs, which must use double
|
||||
// quotes.
|
||||
'quotes': ['off'],
|
||||
'semi': ['error', 'always'],
|
||||
// Blockly doesn't have space before function paren when defining functions.
|
||||
'space-before-function-paren': ['error', 'never'],
|
||||
// Blockly doesn't have space before function paren when calling functions.
|
||||
'func-call-spacing': ['error', 'never'],
|
||||
'space-infix-ops': ['error'],
|
||||
// Blockly uses 'use strict' in files.
|
||||
'strict': ['off'],
|
||||
// Closure style allows redeclarations.
|
||||
'no-redeclare': ['off'],
|
||||
'valid-jsdoc': ['error'],
|
||||
'no-console': ['off'],
|
||||
'no-multi-spaces': ['error', {'ignoreEOLComments': true}],
|
||||
'operator-linebreak': ['error', 'after'],
|
||||
'spaced-comment': [
|
||||
'error',
|
||||
'always',
|
||||
@@ -61,27 +38,13 @@ const rules = {
|
||||
'allow': ['^opt_', '^_opt_', '^testOnly_'],
|
||||
},
|
||||
],
|
||||
// Use clang-format for indentation by running `npm run format`.
|
||||
'indent': ['off'],
|
||||
// Blockly uses capital letters for some non-constructor namespaces.
|
||||
// Keep them for legacy reasons.
|
||||
'new-cap': ['off'],
|
||||
// Mostly use default rules for brace style, but allow single-line blocks.
|
||||
'brace-style': ['error', '1tbs', {'allowSingleLine': true}],
|
||||
// Blockly uses objects as maps, but uses Object.create(null) to
|
||||
// instantiate them.
|
||||
'guard-for-in': ['off'],
|
||||
'prefer-spread': ['off'],
|
||||
'comma-dangle': [
|
||||
'error',
|
||||
{
|
||||
'arrays': 'always-multiline',
|
||||
'objects': 'always-multiline',
|
||||
'imports': 'always-multiline',
|
||||
'exports': 'always-multiline',
|
||||
'functions': 'ignore',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -92,10 +55,7 @@ const rules = {
|
||||
function buildTSOverride({files, tsconfig}) {
|
||||
return {
|
||||
'files': files,
|
||||
'plugins': [
|
||||
'@typescript-eslint/eslint-plugin',
|
||||
'jsdoc',
|
||||
],
|
||||
'plugins': ['@typescript-eslint/eslint-plugin', 'jsdoc'],
|
||||
'settings': {
|
||||
'jsdoc': {
|
||||
'mode': 'typescript',
|
||||
@@ -111,6 +71,7 @@ function buildTSOverride({files, tsconfig}) {
|
||||
'extends': [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:jsdoc/recommended',
|
||||
'prettier', // Extend again so that these rules are applied last
|
||||
],
|
||||
'rules': {
|
||||
// TS rules
|
||||
@@ -124,14 +85,12 @@ function buildTSOverride({files, tsconfig}) {
|
||||
// Use TS-specific rule.
|
||||
'no-unused-vars': ['off'],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
'error',
|
||||
{
|
||||
'argsIgnorePattern': '^_',
|
||||
'varsIgnorePattern': '^_',
|
||||
},
|
||||
],
|
||||
'func-call-spacing': ['off'],
|
||||
'@typescript-eslint/func-call-spacing': ['warn'],
|
||||
// Temporarily disable. 23 problems.
|
||||
'@typescript-eslint/no-explicit-any': ['off'],
|
||||
// Temporarily disable. 128 problems.
|
||||
@@ -162,9 +121,19 @@ function buildTSOverride({files, tsconfig}) {
|
||||
'publicOnly': true,
|
||||
},
|
||||
],
|
||||
// Disable because of false alarms with Closure-supported tags.
|
||||
// Re-enable after Closure is removed.
|
||||
'jsdoc/check-tag-names': ['off'],
|
||||
'jsdoc/check-tag-names': [
|
||||
'error',
|
||||
{
|
||||
'definedTags': [
|
||||
'sealed',
|
||||
'typeParam',
|
||||
'remarks',
|
||||
'define',
|
||||
'nocollapse',
|
||||
'suppress',
|
||||
],
|
||||
},
|
||||
],
|
||||
// Re-enable after Closure is removed. There shouldn't even be
|
||||
// types in the TsDoc.
|
||||
// These are "types" because of Closure's @suppress {warningName}
|
||||
@@ -176,7 +145,9 @@ function buildTSOverride({files, tsconfig}) {
|
||||
'jsdoc/check-param-names': ['off', {'checkDestructured': false}],
|
||||
// Allow any text in the license tag. Other checks are not relevant.
|
||||
'jsdoc/check-values': ['off'],
|
||||
'jsdoc/newline-after-description': ['error'],
|
||||
// Ensure there is a blank line between the body and any @tags,
|
||||
// as required by the tsdoc spec (see #6353).
|
||||
'jsdoc/tag-lines': ['error', 'any', {'startLines': 1}],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -193,21 +164,15 @@ const eslintJSON = {
|
||||
'goog': true,
|
||||
'exports': true,
|
||||
},
|
||||
'extends': [
|
||||
'eslint:recommended',
|
||||
'google',
|
||||
],
|
||||
'extends': ['eslint:recommended', 'google', 'prettier'],
|
||||
// TypeScript-specific config. Uses above rules plus these.
|
||||
'overrides': [
|
||||
buildTSOverride({
|
||||
files: ['./core/**/*.ts', './core/**/*.tsx'],
|
||||
files: ['./**/*.ts', './**/*.tsx'],
|
||||
tsconfig: './tsconfig.json',
|
||||
}),
|
||||
buildTSOverride({
|
||||
files: [
|
||||
'./tests/typescript/**/*.ts',
|
||||
'./tests/typescript/**/*.tsx',
|
||||
],
|
||||
files: ['./tests/typescript/**/*.ts', './tests/typescript/**/*.tsx'],
|
||||
tsconfig: './tests/typescript/tsconfig.json',
|
||||
}),
|
||||
{
|
||||
|
||||
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
@@ -1,6 +1,7 @@
|
||||
# Contributing to Blockly
|
||||
|
||||
Want to contribute? Great!
|
||||
|
||||
- First, read this page (including the small print at the end).
|
||||
- Second, please make pull requests against develop, not master. If your patch
|
||||
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).
|
||||
|
||||
### Before you contribute
|
||||
|
||||
Before we can use your code, you must sign the
|
||||
[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
|
||||
@@ -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.
|
||||
|
||||
### Larger changes
|
||||
|
||||
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
|
||||
possibly guide you. Coordinating up front makes it much easier to avoid
|
||||
frustration later on.
|
||||
|
||||
### Code reviews
|
||||
|
||||
All submissions, including submissions by project members, require review. We
|
||||
use Github pull requests for this purpose.
|
||||
|
||||
### 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
|
||||
work on a subset of those browsers. You can check [caniuse.com](https://caniuse.com/)
|
||||
for compatibility information.
|
||||
|
||||
### The small print
|
||||
|
||||
Contributions made by corporations are covered by a different agreement than
|
||||
the one above, the
|
||||
[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/documentation.yaml
vendored
1
.github/ISSUE_TEMPLATE/documentation.yaml
vendored
@@ -1,4 +1,3 @@
|
||||
|
||||
name: Documentation
|
||||
description: Report an issue with our documentation
|
||||
labels: 'issue: docs, issue: triage'
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -1,4 +1,3 @@
|
||||
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
labels: 'issue: feature request, issue: triage'
|
||||
|
||||
28
.github/dependabot.yml
vendored
28
.github/dependabot.yml
vendored
@@ -5,23 +5,23 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
target-branch: "develop"
|
||||
- package-ecosystem: 'npm' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
target-branch: 'develop'
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: 'weekly'
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
prefix: 'chore(deps)'
|
||||
labels:
|
||||
- "PR: chore"
|
||||
- "PR: dependencies"
|
||||
- package-ecosystem: "github-actions" # See documentation for possible values
|
||||
directory: "/"
|
||||
target-branch: "develop"
|
||||
- 'PR: chore'
|
||||
- 'PR: dependencies'
|
||||
- package-ecosystem: 'github-actions' # See documentation for possible values
|
||||
directory: '/'
|
||||
target-branch: 'develop'
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: 'weekly'
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
prefix: 'chore(deps)'
|
||||
labels:
|
||||
- "PR: chore"
|
||||
- "PR: dependencies"
|
||||
- 'PR: chore'
|
||||
- 'PR: dependencies'
|
||||
|
||||
14
.github/release.yml
vendored
14
.github/release.yml
vendored
@@ -4,7 +4,7 @@ changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
- "PR: chore"
|
||||
- 'PR: chore'
|
||||
authors:
|
||||
- dependabot
|
||||
categories:
|
||||
@@ -16,17 +16,17 @@ changelog:
|
||||
- deprecation
|
||||
- title: New features ✨
|
||||
labels:
|
||||
- "PR: feature"
|
||||
- 'PR: feature'
|
||||
- title: Bug fixes 🐛
|
||||
labels:
|
||||
- "PR: fix"
|
||||
- 'PR: fix'
|
||||
- title: Cleanup ♻️
|
||||
labels:
|
||||
- "PR: docs"
|
||||
- "PR: refactor"
|
||||
- 'PR: docs'
|
||||
- 'PR: refactor'
|
||||
- title: Reverted changes ⎌
|
||||
labels:
|
||||
- "PR: revert"
|
||||
- 'PR: revert'
|
||||
- title: Other changes
|
||||
labels:
|
||||
- "*"
|
||||
- '*'
|
||||
|
||||
2
.github/workflows/appengine_deploy.yml
vendored
2
.github/workflows/appengine_deploy.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
path: _deploy/
|
||||
|
||||
- name: Deploy to App Engine
|
||||
uses: google-github-actions/deploy-appengine@v1.2.2
|
||||
uses: google-github-actions/deploy-appengine@v1.2.7
|
||||
# For parameters see:
|
||||
# https://github.com/google-github-actions/deploy-appengine#inputs
|
||||
with:
|
||||
|
||||
59
.github/workflows/browser_test.yml
vendored
Normal file
59
.github/workflows/browser_test.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
# This workflow will do a clean install, start the selenium server, and run
|
||||
# all of our browser based tests
|
||||
|
||||
name: Run browser manually
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
timeout-minutes: 10
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
# TODO (#2114): re-enable osx build.
|
||||
# os: [ubuntu-latest, macos-latest]
|
||||
os: [macos-latest]
|
||||
node-version: [16.x, 18.x, 20.x]
|
||||
# See supported Node.js release schedule at
|
||||
# https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Reconfigure git to use HTTP authentication
|
||||
run: >
|
||||
git config --global url."https://github.com/".insteadOf
|
||||
ssh://git@github.com/
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Npm Install
|
||||
run: npm install
|
||||
|
||||
- name: Linux Test Setup
|
||||
if: runner.os == 'Linux'
|
||||
run: source ./tests/scripts/setup_linux_env.sh
|
||||
|
||||
- name: MacOS Test Setup
|
||||
if: runner.os == 'macOS'
|
||||
run: source ./tests/scripts/setup_osx_env.sh
|
||||
|
||||
- name: Run Build
|
||||
run: npm run build
|
||||
|
||||
- name: Run Test
|
||||
run: npm run test:browser
|
||||
|
||||
env:
|
||||
CI: true
|
||||
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
# TODO (#2114): re-enable osx build.
|
||||
# os: [ubuntu-latest, macos-latest]
|
||||
os: [ubuntu-latest]
|
||||
node-version: [14.x, 16.x, 18.x]
|
||||
node-version: [16.x, 18.x, 20.x]
|
||||
# See supported Node.js release schedule at
|
||||
# https://nodejs.org/en/about/releases/
|
||||
|
||||
@@ -60,10 +60,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 16.x
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Npm Install
|
||||
run: npm install
|
||||
@@ -71,17 +71,19 @@ jobs:
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
clang-formatter:
|
||||
format:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: DoozyX/clang-format-lint-action@v0.15
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
source: 'core'
|
||||
extensions: 'js,ts'
|
||||
# This should be as close as possible to the version that the npm
|
||||
# package supports. This can be found by running:
|
||||
# npx clang-format --version.
|
||||
clangFormatVersion: 15
|
||||
node-version: 20.x
|
||||
|
||||
- name: Npm Install
|
||||
run: npm install
|
||||
|
||||
- name: Check Format
|
||||
run: npm run format:check
|
||||
|
||||
3
.github/workflows/conventional-label.yml
vendored
3
.github/workflows/conventional-label.yml
vendored
@@ -10,7 +10,8 @@ jobs:
|
||||
steps:
|
||||
- uses: bcoe/conventional-release-labels@v1
|
||||
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:
|
||||
refactor", "revert": "PR: revert", "deprecate": "deprecation"}'
|
||||
ignored_types: '[]'
|
||||
|
||||
2
.github/workflows/develop_freeze.yml
vendored
2
.github/workflows/develop_freeze.yml
vendored
@@ -23,4 +23,4 @@ jobs:
|
||||
uses: github-actions-up-and-running/pr-comment@f1f8ab2bf00dce6880a369ce08758a60c61d6c0b
|
||||
with:
|
||||
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.'
|
||||
|
||||
1
.github/workflows/tag_module_cleanup.yml
vendored
1
.github/workflows/tag_module_cleanup.yml
vendored
@@ -12,7 +12,6 @@ on:
|
||||
|
||||
jobs:
|
||||
tag-module-cleanup:
|
||||
|
||||
# Add the type: cleanup label
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
46
.github/workflows/update_metadata.yml
vendored
46
.github/workflows/update_metadata.yml
vendored
@@ -1,46 +0,0 @@
|
||||
# This workflow updates the check_metadata.sh script, which compares the current
|
||||
# size of build artifacts against their size in the previous version of Blockly.
|
||||
|
||||
name: Update Metadata
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update-metadata:
|
||||
permissions:
|
||||
contents: write # for peter-evans/create-pull-request to create branch
|
||||
pull-requests: write # for peter-evans/create-pull-request to create a PR
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Blockly
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'develop'
|
||||
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Build Blockly
|
||||
run: npm run build:compressed
|
||||
|
||||
- name: Build Blockly blocks
|
||||
run: npm run build:blocks
|
||||
|
||||
- name: Update Metadata
|
||||
run: source ./tests/scripts/update_metadata.sh
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54
|
||||
with:
|
||||
commit-message: Update build artifact sizes in check_metadata.sh
|
||||
delete-branch: true
|
||||
title: Update build artifact sizes in check_metadata.sh
|
||||
|
||||
- name: View Pull Request
|
||||
run: echo "View Pull Request - ${{ steps.cpr.outputs.pull-request-url }}"
|
||||
31
.prettierignore
Normal file
31
.prettierignore
Normal 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
13
.prettierrc.js
Normal 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,
|
||||
};
|
||||
18
README.md
18
README.md
@@ -8,10 +8,10 @@ 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:
|
||||
|
||||
* [Getting Started article](https://developers.google.com/blockly/guides/get-started/web)
|
||||
* [Getting Started codelab](https://blocklycodelabs.dev/codelabs/getting-started/index.html#0)
|
||||
* [More codelabs](https://blocklycodelabs.dev/)
|
||||
* [Demos and plugins](https://google.github.io/blockly-samples/)
|
||||
- [Getting Started article](https://developers.google.com/blockly/guides/get-started/web)
|
||||
- [Getting Started codelab](https://blocklycodelabs.dev/codelabs/getting-started/index.html#0)
|
||||
- [More codelabs](https://blocklycodelabs.dev/)
|
||||
- [Demos and plugins](https://google.github.io/blockly-samples/)
|
||||
|
||||
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
|
||||
@@ -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).
|
||||
|
||||
### 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
|
||||
|
||||
@@ -50,6 +51,7 @@ We now have a [beta release on npm](https://www.npmjs.com/package/blockly?active
|
||||
```bash
|
||||
npm install blockly@beta
|
||||
```
|
||||
|
||||
As it is a beta channel, it may be less stable, and the APIs there are subject to change.
|
||||
|
||||
### Branches
|
||||
@@ -82,5 +84,5 @@ We typically triage all bugs within 2 working days, which includes adding any ap
|
||||
|
||||
## Good to Know
|
||||
|
||||
* Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com)
|
||||
* We test browsers using [BrowserStack](https://browserstack.com)
|
||||
- Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com)
|
||||
- We test browsers using [BrowserStack](https://browserstack.com)
|
||||
|
||||
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
exclude: []
|
||||
@@ -109,7 +109,7 @@
|
||||
/**
|
||||
* (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
|
||||
@@ -195,7 +195,7 @@
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* 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.
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview All the blocks. (Entry point for blocks_compressed.js.)
|
||||
* @suppress {extraRequire}
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
goog.module('Blockly.libraryBlocks');
|
||||
|
||||
const colour = goog.require('Blockly.libraryBlocks.colour');
|
||||
const lists = goog.require('Blockly.libraryBlocks.lists');
|
||||
const logic = goog.require('Blockly.libraryBlocks.logic');
|
||||
const loops = goog.require('Blockly.libraryBlocks.loops');
|
||||
const math = goog.require('Blockly.libraryBlocks.math');
|
||||
const procedures = goog.require('Blockly.libraryBlocks.procedures');
|
||||
const texts = goog.require('Blockly.libraryBlocks.texts');
|
||||
const variables = goog.require('Blockly.libraryBlocks.variables');
|
||||
const variablesDynamic = goog.require('Blockly.libraryBlocks.variablesDynamic');
|
||||
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
|
||||
// TODO (6248): Properly import the BlockDefinition type.
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const BlockDefinition = Object;
|
||||
|
||||
|
||||
exports.colour = colour;
|
||||
exports.lists = lists;
|
||||
exports.logic = logic;
|
||||
exports.loops = loops;
|
||||
exports.math = math;
|
||||
exports.procedures = procedures;
|
||||
exports.texts = texts;
|
||||
exports.variables = variables;
|
||||
exports.variablesDynamic = variablesDynamic;
|
||||
|
||||
/**
|
||||
* A dictionary of the block definitions provided by all the
|
||||
* Blockly.libraryBlocks.* modules.
|
||||
* @type {!Object<string, !BlockDefinition>}
|
||||
*/
|
||||
const blocks = Object.assign(
|
||||
{}, colour.blocks, lists.blocks, logic.blocks, loops.blocks, math.blocks,
|
||||
procedures.blocks, variables.blocks, variablesDynamic.blocks);
|
||||
exports.blocks = blocks;
|
||||
46
blocks/blocks.ts
Normal file
46
blocks/blocks.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.libraryBlocks');
|
||||
|
||||
import * as colour from './colour.js';
|
||||
import * as lists from './lists.js';
|
||||
import * as logic from './logic.js';
|
||||
import * as loops from './loops.js';
|
||||
import * as math from './math.js';
|
||||
import * as procedures from './procedures.js';
|
||||
import * as texts from './text.js';
|
||||
import * as variables from './variables.js';
|
||||
import * as variablesDynamic from './variables_dynamic.js';
|
||||
import type {BlockDefinition} from '../core/blocks.js';
|
||||
|
||||
export {
|
||||
colour,
|
||||
lists,
|
||||
loops,
|
||||
math,
|
||||
procedures,
|
||||
texts,
|
||||
variables,
|
||||
variablesDynamic,
|
||||
};
|
||||
|
||||
/**
|
||||
* A dictionary of the block definitions provided by all the
|
||||
* Blockly.libraryBlocks.* modules.
|
||||
*/
|
||||
export const blocks: {[key: string]: BlockDefinition} = Object.assign(
|
||||
{},
|
||||
colour.blocks,
|
||||
lists.blocks,
|
||||
logic.blocks,
|
||||
loops.blocks,
|
||||
math.blocks,
|
||||
procedures.blocks,
|
||||
variables.blocks,
|
||||
variablesDynamic.blocks
|
||||
);
|
||||
@@ -4,18 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Colour blocks for Blockly.
|
||||
*/
|
||||
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.libraryBlocks.colour');
|
||||
|
||||
import type {BlockDefinition} from '../core/blocks.js';
|
||||
import {createBlockDefinitionsFromJsonArray, defineBlocks} from '../core/common.js';
|
||||
import {
|
||||
createBlockDefinitionsFromJsonArray,
|
||||
defineBlocks,
|
||||
} from '../core/common.js';
|
||||
import '../core/field_colour.js';
|
||||
|
||||
|
||||
/**
|
||||
* A dictionary of the block definitions provided by this module.
|
||||
*/
|
||||
@@ -82,7 +79,8 @@ export const blocks = createBlockDefinitionsFromJsonArray([
|
||||
// Block for blending two colours together.
|
||||
{
|
||||
'type': 'colour_blend',
|
||||
'message0': '%{BKY_COLOUR_BLEND_TITLE} %{BKY_COLOUR_BLEND_COLOUR1} ' +
|
||||
'message0':
|
||||
'%{BKY_COLOUR_BLEND_TITLE} %{BKY_COLOUR_BLEND_COLOUR1} ' +
|
||||
'%1 %{BKY_COLOUR_BLEND_COLOUR2} %2 %{BKY_COLOUR_BLEND_RATIO} %3',
|
||||
'args0': [
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,43 +4,30 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Logic blocks for Blockly.
|
||||
* @suppress {checkTypes}
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
goog.module('Blockly.libraryBlocks.logic');
|
||||
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const AbstractEvent = goog.requireType('Blockly.Events.Abstract');
|
||||
const Events = goog.require('Blockly.Events');
|
||||
const Extensions = goog.require('Blockly.Extensions');
|
||||
const xmlUtils = goog.require('Blockly.utils.xml');
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const {Block} = goog.requireType('Blockly.Block');
|
||||
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
|
||||
// TODO (6248): Properly import the BlockDefinition type.
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const BlockDefinition = Object;
|
||||
const {Msg} = goog.require('Blockly.Msg');
|
||||
const {Mutator} = goog.require('Blockly.Mutator');
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const {Workspace} = goog.requireType('Blockly.Workspace');
|
||||
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
|
||||
/** @suppress {extraRequire} */
|
||||
goog.require('Blockly.FieldDropdown');
|
||||
/** @suppress {extraRequire} */
|
||||
goog.require('Blockly.FieldLabel');
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.libraryBlocks.logic');
|
||||
|
||||
import * as Events from '../core/events/events.js';
|
||||
import * as Extensions from '../core/extensions.js';
|
||||
import * as xmlUtils from '../core/utils/xml.js';
|
||||
import type {Abstract as AbstractEvent} from '../core/events/events_abstract.js';
|
||||
import type {Block} from '../core/block.js';
|
||||
import type {BlockSvg} from '../core/block_svg.js';
|
||||
import type {Connection} from '../core/connection.js';
|
||||
import {Msg} from '../core/msg.js';
|
||||
import type {Workspace} from '../core/workspace.js';
|
||||
import {
|
||||
createBlockDefinitionsFromJsonArray,
|
||||
defineBlocks,
|
||||
} from '../core/common.js';
|
||||
import '../core/field_dropdown.js';
|
||||
import '../core/field_label.js';
|
||||
import '../core/icons/mutator_icon.js';
|
||||
|
||||
/**
|
||||
* A dictionary of the block definitions provided by this module.
|
||||
* @type {!Object<string, !BlockDefinition>}
|
||||
*/
|
||||
const blocks = createBlockDefinitionsFromJsonArray([
|
||||
export const blocks = createBlockDefinitionsFromJsonArray([
|
||||
// Block for boolean data type: true and false.
|
||||
{
|
||||
'type': 'logic_boolean',
|
||||
@@ -266,13 +253,12 @@ const blocks = createBlockDefinitionsFromJsonArray([
|
||||
'tooltip': '%{BKY_CONTROLS_IF_ELSE_TOOLTIP}',
|
||||
},
|
||||
]);
|
||||
exports.blocks = blocks;
|
||||
|
||||
/**
|
||||
* Tooltip text, keyed by block OP value. Used by logic_compare and
|
||||
* logic_operation blocks.
|
||||
*
|
||||
* @see {Extensions#buildTooltipForDropdown}
|
||||
* @readonly
|
||||
*/
|
||||
const TOOLTIPS_BY_OP = {
|
||||
// logic_compare
|
||||
@@ -290,13 +276,32 @@ const TOOLTIPS_BY_OP = {
|
||||
|
||||
Extensions.register(
|
||||
'logic_op_tooltip',
|
||||
Extensions.buildTooltipForDropdown('OP', TOOLTIPS_BY_OP));
|
||||
Extensions.buildTooltipForDropdown('OP', TOOLTIPS_BY_OP)
|
||||
);
|
||||
|
||||
/** Type of a block that has CONTROLS_IF_MUTATOR_MIXIN */
|
||||
type IfBlock = Block & IfMixin;
|
||||
interface IfMixin extends IfMixinType {}
|
||||
type IfMixinType = typeof CONTROLS_IF_MUTATOR_MIXIN;
|
||||
|
||||
// Types for quarks defined in JSON.
|
||||
/** Type of a controls_if_if (if mutator container) block. */
|
||||
interface ContainerBlock extends Block {}
|
||||
|
||||
/** Type of a controls_if_elseif or controls_if_else block. */
|
||||
interface ClauseBlock extends Block {
|
||||
valueConnection_?: Connection | null;
|
||||
statementConnection_?: Connection | null;
|
||||
}
|
||||
|
||||
/** Extra state for serialising controls_if blocks. */
|
||||
type IfExtraState = {
|
||||
elseIfCount?: number;
|
||||
hasElse?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mutator methods added to controls_if blocks.
|
||||
* @mixin
|
||||
* @augments Block
|
||||
* @readonly
|
||||
*/
|
||||
const CONTROLS_IF_MUTATOR_MIXIN = {
|
||||
elseifCount_: 0,
|
||||
@@ -305,39 +310,39 @@ const CONTROLS_IF_MUTATOR_MIXIN = {
|
||||
/**
|
||||
* Create XML to represent the number of else-if and else inputs.
|
||||
* Backwards compatible serialization implementation.
|
||||
* @return {Element} XML storage element.
|
||||
* @this {Block}
|
||||
*
|
||||
* @returns XML storage element.
|
||||
*/
|
||||
mutationToDom: function() {
|
||||
mutationToDom: function (this: IfBlock): Element | null {
|
||||
if (!this.elseifCount_ && !this.elseCount_) {
|
||||
return null;
|
||||
}
|
||||
const container = xmlUtils.createElement('mutation');
|
||||
if (this.elseifCount_) {
|
||||
container.setAttribute('elseif', this.elseifCount_);
|
||||
container.setAttribute('elseif', String(this.elseifCount_));
|
||||
}
|
||||
if (this.elseCount_) {
|
||||
container.setAttribute('else', 1);
|
||||
container.setAttribute('else', '1');
|
||||
}
|
||||
return container;
|
||||
},
|
||||
/**
|
||||
* Parse XML to restore the else-if and else inputs.
|
||||
* Backwards compatible serialization implementation.
|
||||
* @param {!Element} xmlElement XML storage element.
|
||||
* @this {Block}
|
||||
*
|
||||
* @param xmlElement XML storage element.
|
||||
*/
|
||||
domToMutation: function(xmlElement) {
|
||||
this.elseifCount_ = parseInt(xmlElement.getAttribute('elseif'), 10) || 0;
|
||||
this.elseCount_ = parseInt(xmlElement.getAttribute('else'), 10) || 0;
|
||||
domToMutation: function (this: IfBlock, xmlElement: Element) {
|
||||
this.elseifCount_ = parseInt(xmlElement.getAttribute('elseif')!, 10) || 0;
|
||||
this.elseCount_ = parseInt(xmlElement.getAttribute('else')!, 10) || 0;
|
||||
this.rebuildShape_();
|
||||
},
|
||||
/**
|
||||
* Returns the state of this block as a JSON serializable object.
|
||||
* @return {?{elseIfCount: (number|undefined), haseElse: (boolean|undefined)}}
|
||||
* The state of this block, ie the else if count and else state.
|
||||
*
|
||||
* @returns The state of this block, ie the else if count and else state.
|
||||
*/
|
||||
saveExtraState: function() {
|
||||
saveExtraState: function (this: IfBlock): IfExtraState | null {
|
||||
if (!this.elseifCount_ && !this.elseCount_) {
|
||||
return null;
|
||||
}
|
||||
@@ -352,86 +357,102 @@ const CONTROLS_IF_MUTATOR_MIXIN = {
|
||||
},
|
||||
/**
|
||||
* Applies the given state to this block.
|
||||
* @param {*} state The state to apply to this block, ie the else if count and
|
||||
*
|
||||
* @param state The state to apply to this block, ie the else if count
|
||||
and
|
||||
* else state.
|
||||
*/
|
||||
loadExtraState: function(state) {
|
||||
loadExtraState: function (this: IfBlock, state: IfExtraState) {
|
||||
this.elseifCount_ = state['elseIfCount'] || 0;
|
||||
this.elseCount_ = state['hasElse'] ? 1 : 0;
|
||||
this.updateShape_();
|
||||
},
|
||||
/**
|
||||
* Populate the mutator's dialog with this block's components.
|
||||
* @param {!Workspace} workspace Mutator's workspace.
|
||||
* @return {!Block} Root block in mutator.
|
||||
* @this {Block}
|
||||
*
|
||||
* @param workspace MutatorIcon's workspace.
|
||||
* @returns Root block in mutator.
|
||||
*/
|
||||
decompose: function(workspace) {
|
||||
decompose: function (this: IfBlock, workspace: Workspace): ContainerBlock {
|
||||
const containerBlock = workspace.newBlock('controls_if_if');
|
||||
containerBlock.initSvg();
|
||||
let connection = containerBlock.nextConnection;
|
||||
(containerBlock as BlockSvg).initSvg();
|
||||
let connection = containerBlock.nextConnection!;
|
||||
for (let i = 1; i <= this.elseifCount_; i++) {
|
||||
const elseifBlock = workspace.newBlock('controls_if_elseif');
|
||||
elseifBlock.initSvg();
|
||||
connection.connect(elseifBlock.previousConnection);
|
||||
connection = elseifBlock.nextConnection;
|
||||
(elseifBlock as BlockSvg).initSvg();
|
||||
connection.connect(elseifBlock.previousConnection!);
|
||||
connection = elseifBlock.nextConnection!;
|
||||
}
|
||||
if (this.elseCount_) {
|
||||
const elseBlock = workspace.newBlock('controls_if_else');
|
||||
elseBlock.initSvg();
|
||||
connection.connect(elseBlock.previousConnection);
|
||||
(elseBlock as BlockSvg).initSvg();
|
||||
connection.connect(elseBlock.previousConnection!);
|
||||
}
|
||||
return containerBlock;
|
||||
},
|
||||
/**
|
||||
* Reconfigure this block based on the mutator dialog's components.
|
||||
* @param {!Block} containerBlock Root block in mutator.
|
||||
* @this {Block}
|
||||
*
|
||||
* @param containerBlock Root block in mutator.
|
||||
*/
|
||||
compose: function(containerBlock) {
|
||||
let clauseBlock = containerBlock.nextConnection.targetBlock();
|
||||
compose: function (this: IfBlock, containerBlock: ContainerBlock) {
|
||||
let clauseBlock =
|
||||
containerBlock.nextConnection!.targetBlock() as ClauseBlock | null;
|
||||
// Count number of inputs.
|
||||
this.elseifCount_ = 0;
|
||||
this.elseCount_ = 0;
|
||||
const valueConnections = [null];
|
||||
const statementConnections = [null];
|
||||
let elseStatementConnection = null;
|
||||
// Connections arrays are passed to .reconnectChildBlocks_() which
|
||||
// takes 1-based arrays, so are initialised with a dummy value at
|
||||
// index 0 for convenience.
|
||||
const valueConnections: Array<Connection | null> = [null];
|
||||
const statementConnections: Array<Connection | null> = [null];
|
||||
let elseStatementConnection: Connection | null = null;
|
||||
while (clauseBlock) {
|
||||
if (clauseBlock.isInsertionMarker()) {
|
||||
clauseBlock = clauseBlock.getNextBlock();
|
||||
clauseBlock = clauseBlock.getNextBlock() as ClauseBlock | null;
|
||||
continue;
|
||||
}
|
||||
switch (clauseBlock.type) {
|
||||
case 'controls_if_elseif':
|
||||
this.elseifCount_++;
|
||||
valueConnections.push(clauseBlock.valueConnection_);
|
||||
statementConnections.push(clauseBlock.statementConnection_);
|
||||
// TODO(#6920): null valid, undefined not.
|
||||
valueConnections.push(
|
||||
clauseBlock.valueConnection_ as Connection | null
|
||||
);
|
||||
statementConnections.push(
|
||||
clauseBlock.statementConnection_ as Connection | null
|
||||
);
|
||||
break;
|
||||
case 'controls_if_else':
|
||||
this.elseCount_++;
|
||||
elseStatementConnection = clauseBlock.statementConnection_;
|
||||
elseStatementConnection =
|
||||
clauseBlock.statementConnection_ as Connection | null;
|
||||
break;
|
||||
default:
|
||||
throw TypeError('Unknown block type: ' + clauseBlock.type);
|
||||
}
|
||||
clauseBlock = clauseBlock.getNextBlock();
|
||||
clauseBlock = clauseBlock.getNextBlock() as ClauseBlock | null;
|
||||
}
|
||||
this.updateShape_();
|
||||
// Reconnect any child blocks.
|
||||
this.reconnectChildBlocks_(
|
||||
valueConnections, statementConnections, elseStatementConnection);
|
||||
valueConnections,
|
||||
statementConnections,
|
||||
elseStatementConnection
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Store pointers to any connected child blocks.
|
||||
* @param {!Block} containerBlock Root block in mutator.
|
||||
* @this {Block}
|
||||
*
|
||||
* @param containerBlock Root block in mutator.
|
||||
*/
|
||||
saveConnections: function(containerBlock) {
|
||||
let clauseBlock = containerBlock.nextConnection.targetBlock();
|
||||
saveConnections: function (this: IfBlock, containerBlock: ContainerBlock) {
|
||||
let clauseBlock =
|
||||
containerBlock!.nextConnection!.targetBlock() as ClauseBlock | null;
|
||||
let i = 1;
|
||||
while (clauseBlock) {
|
||||
if (clauseBlock.isInsertionMarker()) {
|
||||
clauseBlock = clauseBlock.getNextBlock();
|
||||
clauseBlock = clauseBlock.getNextBlock() as ClauseBlock | null;
|
||||
continue;
|
||||
}
|
||||
switch (clauseBlock.type) {
|
||||
@@ -439,53 +460,55 @@ const CONTROLS_IF_MUTATOR_MIXIN = {
|
||||
const inputIf = this.getInput('IF' + i);
|
||||
const inputDo = this.getInput('DO' + i);
|
||||
clauseBlock.valueConnection_ =
|
||||
inputIf && inputIf.connection.targetConnection;
|
||||
inputIf && inputIf.connection!.targetConnection;
|
||||
clauseBlock.statementConnection_ =
|
||||
inputDo && inputDo.connection.targetConnection;
|
||||
inputDo && inputDo.connection!.targetConnection;
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
case 'controls_if_else': {
|
||||
const inputDo = this.getInput('ELSE');
|
||||
clauseBlock.statementConnection_ =
|
||||
inputDo && inputDo.connection.targetConnection;
|
||||
inputDo && inputDo.connection!.targetConnection;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw TypeError('Unknown block type: ' + clauseBlock.type);
|
||||
}
|
||||
clauseBlock = clauseBlock.getNextBlock();
|
||||
clauseBlock = clauseBlock.getNextBlock() as ClauseBlock | null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Reconstructs the block with all child blocks attached.
|
||||
* @this {Block}
|
||||
*/
|
||||
rebuildShape_: function() {
|
||||
const valueConnections = [null];
|
||||
const statementConnections = [null];
|
||||
let elseStatementConnection = null;
|
||||
rebuildShape_: function (this: IfBlock) {
|
||||
const valueConnections: Array<Connection | null> = [null];
|
||||
const statementConnections: Array<Connection | null> = [null];
|
||||
let elseStatementConnection: Connection | null = null;
|
||||
|
||||
if (this.getInput('ELSE')) {
|
||||
elseStatementConnection =
|
||||
this.getInput('ELSE').connection.targetConnection;
|
||||
this.getInput('ELSE')!.connection!.targetConnection;
|
||||
}
|
||||
for (let i = 1; this.getInput('IF' + i); i++) {
|
||||
const inputIf = this.getInput('IF' + i);
|
||||
const inputDo = this.getInput('DO' + i);
|
||||
valueConnections.push(inputIf.connection.targetConnection);
|
||||
statementConnections.push(inputDo.connection.targetConnection);
|
||||
valueConnections.push(inputIf!.connection!.targetConnection);
|
||||
statementConnections.push(inputDo!.connection!.targetConnection);
|
||||
}
|
||||
this.updateShape_();
|
||||
this.reconnectChildBlocks_(
|
||||
valueConnections, statementConnections, elseStatementConnection);
|
||||
valueConnections,
|
||||
statementConnections,
|
||||
elseStatementConnection
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Modify this block to have the correct number of inputs.
|
||||
* @this {Block}
|
||||
* @private
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
updateShape_: function() {
|
||||
updateShape_: function (this: IfBlock) {
|
||||
// Delete everything.
|
||||
if (this.getInput('ELSE')) {
|
||||
this.removeInput('ELSE');
|
||||
@@ -496,46 +519,56 @@ const CONTROLS_IF_MUTATOR_MIXIN = {
|
||||
}
|
||||
// Rebuild block.
|
||||
for (let i = 1; i <= this.elseifCount_; i++) {
|
||||
this.appendValueInput('IF' + i).setCheck('Boolean').appendField(
|
||||
Msg['CONTROLS_IF_MSG_ELSEIF']);
|
||||
this.appendValueInput('IF' + i)
|
||||
.setCheck('Boolean')
|
||||
.appendField(Msg['CONTROLS_IF_MSG_ELSEIF']);
|
||||
this.appendStatementInput('DO' + i).appendField(
|
||||
Msg['CONTROLS_IF_MSG_THEN']);
|
||||
Msg['CONTROLS_IF_MSG_THEN']
|
||||
);
|
||||
}
|
||||
if (this.elseCount_) {
|
||||
this.appendStatementInput('ELSE').appendField(
|
||||
Msg['CONTROLS_IF_MSG_ELSE']);
|
||||
Msg['CONTROLS_IF_MSG_ELSE']
|
||||
);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Reconnects child blocks.
|
||||
* @param {!Array<?RenderedConnection>} valueConnections List of
|
||||
* value connections for 'if' input.
|
||||
* @param {!Array<?RenderedConnection>} statementConnections List of
|
||||
* statement connections for 'do' input.
|
||||
* @param {?RenderedConnection} elseStatementConnection Statement
|
||||
* connection for else input.
|
||||
* @this {Block}
|
||||
*
|
||||
* @param valueConnections 1-based array of value connections for
|
||||
* 'if' input. Value at index [0] ignored.
|
||||
* @param statementConnections 1-based array of statement
|
||||
* connections for 'do' input. Value at index [0] ignored.
|
||||
* @param elseStatementConnection Statement connection for else input.
|
||||
*/
|
||||
reconnectChildBlocks_: function (
|
||||
valueConnections, statementConnections, elseStatementConnection) {
|
||||
this: IfBlock,
|
||||
valueConnections: Array<Connection | null>,
|
||||
statementConnections: Array<Connection | null>,
|
||||
elseStatementConnection: Connection | null
|
||||
) {
|
||||
for (let i = 1; i <= this.elseifCount_; i++) {
|
||||
Mutator.reconnect(valueConnections[i], this, 'IF' + i);
|
||||
Mutator.reconnect(statementConnections[i], this, 'DO' + i);
|
||||
valueConnections[i]?.reconnect(this, 'IF' + i);
|
||||
statementConnections[i]?.reconnect(this, 'DO' + i);
|
||||
}
|
||||
Mutator.reconnect(elseStatementConnection, this, 'ELSE');
|
||||
elseStatementConnection?.reconnect(this, 'ELSE');
|
||||
},
|
||||
};
|
||||
|
||||
Extensions.registerMutator(
|
||||
'controls_if_mutator', CONTROLS_IF_MUTATOR_MIXIN, null,
|
||||
['controls_if_elseif', 'controls_if_else']);
|
||||
'controls_if_mutator',
|
||||
CONTROLS_IF_MUTATOR_MIXIN,
|
||||
null as unknown as undefined, // TODO(#6920)
|
||||
['controls_if_elseif', 'controls_if_else']
|
||||
);
|
||||
|
||||
/**
|
||||
* "controls_if" extension function. Adds mutator, shape updating methods, and
|
||||
* dynamic tooltip to "controls_if" blocks.
|
||||
* @this {Block}
|
||||
* "controls_if" extension function. Adds mutator, shape updating methods,
|
||||
* and dynamic tooltip to "controls_if" blocks.
|
||||
*/
|
||||
const CONTROLS_IF_TOOLTIP_EXTENSION = function() {
|
||||
this.setTooltip(function() {
|
||||
const CONTROLS_IF_TOOLTIP_EXTENSION = function (this: IfBlock) {
|
||||
this.setTooltip(
|
||||
function (this: IfBlock) {
|
||||
if (!this.elseifCount_ && !this.elseCount_) {
|
||||
return Msg['CONTROLS_IF_TOOLTIP_1'];
|
||||
} else if (!this.elseifCount_ && this.elseCount_) {
|
||||
@@ -546,36 +579,47 @@ const CONTROLS_IF_TOOLTIP_EXTENSION = function() {
|
||||
return Msg['CONTROLS_IF_TOOLTIP_4'];
|
||||
}
|
||||
return '';
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
Extensions.register('controls_if_tooltip', CONTROLS_IF_TOOLTIP_EXTENSION);
|
||||
|
||||
/** Type of a block that has LOGIC_COMPARE_ONCHANGE_MIXIN */
|
||||
type CompareBlock = Block & CompareMixin;
|
||||
interface CompareMixin extends CompareMixinType {
|
||||
prevBlocks_?: Array<Block | null>;
|
||||
}
|
||||
type CompareMixinType = typeof LOGIC_COMPARE_ONCHANGE_MIXIN;
|
||||
|
||||
/**
|
||||
* Adds dynamic type validation for the left and right sides of a logic_compare
|
||||
* block.
|
||||
* @mixin
|
||||
* @augments Block
|
||||
* @readonly
|
||||
* Adds dynamic type validation for the left and right sides of a
|
||||
* logic_compare block.
|
||||
*/
|
||||
const LOGIC_COMPARE_ONCHANGE_MIXIN = {
|
||||
/**
|
||||
* Called whenever anything on the workspace changes.
|
||||
* Prevent mismatched types from being compared.
|
||||
* @param {!AbstractEvent} e Change event.
|
||||
* @this {Block}
|
||||
*
|
||||
* @param e Change event.
|
||||
*/
|
||||
onchange: function(e) {
|
||||
onchange: function (this: CompareBlock, e: AbstractEvent) {
|
||||
if (!this.prevBlocks_) {
|
||||
this.prevBlocks_ = [null, null];
|
||||
}
|
||||
|
||||
const blockA = this.getInputTargetBlock('A');
|
||||
const blockB = this.getInputTargetBlock('B');
|
||||
// Disconnect blocks that existed prior to this change if they don't match.
|
||||
if (blockA && blockB &&
|
||||
// Disconnect blocks that existed prior to this change if they don't
|
||||
// match.
|
||||
if (
|
||||
blockA &&
|
||||
blockB &&
|
||||
!this.workspace.connectionChecker.doTypeChecks(
|
||||
blockA.outputConnection, blockB.outputConnection)) {
|
||||
blockA.outputConnection!,
|
||||
blockB.outputConnection!
|
||||
)
|
||||
) {
|
||||
// Mismatch between two inputs. Revert the block connections,
|
||||
// bumping away the newly connected block(s).
|
||||
Events.setGroup(e.group);
|
||||
@@ -584,7 +628,7 @@ const LOGIC_COMPARE_ONCHANGE_MIXIN = {
|
||||
blockA.unplug();
|
||||
if (prevA && !prevA.isDisposed() && !prevA.isShadow()) {
|
||||
// The shadow block is automatically replaced during unplug().
|
||||
this.getInput('A').connection.connect(prevA.outputConnection);
|
||||
this.getInput('A')!.connection!.connect(prevA.outputConnection!);
|
||||
}
|
||||
}
|
||||
const prevB = this.prevBlocks_[1];
|
||||
@@ -592,7 +636,7 @@ const LOGIC_COMPARE_ONCHANGE_MIXIN = {
|
||||
blockB.unplug();
|
||||
if (prevB && !prevB.isDisposed() && !prevB.isShadow()) {
|
||||
// The shadow block is automatically replaced during unplug().
|
||||
this.getInput('B').connection.connect(prevB.outputConnection);
|
||||
this.getInput('B')!.connection!.connect(prevB.outputConnection!);
|
||||
}
|
||||
}
|
||||
this.bumpNeighbours();
|
||||
@@ -606,43 +650,47 @@ const LOGIC_COMPARE_ONCHANGE_MIXIN = {
|
||||
/**
|
||||
* "logic_compare" extension function. Adds type left and right side type
|
||||
* checking to "logic_compare" blocks.
|
||||
* @this {Block}
|
||||
* @readonly
|
||||
*/
|
||||
const LOGIC_COMPARE_EXTENSION = function() {
|
||||
const LOGIC_COMPARE_EXTENSION = function (this: CompareBlock) {
|
||||
// Add onchange handler to ensure types are compatible.
|
||||
this.mixin(LOGIC_COMPARE_ONCHANGE_MIXIN);
|
||||
};
|
||||
|
||||
Extensions.register('logic_compare', LOGIC_COMPARE_EXTENSION);
|
||||
|
||||
/** Type of a block that has LOGIC_TERNARY_ONCHANGE_MIXIN */
|
||||
type TernaryBlock = Block & TernaryMixin;
|
||||
interface TernaryMixin extends TernaryMixinType {}
|
||||
type TernaryMixinType = typeof LOGIC_TERNARY_ONCHANGE_MIXIN;
|
||||
|
||||
/**
|
||||
* Adds type coordination between inputs and output.
|
||||
* @mixin
|
||||
* @augments Block
|
||||
* @readonly
|
||||
*/
|
||||
const LOGIC_TERNARY_ONCHANGE_MIXIN = {
|
||||
prevParentConnection_: null,
|
||||
prevParentConnection_: null as Connection | null,
|
||||
|
||||
/**
|
||||
* Called whenever anything on the workspace changes.
|
||||
* Prevent mismatched types.
|
||||
* @param {!AbstractEvent} e Change event.
|
||||
* @this {Block}
|
||||
*/
|
||||
onchange: function(e) {
|
||||
onchange: function (this: TernaryBlock, e: AbstractEvent) {
|
||||
const blockA = this.getInputTargetBlock('THEN');
|
||||
const blockB = this.getInputTargetBlock('ELSE');
|
||||
const parentConnection = this.outputConnection.targetConnection;
|
||||
// Disconnect blocks that existed prior to this change if they don't match.
|
||||
const parentConnection = this.outputConnection!.targetConnection;
|
||||
// Disconnect blocks that existed prior to this change if they don't
|
||||
// match.
|
||||
if ((blockA || blockB) && parentConnection) {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const block = (i === 1) ? blockA : blockB;
|
||||
if (block &&
|
||||
const block = i === 1 ? blockA : blockB;
|
||||
if (
|
||||
block &&
|
||||
!block.workspace.connectionChecker.doTypeChecks(
|
||||
block.outputConnection, parentConnection)) {
|
||||
// Ensure that any disconnections are grouped with the causing event.
|
||||
block.outputConnection!,
|
||||
parentConnection
|
||||
)
|
||||
) {
|
||||
// Ensure that any disconnections are grouped with the causing
|
||||
// event.
|
||||
Events.setGroup(e.group);
|
||||
if (parentConnection === this.prevParentConnection_) {
|
||||
this.unplug();
|
||||
@@ -4,60 +4,55 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Loop blocks for Blockly.
|
||||
* @suppress {checkTypes}
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
goog.module('Blockly.libraryBlocks.loops');
|
||||
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const AbstractEvent = goog.requireType('Blockly.Events.Abstract');
|
||||
const ContextMenu = goog.require('Blockly.ContextMenu');
|
||||
const Events = goog.require('Blockly.Events');
|
||||
const Extensions = goog.require('Blockly.Extensions');
|
||||
const Variables = goog.require('Blockly.Variables');
|
||||
const xmlUtils = goog.require('Blockly.utils.xml');
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const {Block} = goog.requireType('Blockly.Block');
|
||||
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
|
||||
// TODO (6248): Properly import the BlockDefinition type.
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const BlockDefinition = Object;
|
||||
const {Msg} = goog.require('Blockly.Msg');
|
||||
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
|
||||
/** @suppress {extraRequire} */
|
||||
goog.require('Blockly.FieldDropdown');
|
||||
/** @suppress {extraRequire} */
|
||||
goog.require('Blockly.FieldLabel');
|
||||
/** @suppress {extraRequire} */
|
||||
goog.require('Blockly.FieldNumber');
|
||||
/** @suppress {extraRequire} */
|
||||
goog.require('Blockly.FieldVariable');
|
||||
/** @suppress {extraRequire} */
|
||||
goog.require('Blockly.Warning');
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.libraryBlocks.loops');
|
||||
|
||||
import type {Abstract as AbstractEvent} from '../core/events/events_abstract.js';
|
||||
import type {Block} from '../core/block.js';
|
||||
import * as ContextMenu from '../core/contextmenu.js';
|
||||
import type {
|
||||
ContextMenuOption,
|
||||
LegacyContextMenuOption,
|
||||
} from '../core/contextmenu_registry.js';
|
||||
import * as Events from '../core/events/events.js';
|
||||
import * as Extensions from '../core/extensions.js';
|
||||
import * as Variables from '../core/variables.js';
|
||||
import * as xmlUtils from '../core/utils/xml.js';
|
||||
import {Msg} from '../core/msg.js';
|
||||
import {
|
||||
createBlockDefinitionsFromJsonArray,
|
||||
defineBlocks,
|
||||
} from '../core/common.js';
|
||||
import '../core/field_dropdown.js';
|
||||
import '../core/field_label.js';
|
||||
import '../core/field_number.js';
|
||||
import '../core/field_variable.js';
|
||||
import '../core/icons/warning_icon.js';
|
||||
import {FieldVariable} from '../core/field_variable.js';
|
||||
import {WorkspaceSvg} from '../core/workspace_svg.js';
|
||||
|
||||
/**
|
||||
* A dictionary of the block definitions provided by this module.
|
||||
* @type {!Object<string, !BlockDefinition>}
|
||||
*/
|
||||
const blocks = createBlockDefinitionsFromJsonArray([
|
||||
export const blocks = createBlockDefinitionsFromJsonArray([
|
||||
// Block for repeat n times (external number).
|
||||
{
|
||||
'type': 'controls_repeat_ext',
|
||||
'message0': '%{BKY_CONTROLS_REPEAT_TITLE}',
|
||||
'args0': [{
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'TIMES',
|
||||
'check': 'Number',
|
||||
}],
|
||||
},
|
||||
],
|
||||
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
|
||||
'args1': [{
|
||||
'args1': [
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'DO',
|
||||
}],
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
'style': 'loop_blocks',
|
||||
@@ -69,18 +64,22 @@ const blocks = createBlockDefinitionsFromJsonArray([
|
||||
{
|
||||
'type': 'controls_repeat',
|
||||
'message0': '%{BKY_CONTROLS_REPEAT_TITLE}',
|
||||
'args0': [{
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_number',
|
||||
'name': 'TIMES',
|
||||
'value': 10,
|
||||
'min': 0,
|
||||
'precision': 1,
|
||||
}],
|
||||
},
|
||||
],
|
||||
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
|
||||
'args1': [{
|
||||
'args1': [
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'DO',
|
||||
}],
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
'style': 'loop_blocks',
|
||||
@@ -107,10 +106,12 @@ const blocks = createBlockDefinitionsFromJsonArray([
|
||||
},
|
||||
],
|
||||
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
|
||||
'args1': [{
|
||||
'args1': [
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'DO',
|
||||
}],
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
'style': 'loop_blocks',
|
||||
@@ -147,19 +148,18 @@ const blocks = createBlockDefinitionsFromJsonArray([
|
||||
},
|
||||
],
|
||||
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
|
||||
'args1': [{
|
||||
'args1': [
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'DO',
|
||||
}],
|
||||
},
|
||||
],
|
||||
'inputsInline': true,
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
'style': 'loop_blocks',
|
||||
'helpUrl': '%{BKY_CONTROLS_FOR_HELPURL}',
|
||||
'extensions': [
|
||||
'contextMenu_newGetVariableBlock',
|
||||
'controls_for_tooltip',
|
||||
],
|
||||
'extensions': ['contextMenu_newGetVariableBlock', 'controls_for_tooltip'],
|
||||
},
|
||||
// Block for 'for each' loop.
|
||||
{
|
||||
@@ -178,10 +178,12 @@ const blocks = createBlockDefinitionsFromJsonArray([
|
||||
},
|
||||
],
|
||||
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
|
||||
'args1': [{
|
||||
'args1': [
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'DO',
|
||||
}],
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
'style': 'loop_blocks',
|
||||
@@ -195,30 +197,28 @@ const blocks = createBlockDefinitionsFromJsonArray([
|
||||
{
|
||||
'type': 'controls_flow_statements',
|
||||
'message0': '%1',
|
||||
'args0': [{
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_dropdown',
|
||||
'name': 'FLOW',
|
||||
'options': [
|
||||
['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}', 'BREAK'],
|
||||
['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}', 'CONTINUE'],
|
||||
],
|
||||
}],
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'style': 'loop_blocks',
|
||||
'helpUrl': '%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}',
|
||||
'suppressPrefixSuffix': true,
|
||||
'extensions': [
|
||||
'controls_flow_tooltip',
|
||||
'controls_flow_in_loop_check',
|
||||
],
|
||||
'extensions': ['controls_flow_tooltip', 'controls_flow_in_loop_check'],
|
||||
},
|
||||
]);
|
||||
exports.blocks = blocks;
|
||||
|
||||
/**
|
||||
* Tooltips for the 'controls_whileUntil' block, keyed by MODE value.
|
||||
*
|
||||
* @see {Extensions#buildTooltipForDropdown}
|
||||
* @readonly
|
||||
*/
|
||||
const WHILE_UNTIL_TOOLTIPS = {
|
||||
'WHILE': '%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_WHILE}',
|
||||
@@ -227,12 +227,13 @@ const WHILE_UNTIL_TOOLTIPS = {
|
||||
|
||||
Extensions.register(
|
||||
'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.
|
||||
*
|
||||
* @see {Extensions#buildTooltipForDropdown}
|
||||
* @readonly
|
||||
*/
|
||||
const BREAK_CONTINUE_TOOLTIPS = {
|
||||
'BREAK': '%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK}',
|
||||
@@ -241,54 +242,65 @@ const BREAK_CONTINUE_TOOLTIPS = {
|
||||
|
||||
Extensions.register(
|
||||
'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 CustomContextMenuBlock = Block & CustomContextMenuMixin;
|
||||
interface CustomContextMenuMixin extends CustomContextMenuMixinType {}
|
||||
type CustomContextMenuMixinType =
|
||||
typeof CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN;
|
||||
|
||||
/**
|
||||
* Mixin to add a context menu item to create a 'variables_get' block.
|
||||
* Used by blocks 'controls_for' and 'controls_forEach'.
|
||||
* @mixin
|
||||
* @augments Block
|
||||
* @package
|
||||
* @readonly
|
||||
*/
|
||||
const CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN = {
|
||||
/**
|
||||
* Add context menu option to create getter block for the loop's variable.
|
||||
* (customContextMenu support limited to web BlockSvg.)
|
||||
* @param {!Array} options List of menu options to add to.
|
||||
* @this {Block}
|
||||
*
|
||||
* @param options List of menu options to add to.
|
||||
*/
|
||||
customContextMenu: function(options) {
|
||||
customContextMenu: function (
|
||||
this: CustomContextMenuBlock,
|
||||
options: Array<ContextMenuOption | LegacyContextMenuOption>
|
||||
) {
|
||||
if (this.isInFlyout) {
|
||||
return;
|
||||
}
|
||||
const variable = this.getField('VAR').getVariable();
|
||||
const varField = this.getField('VAR') as FieldVariable;
|
||||
const variable = varField.getVariable()!;
|
||||
const varName = variable.name;
|
||||
if (!this.isCollapsed() && varName !== null) {
|
||||
const option = {enabled: true};
|
||||
option.text = Msg['VARIABLES_SET_CREATE_GET'].replace('%1', varName);
|
||||
const xmlField = Variables.generateVariableFieldDom(variable);
|
||||
const xmlBlock = xmlUtils.createElement('block');
|
||||
xmlBlock.setAttribute('type', 'variables_get');
|
||||
xmlBlock.appendChild(xmlField);
|
||||
option.callback = ContextMenu.callbackFactory(this, xmlBlock);
|
||||
options.push(option);
|
||||
|
||||
options.push({
|
||||
enabled: true,
|
||||
text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', varName),
|
||||
callback: ContextMenu.callbackFactory(this, xmlBlock),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Extensions.registerMixin(
|
||||
'contextMenu_newGetVariableBlock',
|
||||
CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN);
|
||||
CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN
|
||||
);
|
||||
|
||||
Extensions.register(
|
||||
'controls_for_tooltip',
|
||||
Extensions.buildTooltipWithFieldText('%{BKY_CONTROLS_FOR_TOOLTIP}', 'VAR'));
|
||||
Extensions.buildTooltipWithFieldText('%{BKY_CONTROLS_FOR_TOOLTIP}', 'VAR')
|
||||
);
|
||||
|
||||
Extensions.register(
|
||||
'controls_forEach_tooltip',
|
||||
Extensions.buildTooltipWithFieldText(
|
||||
'%{BKY_CONTROLS_FOREACH_TOOLTIP}', 'VAR'));
|
||||
Extensions.buildTooltipWithFieldText('%{BKY_CONTROLS_FOREACH_TOOLTIP}', 'VAR')
|
||||
);
|
||||
|
||||
/**
|
||||
* List of block types that are loops and thus do not need warnings.
|
||||
@@ -304,34 +316,33 @@ Extensions.register(
|
||||
*
|
||||
* // Else if using blockly_compressed + blockss_compressed.js in browser:
|
||||
* Blockly.libraryBlocks.loopTypes.add('custom_loop');
|
||||
*
|
||||
* @type {!Set<string>}
|
||||
*/
|
||||
const loopTypes = new Set([
|
||||
export const loopTypes: Set<string> = new Set([
|
||||
'controls_repeat',
|
||||
'controls_repeat_ext',
|
||||
'controls_forEach',
|
||||
'controls_for',
|
||||
'controls_whileUntil',
|
||||
]);
|
||||
exports.loopTypes = loopTypes;
|
||||
|
||||
/** Type of a block that has CONTROL_FLOW_IN_LOOP_CHECK_MIXIN */
|
||||
type ControlFlowInLoopBlock = Block & ControlFlowInLoopMixin;
|
||||
interface ControlFlowInLoopMixin extends ControlFlowInLoopMixinType {}
|
||||
type ControlFlowInLoopMixinType = typeof CONTROL_FLOW_IN_LOOP_CHECK_MIXIN;
|
||||
|
||||
/**
|
||||
* This mixin adds a check to make sure the 'controls_flow_statements' block
|
||||
* is contained in a loop. Otherwise a warning is added to the block.
|
||||
* @mixin
|
||||
* @augments Block
|
||||
* @public
|
||||
* @readonly
|
||||
*/
|
||||
const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
|
||||
/**
|
||||
* Is this block enclosed (at any level) by a loop?
|
||||
* @return {Block} The nearest surrounding loop, or null if none.
|
||||
* @this {Block}
|
||||
*
|
||||
* @returns The nearest surrounding loop, or null if none.
|
||||
*/
|
||||
getSurroundLoop: function() {
|
||||
let block = this;
|
||||
getSurroundLoop: function (this: ControlFlowInLoopBlock): Block | null {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
let block: Block | null = this;
|
||||
do {
|
||||
if (loopTypes.has(block.type)) {
|
||||
return block;
|
||||
@@ -344,20 +355,19 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
|
||||
/**
|
||||
* Called whenever anything on the workspace changes.
|
||||
* Add warning if this flow block is not nested inside a loop.
|
||||
* @param {!AbstractEvent} e Move event.
|
||||
* @this {Block}
|
||||
*/
|
||||
onchange: function(e) {
|
||||
onchange: function (this: ControlFlowInLoopBlock, e: AbstractEvent) {
|
||||
const ws = this.workspace as WorkspaceSvg;
|
||||
// Don't change state if:
|
||||
// * It's at the start of a drag.
|
||||
// * It's not a move event.
|
||||
if (!this.workspace.isDragging || this.workspace.isDragging() ||
|
||||
e.type !== Events.BLOCK_MOVE) {
|
||||
if (!ws.isDragging || ws.isDragging() || e.type !== Events.BLOCK_MOVE) {
|
||||
return;
|
||||
}
|
||||
const enabled = this.getSurroundLoop(this);
|
||||
const enabled = !!this.getSurroundLoop();
|
||||
this.setWarningText(
|
||||
enabled ? null : Msg['CONTROLS_FLOW_STATEMENTS_WARNING']);
|
||||
enabled ? null : Msg['CONTROLS_FLOW_STATEMENTS_WARNING']
|
||||
);
|
||||
if (!this.isInFlyout) {
|
||||
const group = Events.getGroup();
|
||||
// Makes it so the move and the disable event get undone together.
|
||||
@@ -369,7 +379,9 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
|
||||
};
|
||||
|
||||
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.
|
||||
defineBlocks(blocks);
|
||||
@@ -4,26 +4,22 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Math blocks for Blockly.
|
||||
*/
|
||||
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.libraryBlocks.math');
|
||||
|
||||
import * as Extensions from '../core/extensions.js';
|
||||
import type {Field} from '../core/field.js';
|
||||
import type {FieldDropdown} from '../core/field_dropdown.js';
|
||||
import * as xmlUtils from '../core/utils/xml.js';
|
||||
import type {Block} from '../core/block.js';
|
||||
import type {BlockDefinition} from '../core/blocks.js';
|
||||
import {createBlockDefinitionsFromJsonArray, defineBlocks} from '../core/common.js';
|
||||
import {
|
||||
createBlockDefinitionsFromJsonArray,
|
||||
defineBlocks,
|
||||
} from '../core/common.js';
|
||||
import '../core/field_dropdown.js';
|
||||
import '../core/field_label.js';
|
||||
import '../core/field_number.js';
|
||||
import '../core/field_variable.js';
|
||||
|
||||
|
||||
/**
|
||||
* A dictionary of the block definitions provided by this module.
|
||||
*/
|
||||
@@ -32,11 +28,13 @@ export const blocks = createBlockDefinitionsFromJsonArray([
|
||||
{
|
||||
'type': 'math_number',
|
||||
'message0': '%1',
|
||||
'args0': [{
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_number',
|
||||
'name': 'NUM',
|
||||
'value': 0,
|
||||
}],
|
||||
},
|
||||
],
|
||||
'output': 'Number',
|
||||
'helpUrl': '%{BKY_MATH_NUMBER_HELPURL}',
|
||||
'style': 'math_blocks',
|
||||
@@ -429,11 +427,12 @@ const TOOLTIPS_BY_OP = {
|
||||
|
||||
Extensions.register(
|
||||
'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 DivisiblebyBlock = Block & DivisiblebyMixin;
|
||||
interface DivisiblebyMixin extends DivisiblebyMixinType {};
|
||||
interface DivisiblebyMixin extends DivisiblebyMixinType {}
|
||||
type DivisiblebyMixinType = typeof IS_DIVISIBLEBY_MUTATOR_MIXIN;
|
||||
|
||||
/**
|
||||
@@ -453,7 +452,7 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
|
||||
*/
|
||||
mutationToDom: function (this: DivisiblebyBlock): Element {
|
||||
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));
|
||||
return container;
|
||||
},
|
||||
@@ -464,7 +463,7 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
|
||||
* @param xmlElement XML storage element.
|
||||
*/
|
||||
domToMutation: function (this: DivisiblebyBlock, xmlElement: Element) {
|
||||
const divisorInput = (xmlElement.getAttribute('divisor_input') === 'true');
|
||||
const divisorInput = xmlElement.getAttribute('divisor_input') === 'true';
|
||||
this.updateShape_(divisorInput);
|
||||
},
|
||||
|
||||
@@ -500,24 +499,28 @@ const IS_DIVISIBLE_MUTATOR_EXTENSION = function(this: DivisiblebyBlock) {
|
||||
this.getField('PROPERTY')!.setValidator(
|
||||
/** @param option The selected dropdown option. */
|
||||
function (this: FieldDropdown, option: string) {
|
||||
const divisorInput = (option === 'DIVISIBLE_BY');
|
||||
const divisorInput = option === 'DIVISIBLE_BY';
|
||||
(this.getSourceBlock() as DivisiblebyBlock).updateShape_(divisorInput);
|
||||
return undefined; // FieldValidators can't be void. Use option as-is.
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
Extensions.registerMutator(
|
||||
'math_is_divisibleby_mutator', IS_DIVISIBLEBY_MUTATOR_MIXIN,
|
||||
IS_DIVISIBLE_MUTATOR_EXTENSION);
|
||||
'math_is_divisibleby_mutator',
|
||||
IS_DIVISIBLEBY_MUTATOR_MIXIN,
|
||||
IS_DIVISIBLE_MUTATOR_EXTENSION
|
||||
);
|
||||
|
||||
// Update the tooltip of 'math_change' block to reference the variable.
|
||||
Extensions.register(
|
||||
'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 ListModesBlock = Block & ListModesMixin;
|
||||
interface ListModesMixin extends ListModesMixinType {};
|
||||
interface ListModesMixin extends ListModesMixinType {}
|
||||
type ListModesMixinType = typeof LIST_MODES_MUTATOR_MIXIN;
|
||||
|
||||
/**
|
||||
@@ -575,12 +578,15 @@ const LIST_MODES_MUTATOR_EXTENSION = function(this: ListModesBlock) {
|
||||
function (this: ListModesBlock, newOp: string) {
|
||||
this.updateType_(newOp);
|
||||
return undefined;
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
Extensions.registerMutator(
|
||||
'math_modes_of_list_mutator', LIST_MODES_MUTATOR_MIXIN,
|
||||
LIST_MODES_MUTATOR_EXTENSION);
|
||||
'math_modes_of_list_mutator',
|
||||
LIST_MODES_MUTATOR_MIXIN,
|
||||
LIST_MODES_MUTATOR_EXTENSION
|
||||
);
|
||||
|
||||
// Register provided blocks.
|
||||
defineBlocks(blocks);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,37 +4,31 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Variable blocks for Blockly.
|
||||
* @suppress {checkTypes}
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
goog.module('Blockly.libraryBlocks.variables');
|
||||
|
||||
const ContextMenu = goog.require('Blockly.ContextMenu');
|
||||
const Extensions = goog.require('Blockly.Extensions');
|
||||
const Variables = goog.require('Blockly.Variables');
|
||||
const xmlUtils = goog.require('Blockly.utils.xml');
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const {Block} = goog.requireType('Blockly.Block');
|
||||
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
|
||||
// TODO (6248): Properly import the BlockDefinition type.
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const BlockDefinition = Object;
|
||||
const {Msg} = goog.require('Blockly.Msg');
|
||||
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
|
||||
/** @suppress {extraRequire} */
|
||||
goog.require('Blockly.FieldLabel');
|
||||
/** @suppress {extraRequire} */
|
||||
goog.require('Blockly.FieldVariable');
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.libraryBlocks.variables');
|
||||
|
||||
import * as ContextMenu from '../core/contextmenu.js';
|
||||
import * as Extensions from '../core/extensions.js';
|
||||
import * as Variables from '../core/variables.js';
|
||||
import * as xmlUtils from '../core/utils/xml.js';
|
||||
import type {Block} from '../core/block.js';
|
||||
import type {
|
||||
ContextMenuOption,
|
||||
LegacyContextMenuOption,
|
||||
} from '../core/contextmenu_registry.js';
|
||||
import {FieldVariable} from '../core/field_variable.js';
|
||||
import {Msg} from '../core/msg.js';
|
||||
import type {WorkspaceSvg} from '../core/workspace_svg.js';
|
||||
import {
|
||||
createBlockDefinitionsFromJsonArray,
|
||||
defineBlocks,
|
||||
} from '../core/common.js';
|
||||
import '../core/field_label.js';
|
||||
|
||||
/**
|
||||
* A dictionary of the block definitions provided by this module.
|
||||
* @type {!Object<string, !BlockDefinition>}
|
||||
*/
|
||||
const blocks = createBlockDefinitionsFromJsonArray([
|
||||
export const blocks = createBlockDefinitionsFromJsonArray([
|
||||
// Block for variable getter.
|
||||
{
|
||||
'type': 'variables_get',
|
||||
@@ -75,25 +69,28 @@ const blocks = createBlockDefinitionsFromJsonArray([
|
||||
'extensions': ['contextMenu_variableSetterGetter'],
|
||||
},
|
||||
]);
|
||||
exports.blocks = blocks;
|
||||
|
||||
/** Type of a block that has CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN */
|
||||
type VariableBlock = Block & VariableMixin;
|
||||
interface VariableMixin extends VariableMixinType {}
|
||||
type VariableMixinType =
|
||||
typeof CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN;
|
||||
|
||||
/**
|
||||
* Mixin to add context menu items to create getter/setter blocks for this
|
||||
* setter/getter.
|
||||
* Used by blocks 'variables_set' and 'variables_get'.
|
||||
* @mixin
|
||||
* @augments Block
|
||||
* @package
|
||||
* @readonly
|
||||
*/
|
||||
const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
|
||||
/**
|
||||
* Add menu option to create getter/setter block for this setter/getter.
|
||||
* @param {!Array} options List of menu options to add to.
|
||||
* @this {Block}
|
||||
*
|
||||
* @param options List of menu options to add to.
|
||||
*/
|
||||
customContextMenu: function(options) {
|
||||
customContextMenu: function (
|
||||
this: VariableBlock,
|
||||
options: Array<ContextMenuOption | LegacyContextMenuOption>
|
||||
) {
|
||||
if (!this.isInFlyout) {
|
||||
let oppositeType;
|
||||
let contextMenuMsg;
|
||||
@@ -106,27 +103,31 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
|
||||
contextMenuMsg = Msg['VARIABLES_SET_CREATE_GET'];
|
||||
}
|
||||
|
||||
const option = {enabled: this.workspace.remainingCapacity() > 0};
|
||||
const name = this.getField('VAR').getText();
|
||||
option.text = contextMenuMsg.replace('%1', name);
|
||||
const name = this.getField('VAR')!.getText();
|
||||
const xmlField = xmlUtils.createElement('field');
|
||||
xmlField.setAttribute('name', 'VAR');
|
||||
xmlField.appendChild(xmlUtils.createTextNode(name));
|
||||
const xmlBlock = xmlUtils.createElement('block');
|
||||
xmlBlock.setAttribute('type', oppositeType);
|
||||
xmlBlock.appendChild(xmlField);
|
||||
option.callback = ContextMenu.callbackFactory(this, xmlBlock);
|
||||
options.push(option);
|
||||
|
||||
options.push({
|
||||
enabled: this.workspace.remainingCapacity() > 0,
|
||||
text: contextMenuMsg.replace('%1', name),
|
||||
callback: ContextMenu.callbackFactory(this, xmlBlock),
|
||||
});
|
||||
// Getter blocks have the option to rename or delete that variable.
|
||||
} else {
|
||||
if (this.type === 'variables_get' ||
|
||||
this.type === 'variables_get_reporter') {
|
||||
if (
|
||||
this.type === 'variables_get' ||
|
||||
this.type === 'variables_get_reporter'
|
||||
) {
|
||||
const renameOption = {
|
||||
text: Msg['RENAME_VARIABLE'],
|
||||
enabled: true,
|
||||
callback: renameOptionCallbackFactory(this),
|
||||
};
|
||||
const name = this.getField('VAR').getText();
|
||||
const name = this.getField('VAR')!.getText();
|
||||
const deleteOption = {
|
||||
text: Msg['DELETE_VARIABLE'].replace('%1', name),
|
||||
enabled: true,
|
||||
@@ -142,13 +143,17 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
|
||||
/**
|
||||
* Factory for callbacks for rename variable dropdown menu option
|
||||
* associated with a variable getter block.
|
||||
* @param {!Block} block The block with the variable to rename.
|
||||
* @return {!function()} A function that renames the variable.
|
||||
*
|
||||
* @param block The block with the variable to rename.
|
||||
* @returns A function that renames the variable.
|
||||
*/
|
||||
const renameOptionCallbackFactory = function(block) {
|
||||
const renameOptionCallbackFactory = function (
|
||||
block: VariableBlock
|
||||
): () => void {
|
||||
return function () {
|
||||
const workspace = block.workspace;
|
||||
const variable = block.getField('VAR').getVariable();
|
||||
const variableField = block.getField('VAR') as FieldVariable;
|
||||
const variable = variableField.getVariable()!;
|
||||
Variables.renameVariable(workspace, variable);
|
||||
};
|
||||
};
|
||||
@@ -156,21 +161,26 @@ const renameOptionCallbackFactory = function(block) {
|
||||
/**
|
||||
* Factory for callbacks for delete variable dropdown menu option
|
||||
* associated with a variable getter block.
|
||||
* @param {!Block} block The block with the variable to delete.
|
||||
* @return {!function()} A function that deletes the variable.
|
||||
*
|
||||
* @param block The block with the variable to delete.
|
||||
* @returns A function that deletes the variable.
|
||||
*/
|
||||
const deleteOptionCallbackFactory = function(block) {
|
||||
const deleteOptionCallbackFactory = function (
|
||||
block: VariableBlock
|
||||
): () => void {
|
||||
return function () {
|
||||
const workspace = block.workspace;
|
||||
const variable = block.getField('VAR').getVariable();
|
||||
const variableField = block.getField('VAR') as FieldVariable;
|
||||
const variable = variableField.getVariable()!;
|
||||
workspace.deleteVariableById(variable.getId());
|
||||
workspace.refreshToolboxSelection();
|
||||
(workspace as WorkspaceSvg).refreshToolboxSelection();
|
||||
};
|
||||
};
|
||||
|
||||
Extensions.registerMixin(
|
||||
'contextMenu_variableSetterGetter',
|
||||
CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN);
|
||||
CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN
|
||||
);
|
||||
|
||||
// Register provided blocks.
|
||||
defineBlocks(blocks);
|
||||
@@ -4,48 +4,43 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Variable blocks for Blockly.
|
||||
* @suppress {checkTypes}
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
goog.module('Blockly.libraryBlocks.variablesDynamic');
|
||||
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const AbstractEvent = goog.requireType('Blockly.Events.Abstract');
|
||||
const ContextMenu = goog.require('Blockly.ContextMenu');
|
||||
const Extensions = goog.require('Blockly.Extensions');
|
||||
const Variables = goog.require('Blockly.Variables');
|
||||
const xml = goog.require('Blockly.utils.xml');
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const {Block} = goog.requireType('Blockly.Block');
|
||||
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
|
||||
// TODO (6248): Properly import the BlockDefinition type.
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const BlockDefinition = Object;
|
||||
const {Msg} = goog.require('Blockly.Msg');
|
||||
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
|
||||
/** @suppress {extraRequire} */
|
||||
goog.require('Blockly.FieldLabel');
|
||||
/** @suppress {extraRequire} */
|
||||
goog.require('Blockly.FieldVariable');
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.libraryBlocks.variablesDynamic');
|
||||
|
||||
import * as ContextMenu from '../core/contextmenu.js';
|
||||
import * as Extensions from '../core/extensions.js';
|
||||
import * as Variables from '../core/variables.js';
|
||||
import * as xml from '../core/utils/xml.js';
|
||||
import {Abstract as AbstractEvent} from '../core/events/events_abstract.js';
|
||||
import type {Block} from '../core/block.js';
|
||||
import type {
|
||||
ContextMenuOption,
|
||||
LegacyContextMenuOption,
|
||||
} from '../core/contextmenu_registry.js';
|
||||
import {FieldVariable} from '../core/field_variable.js';
|
||||
import {Msg} from '../core/msg.js';
|
||||
import type {WorkspaceSvg} from '../core/workspace_svg.js';
|
||||
import {
|
||||
createBlockDefinitionsFromJsonArray,
|
||||
defineBlocks,
|
||||
} from '../core/common.js';
|
||||
import '../core/field_label.js';
|
||||
|
||||
/**
|
||||
* A dictionary of the block definitions provided by this module.
|
||||
* @type {!Object<string, !BlockDefinition>}
|
||||
*/
|
||||
const blocks = createBlockDefinitionsFromJsonArray([
|
||||
export const blocks = createBlockDefinitionsFromJsonArray([
|
||||
// Block for variable getter.
|
||||
{
|
||||
'type': 'variables_get_dynamic',
|
||||
'message0': '%1',
|
||||
'args0': [{
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_variable',
|
||||
'name': 'VAR',
|
||||
'variable': '%{BKY_VARIABLES_DEFAULT_NAME}',
|
||||
}],
|
||||
},
|
||||
],
|
||||
'output': null,
|
||||
'style': 'variable_dynamic_blocks',
|
||||
'helpUrl': '%{BKY_VARIABLES_GET_HELPURL}',
|
||||
@@ -75,30 +70,35 @@ const blocks = createBlockDefinitionsFromJsonArray([
|
||||
'extensions': ['contextMenu_variableDynamicSetterGetter'],
|
||||
},
|
||||
]);
|
||||
exports.blocks = blocks;
|
||||
|
||||
/** Type of a block that has CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN */
|
||||
type VariableBlock = Block & VariableMixin;
|
||||
interface VariableMixin extends VariableMixinType {}
|
||||
type VariableMixinType =
|
||||
typeof CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN;
|
||||
|
||||
/**
|
||||
* Mixin to add context menu items to create getter/setter blocks for this
|
||||
* setter/getter.
|
||||
* Used by blocks 'variables_set_dynamic' and 'variables_get_dynamic'.
|
||||
* @mixin
|
||||
* @augments Block
|
||||
* @readonly
|
||||
*/
|
||||
const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
|
||||
/**
|
||||
* Add menu option to create getter/setter block for this setter/getter.
|
||||
* @param {!Array} options List of menu options to add to.
|
||||
* @this {Block}
|
||||
*
|
||||
* @param options List of menu options to add to.
|
||||
*/
|
||||
customContextMenu: function(options) {
|
||||
customContextMenu: function (
|
||||
this: VariableBlock,
|
||||
options: Array<ContextMenuOption | LegacyContextMenuOption>
|
||||
) {
|
||||
// Getter blocks have the option to create a setter block, and vice versa.
|
||||
if (!this.isInFlyout) {
|
||||
let oppositeType;
|
||||
let contextMenuMsg;
|
||||
const id = this.getFieldValue('VAR');
|
||||
const variableModel = this.workspace.getVariableById(id);
|
||||
const varType = variableModel.type;
|
||||
const varType = variableModel!.type;
|
||||
if (this.type === 'variables_get_dynamic') {
|
||||
oppositeType = 'variables_set_dynamic';
|
||||
contextMenuMsg = Msg['VARIABLES_GET_CREATE_SET'];
|
||||
@@ -107,9 +107,7 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
|
||||
contextMenuMsg = Msg['VARIABLES_SET_CREATE_GET'];
|
||||
}
|
||||
|
||||
const option = {enabled: this.workspace.remainingCapacity() > 0};
|
||||
const name = this.getField('VAR').getText();
|
||||
option.text = contextMenuMsg.replace('%1', name);
|
||||
const name = this.getField('VAR')!.getText();
|
||||
const xmlField = xml.createElement('field');
|
||||
xmlField.setAttribute('name', 'VAR');
|
||||
xmlField.setAttribute('variabletype', varType);
|
||||
@@ -117,17 +115,23 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
|
||||
const xmlBlock = xml.createElement('block');
|
||||
xmlBlock.setAttribute('type', oppositeType);
|
||||
xmlBlock.appendChild(xmlField);
|
||||
option.callback = ContextMenu.callbackFactory(this, xmlBlock);
|
||||
options.push(option);
|
||||
|
||||
options.push({
|
||||
enabled: this.workspace.remainingCapacity() > 0,
|
||||
text: contextMenuMsg.replace('%1', name),
|
||||
callback: ContextMenu.callbackFactory(this, xmlBlock),
|
||||
});
|
||||
} else {
|
||||
if (this.type === 'variables_get_dynamic' ||
|
||||
this.type === 'variables_get_reporter_dynamic') {
|
||||
if (
|
||||
this.type === 'variables_get_dynamic' ||
|
||||
this.type === 'variables_get_reporter_dynamic'
|
||||
) {
|
||||
const renameOption = {
|
||||
text: Msg['RENAME_VARIABLE'],
|
||||
enabled: true,
|
||||
callback: renameOptionCallbackFactory(this),
|
||||
};
|
||||
const name = this.getField('VAR').getText();
|
||||
const name = this.getField('VAR')!.getText();
|
||||
const deleteOption = {
|
||||
text: Msg['DELETE_VARIABLE'].replace('%1', name),
|
||||
enabled: true,
|
||||
@@ -141,16 +145,16 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
|
||||
/**
|
||||
* Called whenever anything on the workspace changes.
|
||||
* Set the connection type for this block.
|
||||
* @param {AbstractEvent} _e Change event.
|
||||
* @this {Block}
|
||||
*
|
||||
* @param _e Change event.
|
||||
*/
|
||||
onchange: function(_e) {
|
||||
onchange: function (this: VariableBlock, _e: AbstractEvent) {
|
||||
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') {
|
||||
this.outputConnection.setCheck(variableModel.type);
|
||||
this.outputConnection!.setCheck(variableModel.type);
|
||||
} else {
|
||||
this.getInput('VALUE').connection.setCheck(variableModel.type);
|
||||
this.getInput('VALUE')!.connection!.setCheck(variableModel.type);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -158,13 +162,15 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
|
||||
/**
|
||||
* Factory for callbacks for rename variable dropdown menu option
|
||||
* associated with a variable getter block.
|
||||
* @param {!Block} block The block with the variable to rename.
|
||||
* @return {!function()} A function that renames the variable.
|
||||
*
|
||||
* @param block The block with the variable to rename.
|
||||
* @returns A function that renames the variable.
|
||||
*/
|
||||
const renameOptionCallbackFactory = function(block) {
|
||||
const renameOptionCallbackFactory = function (block: VariableBlock) {
|
||||
return function () {
|
||||
const workspace = block.workspace;
|
||||
const variable = block.getField('VAR').getVariable();
|
||||
const variableField = block.getField('VAR') as FieldVariable;
|
||||
const variable = variableField.getVariable()!;
|
||||
Variables.renameVariable(workspace, variable);
|
||||
};
|
||||
};
|
||||
@@ -172,21 +178,24 @@ const renameOptionCallbackFactory = function(block) {
|
||||
/**
|
||||
* Factory for callbacks for delete variable dropdown menu option
|
||||
* associated with a variable getter block.
|
||||
* @param {!Block} block The block with the variable to delete.
|
||||
* @return {!function()} A function that deletes the variable.
|
||||
*
|
||||
* @param block The block with the variable to delete.
|
||||
* @returns A function that deletes the variable.
|
||||
*/
|
||||
const deleteOptionCallbackFactory = function(block) {
|
||||
const deleteOptionCallbackFactory = function (block: VariableBlock) {
|
||||
return function () {
|
||||
const workspace = block.workspace;
|
||||
const variable = block.getField('VAR').getVariable();
|
||||
const variableField = block.getField('VAR') as FieldVariable;
|
||||
const variable = variableField.getVariable()!;
|
||||
workspace.deleteVariableById(variable.getId());
|
||||
workspace.refreshToolboxSelection();
|
||||
(workspace as WorkspaceSvg).refreshToolboxSelection();
|
||||
};
|
||||
};
|
||||
|
||||
Extensions.registerMixin(
|
||||
'contextMenu_variableDynamicSetterGetter',
|
||||
CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN);
|
||||
CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN
|
||||
);
|
||||
|
||||
// Register provided blocks.
|
||||
defineBlocks(blocks);
|
||||
568
core/block.ts
568
core/block.ts
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,6 @@ import type {BlockSvg} from './block_svg.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
|
||||
|
||||
/** A bounding box for a cloned block. */
|
||||
interface CloneRect {
|
||||
x: number;
|
||||
@@ -26,7 +25,6 @@ let disconnectPid: ReturnType<typeof setTimeout>|null = null;
|
||||
/** The wobbling block. There can only be one at a time. */
|
||||
let wobblingBlock: BlockSvg | null = null;
|
||||
|
||||
|
||||
/**
|
||||
* Play some UI effects (sound, animation) when disposing of a block.
|
||||
*
|
||||
@@ -46,8 +44,12 @@ export function disposeUiEffect(block: BlockSvg) {
|
||||
const clone: SVGGElement = svgGroup.cloneNode(true) as SVGGElement;
|
||||
clone.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')');
|
||||
workspace.getParentSvg().appendChild(clone);
|
||||
const cloneRect =
|
||||
{'x': xy.x, 'y': xy.y, 'width': block.width, 'height': block.height};
|
||||
const cloneRect = {
|
||||
'x': xy.x,
|
||||
'y': xy.y,
|
||||
'width': block.width,
|
||||
'height': block.height,
|
||||
};
|
||||
disposeUiStep(clone, cloneRect, workspace.RTL, new Date(), workspace.scale);
|
||||
}
|
||||
/**
|
||||
@@ -62,21 +64,25 @@ export function disposeUiEffect(block: BlockSvg) {
|
||||
* @param workspaceScale Scale of workspace.
|
||||
*/
|
||||
function disposeUiStep(
|
||||
clone: Element, rect: CloneRect, rtl: boolean, start: Date,
|
||||
workspaceScale: number) {
|
||||
clone: Element,
|
||||
rect: CloneRect,
|
||||
rtl: boolean,
|
||||
start: Date,
|
||||
workspaceScale: number
|
||||
) {
|
||||
const ms = new Date().getTime() - start.getTime();
|
||||
const percent = ms / 150;
|
||||
if (percent > 1) {
|
||||
dom.removeNode(clone);
|
||||
} else {
|
||||
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 scale = (1 - percent) * workspaceScale;
|
||||
clone.setAttribute(
|
||||
'transform',
|
||||
'translate(' + x + ',' + y + ')' +
|
||||
' scale(' + scale + ')');
|
||||
'translate(' + x + ',' + y + ')' + ' scale(' + scale + ')'
|
||||
);
|
||||
setTimeout(disposeUiStep, 10, clone, rect, rtl, start, workspaceScale);
|
||||
}
|
||||
}
|
||||
@@ -105,7 +111,8 @@ export function connectionUiEffect(block: BlockSvg) {
|
||||
xy.y += 3 * scale;
|
||||
}
|
||||
const ripple = dom.createSvgElement(
|
||||
Svg.CIRCLE, {
|
||||
Svg.CIRCLE,
|
||||
{
|
||||
'cx': xy.x,
|
||||
'cy': xy.y,
|
||||
'r': 0,
|
||||
@@ -113,7 +120,8 @@ export function connectionUiEffect(block: BlockSvg) {
|
||||
'stroke': '#888',
|
||||
'stroke-width': 10,
|
||||
},
|
||||
workspace.getParentSvg());
|
||||
workspace.getParentSvg()
|
||||
);
|
||||
// Start the animation.
|
||||
connectionUiStep(ripple, new Date(), scale);
|
||||
}
|
||||
@@ -153,7 +161,7 @@ export function disconnectUiEffect(block: BlockSvg) {
|
||||
const DISPLACEMENT = 10;
|
||||
// Scale magnitude of skew to height of block.
|
||||
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) {
|
||||
magnitude *= -1;
|
||||
}
|
||||
@@ -179,13 +187,15 @@ function disconnectUiStep(block: BlockSvg, magnitude: number, start: Date) {
|
||||
let skew = '';
|
||||
if (percent <= 1) {
|
||||
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})`;
|
||||
disconnectPid = setTimeout(disconnectUiStep, 10, block, magnitude, start);
|
||||
}
|
||||
|
||||
block.getSvgRoot().setAttribute(
|
||||
'transform', `${block.getTranslation()} ${skew}`);
|
||||
block
|
||||
.getSvgRoot()
|
||||
.setAttribute('transform', `${block.getTranslation()} ${skew}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,7 +209,8 @@ export function disconnectUiStop() {
|
||||
clearTimeout(disconnectPid);
|
||||
disconnectPid = null;
|
||||
}
|
||||
wobblingBlock.getSvgRoot().setAttribute(
|
||||
'transform', wobblingBlock.getTranslation());
|
||||
wobblingBlock
|
||||
.getSvgRoot()
|
||||
.setAttribute('transform', wobblingBlock.getTranslation());
|
||||
wobblingBlock = null;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import * as dom from './utils/dom.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
import * as svgMath from './utils/svg_math.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for a drag surface for the currently dragged block. This is a separate
|
||||
* SVG that contains only the currently moving block, or nothing.
|
||||
@@ -65,14 +64,16 @@ export class BlockDragSurfaceSvg {
|
||||
/** @param container Containing element. */
|
||||
constructor(private readonly container: Element) {
|
||||
this.svg = dom.createSvgElement(
|
||||
Svg.SVG, {
|
||||
Svg.SVG,
|
||||
{
|
||||
'xmlns': dom.SVG_NS,
|
||||
'xmlns:html': dom.HTML_NS,
|
||||
'xmlns:xlink': dom.XLINK_NS,
|
||||
'version': '1.1',
|
||||
'class': 'blocklyBlockDragSurface',
|
||||
},
|
||||
this.container);
|
||||
this.container
|
||||
);
|
||||
|
||||
this.dragGroup = dom.createSvgElement(Svg.G, {}, this.svg);
|
||||
}
|
||||
@@ -121,7 +122,8 @@ export class BlockDragSurfaceSvg {
|
||||
this.childSurfaceXY.y = roundY;
|
||||
this.dragGroup.setAttribute(
|
||||
'transform',
|
||||
'translate(' + roundX + ',' + roundY + ') scale(' + scale + ')');
|
||||
'translate(' + roundX + ',' + roundY + ') scale(' + scale + ')'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ import * as bumpObjects from './bump_objects.js';
|
||||
import * as common from './common.js';
|
||||
import type {BlockMove} from './events/events_block_move.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import type {Icon} from './icon.js';
|
||||
import type {Icon} from './icons/icon.js';
|
||||
import {InsertionMarkerManager} from './insertion_marker_manager.js';
|
||||
import type {IBlockDragger} from './interfaces/i_block_dragger.js';
|
||||
import type {IDragTarget} from './interfaces/i_drag_target.js';
|
||||
@@ -29,7 +29,7 @@ import * as registry from './registry.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
import {hasBubble} from './interfaces/i_has_bubble.js';
|
||||
|
||||
/**
|
||||
* Class for a block dragger. It moves blocks around the workspace when they
|
||||
@@ -59,8 +59,9 @@ export class BlockDragger implements IBlockDragger {
|
||||
this.draggingBlock_ = block;
|
||||
|
||||
/** Object that keeps track of connections on dragged blocks. */
|
||||
this.draggedConnectionManager_ =
|
||||
new InsertionMarkerManager(this.draggingBlock_);
|
||||
this.draggedConnectionManager_ = new InsertionMarkerManager(
|
||||
this.draggingBlock_
|
||||
);
|
||||
|
||||
this.workspace_ = workspace;
|
||||
|
||||
@@ -75,7 +76,7 @@ export class BlockDragger implements IBlockDragger {
|
||||
* on this block and its descendants. Moving an icon moves the bubble that
|
||||
* extends from it if that bubble is open.
|
||||
*/
|
||||
this.dragIconData_ = initIconData(block);
|
||||
this.dragIconData_ = initIconData(block, this.startXY_);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +93,7 @@ export class BlockDragger implements IBlockDragger {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start dragging a block. This includes moving it to the drag surface.
|
||||
* Start dragging a block.
|
||||
*
|
||||
* @param currentDragDeltaXY How far the pointer has moved from the position
|
||||
* at mouse down, in pixel units.
|
||||
@@ -122,10 +123,6 @@ export class BlockDragger implements IBlockDragger {
|
||||
this.disconnectBlock_(healStack, currentDragDeltaXY);
|
||||
}
|
||||
this.draggingBlock_.setDragging(true);
|
||||
// For future consideration: we may be able to put moveToDragSurface inside
|
||||
// the block dragger, which would also let the block not track the block
|
||||
// drag surface.
|
||||
this.draggingBlock_.moveToDragSurface();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,8 +134,10 @@ export class BlockDragger implements IBlockDragger {
|
||||
protected shouldDisconnect_(healStack: boolean): boolean {
|
||||
return !!(
|
||||
this.draggingBlock_.getParent() ||
|
||||
healStack && this.draggingBlock_.nextConnection &&
|
||||
this.draggingBlock_.nextConnection.targetBlock());
|
||||
(healStack &&
|
||||
this.draggingBlock_.nextConnection &&
|
||||
this.draggingBlock_.nextConnection.targetBlock())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,7 +148,9 @@ export class BlockDragger implements IBlockDragger {
|
||||
* at mouse down, in pixel units.
|
||||
*/
|
||||
protected disconnectBlock_(
|
||||
healStack: boolean, currentDragDeltaXY: Coordinate) {
|
||||
healStack: boolean,
|
||||
currentDragDeltaXY: Coordinate
|
||||
) {
|
||||
this.draggingBlock_.unplug(healStack);
|
||||
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
|
||||
const newLoc = Coordinate.sum(this.startXY_, delta);
|
||||
@@ -162,7 +163,10 @@ export class BlockDragger implements IBlockDragger {
|
||||
/** Fire a UI event at the start of a block drag. */
|
||||
protected fireDragStartEvent_() {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -217,18 +221,14 @@ export class BlockDragger implements IBlockDragger {
|
||||
|
||||
blockAnimation.disconnectUiStop();
|
||||
|
||||
const preventMove = !!this.dragTarget_ &&
|
||||
const preventMove =
|
||||
!!this.dragTarget_ &&
|
||||
this.dragTarget_.shouldPreventMove(this.draggingBlock_);
|
||||
let newLoc: Coordinate;
|
||||
let delta: Coordinate | null = null;
|
||||
if (preventMove) {
|
||||
newLoc = this.startXY_;
|
||||
} else {
|
||||
if (!preventMove) {
|
||||
const newValues = this.getNewLocationAfterDrag_(currentDragDeltaXY);
|
||||
delta = newValues.delta;
|
||||
newLoc = newValues.newLocation;
|
||||
}
|
||||
this.draggingBlock_.moveOffDragSurface(newLoc);
|
||||
|
||||
if (this.dragTarget_) {
|
||||
this.dragTarget_.onDrop(this.draggingBlock_);
|
||||
@@ -238,7 +238,8 @@ export class BlockDragger implements IBlockDragger {
|
||||
if (!deleted) {
|
||||
// These are expensive and don't need to be done if we're deleting.
|
||||
this.draggingBlock_.setDragging(false);
|
||||
if (delta) { // !preventMove
|
||||
if (delta) {
|
||||
// !preventMove
|
||||
this.updateBlockAfterMove_();
|
||||
} else {
|
||||
// Blocks dragged directly from a flyout may need to be bumped into
|
||||
@@ -246,7 +247,8 @@ export class BlockDragger implements IBlockDragger {
|
||||
bumpObjects.bumpIntoBounds(
|
||||
this.draggingBlock_.workspace,
|
||||
this.workspace_.getMetricsManager().getScrollMetrics(true),
|
||||
this.draggingBlock_);
|
||||
this.draggingBlock_
|
||||
);
|
||||
}
|
||||
}
|
||||
this.workspace_.setResizesEnabled(true);
|
||||
@@ -262,8 +264,10 @@ export class BlockDragger implements IBlockDragger {
|
||||
* @returns New location after drag. delta is in workspace units. newLocation
|
||||
* is the new coordinate where the block should end up.
|
||||
*/
|
||||
protected getNewLocationAfterDrag_(currentDragDeltaXY: Coordinate):
|
||||
{delta: Coordinate, newLocation: Coordinate} {
|
||||
protected getNewLocationAfterDrag_(currentDragDeltaXY: Coordinate): {
|
||||
delta: Coordinate;
|
||||
newLocation: Coordinate;
|
||||
} {
|
||||
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
|
||||
const newLocation = Coordinate.sum(this.startXY_, delta);
|
||||
return {
|
||||
@@ -307,7 +311,10 @@ export class BlockDragger implements IBlockDragger {
|
||||
/** Fire a UI event at the end of a block drag. */
|
||||
protected fireDragEndEvent_() {
|
||||
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
|
||||
this.draggingBlock_, false, this.draggingBlock_.getDescendants(false));
|
||||
this.draggingBlock_,
|
||||
false,
|
||||
this.draggingBlock_.getDescendants(false)
|
||||
);
|
||||
eventUtils.fire(event);
|
||||
}
|
||||
|
||||
@@ -322,13 +329,16 @@ export class BlockDragger implements IBlockDragger {
|
||||
const toolbox = this.workspace_.getToolbox();
|
||||
|
||||
if (toolbox) {
|
||||
const style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' :
|
||||
'blocklyToolboxGrab';
|
||||
const style = this.draggingBlock_.isDeletable()
|
||||
? 'blocklyToolboxDelete'
|
||||
: 'blocklyToolboxGrab';
|
||||
|
||||
// AnyDuringMigration because: Property 'removeStyle' does not exist on
|
||||
// type 'IToolbox'.
|
||||
if (isEnd &&
|
||||
typeof (toolbox as AnyDuringMigration).removeStyle === 'function') {
|
||||
if (
|
||||
isEnd &&
|
||||
typeof (toolbox as AnyDuringMigration).removeStyle === 'function'
|
||||
) {
|
||||
// AnyDuringMigration because: Property 'removeStyle' does not exist on
|
||||
// type 'IToolbox'.
|
||||
(toolbox as AnyDuringMigration).removeStyle(style);
|
||||
@@ -336,7 +346,8 @@ export class BlockDragger implements IBlockDragger {
|
||||
// type 'IToolbox'.
|
||||
} else if (
|
||||
!isEnd &&
|
||||
typeof (toolbox as AnyDuringMigration).addStyle === 'function') {
|
||||
typeof (toolbox as AnyDuringMigration).addStyle === 'function'
|
||||
) {
|
||||
// AnyDuringMigration because: Property 'addStyle' does not exist on
|
||||
// type 'IToolbox'.
|
||||
(toolbox as AnyDuringMigration).addStyle(style);
|
||||
@@ -348,7 +359,9 @@ export class BlockDragger implements IBlockDragger {
|
||||
protected fireMoveEvent_() {
|
||||
if (this.draggingBlock_.isDeadOrDying()) return;
|
||||
const event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(
|
||||
this.draggingBlock_) as BlockMove;
|
||||
this.draggingBlock_
|
||||
) as BlockMove;
|
||||
event.setReason(['drag']);
|
||||
event.oldCoordinate = this.startXY_;
|
||||
event.recordNew();
|
||||
eventUtils.fire(event);
|
||||
@@ -374,7 +387,8 @@ export class BlockDragger implements IBlockDragger {
|
||||
protected pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate {
|
||||
const result = new Coordinate(
|
||||
pixelCoord.x / this.workspace_.scale,
|
||||
pixelCoord.y / this.workspace_.scale);
|
||||
pixelCoord.y / this.workspace_.scale
|
||||
);
|
||||
if (this.workspace_.isMutator) {
|
||||
// If we're in a mutator, its scale is always 1, purely because of some
|
||||
// oddities in our rendering optimizations. The actual scale is the same
|
||||
@@ -393,9 +407,8 @@ export class BlockDragger implements IBlockDragger {
|
||||
*/
|
||||
protected dragIcons_(dxy: Coordinate) {
|
||||
// Moving icons moves their associated bubbles.
|
||||
for (let i = 0; i < this.dragIconData_.length; i++) {
|
||||
const data = this.dragIconData_[i];
|
||||
data.icon.setIconLocation(Coordinate.sum(data.location, dxy));
|
||||
for (const data of this.dragIconData_) {
|
||||
data.icon.onLocationChange(Coordinate.sum(data.location, dxy));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,8 +420,10 @@ export class BlockDragger implements IBlockDragger {
|
||||
*/
|
||||
getInsertionMarkers(): BlockSvg[] {
|
||||
// No insertion markers with the old style of dragged connection managers.
|
||||
if (this.draggedConnectionManager_ &&
|
||||
this.draggedConnectionManager_.getInsertionMarkers) {
|
||||
if (
|
||||
this.draggedConnectionManager_ &&
|
||||
this.draggedConnectionManager_.getInsertionMarkers
|
||||
) {
|
||||
return this.draggedConnectionManager_.getInsertionMarkers();
|
||||
}
|
||||
return [];
|
||||
@@ -427,24 +442,28 @@ export interface IconPositionData {
|
||||
* extends from it if that bubble is open.
|
||||
*
|
||||
* @param block The root block that is being dragged.
|
||||
* @param blockOrigin The top left of the given block in workspace coordinates.
|
||||
* @returns The list of all icons and their locations.
|
||||
*/
|
||||
function initIconData(block: BlockSvg): IconPositionData[] {
|
||||
function initIconData(
|
||||
block: BlockSvg,
|
||||
blockOrigin: Coordinate
|
||||
): IconPositionData[] {
|
||||
// Build a list of icons that need to be moved and where they started.
|
||||
const dragIconData = [];
|
||||
const descendants = (block.getDescendants(false));
|
||||
|
||||
for (let i = 0, descendant; descendant = descendants[i]; i++) {
|
||||
const icons = descendant.getIcons();
|
||||
for (let j = 0; j < icons.length; j++) {
|
||||
const data = {
|
||||
// Coordinate with x and y properties (workspace
|
||||
// coordinates).
|
||||
location: icons[j].getIconLocation(), // Blockly.Icon
|
||||
icon: icons[j],
|
||||
};
|
||||
dragIconData.push(data);
|
||||
for (const icon of block.getIcons()) {
|
||||
// Only bother to track icons whose bubble is visible.
|
||||
if (hasBubble(icon) && !icon.bubbleIsVisible()) continue;
|
||||
|
||||
dragIconData.push({location: blockOrigin, icon: icon});
|
||||
icon.onLocationChange(blockOrigin);
|
||||
}
|
||||
|
||||
for (const child of block.getChildren(false)) {
|
||||
dragIconData.push(
|
||||
...initIconData(child, Coordinate.sum(blockOrigin, child.relativeCoords))
|
||||
);
|
||||
}
|
||||
// AnyDuringMigration because: Type '{ location: Coordinate | null; icon:
|
||||
// Icon; }[]' is not assignable to type 'IconPositionData[]'.
|
||||
|
||||
@@ -18,32 +18,35 @@ import './events/events_selected.js';
|
||||
import {Block} from './block.js';
|
||||
import * as blockAnimations from './block_animations.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import {Comment} from './comment.js';
|
||||
import {CommentIcon} from './icons/comment_icon.js';
|
||||
import * as common from './common.js';
|
||||
import {config} from './config.js';
|
||||
import type {Connection} from './connection.js';
|
||||
import {ConnectionType} from './connection_type.js';
|
||||
import * as constants from './constants.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 * as eventUtils from './events/utils.js';
|
||||
import type {Field} from './field.js';
|
||||
import {FieldLabel} from './field_label.js';
|
||||
import type {Icon} from './icon.js';
|
||||
import type {Input} from './input.js';
|
||||
import type {Input} from './inputs/input.js';
|
||||
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
|
||||
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||
import type {CopyData, ICopyable} from './interfaces/i_copyable.js';
|
||||
import type {IDraggable} from './interfaces/i_draggable.js';
|
||||
import {IIcon} from './interfaces/i_icon.js';
|
||||
import * as internalConstants from './internal_constants.js';
|
||||
import {ASTNode} from './keyboard_nav/ast_node.js';
|
||||
import {TabNavigateCursor} from './keyboard_nav/tab_navigate_cursor.js';
|
||||
import {MarkerManager} from './marker_manager.js';
|
||||
import {Msg} from './msg.js';
|
||||
import type {Mutator} from './mutator.js';
|
||||
import {MutatorIcon} from './icons/mutator_icon.js';
|
||||
import {RenderedConnection} from './rendered_connection.js';
|
||||
import type {Debug as BlockRenderingDebug} from './renderers/common/debugger.js';
|
||||
import type {IPathObject} from './renderers/common/i_path_object.js';
|
||||
import * as blocks from './serialization/blocks.js';
|
||||
import type {BlockStyle} from './theme.js';
|
||||
@@ -53,19 +56,21 @@ import * as dom from './utils/dom.js';
|
||||
import {Rect} from './utils/rect.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
import * as svgMath from './utils/svg_math.js';
|
||||
import {Warning} from './warning.js';
|
||||
import {WarningIcon} from './icons/warning_icon.js';
|
||||
import type {Workspace} from './workspace.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import {queueRender} from './render_management.js';
|
||||
|
||||
import * as deprecation from './utils/deprecation.js';
|
||||
import {IconType} from './icons/icon_types.js';
|
||||
|
||||
/**
|
||||
* Class for a block's SVG representation.
|
||||
* Not normally called directly, workspace.newBlock() is preferred.
|
||||
*/
|
||||
export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
IBoundedElement, ICopyable,
|
||||
IDraggable {
|
||||
export class BlockSvg
|
||||
extends Block
|
||||
implements IASTNodeLocationSvg, IBoundedElement, ICopyable, IDraggable
|
||||
{
|
||||
/**
|
||||
* Constant for identifying rows that are to be rendered inline.
|
||||
* Don't collide with Blockly.inputTypes.
|
||||
@@ -81,15 +86,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
override decompose?: (p1: Workspace) => BlockSvg;
|
||||
// override compose?: ((p1: BlockSvg) => void)|null;
|
||||
saveConnections?: (p1: BlockSvg) => void;
|
||||
customContextMenu?:
|
||||
(p1: Array<ContextMenuOption|LegacyContextMenuOption>) => void;
|
||||
|
||||
/**
|
||||
* An property used internally to reference the block's rendering debugger.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
renderingDebugger: BlockRenderingDebug|null = null;
|
||||
customContextMenu?: (
|
||||
p1: Array<ContextMenuOption | LegacyContextMenuOption>
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Height of this block, not including any statement blocks above or below.
|
||||
@@ -110,13 +109,14 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
private warningTextDb = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
/** Block's mutator icon (if any). */
|
||||
mutator: Mutator|null = null;
|
||||
mutator: MutatorIcon | null = null;
|
||||
|
||||
/** Block's comment icon (if any). */
|
||||
private commentIcon_: Comment|null = null;
|
||||
|
||||
/** Block's warning icon (if any). */
|
||||
warning: Warning|null = null;
|
||||
/**
|
||||
* Block's warning icon (if any).
|
||||
*
|
||||
* @deprecated Use `setWarningText` to modify warnings on this block.
|
||||
*/
|
||||
warning: WarningIcon | null = null;
|
||||
|
||||
private svgGroup_: SVGGElement;
|
||||
style: BlockStyle;
|
||||
@@ -141,7 +141,6 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
override nextConnection!: RenderedConnection;
|
||||
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
|
||||
override previousConnection!: RenderedConnection;
|
||||
private readonly useDragSurface_: boolean;
|
||||
|
||||
private translation = '';
|
||||
|
||||
@@ -160,7 +159,6 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*/
|
||||
relativeCoords = new Coordinate(0, 0);
|
||||
|
||||
|
||||
/**
|
||||
* @param workspace The block's workspace.
|
||||
* @param prototypeName Name of the language object containing type-specific
|
||||
@@ -177,14 +175,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
this.style = workspace.getRenderer().getConstants().getBlockStyle(null);
|
||||
|
||||
/** The renderer's path object. */
|
||||
this.pathObject =
|
||||
workspace.getRenderer().makePathObject(this.svgGroup_, this.style);
|
||||
|
||||
/**
|
||||
* Whether to move the block to the drag surface when it is dragged.
|
||||
* True if it should move, false if it should be translated directly.
|
||||
*/
|
||||
this.useDragSurface_ = !!workspace.getBlockDragSurface();
|
||||
this.pathObject = workspace
|
||||
.getRenderer()
|
||||
.makePathObject(this.svgGroup_, this.style);
|
||||
|
||||
const svgPath = this.pathObject.svgPath;
|
||||
(svgPath as any).tooltip = this;
|
||||
@@ -204,19 +197,23 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
if (!this.workspace.rendered) {
|
||||
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();
|
||||
}
|
||||
const icons = this.getIcons();
|
||||
for (let i = 0; i < icons.length; i++) {
|
||||
icons[i].createIcon();
|
||||
for (const icon of this.getIcons()) {
|
||||
icon.initView(this.createIconPointerDownListener(icon));
|
||||
icon.updateEditable();
|
||||
}
|
||||
this.applyColour();
|
||||
this.pathObject.updateMovable(this.isMovable());
|
||||
const svg = this.getSvgRoot();
|
||||
if (!this.workspace.options.readOnly && !this.eventsInit_ && svg) {
|
||||
browserEvents.conditionalBind(
|
||||
svg, 'pointerdown', this, this.onMouseDown_);
|
||||
svg,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.onMouseDown_
|
||||
);
|
||||
}
|
||||
this.eventsInit_ = true;
|
||||
|
||||
@@ -268,7 +265,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
}
|
||||
}
|
||||
const event = new (eventUtils.get(eventUtils.SELECTED))(
|
||||
oldId, this.id, this.workspace.id);
|
||||
oldId,
|
||||
this.id,
|
||||
this.workspace.id
|
||||
);
|
||||
eventUtils.fire(event);
|
||||
common.setSelected(this);
|
||||
this.addSelect();
|
||||
@@ -283,32 +283,16 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
return;
|
||||
}
|
||||
const event = new (eventUtils.get(eventUtils.SELECTED))(
|
||||
this.id, null, this.workspace.id);
|
||||
this.id,
|
||||
null,
|
||||
this.workspace.id
|
||||
);
|
||||
event.workspaceId = this.workspace.id;
|
||||
eventUtils.fire(event);
|
||||
common.setSelected(null);
|
||||
this.removeSelect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of mutator, comment, and warning icons.
|
||||
*
|
||||
* @returns List of icons.
|
||||
*/
|
||||
getIcons(): Icon[] {
|
||||
const icons = [];
|
||||
if (this.mutator) {
|
||||
icons.push(this.mutator);
|
||||
}
|
||||
if (this.commentIcon_) {
|
||||
icons.push(this.commentIcon_);
|
||||
}
|
||||
if (this.warning) {
|
||||
icons.push(this.warning);
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parent of this block to be a new block or null.
|
||||
*
|
||||
@@ -359,10 +343,6 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
const dragSurfaceGroup = this.useDragSurface_ ?
|
||||
this.workspace.getBlockDragSurface()!.getGroup() :
|
||||
null;
|
||||
|
||||
let element: SVGElement = this.getSvgRoot();
|
||||
if (element) {
|
||||
do {
|
||||
@@ -370,19 +350,8 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
const xy = svgMath.getRelativeXY(element);
|
||||
x += xy.x;
|
||||
y += xy.y;
|
||||
// If this element is the current element on the drag surface, include
|
||||
// the translation of the drag surface itself.
|
||||
if (this.useDragSurface_ &&
|
||||
this.workspace.getBlockDragSurface()!.getCurrentBlock() ===
|
||||
element) {
|
||||
const surfaceTranslation =
|
||||
this.workspace.getBlockDragSurface()!.getSurfaceTranslation();
|
||||
x += surfaceTranslation.x;
|
||||
y += surfaceTranslation.y;
|
||||
}
|
||||
element = element.parentNode as SVGElement;
|
||||
} while (element && element !== this.workspace.getCanvas() &&
|
||||
element !== dragSurfaceGroup);
|
||||
} while (element && element !== this.workspace.getCanvas());
|
||||
}
|
||||
return new Coordinate(x, y);
|
||||
}
|
||||
@@ -392,15 +361,17 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*
|
||||
* @param dx Horizontal offset in workspace units.
|
||||
* @param dy Vertical offset in workspace units.
|
||||
* @param reason Why is this move happening? 'drag', 'bump', 'snap', ...
|
||||
*/
|
||||
override moveBy(dx: number, dy: number) {
|
||||
override moveBy(dx: number, dy: number, reason?: string[]) {
|
||||
if (this.parentBlock_) {
|
||||
throw Error('Block has parent.');
|
||||
throw Error('Block has parent');
|
||||
}
|
||||
const eventsEnabled = eventUtils.isEnabled();
|
||||
let event: BlockMove | null = null;
|
||||
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);
|
||||
}
|
||||
const xy = this.getRelativeToSurfaceXY();
|
||||
this.translate(xy.x + dx, xy.y + dy);
|
||||
@@ -434,77 +405,28 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
return this.translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this block to its workspace's drag surface, accounting for
|
||||
* positioning. Generally should be called at the same time as
|
||||
* setDragging_(true). Does nothing if useDragSurface_ is false.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
moveToDragSurface() {
|
||||
if (!this.useDragSurface_) {
|
||||
return;
|
||||
}
|
||||
// The translation for drag surface blocks,
|
||||
// is equal to the current relative-to-surface position,
|
||||
// to keep the position in sync as it move on/off the surface.
|
||||
// This is in workspace coordinates.
|
||||
const xy = this.getRelativeToSurfaceXY();
|
||||
this.clearTransformAttributes_();
|
||||
this.workspace.getBlockDragSurface()!.translateSurface(xy.x, xy.y);
|
||||
// Execute the move on the top-level SVG component
|
||||
const svg = this.getSvgRoot();
|
||||
if (svg) {
|
||||
this.workspace.getBlockDragSurface()!.setBlocksAndShow(svg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a block to a position.
|
||||
*
|
||||
* @param xy The position to move to in workspace units.
|
||||
* @param reason Why is this move happening? 'drag', 'bump', 'snap', ...
|
||||
*/
|
||||
moveTo(xy: Coordinate) {
|
||||
moveTo(xy: Coordinate, reason?: string[]) {
|
||||
const curXY = this.getRelativeToSurfaceXY();
|
||||
this.moveBy(xy.x - curXY.x, xy.y - curXY.y);
|
||||
this.moveBy(xy.x - curXY.x, xy.y - curXY.y, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this block back to the workspace block canvas.
|
||||
* Generally should be called at the same time as setDragging_(false).
|
||||
* Does nothing if useDragSurface_ is false.
|
||||
*
|
||||
* @param newXY The position the block should take on on the workspace canvas,
|
||||
* in workspace coordinates.
|
||||
* @internal
|
||||
*/
|
||||
moveOffDragSurface(newXY: Coordinate) {
|
||||
if (!this.useDragSurface_) {
|
||||
return;
|
||||
}
|
||||
// Translate to current position, turning off 3d.
|
||||
this.translate(newXY.x, newXY.y);
|
||||
this.workspace.getBlockDragSurface()!.clearAndHide(
|
||||
this.workspace.getCanvas());
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this block during a drag, taking into account whether we are using a
|
||||
* drag surface to translate blocks.
|
||||
* Move this block during a drag.
|
||||
* This block must be a top-level block.
|
||||
*
|
||||
* @param newLoc The location to translate to, in workspace coordinates.
|
||||
* @internal
|
||||
*/
|
||||
moveDuringDrag(newLoc: Coordinate) {
|
||||
if (this.useDragSurface_) {
|
||||
this.workspace.getBlockDragSurface()!.translateSurface(
|
||||
newLoc.x, newLoc.y);
|
||||
} else {
|
||||
this.translate(newLoc.x, newLoc.y);
|
||||
this.getSvgRoot().setAttribute('transform', this.getTranslation());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the block of transform="..." attributes.
|
||||
@@ -536,12 +458,14 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
const spacing = grid.getSpacing();
|
||||
const half = spacing / 2;
|
||||
const xy = this.getRelativeToSurfaceXY();
|
||||
const dx =
|
||||
Math.round(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 dx = Math.round(
|
||||
Math.round((xy.x - half) / spacing) * spacing + half - xy.x
|
||||
);
|
||||
const dy = Math.round(
|
||||
Math.round((xy.y - half) / spacing) * spacing + half - xy.y
|
||||
);
|
||||
if (dx || dy) {
|
||||
this.moveBy(dx, dy);
|
||||
this.moveBy(dx, dy, ['snap']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,7 +497,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*/
|
||||
markDirty() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -600,30 +524,30 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
const collapsedInputName = constants.COLLAPSED_INPUT_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) {
|
||||
input.setVisible(!collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
for (const icon of this.getIcons()) {
|
||||
icon.updateCollapsed();
|
||||
}
|
||||
|
||||
if (!collapsed) {
|
||||
this.updateDisabled();
|
||||
this.removeInput(collapsedInputName);
|
||||
return;
|
||||
}
|
||||
|
||||
const icons = this.getIcons();
|
||||
for (let i = 0, icon; icon = icons[i]; i++) {
|
||||
icon.setVisible(false);
|
||||
}
|
||||
|
||||
const text = this.toString(internalConstants.COLLAPSE_CHARS);
|
||||
const field = this.getField(collapsedFieldName);
|
||||
if (field) {
|
||||
field.setValue(text);
|
||||
return;
|
||||
}
|
||||
const input = this.getInput(collapsedInputName) ||
|
||||
const input =
|
||||
this.getInput(collapsedInputName) ||
|
||||
this.appendDummyInput(collapsedInputName);
|
||||
input.appendField(new FieldLabel(text), collapsedFieldName);
|
||||
}
|
||||
@@ -687,13 +611,16 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*
|
||||
* @returns Context menu options or null if no menu.
|
||||
*/
|
||||
protected generateContextMenu():
|
||||
Array<ContextMenuOption|LegacyContextMenuOption>|null {
|
||||
protected generateContextMenu(): Array<
|
||||
ContextMenuOption | LegacyContextMenuOption
|
||||
> | null {
|
||||
if (this.workspace.options.readOnly || !this.contextMenu) {
|
||||
return null;
|
||||
}
|
||||
const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
|
||||
ContextMenuRegistry.ScopeType.BLOCK, {block: this});
|
||||
ContextMenuRegistry.ScopeType.BLOCK,
|
||||
{block: this}
|
||||
);
|
||||
|
||||
// Allow the block to add or modify menuOptions.
|
||||
if (this.customContextMenu) {
|
||||
@@ -737,8 +664,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
myConnections[i].moveBy(dx, dy);
|
||||
}
|
||||
const icons = this.getIcons();
|
||||
for (let i = 0; i < icons.length; i++) {
|
||||
icons[i].computeIconLocation();
|
||||
const pos = this.getRelativeToSurfaceXY();
|
||||
for (const icon of icons) {
|
||||
icon.onLocationChange(pos);
|
||||
}
|
||||
|
||||
// Recurse through all blocks attached under this one.
|
||||
@@ -817,7 +745,8 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
this.isInsertionMarker_ = insertionMarker;
|
||||
if (this.isInsertionMarker_) {
|
||||
this.setColour(
|
||||
this.workspace.getRenderer().getConstants().INSERTION_MARKER_COLOUR);
|
||||
this.workspace.getRenderer().getConstants().INSERTION_MARKER_COLOUR
|
||||
);
|
||||
this.pathObject.updateInsertionMarker(true);
|
||||
}
|
||||
}
|
||||
@@ -838,7 +767,6 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
* statement with the previous statement. Otherwise, dispose of all
|
||||
* children of this block.
|
||||
* @param animate If true, show a disposal animation and sound.
|
||||
* @suppress {checkTypes}
|
||||
*/
|
||||
override dispose(healStack?: boolean, animate?: boolean) {
|
||||
if (this.isDeadOrDying()) return;
|
||||
@@ -894,8 +822,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
// (https://github.com/google/blockly/issues/4832)
|
||||
this.dispose(false, true);
|
||||
} else {
|
||||
this.dispose(/* heal */
|
||||
true, true);
|
||||
this.dispose(/* heal */ true, true);
|
||||
}
|
||||
eventUtils.setGroup(false);
|
||||
}
|
||||
@@ -911,9 +838,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
saveInfo:
|
||||
blocks.save(this, {addCoordinates: true, addNextBlocks: false}) as
|
||||
blocks.State,
|
||||
saveInfo: blocks.save(this, {
|
||||
addCoordinates: true,
|
||||
addNextBlocks: false,
|
||||
}) as blocks.State,
|
||||
source: this.workspace,
|
||||
typeCounts: common.getBlockTypeCounts(this, true),
|
||||
};
|
||||
@@ -932,8 +860,8 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
icons[i].applyColour();
|
||||
}
|
||||
|
||||
for (let x = 0, input; input = this.inputList[x]; x++) {
|
||||
for (let y = 0, field; field = input.fieldRow[y]; y++) {
|
||||
for (let x = 0, input; (input = this.inputList[x]); x++) {
|
||||
for (let y = 0, field; (field = input.fieldRow[y]); y++) {
|
||||
field.applyColour();
|
||||
}
|
||||
}
|
||||
@@ -947,7 +875,12 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*/
|
||||
updateDisabled() {
|
||||
const disabled = !this.isEnabled() || this.getInheritedDisabled();
|
||||
if (this.visuallyDisabled === disabled) return;
|
||||
|
||||
if (this.visuallyDisabled === disabled) {
|
||||
this.getNextBlock()?.updateDisabled();
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyColour();
|
||||
this.visuallyDisabled = disabled;
|
||||
for (const child of this.getChildren(false)) {
|
||||
@@ -960,42 +893,11 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
* comment.
|
||||
*
|
||||
* @returns The comment icon attached to this block, or null.
|
||||
* @deprecated Use getIcon. To be remove in v11.
|
||||
*/
|
||||
getCommentIcon(): Comment|null {
|
||||
return this.commentIcon_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this block's comment text.
|
||||
*
|
||||
* @param text The text, or null to delete.
|
||||
*/
|
||||
override setCommentText(text: string|null) {
|
||||
if (this.commentModel.text === text) {
|
||||
return;
|
||||
}
|
||||
super.setCommentText(text);
|
||||
|
||||
const shouldHaveComment = text !== null;
|
||||
if (!!this.commentIcon_ === shouldHaveComment) {
|
||||
// If the comment's state of existence is correct, but the text is new
|
||||
// that means we're just updating a comment.
|
||||
this.commentIcon_!.updateText();
|
||||
return;
|
||||
}
|
||||
if (shouldHaveComment) {
|
||||
this.commentIcon_ = new Comment(this);
|
||||
this.comment = this.commentIcon_; // For backwards compatibility.
|
||||
} else {
|
||||
this.commentIcon_!.dispose();
|
||||
this.commentIcon_ = null;
|
||||
this.comment = null; // For backwards compatibility.
|
||||
}
|
||||
if (this.rendered) {
|
||||
this.render();
|
||||
// Adding or removing a comment icon will cause the block to change shape.
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
getCommentIcon(): CommentIcon | null {
|
||||
deprecation.warn('getCommentIcon', 'v10', 'v11', 'getIcon');
|
||||
return (this.getIcon(CommentIcon.TYPE) ?? null) as CommentIcon | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1021,19 +923,22 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
if (this.workspace.isDragging()) {
|
||||
// Don't change the warning text during a drag.
|
||||
// Wait until the drag finishes.
|
||||
this.warningTextDb.set(id, setTimeout(() => {
|
||||
this.warningTextDb.set(
|
||||
id,
|
||||
setTimeout(() => {
|
||||
if (!this.isDeadOrDying()) {
|
||||
this.warningTextDb.delete(id);
|
||||
this.setWarningText(text, id);
|
||||
}
|
||||
}, 100));
|
||||
}, 100)
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.isInFlyout) {
|
||||
text = null;
|
||||
}
|
||||
|
||||
let changedState = false;
|
||||
const icon = this.getIcon(WarningIcon.TYPE) as WarningIcon | undefined;
|
||||
if (typeof text === 'string') {
|
||||
// Bubble up to add a warning on top-most collapsed block.
|
||||
let parent = this.getSurroundParent();
|
||||
@@ -1046,33 +951,23 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
}
|
||||
if (collapsedParent) {
|
||||
collapsedParent.setWarningText(
|
||||
Msg['COLLAPSED_WARNINGS_WARNING'], BlockSvg.COLLAPSED_WARNING_ID);
|
||||
Msg['COLLAPSED_WARNINGS_WARNING'],
|
||||
BlockSvg.COLLAPSED_WARNING_ID
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.warning) {
|
||||
this.warning = new Warning(this);
|
||||
changedState = true;
|
||||
}
|
||||
this.warning!.setText((text), id);
|
||||
if (icon) {
|
||||
(icon as WarningIcon).addMessage(text, id);
|
||||
} else {
|
||||
this.addIcon(new WarningIcon(this).addMessage(text, id));
|
||||
}
|
||||
} else if (icon) {
|
||||
// Dispose all warnings if no ID is given.
|
||||
if (this.warning && !id) {
|
||||
this.warning.dispose();
|
||||
changedState = true;
|
||||
} else if (this.warning) {
|
||||
const oldText = this.warning.getText();
|
||||
this.warning.setText('', id);
|
||||
const newText = this.warning.getText();
|
||||
if (!newText) {
|
||||
this.warning.dispose();
|
||||
if (!id) {
|
||||
this.removeIcon(WarningIcon.TYPE);
|
||||
} else {
|
||||
if (!icon.getText()) this.removeIcon(WarningIcon.TYPE);
|
||||
}
|
||||
changedState = oldText !== newText;
|
||||
}
|
||||
}
|
||||
if (changedState && this.rendered) {
|
||||
this.render();
|
||||
// Adding or removing a warning icon will cause the block to change shape.
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1081,20 +976,55 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*
|
||||
* @param mutator A mutator dialog instance or null to remove.
|
||||
*/
|
||||
override setMutator(mutator: Mutator|null) {
|
||||
if (this.mutator && this.mutator !== mutator) {
|
||||
this.mutator.dispose();
|
||||
}
|
||||
if (mutator) {
|
||||
mutator.setBlock(this);
|
||||
this.mutator = mutator;
|
||||
mutator.createIcon();
|
||||
override setMutator(mutator: MutatorIcon | null) {
|
||||
this.removeIcon(MutatorIcon.TYPE);
|
||||
if (mutator) this.addIcon(mutator);
|
||||
}
|
||||
|
||||
override addIcon<T extends IIcon>(icon: T): T {
|
||||
super.addIcon(icon);
|
||||
|
||||
if (icon instanceof WarningIcon) this.warning = icon;
|
||||
if (icon instanceof MutatorIcon) this.mutator = icon;
|
||||
|
||||
if (this.rendered) {
|
||||
icon.initView(this.createIconPointerDownListener(icon));
|
||||
icon.applyColour();
|
||||
icon.updateEditable();
|
||||
// TODO: Change this based on #7068.
|
||||
this.render();
|
||||
// Adding or removing a mutator icon will cause the block to change shape.
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pointer down event listener for the icon to append to its
|
||||
* root svg.
|
||||
*/
|
||||
private createIconPointerDownListener(icon: IIcon) {
|
||||
return (e: PointerEvent) => {
|
||||
if (this.isDeadOrDying()) return;
|
||||
const gesture = this.workspace.getGesture(e);
|
||||
if (gesture) {
|
||||
gesture.setStartIcon(icon);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
override removeIcon(type: IconType<IIcon>): boolean {
|
||||
const removed = super.removeIcon(type);
|
||||
|
||||
if (type.equals(WarningIcon.TYPE)) this.warning = null;
|
||||
if (type.equals(MutatorIcon.TYPE)) this.mutator = null;
|
||||
|
||||
if (this.rendered) {
|
||||
// TODO: Change this based on #7068.
|
||||
this.render();
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1174,9 +1104,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*/
|
||||
override setColour(colour: number | string) {
|
||||
super.setColour(colour);
|
||||
const styleObj =
|
||||
this.workspace.getRenderer().getConstants().getBlockStyleForColour(
|
||||
this.colour_);
|
||||
const styleObj = this.workspace
|
||||
.getRenderer()
|
||||
.getConstants()
|
||||
.getBlockStyleForColour(this.colour_);
|
||||
|
||||
this.pathObject.setStyle(styleObj.style);
|
||||
this.style = styleObj.style;
|
||||
@@ -1192,9 +1123,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
* @throws {Error} if the block style does not exist.
|
||||
*/
|
||||
override setStyle(blockStyleName: string) {
|
||||
const blockStyle =
|
||||
this.workspace.getRenderer().getConstants().getBlockStyle(
|
||||
blockStyleName);
|
||||
const blockStyle = this.workspace
|
||||
.getRenderer()
|
||||
.getConstants()
|
||||
.getBlockStyle(blockStyleName);
|
||||
this.styleName_ = blockStyleName;
|
||||
|
||||
if (blockStyle) {
|
||||
@@ -1241,7 +1173,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
* if any type could be connected.
|
||||
*/
|
||||
override setPreviousStatement(
|
||||
newBoolean: boolean, opt_check?: string|string[]|null) {
|
||||
newBoolean: boolean,
|
||||
opt_check?: string | string[] | null
|
||||
) {
|
||||
super.setPreviousStatement(newBoolean, opt_check);
|
||||
|
||||
if (this.rendered) {
|
||||
@@ -1258,7 +1192,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
* if any type could be connected.
|
||||
*/
|
||||
override setNextStatement(
|
||||
newBoolean: boolean, opt_check?: string|string[]|null) {
|
||||
newBoolean: boolean,
|
||||
opt_check?: string | string[] | null
|
||||
) {
|
||||
super.setNextStatement(newBoolean, opt_check);
|
||||
|
||||
if (this.rendered) {
|
||||
@@ -1274,7 +1210,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
* @param opt_check Returned type or list of returned types. Null or
|
||||
* 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);
|
||||
|
||||
if (this.rendered) {
|
||||
@@ -1334,16 +1273,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a value input, statement input or local variable to this block.
|
||||
*
|
||||
* @param type One of Blockly.inputTypes.
|
||||
* @param name Language-neutral identifier which may used to find this input
|
||||
* again. Should be unique to this block.
|
||||
* @returns The input object created.
|
||||
*/
|
||||
protected override appendInput_(type: number, name: string): Input {
|
||||
const input = super.appendInput_(type, name);
|
||||
/** @override */
|
||||
override appendInput(input: Input): Input {
|
||||
super.appendInput(input);
|
||||
|
||||
if (this.rendered) {
|
||||
this.queueRender();
|
||||
@@ -1365,14 +1297,14 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*/
|
||||
setConnectionTracking(track: boolean) {
|
||||
if (this.previousConnection) {
|
||||
(this.previousConnection).setTracking(track);
|
||||
this.previousConnection.setTracking(track);
|
||||
}
|
||||
if (this.outputConnection) {
|
||||
(this.outputConnection).setTracking(track);
|
||||
this.outputConnection.setTracking(track);
|
||||
}
|
||||
if (this.nextConnection) {
|
||||
(this.nextConnection).setTracking(track);
|
||||
const child = (this.nextConnection).targetBlock();
|
||||
this.nextConnection.setTracking(track);
|
||||
const child = this.nextConnection.targetBlock();
|
||||
if (child) {
|
||||
child.setConnectionTracking(track);
|
||||
}
|
||||
@@ -1421,7 +1353,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
myConnections.push(this.nextConnection);
|
||||
}
|
||||
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) {
|
||||
myConnections.push(input.connection as RenderedConnection);
|
||||
}
|
||||
@@ -1441,8 +1373,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
* @returns The last next connection on the stack, or null.
|
||||
* @internal
|
||||
*/
|
||||
override lastConnectionInStack(ignoreShadows: boolean): RenderedConnection
|
||||
|null {
|
||||
override lastConnectionInStack(
|
||||
ignoreShadows: boolean
|
||||
): RenderedConnection | null {
|
||||
return super.lastConnectionInStack(ignoreShadows) as RenderedConnection;
|
||||
}
|
||||
|
||||
@@ -1456,8 +1389,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
* @returns The matching connection on this block, or null.
|
||||
* @internal
|
||||
*/
|
||||
override getMatchingConnection(otherBlock: Block, conn: Connection):
|
||||
RenderedConnection|null {
|
||||
override getMatchingConnection(
|
||||
otherBlock: Block,
|
||||
conn: Connection
|
||||
): RenderedConnection | null {
|
||||
return super.getMatchingConnection(otherBlock, conn) as RenderedConnection;
|
||||
}
|
||||
|
||||
@@ -1466,8 +1401,9 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*
|
||||
* @param type The type of the connection to create.
|
||||
* @returns A new connection of the specified type.
|
||||
* @internal
|
||||
*/
|
||||
protected override makeConnection_(type: number): RenderedConnection {
|
||||
override makeConnection_(type: ConnectionType): RenderedConnection {
|
||||
return new RenderedConnection(this, type);
|
||||
}
|
||||
|
||||
@@ -1513,8 +1449,11 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*/
|
||||
private bumpNeighboursInternal() {
|
||||
const root = this.getRootBlock();
|
||||
if (this.isDeadOrDying() || this.workspace.isDragging() ||
|
||||
root.isInFlyout) {
|
||||
if (
|
||||
this.isDeadOrDying() ||
|
||||
this.workspace.isDragging() ||
|
||||
root.isInFlyout
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1575,11 +1514,14 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*/
|
||||
positionNearConnection(
|
||||
sourceConnection: RenderedConnection,
|
||||
targetConnection: RenderedConnection) {
|
||||
targetConnection: RenderedConnection
|
||||
) {
|
||||
// We only need to position the new block if it's before the existing one,
|
||||
// otherwise its position is set by the previous block.
|
||||
if (sourceConnection.type === ConnectionType.NEXT_STATEMENT ||
|
||||
sourceConnection.type === ConnectionType.INPUT_VALUE) {
|
||||
if (
|
||||
sourceConnection.type === ConnectionType.NEXT_STATEMENT ||
|
||||
sourceConnection.type === ConnectionType.INPUT_VALUE
|
||||
) {
|
||||
const dx = targetConnection.x - sourceConnection.x;
|
||||
const dy = targetConnection.y - sourceConnection.y;
|
||||
|
||||
@@ -1611,10 +1553,13 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
/**
|
||||
* Triggers a rerender after a delay to allow for batching.
|
||||
*
|
||||
* @returns A promise that resolves after the currently queued renders have
|
||||
* been completed. Used for triggering other behavior that relies on
|
||||
* updated size/position location for the block.
|
||||
* @internal
|
||||
*/
|
||||
queueRender() {
|
||||
queueRender(this);
|
||||
queueRender(): Promise<void> {
|
||||
return queueRender(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1633,11 +1578,16 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
this.rendered = true;
|
||||
dom.startTextWidthCache();
|
||||
|
||||
if (!this.isEnabled()) {
|
||||
// Apply disabled styles if needed.
|
||||
this.updateDisabled();
|
||||
}
|
||||
|
||||
if (this.isCollapsed()) {
|
||||
this.updateCollapsed_();
|
||||
}
|
||||
this.workspace.getRenderer().render(this);
|
||||
this.updateConnectionLocations();
|
||||
this.updateConnectionAndIconLocations();
|
||||
|
||||
if (opt_bubble !== false) {
|
||||
const parentBlock = this.getParent();
|
||||
@@ -1716,7 +1666,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
updateConnectionLocations() {
|
||||
private updateConnectionAndIconLocations() {
|
||||
const blockTL = this.getRelativeToSurfaceXY();
|
||||
// Don't tighten previous or output connections because they are inferior
|
||||
// connections.
|
||||
@@ -1743,6 +1693,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
this.nextConnection.tighten();
|
||||
}
|
||||
}
|
||||
|
||||
for (const icon of this.getIcons()) {
|
||||
icon.onLocationChange(blockTL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1774,15 +1728,16 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
|
||||
* @returns Object with height and width properties in workspace units.
|
||||
* @internal
|
||||
*/
|
||||
getHeightWidth(): {height: number, width: number} {
|
||||
getHeightWidth(): {height: number; width: number} {
|
||||
let height = this.height;
|
||||
let width = this.width;
|
||||
// Recursively add size of subsequent blocks.
|
||||
const nextBlock = this.getNextBlock();
|
||||
if (nextBlock) {
|
||||
const nextHeightWidth = nextBlock.getHeightWidth();
|
||||
const tabHeight =
|
||||
this.workspace.getRenderer().getConstants().NOTCH_HEIGHT;
|
||||
const tabHeight = this.workspace
|
||||
.getRenderer()
|
||||
.getConstants().NOTCH_HEIGHT;
|
||||
height += nextHeightWidth.height - tabHeight;
|
||||
width = Math.max(width, nextHeightWidth.width);
|
||||
}
|
||||
|
||||
321
core/blockly.ts
321
core/blockly.ts
@@ -12,25 +12,22 @@ import './events/events_block_create.js';
|
||||
// Unused import preserved for side-effects. Remove if unneeded.
|
||||
import './events/workspace_events.js';
|
||||
// Unused import preserved for side-effects. Remove if unneeded.
|
||||
import './events/events_ui.js';
|
||||
// Unused import preserved for side-effects. Remove if unneeded.
|
||||
import './events/events_ui_base.js';
|
||||
// Unused import preserved for side-effects. Remove if unneeded.
|
||||
import './events/events_var_create.js';
|
||||
|
||||
import {Block} from './block.js';
|
||||
import * as blockAnimations from './block_animations.js';
|
||||
import {BlockDragSurfaceSvg} from './block_drag_surface.js';
|
||||
import {BlockDragger} from './block_dragger.js';
|
||||
import {BlockSvg} from './block_svg.js';
|
||||
import {BlocklyOptions} from './blockly_options.js';
|
||||
import {Blocks} from './blocks.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import {Bubble} from './bubble.js';
|
||||
import {Bubble} from './bubbles/bubble.js';
|
||||
import * as bubbles from './bubbles.js';
|
||||
import {BubbleDragger} from './bubble_dragger.js';
|
||||
import * as bumpObjects from './bump_objects.js';
|
||||
import * as clipboard from './clipboard.js';
|
||||
import {Comment} from './comment.js';
|
||||
import * as common from './common.js';
|
||||
import {ComponentManager} from './component_manager.js';
|
||||
import {config} from './config.js';
|
||||
@@ -48,19 +45,75 @@ import {DragTarget} from './drag_target.js';
|
||||
import * as dropDownDiv from './dropdowndiv.js';
|
||||
import * as Events from './events/events.js';
|
||||
import * as Extensions from './extensions.js';
|
||||
import {Field, FieldConfig, FieldValidator, UnattachedFieldError} from './field.js';
|
||||
import {FieldAngle, FieldAngleConfig, FieldAngleFromJsonConfig, FieldAngleValidator} from './field_angle.js';
|
||||
import {FieldCheckbox, FieldCheckboxConfig, FieldCheckboxFromJsonConfig, FieldCheckboxValidator} from './field_checkbox.js';
|
||||
import {FieldColour, FieldColourConfig, FieldColourFromJsonConfig, FieldColourValidator} from './field_colour.js';
|
||||
import {FieldDropdown, FieldDropdownConfig, FieldDropdownFromJsonConfig, FieldDropdownValidator, MenuGenerator, MenuGeneratorFunction, MenuOption} from './field_dropdown.js';
|
||||
import {FieldImage, FieldImageConfig, FieldImageFromJsonConfig} from './field_image.js';
|
||||
import {FieldLabel, FieldLabelConfig, FieldLabelFromJsonConfig} from './field_label.js';
|
||||
import {
|
||||
Field,
|
||||
FieldConfig,
|
||||
FieldValidator,
|
||||
UnattachedFieldError,
|
||||
} from './field.js';
|
||||
import {
|
||||
FieldAngle,
|
||||
FieldAngleConfig,
|
||||
FieldAngleFromJsonConfig,
|
||||
FieldAngleValidator,
|
||||
} from './field_angle.js';
|
||||
import {
|
||||
FieldCheckbox,
|
||||
FieldCheckboxConfig,
|
||||
FieldCheckboxFromJsonConfig,
|
||||
FieldCheckboxValidator,
|
||||
} from './field_checkbox.js';
|
||||
import {
|
||||
FieldColour,
|
||||
FieldColourConfig,
|
||||
FieldColourFromJsonConfig,
|
||||
FieldColourValidator,
|
||||
} from './field_colour.js';
|
||||
import {
|
||||
FieldDropdown,
|
||||
FieldDropdownConfig,
|
||||
FieldDropdownFromJsonConfig,
|
||||
FieldDropdownValidator,
|
||||
MenuGenerator,
|
||||
MenuGeneratorFunction,
|
||||
MenuOption,
|
||||
} from './field_dropdown.js';
|
||||
import {
|
||||
FieldImage,
|
||||
FieldImageConfig,
|
||||
FieldImageFromJsonConfig,
|
||||
} from './field_image.js';
|
||||
import {
|
||||
FieldLabel,
|
||||
FieldLabelConfig,
|
||||
FieldLabelFromJsonConfig,
|
||||
} from './field_label.js';
|
||||
import {FieldLabelSerializable} from './field_label_serializable.js';
|
||||
import {FieldMultilineInput, FieldMultilineInputConfig, FieldMultilineInputFromJsonConfig, FieldMultilineInputValidator} from './field_multilineinput.js';
|
||||
import {FieldNumber, FieldNumberConfig, FieldNumberFromJsonConfig, FieldNumberValidator} from './field_number.js';
|
||||
import {
|
||||
FieldMultilineInput,
|
||||
FieldMultilineInputConfig,
|
||||
FieldMultilineInputFromJsonConfig,
|
||||
FieldMultilineInputValidator,
|
||||
} from './field_multilineinput.js';
|
||||
import {
|
||||
FieldNumber,
|
||||
FieldNumberConfig,
|
||||
FieldNumberFromJsonConfig,
|
||||
FieldNumberValidator,
|
||||
} from './field_number.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {FieldTextInput, FieldTextInputConfig, FieldTextInputFromJsonConfig, FieldTextInputValidator} from './field_textinput.js';
|
||||
import {FieldVariable, FieldVariableConfig, FieldVariableFromJsonConfig, FieldVariableValidator} from './field_variable.js';
|
||||
import {
|
||||
FieldTextInput,
|
||||
FieldTextInputConfig,
|
||||
FieldTextInputFromJsonConfig,
|
||||
FieldTextInputValidator,
|
||||
} from './field_textinput.js';
|
||||
import {
|
||||
FieldVariable,
|
||||
FieldVariableConfig,
|
||||
FieldVariableFromJsonConfig,
|
||||
FieldVariableValidator,
|
||||
} from './field_variable.js';
|
||||
import {Flyout} from './flyout_base.js';
|
||||
import {FlyoutButton} from './flyout_button.js';
|
||||
import {HorizontalFlyout} from './flyout_horizontal.js';
|
||||
@@ -69,10 +122,12 @@ import {VerticalFlyout} from './flyout_vertical.js';
|
||||
import {CodeGenerator} from './generator.js';
|
||||
import {Gesture} from './gesture.js';
|
||||
import {Grid} from './grid.js';
|
||||
import {Icon} from './icon.js';
|
||||
import * as icons from './icons.js';
|
||||
import {inject} from './inject.js';
|
||||
import {Align, Input} from './input.js';
|
||||
import {inputTypes} from './input_types.js';
|
||||
import {Align} from './inputs/align.js';
|
||||
import {Input} from './inputs/input.js';
|
||||
import {inputTypes} from './inputs/input_types.js';
|
||||
import * as inputs from './inputs.js';
|
||||
import {InsertionMarkerManager} from './insertion_marker_manager.js';
|
||||
import {IASTNodeLocation} from './interfaces/i_ast_node_location.js';
|
||||
import {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
|
||||
@@ -91,6 +146,8 @@ import {IDeleteArea} from './interfaces/i_delete_area.js';
|
||||
import {IDragTarget} from './interfaces/i_drag_target.js';
|
||||
import {IDraggable} from './interfaces/i_draggable.js';
|
||||
import {IFlyout} from './interfaces/i_flyout.js';
|
||||
import {IHasBubble, hasBubble} from './interfaces/i_has_bubble.js';
|
||||
import {IIcon, isIcon} from './interfaces/i_icon.js';
|
||||
import {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
|
||||
import {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
||||
import {IMovable} from './interfaces/i_movable.js';
|
||||
@@ -99,10 +156,14 @@ import {IPositionable} from './interfaces/i_positionable.js';
|
||||
import {IRegistrable} from './interfaces/i_registrable.js';
|
||||
import {ISelectable} from './interfaces/i_selectable.js';
|
||||
import {ISelectableToolboxItem} from './interfaces/i_selectable_toolbox_item.js';
|
||||
import {ISerializable, isSerializable} from './interfaces/i_serializable.js';
|
||||
import {IStyleable} from './interfaces/i_styleable.js';
|
||||
import {IToolbox} from './interfaces/i_toolbox.js';
|
||||
import {IToolboxItem} from './interfaces/i_toolbox_item.js';
|
||||
import {IVariableBackedParameterModel, isVariableBackedParameterModel} from './interfaces/i_variable_backed_parameter_model.js';
|
||||
import {
|
||||
IVariableBackedParameterModel,
|
||||
isVariableBackedParameterModel,
|
||||
} from './interfaces/i_variable_backed_parameter_model.js';
|
||||
import * as internalConstants from './internal_constants.js';
|
||||
import {ASTNode} from './keyboard_nav/ast_node.js';
|
||||
import {BasicCursor} from './keyboard_nav/basic_cursor.js';
|
||||
@@ -114,13 +175,14 @@ import {Menu} from './menu.js';
|
||||
import {MenuItem} from './menuitem.js';
|
||||
import {MetricsManager} from './metrics_manager.js';
|
||||
import {Msg, setLocale} from './msg.js';
|
||||
import {Mutator} from './mutator.js';
|
||||
import {MiniWorkspaceBubble} from './bubbles/mini_workspace_bubble.js';
|
||||
import {Names} from './names.js';
|
||||
import {Options} from './options.js';
|
||||
import * as uiPosition from './positionable_helpers.js';
|
||||
import * as Procedures from './procedures.js';
|
||||
import * as registry from './registry.js';
|
||||
import {RenderedConnection} from './rendered_connection.js';
|
||||
import * as renderManagement from './render_management.js';
|
||||
import * as blockRendering from './renderers/common/block_rendering.js';
|
||||
import * as constants from './constants.js';
|
||||
import * as geras from './renderers/geras/geras.js';
|
||||
@@ -144,26 +206,21 @@ import * as Tooltip from './tooltip.js';
|
||||
import * as Touch from './touch.js';
|
||||
import {Trashcan} from './trashcan.js';
|
||||
import * as utils from './utils.js';
|
||||
import * as colour from './utils/colour.js';
|
||||
import * as deprecation from './utils/deprecation.js';
|
||||
import * as toolbox from './utils/toolbox.js';
|
||||
import {VariableMap} from './variable_map.js';
|
||||
import {VariableModel} from './variable_model.js';
|
||||
import * as Variables from './variables.js';
|
||||
import * as VariablesDynamic from './variables_dynamic.js';
|
||||
import {Warning} from './warning.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
import {Workspace} from './workspace.js';
|
||||
import {WorkspaceAudio} from './workspace_audio.js';
|
||||
import {WorkspaceComment} from './workspace_comment.js';
|
||||
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
|
||||
import {WorkspaceDragSurfaceSvg} from './workspace_drag_surface_svg.js';
|
||||
import {WorkspaceDragger} from './workspace_dragger.js';
|
||||
import {resizeSvgContents as realResizeSvgContents, WorkspaceSvg} from './workspace_svg.js';
|
||||
import {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as Xml from './xml.js';
|
||||
import {ZoomControls} from './zoom_controls.js';
|
||||
|
||||
|
||||
/**
|
||||
* Blockly core version.
|
||||
* This constant is overridden by the build script (npm run build) to the value
|
||||
@@ -190,16 +247,19 @@ export const VERSION = 'uncompiled';
|
||||
|
||||
/**
|
||||
* @see Blockly.Input.Align.LEFT
|
||||
* @deprecated Use `Blockly.inputs.Align.LEFT`. To be removed in v11.
|
||||
*/
|
||||
export const ALIGN_LEFT = Align.LEFT;
|
||||
|
||||
/**
|
||||
* @see Blockly.Input.Align.CENTRE
|
||||
* @deprecated Use `Blockly.inputs.Align.CENTER`. To be removed in v11.
|
||||
*/
|
||||
export const ALIGN_CENTRE = Align.CENTRE;
|
||||
|
||||
/**
|
||||
* @see Blockly.Input.Align.RIGHT
|
||||
* @deprecated Use `Blockly.inputs.Align.RIGHT`. To be removed in v11.
|
||||
*/
|
||||
export const ALIGN_RIGHT = Align.RIGHT;
|
||||
/*
|
||||
@@ -228,6 +288,7 @@ export const PREVIOUS_STATEMENT = ConnectionType.PREVIOUS_STATEMENT;
|
||||
|
||||
/**
|
||||
* @see inputTypes.DUMMY_INPUT
|
||||
* @deprecated Use `Blockly.inputs.inputTypes.DUMMY`. To be removed in v11.
|
||||
*/
|
||||
export const DUMMY_INPUT = inputTypes.DUMMY;
|
||||
|
||||
@@ -313,162 +374,6 @@ export const defineBlocksWithJsonArray = common.defineBlocksWithJsonArray;
|
||||
*/
|
||||
export const setParentContainer = common.setParentContainer;
|
||||
|
||||
/**
|
||||
* Size the workspace when the contents change. This also updates
|
||||
* scrollbars accordingly.
|
||||
*
|
||||
* @param workspace The workspace to resize.
|
||||
* @deprecated Use **workspace.resizeContents** instead.
|
||||
* @see Blockly.WorkspaceSvg.resizeContents
|
||||
*/
|
||||
function resizeSvgContentsLocal(workspace: WorkspaceSvg) {
|
||||
deprecation.warn(
|
||||
'Blockly.resizeSvgContents', 'December 2021', 'December 2022',
|
||||
'Blockly.WorkspaceSvg.resizeSvgContents');
|
||||
realResizeSvgContents(workspace);
|
||||
}
|
||||
export const resizeSvgContents = resizeSvgContentsLocal;
|
||||
|
||||
/**
|
||||
* Copy a block or workspace comment onto the local clipboard.
|
||||
*
|
||||
* @param toCopy Block or Workspace Comment to be copied.
|
||||
* @deprecated Use **Blockly.clipboard.copy** instead.
|
||||
* @see Blockly.clipboard.copy
|
||||
*/
|
||||
export function copy(toCopy: ICopyable) {
|
||||
deprecation.warn(
|
||||
'Blockly.copy', 'December 2021', 'December 2022',
|
||||
'Blockly.clipboard.copy');
|
||||
clipboard.copy(toCopy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste a block or workspace comment on to the main workspace.
|
||||
*
|
||||
* @returns True if the paste was successful, false otherwise.
|
||||
* @deprecated Use **Blockly.clipboard.paste** instead.
|
||||
* @see Blockly.clipboard.paste
|
||||
*/
|
||||
export function paste(): boolean {
|
||||
deprecation.warn(
|
||||
'Blockly.paste', 'December 2021', 'December 2022',
|
||||
'Blockly.clipboard.paste');
|
||||
return !!clipboard.paste();
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate this block and its children, or a workspace comment.
|
||||
*
|
||||
* @param toDuplicate Block or Workspace Comment to be copied.
|
||||
* @deprecated Use **Blockly.clipboard.duplicate** instead.
|
||||
* @see Blockly.clipboard.duplicate
|
||||
*/
|
||||
export function duplicate(toDuplicate: ICopyable) {
|
||||
deprecation.warn(
|
||||
'Blockly.duplicate', 'December 2021', 'December 2022',
|
||||
'Blockly.clipboard.duplicate');
|
||||
clipboard.duplicate(toDuplicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the given string a number (includes negative and decimals).
|
||||
*
|
||||
* @param str Input string.
|
||||
* @returns True if number, false otherwise.
|
||||
* @deprecated Use **Blockly.utils.string.isNumber** instead.
|
||||
* @see Blockly.utils.string.isNumber
|
||||
*/
|
||||
export function isNumber(str: string): boolean {
|
||||
deprecation.warn(
|
||||
'Blockly.isNumber', 'December 2021', 'December 2022',
|
||||
'Blockly.utils.string.isNumber');
|
||||
return utils.string.isNumber(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a hue (HSV model) into an RGB hex triplet.
|
||||
*
|
||||
* @param hue Hue on a colour wheel (0-360).
|
||||
* @returns RGB code, e.g. '#5ba65b'.
|
||||
* @deprecated Use **Blockly.utils.colour.hueToHex** instead.
|
||||
* @see Blockly.utils.colour.hueToHex
|
||||
*/
|
||||
export function hueToHex(hue: number): string {
|
||||
deprecation.warn(
|
||||
'Blockly.hueToHex', 'December 2021', 'December 2022',
|
||||
'Blockly.utils.colour.hueToHex');
|
||||
return colour.hueToHex(hue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind an event handler that should be called regardless of whether it is part
|
||||
* of the active touch stream.
|
||||
* Use this for events that are not part of a multi-part gesture (e.g.
|
||||
* mouseover for tooltips).
|
||||
*
|
||||
* @param node Node upon which to listen.
|
||||
* @param name Event name to listen to (e.g. 'mousedown').
|
||||
* @param thisObject The value of 'this' in the function.
|
||||
* @param func Function to call when event is triggered.
|
||||
* @returns Opaque data that can be passed to unbindEvent_.
|
||||
* @deprecated Use **Blockly.browserEvents.bind** instead.
|
||||
* @see Blockly.browserEvents.bind
|
||||
*/
|
||||
export function bindEvent_(
|
||||
node: EventTarget, name: string, thisObject: Object|null,
|
||||
func: Function): browserEvents.Data {
|
||||
deprecation.warn(
|
||||
'Blockly.bindEvent_', 'December 2021', 'December 2022',
|
||||
'Blockly.browserEvents.bind');
|
||||
return browserEvents.bind(node, name, thisObject, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbind one or more events event from a function call.
|
||||
*
|
||||
* @param bindData Opaque data from bindEvent_.
|
||||
* This list is emptied during the course of calling this function.
|
||||
* @returns The function call.
|
||||
* @deprecated Use **Blockly.browserEvents.unbind** instead.
|
||||
* @see browserEvents.unbind
|
||||
*/
|
||||
export function unbindEvent_(bindData: browserEvents.Data): Function {
|
||||
deprecation.warn(
|
||||
'Blockly.unbindEvent_', 'December 2021', 'December 2022',
|
||||
'Blockly.browserEvents.unbind');
|
||||
return browserEvents.unbind(bindData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind an event handler that can be ignored if it is not part of the active
|
||||
* touch stream.
|
||||
* Use this for events that either start or continue a multi-part gesture (e.g.
|
||||
* mousedown or mousemove, which may be part of a drag or click).
|
||||
*
|
||||
* @param node Node upon which to listen.
|
||||
* @param name Event name to listen to (e.g. 'mousedown').
|
||||
* @param thisObject The value of 'this' in the function.
|
||||
* @param func Function to call when event is triggered.
|
||||
* @param opt_noCaptureIdentifier True if triggering on this event should not
|
||||
* block execution of other event handlers on this touch or other
|
||||
* simultaneous touches. False by default.
|
||||
* @param _opt_noPreventDefault No-op, deprecated and will be removed in v10.
|
||||
* @returns Opaque data that can be passed to unbindEvent_.
|
||||
* @deprecated Use **Blockly.browserEvents.conditionalBind** instead.
|
||||
* @see browserEvents.conditionalBind
|
||||
*/
|
||||
export function bindEventWithChecks_(
|
||||
node: EventTarget, name: string, thisObject: Object|null, func: Function,
|
||||
opt_noCaptureIdentifier?: boolean,
|
||||
_opt_noPreventDefault?: boolean): browserEvents.Data {
|
||||
deprecation.warn(
|
||||
'Blockly.bindEventWithChecks_', 'December 2021', 'December 2022',
|
||||
'Blockly.browserEvents.conditionalBind');
|
||||
return browserEvents.conditionalBind(
|
||||
node, name, thisObject, func, opt_noCaptureIdentifier);
|
||||
}
|
||||
|
||||
// Aliases to allow external code to access these values for legacy reasons.
|
||||
export const COLLAPSE_CHARS = internalConstants.COLLAPSE_CHARS;
|
||||
export const DRAG_STACK = internalConstants.DRAG_STACK;
|
||||
@@ -499,18 +404,21 @@ export const VARIABLE_DYNAMIC_CATEGORY_NAME: string =
|
||||
*/
|
||||
export const PROCEDURE_CATEGORY_NAME: string = Procedures.CATEGORY_NAME;
|
||||
|
||||
|
||||
// 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
|
||||
|
||||
// clang-format off
|
||||
Workspace.prototype.newBlock =
|
||||
function(prototypeName: string, opt_id?: string): Block {
|
||||
Workspace.prototype.newBlock = function (
|
||||
prototypeName: string,
|
||||
opt_id?: string
|
||||
): Block {
|
||||
return new Block(this, prototypeName, opt_id);
|
||||
};
|
||||
|
||||
WorkspaceSvg.prototype.newBlock =
|
||||
function(prototypeName: string, opt_id?: string): BlockSvg {
|
||||
WorkspaceSvg.prototype.newBlock = function (
|
||||
prototypeName: string,
|
||||
opt_id?: string
|
||||
): BlockSvg {
|
||||
return new BlockSvg(this, prototypeName, opt_id);
|
||||
};
|
||||
|
||||
@@ -518,8 +426,10 @@ WorkspaceSvg.newTrashcan = function(workspace: WorkspaceSvg): Trashcan {
|
||||
return new Trashcan(workspace);
|
||||
};
|
||||
|
||||
WorkspaceCommentSvg.prototype.showContextMenu =
|
||||
function(this: WorkspaceCommentSvg, e: Event) {
|
||||
WorkspaceCommentSvg.prototype.showContextMenu = function (
|
||||
this: WorkspaceCommentSvg,
|
||||
e: Event
|
||||
) {
|
||||
if (this.workspace.options.readOnly) {
|
||||
return;
|
||||
}
|
||||
@@ -533,24 +443,25 @@ WorkspaceCommentSvg.prototype.showContextMenu =
|
||||
ContextMenu.show(e, menuOptions, this.RTL);
|
||||
};
|
||||
|
||||
Mutator.prototype.newWorkspaceSvg =
|
||||
function(options: Options): WorkspaceSvg {
|
||||
MiniWorkspaceBubble.prototype.newWorkspaceSvg = function (
|
||||
options: Options
|
||||
): WorkspaceSvg {
|
||||
return new WorkspaceSvg(options);
|
||||
};
|
||||
|
||||
Names.prototype.populateProcedures =
|
||||
function(this: Names, workspace: Workspace) {
|
||||
Names.prototype.populateProcedures = function (
|
||||
this: Names,
|
||||
workspace: Workspace
|
||||
) {
|
||||
const procedures = Procedures.allProcedures(workspace);
|
||||
// Flatten the return vs no-return procedure lists.
|
||||
const flattenedProcedures: AnyDuringMigration[][] =
|
||||
procedures[0].concat(procedures[1]);
|
||||
const flattenedProcedures = procedures[0].concat(procedures[1]);
|
||||
for (let i = 0; i < flattenedProcedures.length; i++) {
|
||||
this.getName(flattenedProcedures[i][0], Names.NameType.PROCEDURE);
|
||||
}
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
||||
// Re-export submodules that no longer declareLegacyNamespace.
|
||||
export {browserEvents};
|
||||
export {ContextMenu};
|
||||
@@ -588,13 +499,13 @@ export {BasicCursor};
|
||||
export {Block};
|
||||
export {BlocklyOptions};
|
||||
export {BlockDragger};
|
||||
export {BlockDragSurfaceSvg};
|
||||
export {BlockSvg};
|
||||
export {Blocks};
|
||||
export {bubbles};
|
||||
/** @deprecated Use Blockly.bubbles.Bubble instead. To be removed in v11. */
|
||||
export {Bubble};
|
||||
export {BubbleDragger};
|
||||
export {CollapsibleToolboxCategory};
|
||||
export {Comment};
|
||||
export {ComponentManager};
|
||||
export {Connection};
|
||||
export {ConnectionType};
|
||||
@@ -666,7 +577,6 @@ export {FlyoutMetricsManager};
|
||||
export {CodeGenerator};
|
||||
export {CodeGenerator as Generator}; // Deprecated name, October 2022.
|
||||
export {Gesture};
|
||||
export {Gesture as TouchGesture}; // Remove in v10.
|
||||
export {Grid};
|
||||
export {HorizontalFlyout};
|
||||
export {IASTNodeLocation};
|
||||
@@ -680,23 +590,27 @@ export {ICollapsibleToolboxItem};
|
||||
export {IComponent};
|
||||
export {IConnectionChecker};
|
||||
export {IContextMenu};
|
||||
export {Icon};
|
||||
export {icons};
|
||||
export {ICopyable};
|
||||
export {IDeletable};
|
||||
export {IDeleteArea};
|
||||
export {IDragTarget};
|
||||
export {IDraggable};
|
||||
export {IFlyout};
|
||||
export {IHasBubble, hasBubble};
|
||||
export {IIcon, isIcon};
|
||||
export {IKeyboardAccessible};
|
||||
export {IMetricsManager};
|
||||
export {IMovable};
|
||||
export {Input};
|
||||
export {inputs};
|
||||
export {InsertionMarkerManager};
|
||||
export {IObservable, isObservable};
|
||||
export {IPositionable};
|
||||
export {IRegistrable};
|
||||
export {ISelectable};
|
||||
export {ISelectableToolboxItem};
|
||||
export {ISerializable, isSerializable};
|
||||
export {IStyleable};
|
||||
export {IToolbox};
|
||||
export {IToolboxItem};
|
||||
@@ -706,11 +620,11 @@ export {MarkerManager};
|
||||
export {Menu};
|
||||
export {MenuItem};
|
||||
export {MetricsManager};
|
||||
export {Mutator};
|
||||
export {Msg, setLocale};
|
||||
export {Names};
|
||||
export {Options};
|
||||
export {RenderedConnection};
|
||||
export {renderManagement};
|
||||
export {Scrollbar};
|
||||
export {ScrollbarPair};
|
||||
export {ShortcutRegistry};
|
||||
@@ -725,12 +639,10 @@ export {Trashcan};
|
||||
export {VariableMap};
|
||||
export {VariableModel};
|
||||
export {VerticalFlyout};
|
||||
export {Warning};
|
||||
export {Workspace};
|
||||
export {WorkspaceAudio};
|
||||
export {WorkspaceComment};
|
||||
export {WorkspaceCommentSvg};
|
||||
export {WorkspaceDragSurfaceSvg};
|
||||
export {WorkspaceDragger};
|
||||
export {WorkspaceSvg};
|
||||
export {ZoomControls};
|
||||
@@ -738,5 +650,6 @@ export {config};
|
||||
/** @deprecated Use Blockly.ConnectionType instead. */
|
||||
export const connectionTypes = ConnectionType;
|
||||
export {inject};
|
||||
/** @deprecated Use Blockly.inputs.inputTypes instead. To be removed in v11. */
|
||||
export {inputTypes};
|
||||
export {serialization};
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {Theme, ITheme} from './theme.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import type {ToolboxDefinition} from './utils/toolbox.js';
|
||||
|
||||
|
||||
/**
|
||||
* Blockly options.
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.blocks');
|
||||
|
||||
|
||||
/**
|
||||
* A block definition. For now this very loose, but it can potentially
|
||||
* be refined e.g. by replacing this typedef with a class definition.
|
||||
|
||||
@@ -8,10 +8,8 @@ import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.browserEvents');
|
||||
|
||||
import * as Touch from './touch.js';
|
||||
import * as deprecation from './utils/deprecation.js';
|
||||
import * as userAgent from './utils/useragent.js';
|
||||
|
||||
|
||||
/**
|
||||
* Blockly opaque event data used to unbind events when using
|
||||
* `bind` and `conditionalBind`.
|
||||
@@ -45,17 +43,15 @@ const PAGE_MODE_MULTIPLIER = 125;
|
||||
* @param opt_noCaptureIdentifier True if triggering on this event should not
|
||||
* block execution of other event handlers on this touch or other
|
||||
* simultaneous touches. False by default.
|
||||
* @param opt_noPreventDefault No-op, deprecated and will be removed in v10.
|
||||
* @returns Opaque data that can be passed to unbindEvent_.
|
||||
*/
|
||||
export function conditionalBind(
|
||||
node: EventTarget, name: string, thisObject: Object|null, func: Function,
|
||||
opt_noCaptureIdentifier?: boolean, opt_noPreventDefault?: boolean): Data {
|
||||
if (opt_noPreventDefault !== undefined) {
|
||||
deprecation.warn(
|
||||
'The opt_noPreventDefault argument of conditionalBind', 'version 9',
|
||||
'version 10');
|
||||
}
|
||||
node: EventTarget,
|
||||
name: string,
|
||||
thisObject: Object | null,
|
||||
func: Function,
|
||||
opt_noCaptureIdentifier?: boolean
|
||||
): Data {
|
||||
/**
|
||||
*
|
||||
* @param e
|
||||
@@ -99,8 +95,11 @@ export function conditionalBind(
|
||||
* @returns Opaque data that can be passed to unbindEvent_.
|
||||
*/
|
||||
export function bind(
|
||||
node: EventTarget, name: string, thisObject: Object|null,
|
||||
func: Function): Data {
|
||||
node: EventTarget,
|
||||
name: string,
|
||||
thisObject: Object | null,
|
||||
func: Function
|
||||
): Data {
|
||||
/**
|
||||
*
|
||||
* @param e
|
||||
@@ -154,17 +153,24 @@ export function unbind(bindData: Data): (e: Event) => void {
|
||||
*/
|
||||
export function isTargetInput(e: Event): boolean {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (e.target.isContentEditable ||
|
||||
e.target.getAttribute('data-is-text-input') === 'true') {
|
||||
if (
|
||||
e.target.isContentEditable ||
|
||||
e.target.getAttribute('data-is-text-input') === 'true'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
const target = e.target;
|
||||
return target.type === 'text' || target.type === 'number' ||
|
||||
target.type === 'email' || target.type === 'password' ||
|
||||
target.type === 'search' || target.type === 'tel' ||
|
||||
target.type === 'url';
|
||||
return (
|
||||
target.type === 'text' ||
|
||||
target.type === 'number' ||
|
||||
target.type === 'email' ||
|
||||
target.type === 'password' ||
|
||||
target.type === 'search' ||
|
||||
target.type === 'tel' ||
|
||||
target.type === 'url'
|
||||
);
|
||||
}
|
||||
|
||||
if (e.target instanceof HTMLTextAreaElement) {
|
||||
@@ -200,7 +206,10 @@ export function isRightButton(e: MouseEvent): boolean {
|
||||
* @returns Object with .x and .y properties.
|
||||
*/
|
||||
export function mouseToSvg(
|
||||
e: MouseEvent, svg: SVGSVGElement, matrix: SVGMatrix|null): SVGPoint {
|
||||
e: MouseEvent,
|
||||
svg: SVGSVGElement,
|
||||
matrix: SVGMatrix | null
|
||||
): SVGPoint {
|
||||
const svgPoint = svg.createSVGPoint();
|
||||
svgPoint.x = e.clientX;
|
||||
svgPoint.y = e.clientY;
|
||||
@@ -217,7 +226,7 @@ export function mouseToSvg(
|
||||
* @param e Mouse event.
|
||||
* @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) {
|
||||
case 0x00: // Pixel mode.
|
||||
default:
|
||||
|
||||
908
core/bubble.ts
908
core/bubble.ts
@@ -1,908 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2012 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Object representing a UI bubble.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Bubble');
|
||||
|
||||
import type {BlockDragSurfaceSvg} from './block_drag_surface.js';
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import type {IBubble} from './interfaces/i_bubble.js';
|
||||
import type {ContainerRegion} from './metrics_manager.js';
|
||||
import {Scrollbar} from './scrollbar.js';
|
||||
import * as Touch from './touch.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import * as math from './utils/math.js';
|
||||
import {Size} from './utils/size.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
import * as userAgent from './utils/useragent.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for UI bubble.
|
||||
*/
|
||||
export class Bubble implements IBubble {
|
||||
/** Width of the border around the bubble. */
|
||||
static BORDER_WIDTH = 6;
|
||||
|
||||
/**
|
||||
* Determines the thickness of the base of the arrow in relation to the size
|
||||
* of the bubble. Higher numbers result in thinner arrows.
|
||||
*/
|
||||
static ARROW_THICKNESS = 5;
|
||||
|
||||
/** The number of degrees that the arrow bends counter-clockwise. */
|
||||
static ARROW_ANGLE = 20;
|
||||
|
||||
/**
|
||||
* The sharpness of the arrow's bend. Higher numbers result in smoother
|
||||
* arrows.
|
||||
*/
|
||||
static ARROW_BEND = 4;
|
||||
|
||||
/** Distance between arrow point and anchor point. */
|
||||
static ANCHOR_RADIUS = 8;
|
||||
|
||||
/** Mouse up event data. */
|
||||
private static onMouseUpWrapper: browserEvents.Data|null = null;
|
||||
|
||||
/** Mouse move event data. */
|
||||
private static onMouseMoveWrapper: browserEvents.Data|null = null;
|
||||
|
||||
workspace_: WorkspaceSvg;
|
||||
content_: SVGElement;
|
||||
shape_: SVGElement;
|
||||
|
||||
/** Flag to stop incremental rendering during construction. */
|
||||
private readonly rendered: boolean;
|
||||
|
||||
/** The SVG group containing all parts of the bubble. */
|
||||
private bubbleGroup: SVGGElement|null = null;
|
||||
|
||||
/**
|
||||
* The SVG path for the arrow from the bubble to the icon on the block.
|
||||
*/
|
||||
private bubbleArrow: SVGPathElement|null = null;
|
||||
|
||||
/** The SVG rect for the main body of the bubble. */
|
||||
private bubbleBack: SVGRectElement|null = null;
|
||||
|
||||
/** The SVG group for the resize hash marks on some bubbles. */
|
||||
private resizeGroup: SVGGElement|null = null;
|
||||
|
||||
/** Absolute coordinate of anchor point, in workspace coordinates. */
|
||||
private anchorXY!: Coordinate;
|
||||
|
||||
/**
|
||||
* Relative X coordinate of bubble with respect to the anchor's centre,
|
||||
* in workspace units.
|
||||
* In RTL mode the initial value is negated.
|
||||
*/
|
||||
private relativeLeft = 0;
|
||||
|
||||
/**
|
||||
* Relative Y coordinate of bubble with respect to the anchor's centre, in
|
||||
* workspace units.
|
||||
*/
|
||||
private relativeTop = 0;
|
||||
|
||||
/** Width of bubble, in workspace units. */
|
||||
private width = 0;
|
||||
|
||||
/** Height of bubble, in workspace units. */
|
||||
private height = 0;
|
||||
|
||||
/** Automatically position and reposition the bubble. */
|
||||
private autoLayout = true;
|
||||
|
||||
/** Method to call on resize of bubble. */
|
||||
private resizeCallback: (() => void)|null = null;
|
||||
|
||||
/** Method to call on move of bubble. */
|
||||
private moveCallback: (() => void)|null = null;
|
||||
|
||||
/** Mouse down on bubbleBack event data. */
|
||||
private onMouseDownBubbleWrapper: browserEvents.Data|null = null;
|
||||
|
||||
/** Mouse down on resizeGroup event data. */
|
||||
private onMouseDownResizeWrapper: browserEvents.Data|null = null;
|
||||
|
||||
/**
|
||||
* Describes whether this bubble has been disposed of (nodes and event
|
||||
* listeners removed from the page) or not.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
disposed = false;
|
||||
private arrowRadians: number;
|
||||
|
||||
/**
|
||||
* @param workspace The workspace on which to draw the bubble.
|
||||
* @param content SVG content for the bubble.
|
||||
* @param shape SVG element to avoid eclipsing.
|
||||
* @param anchorXY Absolute position of bubble's anchor point.
|
||||
* @param bubbleWidth Width of bubble, or null if not resizable.
|
||||
* @param bubbleHeight Height of bubble, or null if not resizable.
|
||||
*/
|
||||
constructor(
|
||||
workspace: WorkspaceSvg, content: SVGElement, shape: SVGElement,
|
||||
anchorXY: Coordinate, bubbleWidth: number|null,
|
||||
bubbleHeight: number|null) {
|
||||
this.rendered = false;
|
||||
this.workspace_ = workspace;
|
||||
this.content_ = content;
|
||||
this.shape_ = shape;
|
||||
|
||||
let angle = Bubble.ARROW_ANGLE;
|
||||
if (this.workspace_.RTL) {
|
||||
angle = -angle;
|
||||
}
|
||||
this.arrowRadians = math.toRadians(angle);
|
||||
|
||||
const canvas = workspace.getBubbleCanvas();
|
||||
canvas.appendChild(
|
||||
this.createDom(content, !!(bubbleWidth && bubbleHeight)));
|
||||
|
||||
this.setAnchorLocation(anchorXY);
|
||||
if (!bubbleWidth || !bubbleHeight) {
|
||||
const bBox = (this.content_ as SVGGraphicsElement).getBBox();
|
||||
bubbleWidth = bBox.width + 2 * Bubble.BORDER_WIDTH;
|
||||
bubbleHeight = bBox.height + 2 * Bubble.BORDER_WIDTH;
|
||||
}
|
||||
this.setBubbleSize(bubbleWidth, bubbleHeight);
|
||||
|
||||
// Render the bubble.
|
||||
this.positionBubble();
|
||||
this.renderArrow();
|
||||
this.rendered = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the bubble's DOM.
|
||||
*
|
||||
* @param content SVG content for the bubble.
|
||||
* @param hasResize Add diagonal resize gripper if true.
|
||||
* @returns The bubble's SVG group.
|
||||
*/
|
||||
private createDom(content: Element, hasResize: boolean): SVGElement {
|
||||
/* Create the bubble. Here's the markup that will be generated:
|
||||
<g>
|
||||
<g filter="url(#blocklyEmbossFilter837493)">
|
||||
<path d="... Z" />
|
||||
<rect class="blocklyDraggable" rx="8" ry="8" width="180"
|
||||
height="180"/>
|
||||
</g>
|
||||
<g transform="translate(165, 165)" class="blocklyResizeSE">
|
||||
<polygon points="0,15 15,15 15,0"/>
|
||||
<line class="blocklyResizeLine" x1="5" y1="14" x2="14" y2="5"/>
|
||||
<line class="blocklyResizeLine" x1="10" y1="14" x2="14" y2="10"/>
|
||||
</g>
|
||||
[...content goes here...]
|
||||
</g>
|
||||
*/
|
||||
this.bubbleGroup = dom.createSvgElement(Svg.G, {});
|
||||
let filter: {filter?: string} = {
|
||||
'filter': 'url(#' +
|
||||
this.workspace_.getRenderer().getConstants().embossFilterId + ')',
|
||||
};
|
||||
if (userAgent.JavaFx) {
|
||||
// Multiple reports that JavaFX can't handle filters.
|
||||
// https://github.com/google/blockly/issues/99
|
||||
filter = {};
|
||||
}
|
||||
const bubbleEmboss = dom.createSvgElement(Svg.G, filter, this.bubbleGroup);
|
||||
this.bubbleArrow = dom.createSvgElement(Svg.PATH, {}, bubbleEmboss);
|
||||
this.bubbleBack = dom.createSvgElement(
|
||||
Svg.RECT, {
|
||||
'class': 'blocklyDraggable',
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'rx': Bubble.BORDER_WIDTH,
|
||||
'ry': Bubble.BORDER_WIDTH,
|
||||
},
|
||||
bubbleEmboss);
|
||||
if (hasResize) {
|
||||
this.resizeGroup = dom.createSvgElement(
|
||||
Svg.G, {
|
||||
'class': this.workspace_.RTL ? 'blocklyResizeSW' :
|
||||
'blocklyResizeSE',
|
||||
},
|
||||
this.bubbleGroup);
|
||||
const size = 2 * Bubble.BORDER_WIDTH;
|
||||
dom.createSvgElement(
|
||||
Svg.POLYGON, {'points': `0,${size} ${size},${size} ${size},0`},
|
||||
this.resizeGroup);
|
||||
dom.createSvgElement(
|
||||
Svg.LINE, {
|
||||
'class': 'blocklyResizeLine',
|
||||
'x1': size / 3,
|
||||
'y1': size - 1,
|
||||
'x2': size - 1,
|
||||
'y2': size / 3,
|
||||
},
|
||||
this.resizeGroup);
|
||||
dom.createSvgElement(
|
||||
Svg.LINE, {
|
||||
'class': 'blocklyResizeLine',
|
||||
'x1': size * 2 / 3,
|
||||
'y1': size - 1,
|
||||
'x2': size - 1,
|
||||
'y2': size * 2 / 3,
|
||||
},
|
||||
this.resizeGroup);
|
||||
} else {
|
||||
this.resizeGroup = null;
|
||||
}
|
||||
|
||||
if (!this.workspace_.options.readOnly) {
|
||||
this.onMouseDownBubbleWrapper = browserEvents.conditionalBind(
|
||||
this.bubbleBack, 'pointerdown', this, this.bubbleMouseDown);
|
||||
if (this.resizeGroup) {
|
||||
this.onMouseDownResizeWrapper = browserEvents.conditionalBind(
|
||||
this.resizeGroup, 'pointerdown', this, this.resizeMouseDown);
|
||||
}
|
||||
}
|
||||
this.bubbleGroup.appendChild(content);
|
||||
return this.bubbleGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the root node of the bubble's SVG group.
|
||||
*
|
||||
* @returns The root SVG node of the bubble's group.
|
||||
*/
|
||||
getSvgRoot(): SVGElement {
|
||||
return this.bubbleGroup as SVGElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose the block's ID on the bubble's top-level SVG group.
|
||||
*
|
||||
* @param id ID of block.
|
||||
*/
|
||||
setSvgId(id: string) {
|
||||
this.bubbleGroup?.setAttribute('data-block-id', id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a pointerdown on bubble's border.
|
||||
*
|
||||
* @param e Pointer down event.
|
||||
*/
|
||||
private bubbleMouseDown(e: PointerEvent) {
|
||||
const gesture = this.workspace_.getGesture(e);
|
||||
if (gesture) {
|
||||
gesture.handleBubbleStart(e, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the context menu for this bubble.
|
||||
*
|
||||
* @param _e Mouse event.
|
||||
* @internal
|
||||
*/
|
||||
showContextMenu(_e: Event) {}
|
||||
// NOP on bubbles, but used by the bubble dragger to pass events to
|
||||
// workspace comments.
|
||||
|
||||
/**
|
||||
* Get whether this bubble is deletable or not.
|
||||
*
|
||||
* @returns True if deletable.
|
||||
* @internal
|
||||
*/
|
||||
isDeletable(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the style of this bubble when it is dragged over a delete area.
|
||||
*
|
||||
* @param _enable True if the bubble is about to be deleted, false otherwise.
|
||||
*/
|
||||
setDeleteStyle(_enable: boolean) {}
|
||||
// NOP if bubble is not deletable.
|
||||
|
||||
/**
|
||||
* Handle a pointerdown on bubble's resize corner.
|
||||
*
|
||||
* @param e Pointer down event.
|
||||
*/
|
||||
private resizeMouseDown(e: PointerEvent) {
|
||||
this.promote();
|
||||
Bubble.unbindDragEvents();
|
||||
if (browserEvents.isRightButton(e)) {
|
||||
// No right-click.
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
// Left-click (or middle click)
|
||||
this.workspace_.startDrag(
|
||||
e,
|
||||
new Coordinate(
|
||||
this.workspace_.RTL ? -this.width : this.width, this.height));
|
||||
|
||||
Bubble.onMouseUpWrapper = browserEvents.conditionalBind(
|
||||
document, 'pointerup', this, Bubble.bubbleMouseUp);
|
||||
Bubble.onMouseMoveWrapper = browserEvents.conditionalBind(
|
||||
document, 'pointermove', this, this.resizeMouseMove);
|
||||
this.workspace_.hideChaff();
|
||||
// This event has been handled. No need to bubble up to the document.
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize this bubble to follow the pointer.
|
||||
*
|
||||
* @param e Pointer move event.
|
||||
*/
|
||||
private resizeMouseMove(e: PointerEvent) {
|
||||
this.autoLayout = false;
|
||||
const newXY = this.workspace_.moveDrag(e);
|
||||
this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y);
|
||||
if (this.workspace_.RTL) {
|
||||
// RTL requires the bubble to move its left edge.
|
||||
this.positionBubble();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a function as a callback event for when the bubble is resized.
|
||||
*
|
||||
* @param callback The function to call on resize.
|
||||
*/
|
||||
registerResizeEvent(callback: () => void) {
|
||||
this.resizeCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a function as a callback event for when the bubble is moved.
|
||||
*
|
||||
* @param callback The function to call on move.
|
||||
*/
|
||||
registerMoveEvent(callback: () => void) {
|
||||
this.moveCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this bubble to the top of the stack.
|
||||
*
|
||||
* @returns Whether or not the bubble has been moved.
|
||||
* @internal
|
||||
*/
|
||||
promote(): boolean {
|
||||
const svgGroup = this.bubbleGroup?.parentNode;
|
||||
if (svgGroup?.lastChild !== this.bubbleGroup && this.bubbleGroup) {
|
||||
svgGroup?.appendChild(this.bubbleGroup);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification that the anchor has moved.
|
||||
* Update the arrow and bubble accordingly.
|
||||
*
|
||||
* @param xy Absolute location.
|
||||
*/
|
||||
setAnchorLocation(xy: Coordinate) {
|
||||
this.anchorXY = xy;
|
||||
if (this.rendered) {
|
||||
this.positionBubble();
|
||||
}
|
||||
}
|
||||
|
||||
/** Position the bubble so that it does not fall off-screen. */
|
||||
private layoutBubble() {
|
||||
// Get the metrics in workspace units.
|
||||
const viewMetrics =
|
||||
this.workspace_.getMetricsManager().getViewMetrics(true);
|
||||
|
||||
const optimalLeft = this.getOptimalRelativeLeft(viewMetrics);
|
||||
const optimalTop = this.getOptimalRelativeTop(viewMetrics);
|
||||
const bbox = (this.shape_ as SVGGraphicsElement).getBBox();
|
||||
|
||||
const topPosition = {
|
||||
x: optimalLeft,
|
||||
y: -this.height -
|
||||
this.workspace_.getRenderer().getConstants().MIN_BLOCK_HEIGHT as
|
||||
number,
|
||||
};
|
||||
const startPosition = {x: -this.width - 30, y: optimalTop};
|
||||
const endPosition = {x: bbox.width, y: optimalTop};
|
||||
const bottomPosition = {x: optimalLeft, y: bbox.height};
|
||||
|
||||
const closerPosition =
|
||||
bbox.width < bbox.height ? endPosition : bottomPosition;
|
||||
const fartherPosition =
|
||||
bbox.width < bbox.height ? bottomPosition : endPosition;
|
||||
|
||||
const topPositionOverlap = this.getOverlap(topPosition, viewMetrics);
|
||||
const startPositionOverlap = this.getOverlap(startPosition, viewMetrics);
|
||||
const closerPositionOverlap = this.getOverlap(closerPosition, viewMetrics);
|
||||
const fartherPositionOverlap =
|
||||
this.getOverlap(fartherPosition, viewMetrics);
|
||||
|
||||
// Set the position to whichever position shows the most of the bubble,
|
||||
// with tiebreaks going in the order: top > start > close > far.
|
||||
const mostOverlap = Math.max(
|
||||
topPositionOverlap, startPositionOverlap, closerPositionOverlap,
|
||||
fartherPositionOverlap);
|
||||
if (topPositionOverlap === mostOverlap) {
|
||||
this.relativeLeft = topPosition.x;
|
||||
this.relativeTop = topPosition.y;
|
||||
return;
|
||||
}
|
||||
if (startPositionOverlap === mostOverlap) {
|
||||
this.relativeLeft = startPosition.x;
|
||||
this.relativeTop = startPosition.y;
|
||||
return;
|
||||
}
|
||||
if (closerPositionOverlap === mostOverlap) {
|
||||
this.relativeLeft = closerPosition.x;
|
||||
this.relativeTop = closerPosition.y;
|
||||
return;
|
||||
}
|
||||
// TODO: I believe relativeLeft_ should actually be called relativeStart_
|
||||
// and then the math should be fixed to reflect this. (hopefully it'll
|
||||
// make it look simpler)
|
||||
this.relativeLeft = fartherPosition.x;
|
||||
this.relativeTop = fartherPosition.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the what percentage of the bubble overlaps with the visible
|
||||
* workspace (what percentage of the bubble is visible).
|
||||
*
|
||||
* @param relativeMin The position of the top-left corner of the bubble
|
||||
* relative to the anchor point.
|
||||
* @param viewMetrics The view metrics of the workspace the bubble will appear
|
||||
* in.
|
||||
* @returns The percentage of the bubble that is visible.
|
||||
*/
|
||||
private getOverlap(
|
||||
relativeMin: {x: number, y: number},
|
||||
viewMetrics: ContainerRegion): number {
|
||||
// The position of the top-left corner of the bubble in workspace units.
|
||||
const bubbleMin = {
|
||||
x: this.workspace_.RTL ? this.anchorXY.x - relativeMin.x - this.width :
|
||||
relativeMin.x + this.anchorXY.x,
|
||||
y: relativeMin.y + this.anchorXY.y,
|
||||
};
|
||||
// The position of the bottom-right corner of the bubble in workspace units.
|
||||
const bubbleMax = {
|
||||
x: bubbleMin.x + this.width,
|
||||
y: bubbleMin.y + this.height,
|
||||
};
|
||||
|
||||
// We could adjust these values to account for the scrollbars, but the
|
||||
// bubbles should have been adjusted to not collide with them anyway, so
|
||||
// giving the workspace a slightly larger "bounding box" shouldn't affect
|
||||
// the calculation.
|
||||
|
||||
// The position of the top-left corner of the workspace.
|
||||
const workspaceMin = {x: viewMetrics.left, y: viewMetrics.top};
|
||||
// The position of the bottom-right corner of the workspace.
|
||||
const workspaceMax = {
|
||||
x: viewMetrics.left + viewMetrics.width,
|
||||
y: viewMetrics.top + viewMetrics.height,
|
||||
};
|
||||
|
||||
const overlapWidth = Math.min(bubbleMax.x, workspaceMax.x) -
|
||||
Math.max(bubbleMin.x, workspaceMin.x);
|
||||
const overlapHeight = Math.min(bubbleMax.y, workspaceMax.y) -
|
||||
Math.max(bubbleMin.y, workspaceMin.y);
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(1, overlapWidth * overlapHeight / (this.width * this.height)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate what the optimal horizontal position of the top-left corner of
|
||||
* the bubble is (relative to the anchor point) so that the most area of the
|
||||
* bubble is shown.
|
||||
*
|
||||
* @param viewMetrics The view metrics of the workspace the bubble will appear
|
||||
* in.
|
||||
* @returns The optimal horizontal position of the top-left corner of the
|
||||
* bubble.
|
||||
*/
|
||||
private getOptimalRelativeLeft(viewMetrics: ContainerRegion): number {
|
||||
let relativeLeft = -this.width / 4;
|
||||
|
||||
// No amount of sliding left or right will give us a better overlap.
|
||||
if (this.width > viewMetrics.width) {
|
||||
return relativeLeft;
|
||||
}
|
||||
|
||||
if (this.workspace_.RTL) {
|
||||
// Bubble coordinates are flipped in RTL.
|
||||
const bubbleRight = this.anchorXY.x - relativeLeft;
|
||||
const bubbleLeft = bubbleRight - this.width;
|
||||
|
||||
const workspaceRight = viewMetrics.left + viewMetrics.width;
|
||||
const workspaceLeft = viewMetrics.left +
|
||||
// Thickness in workspace units.
|
||||
Scrollbar.scrollbarThickness / this.workspace_.scale;
|
||||
|
||||
if (bubbleLeft < workspaceLeft) {
|
||||
// Slide the bubble right until it is onscreen.
|
||||
relativeLeft = -(workspaceLeft - this.anchorXY.x + this.width);
|
||||
} else if (bubbleRight > workspaceRight) {
|
||||
// Slide the bubble left until it is onscreen.
|
||||
relativeLeft = -(workspaceRight - this.anchorXY.x);
|
||||
}
|
||||
} else {
|
||||
const bubbleLeft = relativeLeft + this.anchorXY.x;
|
||||
const bubbleRight = bubbleLeft + this.width;
|
||||
|
||||
const workspaceLeft = viewMetrics.left;
|
||||
const workspaceRight = viewMetrics.left + viewMetrics.width -
|
||||
// Thickness in workspace units.
|
||||
Scrollbar.scrollbarThickness / this.workspace_.scale;
|
||||
|
||||
if (bubbleLeft < workspaceLeft) {
|
||||
// Slide the bubble right until it is onscreen.
|
||||
relativeLeft = workspaceLeft - this.anchorXY.x;
|
||||
} else if (bubbleRight > workspaceRight) {
|
||||
// Slide the bubble left until it is onscreen.
|
||||
relativeLeft = workspaceRight - this.anchorXY.x - this.width;
|
||||
}
|
||||
}
|
||||
|
||||
return relativeLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate what the optimal vertical position of the top-left corner of
|
||||
* the bubble is (relative to the anchor point) so that the most area of the
|
||||
* bubble is shown.
|
||||
*
|
||||
* @param viewMetrics The view metrics of the workspace the bubble will appear
|
||||
* in.
|
||||
* @returns The optimal vertical position of the top-left corner of the
|
||||
* bubble.
|
||||
*/
|
||||
private getOptimalRelativeTop(viewMetrics: ContainerRegion): number {
|
||||
let relativeTop = -this.height / 4;
|
||||
|
||||
// No amount of sliding up or down will give us a better overlap.
|
||||
if (this.height > viewMetrics.height) {
|
||||
return relativeTop;
|
||||
}
|
||||
|
||||
const bubbleTop = this.anchorXY.y + relativeTop;
|
||||
const bubbleBottom = bubbleTop + this.height;
|
||||
const workspaceTop = viewMetrics.top;
|
||||
const workspaceBottom = viewMetrics.top +
|
||||
viewMetrics.height - // Thickness in workspace units.
|
||||
Scrollbar.scrollbarThickness / this.workspace_.scale;
|
||||
|
||||
const anchorY = this.anchorXY.y;
|
||||
if (bubbleTop < workspaceTop) {
|
||||
// Slide the bubble down until it is onscreen.
|
||||
relativeTop = workspaceTop - anchorY;
|
||||
} else if (bubbleBottom > workspaceBottom) {
|
||||
// Slide the bubble up until it is onscreen.
|
||||
relativeTop = workspaceBottom - anchorY - this.height;
|
||||
}
|
||||
|
||||
return relativeTop;
|
||||
}
|
||||
|
||||
/** Move the bubble to a location relative to the anchor's centre. */
|
||||
private positionBubble() {
|
||||
let left = this.anchorXY.x;
|
||||
if (this.workspace_.RTL) {
|
||||
left -= this.relativeLeft + this.width;
|
||||
} else {
|
||||
left += this.relativeLeft;
|
||||
}
|
||||
const top = this.relativeTop + this.anchorXY.y;
|
||||
this.moveTo(left, top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the bubble group to the specified location in workspace coordinates.
|
||||
*
|
||||
* @param x The x position to move to.
|
||||
* @param y The y position to move to.
|
||||
* @internal
|
||||
*/
|
||||
moveTo(x: number, y: number) {
|
||||
this.bubbleGroup?.setAttribute(
|
||||
'transform', 'translate(' + x + ',' + y + ')');
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a move callback if one exists at the end of a drag.
|
||||
*
|
||||
* @param adding True if adding, false if removing.
|
||||
* @internal
|
||||
*/
|
||||
setDragging(adding: boolean) {
|
||||
if (!adding && this.moveCallback) {
|
||||
this.moveCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dimensions of this bubble.
|
||||
*
|
||||
* @returns The height and width of the bubble.
|
||||
*/
|
||||
getBubbleSize(): Size {
|
||||
return new Size(this.width, this.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Size this bubble.
|
||||
*
|
||||
* @param width Width of the bubble.
|
||||
* @param height Height of the bubble.
|
||||
*/
|
||||
setBubbleSize(width: number, height: number) {
|
||||
const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
|
||||
// Minimum size of a bubble.
|
||||
width = Math.max(width, doubleBorderWidth + 45);
|
||||
height = Math.max(height, doubleBorderWidth + 20);
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.bubbleBack?.setAttribute('width', `${width}`);
|
||||
this.bubbleBack?.setAttribute('height', `${height}`);
|
||||
if (this.resizeGroup) {
|
||||
if (this.workspace_.RTL) {
|
||||
// Mirror the resize group.
|
||||
const resizeSize = 2 * Bubble.BORDER_WIDTH;
|
||||
this.resizeGroup.setAttribute(
|
||||
'transform',
|
||||
'translate(' + resizeSize + ',' + (height - doubleBorderWidth) +
|
||||
') scale(-1 1)');
|
||||
} else {
|
||||
this.resizeGroup.setAttribute(
|
||||
'transform',
|
||||
'translate(' + (width - doubleBorderWidth) + ',' +
|
||||
(height - doubleBorderWidth) + ')');
|
||||
}
|
||||
}
|
||||
if (this.autoLayout) {
|
||||
this.layoutBubble();
|
||||
}
|
||||
this.positionBubble();
|
||||
this.renderArrow();
|
||||
|
||||
// Allow the contents to resize.
|
||||
if (this.resizeCallback) {
|
||||
this.resizeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/** Draw the arrow between the bubble and the origin. */
|
||||
private renderArrow() {
|
||||
const steps = [];
|
||||
// Find the relative coordinates of the center of the bubble.
|
||||
const relBubbleX = this.width / 2;
|
||||
const relBubbleY = this.height / 2;
|
||||
// Find the relative coordinates of the center of the anchor.
|
||||
let relAnchorX = -this.relativeLeft;
|
||||
let relAnchorY = -this.relativeTop;
|
||||
if (relBubbleX === relAnchorX && relBubbleY === relAnchorY) {
|
||||
// Null case. Bubble is directly on top of the anchor.
|
||||
// Short circuit this rather than wade through divide by zeros.
|
||||
steps.push('M ' + relBubbleX + ',' + relBubbleY);
|
||||
} else {
|
||||
// Compute the angle of the arrow's line.
|
||||
const rise = relAnchorY - relBubbleY;
|
||||
let run = relAnchorX - relBubbleX;
|
||||
if (this.workspace_.RTL) {
|
||||
run *= -1;
|
||||
}
|
||||
const hypotenuse = Math.sqrt(rise * rise + run * run);
|
||||
let angle = Math.acos(run / hypotenuse);
|
||||
if (rise < 0) {
|
||||
angle = 2 * Math.PI - angle;
|
||||
}
|
||||
// Compute a line perpendicular to the arrow.
|
||||
let rightAngle = angle + Math.PI / 2;
|
||||
if (rightAngle > Math.PI * 2) {
|
||||
rightAngle -= Math.PI * 2;
|
||||
}
|
||||
const rightRise = Math.sin(rightAngle);
|
||||
const rightRun = Math.cos(rightAngle);
|
||||
|
||||
// Calculate the thickness of the base of the arrow.
|
||||
const bubbleSize = this.getBubbleSize();
|
||||
let thickness =
|
||||
(bubbleSize.width + bubbleSize.height) / Bubble.ARROW_THICKNESS;
|
||||
thickness = Math.min(thickness, bubbleSize.width, bubbleSize.height) / 4;
|
||||
|
||||
// Back the tip of the arrow off of the anchor.
|
||||
const backoffRatio = 1 - Bubble.ANCHOR_RADIUS / hypotenuse;
|
||||
relAnchorX = relBubbleX + backoffRatio * run;
|
||||
relAnchorY = relBubbleY + backoffRatio * rise;
|
||||
|
||||
// Coordinates for the base of the arrow.
|
||||
const baseX1 = relBubbleX + thickness * rightRun;
|
||||
const baseY1 = relBubbleY + thickness * rightRise;
|
||||
const baseX2 = relBubbleX - thickness * rightRun;
|
||||
const baseY2 = relBubbleY - thickness * rightRise;
|
||||
|
||||
// Distortion to curve the arrow.
|
||||
let swirlAngle = angle + this.arrowRadians;
|
||||
if (swirlAngle > Math.PI * 2) {
|
||||
swirlAngle -= Math.PI * 2;
|
||||
}
|
||||
const swirlRise = Math.sin(swirlAngle) * hypotenuse / Bubble.ARROW_BEND;
|
||||
const swirlRun = Math.cos(swirlAngle) * hypotenuse / Bubble.ARROW_BEND;
|
||||
|
||||
steps.push('M' + baseX1 + ',' + baseY1);
|
||||
steps.push(
|
||||
'C' + (baseX1 + swirlRun) + ',' + (baseY1 + swirlRise) + ' ' +
|
||||
relAnchorX + ',' + relAnchorY + ' ' + relAnchorX + ',' + relAnchorY);
|
||||
steps.push(
|
||||
'C' + relAnchorX + ',' + relAnchorY + ' ' + (baseX2 + swirlRun) +
|
||||
',' + (baseY2 + swirlRise) + ' ' + baseX2 + ',' + baseY2);
|
||||
}
|
||||
steps.push('z');
|
||||
this.bubbleArrow?.setAttribute('d', steps.join(' '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the colour of a bubble.
|
||||
*
|
||||
* @param hexColour Hex code of colour.
|
||||
*/
|
||||
setColour(hexColour: string) {
|
||||
this.bubbleBack?.setAttribute('fill', hexColour);
|
||||
this.bubbleArrow?.setAttribute('fill', hexColour);
|
||||
}
|
||||
|
||||
/** Dispose of this bubble. */
|
||||
dispose() {
|
||||
if (this.onMouseDownBubbleWrapper) {
|
||||
browserEvents.unbind(this.onMouseDownBubbleWrapper);
|
||||
}
|
||||
if (this.onMouseDownResizeWrapper) {
|
||||
browserEvents.unbind(this.onMouseDownResizeWrapper);
|
||||
}
|
||||
Bubble.unbindDragEvents();
|
||||
dom.removeNode(this.bubbleGroup);
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this bubble during a drag, taking into account whether or not there is
|
||||
* a drag surface.
|
||||
*
|
||||
* @param dragSurface The surface that carries rendered items during a drag,
|
||||
* or null if no drag surface is in use.
|
||||
* @param newLoc The location to translate to, in workspace coordinates.
|
||||
* @internal
|
||||
*/
|
||||
moveDuringDrag(dragSurface: BlockDragSurfaceSvg, newLoc: Coordinate) {
|
||||
if (dragSurface) {
|
||||
dragSurface.translateSurface(newLoc.x, newLoc.y);
|
||||
} else {
|
||||
this.moveTo(newLoc.x, newLoc.y);
|
||||
}
|
||||
if (this.workspace_.RTL) {
|
||||
this.relativeLeft = this.anchorXY.x - newLoc.x - this.width;
|
||||
} else {
|
||||
this.relativeLeft = newLoc.x - this.anchorXY.x;
|
||||
}
|
||||
this.relativeTop = newLoc.y - this.anchorXY.y;
|
||||
this.renderArrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the coordinates of the top-left corner of this bubble's body
|
||||
* relative to the drawing surface's origin (0,0), in workspace units.
|
||||
*
|
||||
* @returns Object with .x and .y properties.
|
||||
*/
|
||||
getRelativeToSurfaceXY(): Coordinate {
|
||||
return new Coordinate(
|
||||
this.workspace_.RTL ?
|
||||
-this.relativeLeft + this.anchorXY.x - this.width :
|
||||
this.anchorXY.x + this.relativeLeft,
|
||||
this.anchorXY.y + this.relativeTop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether auto-layout of this bubble is enabled. The first time a bubble
|
||||
* is shown it positions itself to not cover any blocks. Once a user has
|
||||
* dragged it to reposition, it renders where the user put it.
|
||||
*
|
||||
* @param enable True if auto-layout should be enabled, false otherwise.
|
||||
* @internal
|
||||
*/
|
||||
setAutoLayout(enable: boolean) {
|
||||
this.autoLayout = enable;
|
||||
}
|
||||
|
||||
/** Stop binding to the global mouseup and mousemove events. */
|
||||
private static unbindDragEvents() {
|
||||
if (Bubble.onMouseUpWrapper) {
|
||||
browserEvents.unbind(Bubble.onMouseUpWrapper);
|
||||
Bubble.onMouseUpWrapper = null;
|
||||
}
|
||||
if (Bubble.onMouseMoveWrapper) {
|
||||
browserEvents.unbind(Bubble.onMouseMoveWrapper);
|
||||
Bubble.onMouseMoveWrapper = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a pointerup event while dragging a bubble's border or resize handle.
|
||||
*
|
||||
* @param _e Pointer up event.
|
||||
*/
|
||||
private static bubbleMouseUp(_e: PointerEvent) {
|
||||
Touch.clearTouchIdentifier();
|
||||
Bubble.unbindDragEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the text for a non editable bubble.
|
||||
*
|
||||
* @param text The text to display.
|
||||
* @returns The top-level node of the text.
|
||||
* @internal
|
||||
*/
|
||||
static textToDom(text: string): SVGTextElement {
|
||||
const paragraph = dom.createSvgElement(Svg.TEXT, {
|
||||
'class': 'blocklyText blocklyBubbleText blocklyNoPointerEvents',
|
||||
'y': Bubble.BORDER_WIDTH,
|
||||
});
|
||||
const lines = text.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const tspanElement = dom.createSvgElement(
|
||||
Svg.TSPAN, {'dy': '1em', 'x': Bubble.BORDER_WIDTH}, paragraph);
|
||||
const textNode = document.createTextNode(lines[i]);
|
||||
tspanElement.appendChild(textNode);
|
||||
}
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bubble that can not be edited.
|
||||
*
|
||||
* @param paragraphElement The text element for the non editable bubble.
|
||||
* @param block The block that the bubble is attached to.
|
||||
* @param iconXY The coordinate of the icon.
|
||||
* @returns The non editable bubble.
|
||||
* @internal
|
||||
*/
|
||||
static createNonEditableBubble(
|
||||
paragraphElement: SVGTextElement, block: BlockSvg,
|
||||
iconXY: Coordinate): Bubble {
|
||||
const bubble = new Bubble(
|
||||
block.workspace!, paragraphElement, block.pathObject.svgPath, (iconXY),
|
||||
null, null);
|
||||
// Expose this bubble's block's ID on its top-level SVG group.
|
||||
bubble.setSvgId(block.id);
|
||||
if (block.RTL) {
|
||||
// Right-align the paragraph.
|
||||
// This cannot be done until the bubble is rendered on screen.
|
||||
const maxWidth = paragraphElement.getBBox().width;
|
||||
for (let i = 0, textElement;
|
||||
textElement = paragraphElement.childNodes[i] as SVGTSpanElement;
|
||||
i++) {
|
||||
textElement.setAttribute('text-anchor', 'end');
|
||||
textElement.setAttribute('x', String(maxWidth + Bubble.BORDER_WIDTH));
|
||||
}
|
||||
}
|
||||
return bubble;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.BubbleDragger');
|
||||
|
||||
import type {BlockDragSurfaceSvg} from './block_drag_surface.js';
|
||||
import {ComponentManager} from './component_manager.js';
|
||||
import type {CommentMove} from './events/events_comment_move.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
@@ -23,7 +22,6 @@ import {Coordinate} from './utils/coordinate.js';
|
||||
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for a bubble dragger. It moves things on the bubble canvas around the
|
||||
* workspace when they are being dragged by a mouse or touch. These can be
|
||||
@@ -36,7 +34,6 @@ export class BubbleDragger {
|
||||
/** Whether the bubble would be deleted if dropped immediately. */
|
||||
private wouldDeleteBubble_ = false;
|
||||
private readonly startXY_: Coordinate;
|
||||
private dragSurface_: BlockDragSurfaceSvg|null;
|
||||
|
||||
/**
|
||||
* @param bubble The item on the bubble canvas to drag.
|
||||
@@ -48,16 +45,10 @@ export class BubbleDragger {
|
||||
* beginning of the drag, in workspace coordinates.
|
||||
*/
|
||||
this.startXY_ = this.bubble.getRelativeToSurfaceXY();
|
||||
|
||||
/**
|
||||
* The drag surface to move bubbles to during a drag, or null if none should
|
||||
* be used. Block dragging and bubble dragging use the same surface.
|
||||
*/
|
||||
this.dragSurface_ = workspace.getBlockDragSurface();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start dragging a bubble. This includes moving it to the drag surface.
|
||||
* Start dragging a bubble.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@@ -67,12 +58,8 @@ export class BubbleDragger {
|
||||
}
|
||||
|
||||
this.workspace.setResizesEnabled(false);
|
||||
this.bubble.setAutoLayout(false);
|
||||
if (this.dragSurface_) {
|
||||
this.bubble.moveTo(0, 0);
|
||||
this.dragSurface_.translateSurface(this.startXY_.x, this.startXY_.y);
|
||||
// Execute the move on the top-level SVG component.
|
||||
this.dragSurface_.setBlocksAndShow(this.bubble.getSvgRoot());
|
||||
if ((this.bubble as AnyDuringMigration).setAutoLayout) {
|
||||
(this.bubble as AnyDuringMigration).setAutoLayout(false);
|
||||
}
|
||||
|
||||
this.bubble.setDragging && this.bubble.setDragging(true);
|
||||
@@ -90,7 +77,7 @@ export class BubbleDragger {
|
||||
dragBubble(e: PointerEvent, currentDragDeltaXY: Coordinate) {
|
||||
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
|
||||
const newLoc = Coordinate.sum(this.startXY_, delta);
|
||||
this.bubble.moveDuringDrag(this.dragSurface_, newLoc);
|
||||
this.bubble.moveDuringDrag(newLoc);
|
||||
|
||||
const oldDragTarget = this.dragTarget_;
|
||||
this.dragTarget_ = this.workspace.getDragTarget(e);
|
||||
@@ -120,7 +107,9 @@ export class BubbleDragger {
|
||||
if (dragTarget) {
|
||||
const componentManager = this.workspace.getComponentManager();
|
||||
const isDeleteArea = componentManager.hasCapability(
|
||||
dragTarget.id, ComponentManager.Capability.DELETE_AREA);
|
||||
dragTarget.id,
|
||||
ComponentManager.Capability.DELETE_AREA
|
||||
);
|
||||
if (isDeleteArea) {
|
||||
return (dragTarget as IDeleteArea).wouldDelete(this.bubble, false);
|
||||
}
|
||||
@@ -170,9 +159,6 @@ export class BubbleDragger {
|
||||
this.bubble.dispose();
|
||||
} else {
|
||||
// Put everything back onto the bubble canvas.
|
||||
if (this.dragSurface_) {
|
||||
this.dragSurface_.clearAndHide(this.workspace.getBubbleCanvas());
|
||||
}
|
||||
if (this.bubble.setDragging) {
|
||||
this.bubble.setDragging(false);
|
||||
}
|
||||
@@ -187,7 +173,8 @@ export class BubbleDragger {
|
||||
private fireMoveEvent_() {
|
||||
if (this.bubble instanceof WorkspaceCommentSvg) {
|
||||
const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(
|
||||
this.bubble) as CommentMove;
|
||||
this.bubble
|
||||
) as CommentMove;
|
||||
event.setOldCoordinate(this.startXY_);
|
||||
event.recordNew();
|
||||
eventUtils.fire(event);
|
||||
@@ -208,7 +195,8 @@ export class BubbleDragger {
|
||||
private pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate {
|
||||
const result = new Coordinate(
|
||||
pixelCoord.x / this.workspace.scale,
|
||||
pixelCoord.y / this.workspace.scale);
|
||||
pixelCoord.y / this.workspace.scale
|
||||
);
|
||||
if (this.workspace.isMutator) {
|
||||
// If we're in a mutator, its scale is always 1, purely because of some
|
||||
// oddities in our rendering optimizations. The actual scale is the same
|
||||
|
||||
12
core/bubbles.ts
Normal file
12
core/bubbles.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {Bubble} from './bubbles/bubble.js';
|
||||
import {TextBubble} from './bubbles/text_bubble.js';
|
||||
import {TextInputBubble} from './bubbles/textinput_bubble.js';
|
||||
import {MiniWorkspaceBubble} from './bubbles/mini_workspace_bubble.js';
|
||||
|
||||
export {Bubble, TextBubble, TextInputBubble, MiniWorkspaceBubble};
|
||||
607
core/bubbles/bubble.ts
Normal file
607
core/bubbles/bubble.ts
Normal file
@@ -0,0 +1,607 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as browserEvents from '../browser_events.js';
|
||||
import {IBubble} from '../interfaces/i_bubble.js';
|
||||
import {ContainerRegion} from '../metrics_manager.js';
|
||||
import {Scrollbar} from '../scrollbar.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import * as math from '../utils/math.js';
|
||||
import {Rect} from '../utils/rect.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
|
||||
/**
|
||||
* The abstract pop-up bubble class. This creates a UI that looks like a speech
|
||||
* bubble, where it has a "tail" that points to the block, and a "head" that
|
||||
* displays arbitrary svg elements.
|
||||
*/
|
||||
export abstract class Bubble implements IBubble {
|
||||
/** The width of the border around the bubble. */
|
||||
static readonly BORDER_WIDTH = 6;
|
||||
|
||||
/** Double the width of the border around the bubble. */
|
||||
static readonly DOUBLE_BORDER = this.BORDER_WIDTH * 2;
|
||||
|
||||
/** The minimum size the bubble can have. */
|
||||
static readonly MIN_SIZE = this.DOUBLE_BORDER;
|
||||
|
||||
/**
|
||||
* The thickness of the base of the tail in relation to the size of the
|
||||
* bubble. Higher numbers result in thinner tails.
|
||||
*/
|
||||
static readonly TAIL_THICKNESS = 1;
|
||||
|
||||
/** The number of degrees that the tail bends counter-clockwise. */
|
||||
static readonly TAIL_ANGLE = 20;
|
||||
|
||||
/**
|
||||
* The sharpness of the tail's bend. Higher numbers result in smoother
|
||||
* tails.
|
||||
*/
|
||||
static readonly TAIL_BEND = 4;
|
||||
|
||||
/** Distance between arrow point and anchor point. */
|
||||
static readonly ANCHOR_RADIUS = 8;
|
||||
|
||||
/** The SVG group containing all parts of the bubble. */
|
||||
protected svgRoot: SVGGElement;
|
||||
|
||||
/** The SVG path for the arrow from the anchor to the bubble. */
|
||||
private tail: SVGPathElement;
|
||||
|
||||
/** The SVG background rect for the main body of the bubble. */
|
||||
private background: SVGRectElement;
|
||||
|
||||
/** The SVG group containing the contents of the bubble. */
|
||||
protected contentContainer: SVGGElement;
|
||||
|
||||
/**
|
||||
* The size of the bubble (including background and contents but not tail).
|
||||
*/
|
||||
private size = new Size(0, 0);
|
||||
|
||||
/** The colour of the background of the bubble. */
|
||||
private colour = '#ffffff';
|
||||
|
||||
/** True if the bubble has been disposed, false otherwise. */
|
||||
public disposed = false;
|
||||
|
||||
/** The position of the top of the bubble relative to its anchor. */
|
||||
private relativeTop = 0;
|
||||
|
||||
/** The position of the left of the bubble realtive to its anchor. */
|
||||
private relativeLeft = 0;
|
||||
|
||||
/**
|
||||
* @param workspace The workspace this bubble belongs to.
|
||||
* @param anchor The anchor location of the thing this bubble is attached to.
|
||||
* The tail of the bubble will point to this location.
|
||||
* @param ownerRect An optional rect we don't want the bubble to overlap with
|
||||
* when automatically positioning.
|
||||
*/
|
||||
constructor(
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
protected anchor: Coordinate,
|
||||
protected ownerRect?: Rect
|
||||
) {
|
||||
this.svgRoot = dom.createSvgElement(Svg.G, {}, workspace.getBubbleCanvas());
|
||||
const embossGroup = dom.createSvgElement(
|
||||
Svg.G,
|
||||
{
|
||||
'filter': `url(#${
|
||||
this.workspace.getRenderer().getConstants().embossFilterId
|
||||
})`,
|
||||
},
|
||||
this.svgRoot
|
||||
);
|
||||
this.tail = dom.createSvgElement(Svg.PATH, {}, embossGroup);
|
||||
this.background = dom.createSvgElement(
|
||||
Svg.RECT,
|
||||
{
|
||||
'class': 'blocklyDraggable',
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'rx': Bubble.BORDER_WIDTH,
|
||||
'ry': Bubble.BORDER_WIDTH,
|
||||
},
|
||||
embossGroup
|
||||
);
|
||||
this.contentContainer = dom.createSvgElement(Svg.G, {}, this.svgRoot);
|
||||
|
||||
browserEvents.conditionalBind(
|
||||
this.background,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.onMouseDown
|
||||
);
|
||||
}
|
||||
|
||||
/** Dispose of this bubble. */
|
||||
dispose() {
|
||||
dom.removeNode(this.svgRoot);
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the location the tail of this bubble points to.
|
||||
*
|
||||
* @param anchor The location the tail of this bubble points to.
|
||||
* @param relayout If true, reposition the bubble from scratch so that it is
|
||||
* optimally visible. If false, reposition it so it maintains the same
|
||||
* position relative to the anchor.
|
||||
*/
|
||||
setAnchorLocation(anchor: Coordinate, relayout = false) {
|
||||
this.anchor = anchor;
|
||||
if (relayout) {
|
||||
this.positionByRect(this.ownerRect);
|
||||
} else {
|
||||
this.positionRelativeToAnchor();
|
||||
}
|
||||
this.renderTail();
|
||||
}
|
||||
|
||||
/** Sets the position of this bubble relative to its anchor. */
|
||||
setPositionRelativeToAnchor(left: number, top: number) {
|
||||
this.relativeLeft = left;
|
||||
this.relativeTop = top;
|
||||
this.positionRelativeToAnchor();
|
||||
this.renderTail();
|
||||
}
|
||||
|
||||
/** @returns the size of this bubble. */
|
||||
protected getSize() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of this bubble, including the border.
|
||||
*
|
||||
* @param size Sets the size of this bubble, including the border.
|
||||
* @param relayout If true, reposition the bubble from scratch so that it is
|
||||
* optimally visible. If false, reposition it so it maintains the same
|
||||
* position relative to the anchor.
|
||||
*/
|
||||
protected setSize(size: Size, relayout = false) {
|
||||
size.width = Math.max(size.width, Bubble.MIN_SIZE);
|
||||
size.height = Math.max(size.height, Bubble.MIN_SIZE);
|
||||
this.size = size;
|
||||
|
||||
this.background.setAttribute('width', `${size.width}`);
|
||||
this.background.setAttribute('height', `${size.height}`);
|
||||
|
||||
if (relayout) {
|
||||
this.positionByRect(this.ownerRect);
|
||||
} else {
|
||||
this.positionRelativeToAnchor();
|
||||
}
|
||||
this.renderTail();
|
||||
}
|
||||
|
||||
/** Returns the colour of the background and tail of this bubble. */
|
||||
protected getColour(): string {
|
||||
return this.colour;
|
||||
}
|
||||
|
||||
/** Sets the colour of the background and tail of this bubble. */
|
||||
public setColour(colour: string) {
|
||||
this.colour = colour;
|
||||
this.tail.setAttribute('fill', colour);
|
||||
this.background.setAttribute('fill', colour);
|
||||
}
|
||||
|
||||
/** Passes the pointer event off to the gesture system. */
|
||||
private onMouseDown(e: PointerEvent) {
|
||||
this.workspace.getGesture(e)?.handleBubbleStart(e, this);
|
||||
}
|
||||
|
||||
/** Positions the bubble relative to its anchor. Does not render its tail. */
|
||||
protected positionRelativeToAnchor() {
|
||||
let left = this.anchor.x;
|
||||
if (this.workspace.RTL) {
|
||||
left -= this.relativeLeft + this.size.width;
|
||||
} else {
|
||||
left += this.relativeLeft;
|
||||
}
|
||||
const top = this.relativeTop + this.anchor.y;
|
||||
this.moveTo(left, top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the bubble to the given coordinates.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
moveTo(x: number, y: number) {
|
||||
this.svgRoot.setAttribute('transform', `translate(${x}, ${y})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions the bubble "optimally" so that the most of it is visible and
|
||||
* it does not overlap the rect (if provided).
|
||||
*/
|
||||
protected positionByRect(rect = new Rect(0, 0, 0, 0)) {
|
||||
const viewMetrics = this.workspace.getMetricsManager().getViewMetrics(true);
|
||||
|
||||
const optimalLeft = this.getOptimalRelativeLeft(viewMetrics);
|
||||
const optimalTop = this.getOptimalRelativeTop(viewMetrics);
|
||||
|
||||
const topPosition = {
|
||||
x: optimalLeft,
|
||||
y: (-this.size.height -
|
||||
this.workspace.getRenderer().getConstants().MIN_BLOCK_HEIGHT) as number,
|
||||
};
|
||||
const startPosition = {x: -this.size.width - 30, y: optimalTop};
|
||||
const endPosition = {x: rect.getWidth(), y: optimalTop};
|
||||
const bottomPosition = {x: optimalLeft, y: rect.getHeight()};
|
||||
|
||||
const closerPosition =
|
||||
rect.getWidth() < rect.getHeight() ? endPosition : bottomPosition;
|
||||
const fartherPosition =
|
||||
rect.getWidth() < rect.getHeight() ? bottomPosition : endPosition;
|
||||
|
||||
const topPositionOverlap = this.getOverlap(topPosition, viewMetrics);
|
||||
const startPositionOverlap = this.getOverlap(startPosition, viewMetrics);
|
||||
const closerPositionOverlap = this.getOverlap(closerPosition, viewMetrics);
|
||||
const fartherPositionOverlap = this.getOverlap(
|
||||
fartherPosition,
|
||||
viewMetrics
|
||||
);
|
||||
|
||||
// Set the position to whichever position shows the most of the bubble,
|
||||
// with tiebreaks going in the order: top > start > close > far.
|
||||
const mostOverlap = Math.max(
|
||||
topPositionOverlap,
|
||||
startPositionOverlap,
|
||||
closerPositionOverlap,
|
||||
fartherPositionOverlap
|
||||
);
|
||||
if (topPositionOverlap === mostOverlap) {
|
||||
this.relativeLeft = topPosition.x;
|
||||
this.relativeTop = topPosition.y;
|
||||
this.positionRelativeToAnchor();
|
||||
return;
|
||||
}
|
||||
if (startPositionOverlap === mostOverlap) {
|
||||
this.relativeLeft = startPosition.x;
|
||||
this.relativeTop = startPosition.y;
|
||||
this.positionRelativeToAnchor();
|
||||
return;
|
||||
}
|
||||
if (closerPositionOverlap === mostOverlap) {
|
||||
this.relativeLeft = closerPosition.x;
|
||||
this.relativeTop = closerPosition.y;
|
||||
this.positionRelativeToAnchor();
|
||||
return;
|
||||
}
|
||||
// TODO: I believe relativeLeft_ should actually be called relativeStart_
|
||||
// and then the math should be fixed to reflect this. (hopefully it'll
|
||||
// make it look simpler)
|
||||
this.relativeLeft = fartherPosition.x;
|
||||
this.relativeTop = fartherPosition.y;
|
||||
this.positionRelativeToAnchor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the what percentage of the bubble overlaps with the visible
|
||||
* workspace (what percentage of the bubble is visible).
|
||||
*
|
||||
* @param relativeMin The position of the top-left corner of the bubble
|
||||
* relative to the anchor point.
|
||||
* @param viewMetrics The view metrics of the workspace the bubble will appear
|
||||
* in.
|
||||
* @returns The percentage of the bubble that is visible.
|
||||
*/
|
||||
private getOverlap(
|
||||
relativeMin: {x: number; y: number},
|
||||
viewMetrics: ContainerRegion
|
||||
): number {
|
||||
// The position of the top-left corner of the bubble in workspace units.
|
||||
const bubbleMin = {
|
||||
x: this.workspace.RTL
|
||||
? this.anchor.x - relativeMin.x - this.size.width
|
||||
: relativeMin.x + this.anchor.x,
|
||||
y: relativeMin.y + this.anchor.y,
|
||||
};
|
||||
// The position of the bottom-right corner of the bubble in workspace units.
|
||||
const bubbleMax = {
|
||||
x: bubbleMin.x + this.size.width,
|
||||
y: bubbleMin.y + this.size.height,
|
||||
};
|
||||
|
||||
// We could adjust these values to account for the scrollbars, but the
|
||||
// bubbles should have been adjusted to not collide with them anyway, so
|
||||
// giving the workspace a slightly larger "bounding box" shouldn't affect
|
||||
// the calculation.
|
||||
|
||||
// The position of the top-left corner of the workspace.
|
||||
const workspaceMin = {x: viewMetrics.left, y: viewMetrics.top};
|
||||
// The position of the bottom-right corner of the workspace.
|
||||
const workspaceMax = {
|
||||
x: viewMetrics.left + viewMetrics.width,
|
||||
y: viewMetrics.top + viewMetrics.height,
|
||||
};
|
||||
|
||||
const overlapWidth =
|
||||
Math.min(bubbleMax.x, workspaceMax.x) -
|
||||
Math.max(bubbleMin.x, workspaceMin.x);
|
||||
const overlapHeight =
|
||||
Math.min(bubbleMax.y, workspaceMax.y) -
|
||||
Math.max(bubbleMin.y, workspaceMin.y);
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
1,
|
||||
(overlapWidth * overlapHeight) / (this.size.width * this.size.height)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate what the optimal horizontal position of the top-left corner of
|
||||
* the bubble is (relative to the anchor point) so that the most area of the
|
||||
* bubble is shown.
|
||||
*
|
||||
* @param viewMetrics The view metrics of the workspace the bubble will appear
|
||||
* in.
|
||||
* @returns The optimal horizontal position of the top-left corner of the
|
||||
* bubble.
|
||||
*/
|
||||
private getOptimalRelativeLeft(viewMetrics: ContainerRegion): number {
|
||||
// By default, show the bubble just a bit to the left of the anchor.
|
||||
let relativeLeft = -this.size.width / 4;
|
||||
|
||||
// No amount of sliding left or right will give us better overlap.
|
||||
if (this.size.width > viewMetrics.width) return relativeLeft;
|
||||
|
||||
const workspaceRect = this.getWorkspaceViewRect(viewMetrics);
|
||||
|
||||
if (this.workspace.RTL) {
|
||||
// Bubble coordinates are flipped in RTL.
|
||||
const bubbleRight = this.anchor.x - relativeLeft;
|
||||
const bubbleLeft = bubbleRight - this.size.width;
|
||||
|
||||
if (bubbleLeft < workspaceRect.left) {
|
||||
// Slide the bubble right until it is onscreen.
|
||||
relativeLeft = -(workspaceRect.left - this.anchor.x + this.size.width);
|
||||
} else if (bubbleRight > workspaceRect.right) {
|
||||
// Slide the bubble left until it is onscreen.
|
||||
relativeLeft = -(workspaceRect.right - this.anchor.x);
|
||||
}
|
||||
} else {
|
||||
const bubbleLeft = relativeLeft + this.anchor.x;
|
||||
const bubbleRight = bubbleLeft + this.size.width;
|
||||
|
||||
if (bubbleLeft < workspaceRect.left) {
|
||||
// Slide the bubble right until it is onscreen.
|
||||
relativeLeft = workspaceRect.left - this.anchor.x;
|
||||
} else if (bubbleRight > workspaceRect.right) {
|
||||
// Slide the bubble left until it is onscreen.
|
||||
relativeLeft = workspaceRect.right - this.anchor.x - this.size.width;
|
||||
}
|
||||
}
|
||||
|
||||
return relativeLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate what the optimal vertical position of the top-left corner of
|
||||
* the bubble is (relative to the anchor point) so that the most area of the
|
||||
* bubble is shown.
|
||||
*
|
||||
* @param viewMetrics The view metrics of the workspace the bubble will appear
|
||||
* in.
|
||||
* @returns The optimal vertical position of the top-left corner of the
|
||||
* bubble.
|
||||
*/
|
||||
private getOptimalRelativeTop(viewMetrics: ContainerRegion): number {
|
||||
// By default, show the bubble just a bit higher than the anchor.
|
||||
let relativeTop = -this.size.height / 4;
|
||||
|
||||
// No amount of sliding up or down will give us better overlap.
|
||||
if (this.size.height > viewMetrics.height) return relativeTop;
|
||||
|
||||
const top = this.anchor.y + relativeTop;
|
||||
const bottom = top + this.size.height;
|
||||
const workspaceRect = this.getWorkspaceViewRect(viewMetrics);
|
||||
|
||||
if (top < workspaceRect.top) {
|
||||
// Slide the bubble down until it is onscreen.
|
||||
relativeTop = workspaceRect.top - this.anchor.y;
|
||||
} else if (bottom > workspaceRect.bottom) {
|
||||
// Slide the bubble up until it is onscreen.
|
||||
relativeTop = workspaceRect.bottom - this.anchor.y - this.size.height;
|
||||
}
|
||||
|
||||
return relativeTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a rect defining the bounds of the workspace's view in workspace
|
||||
* coordinates.
|
||||
*/
|
||||
private getWorkspaceViewRect(viewMetrics: ContainerRegion): Rect {
|
||||
const top = viewMetrics.top;
|
||||
let bottom = viewMetrics.top + viewMetrics.height;
|
||||
let left = viewMetrics.left;
|
||||
let right = viewMetrics.left + viewMetrics.width;
|
||||
|
||||
bottom -= this.getScrollbarThickness();
|
||||
if (this.workspace.RTL) {
|
||||
left -= this.getScrollbarThickness();
|
||||
} else {
|
||||
right -= this.getScrollbarThickness();
|
||||
}
|
||||
|
||||
return new Rect(top, bottom, left, right);
|
||||
}
|
||||
|
||||
/** @returns the scrollbar thickness in workspace units. */
|
||||
private getScrollbarThickness() {
|
||||
return Scrollbar.scrollbarThickness / this.workspace.scale;
|
||||
}
|
||||
|
||||
/** Draws the tail of the bubble. */
|
||||
private renderTail() {
|
||||
const steps = [];
|
||||
// Find the relative coordinates of the center of the bubble.
|
||||
const relBubbleX = this.size.width / 2;
|
||||
const relBubbleY = this.size.height / 2;
|
||||
// Find the relative coordinates of the center of the anchor.
|
||||
let relAnchorX = -this.relativeLeft;
|
||||
let relAnchorY = -this.relativeTop;
|
||||
if (relBubbleX === relAnchorX && relBubbleY === relAnchorY) {
|
||||
// Null case. Bubble is directly on top of the anchor.
|
||||
// Short circuit this rather than wade through divide by zeros.
|
||||
steps.push('M ' + relBubbleX + ',' + relBubbleY);
|
||||
} else {
|
||||
// Compute the angle of the tail's line.
|
||||
const rise = relAnchorY - relBubbleY;
|
||||
let run = relAnchorX - relBubbleX;
|
||||
if (this.workspace.RTL) {
|
||||
run *= -1;
|
||||
}
|
||||
const hypotenuse = Math.sqrt(rise * rise + run * run);
|
||||
let angle = Math.acos(run / hypotenuse);
|
||||
if (rise < 0) {
|
||||
angle = 2 * Math.PI - angle;
|
||||
}
|
||||
// Compute a line perpendicular to the tail.
|
||||
let rightAngle = angle + Math.PI / 2;
|
||||
if (rightAngle > Math.PI * 2) {
|
||||
rightAngle -= Math.PI * 2;
|
||||
}
|
||||
const rightRise = Math.sin(rightAngle);
|
||||
const rightRun = Math.cos(rightAngle);
|
||||
|
||||
// Calculate the thickness of the base of the tail.
|
||||
let thickness =
|
||||
(this.size.width + this.size.height) / Bubble.TAIL_THICKNESS;
|
||||
thickness = Math.min(thickness, this.size.width, this.size.height) / 4;
|
||||
|
||||
// Back the tip of the tail off of the anchor.
|
||||
const backoffRatio = 1 - Bubble.ANCHOR_RADIUS / hypotenuse;
|
||||
relAnchorX = relBubbleX + backoffRatio * run;
|
||||
relAnchorY = relBubbleY + backoffRatio * rise;
|
||||
|
||||
// Coordinates for the base of the tail.
|
||||
const baseX1 = relBubbleX + thickness * rightRun;
|
||||
const baseY1 = relBubbleY + thickness * rightRise;
|
||||
const baseX2 = relBubbleX - thickness * rightRun;
|
||||
const baseY2 = relBubbleY - thickness * rightRise;
|
||||
|
||||
// Distortion to curve the tail.
|
||||
const radians = math.toRadians(
|
||||
this.workspace.RTL ? -Bubble.TAIL_ANGLE : Bubble.TAIL_ANGLE
|
||||
);
|
||||
let swirlAngle = angle + radians;
|
||||
if (swirlAngle > Math.PI * 2) {
|
||||
swirlAngle -= Math.PI * 2;
|
||||
}
|
||||
const swirlRise = (Math.sin(swirlAngle) * hypotenuse) / Bubble.TAIL_BEND;
|
||||
const swirlRun = (Math.cos(swirlAngle) * hypotenuse) / Bubble.TAIL_BEND;
|
||||
|
||||
steps.push('M' + baseX1 + ',' + baseY1);
|
||||
steps.push(
|
||||
'C' +
|
||||
(baseX1 + swirlRun) +
|
||||
',' +
|
||||
(baseY1 + swirlRise) +
|
||||
' ' +
|
||||
relAnchorX +
|
||||
',' +
|
||||
relAnchorY +
|
||||
' ' +
|
||||
relAnchorX +
|
||||
',' +
|
||||
relAnchorY
|
||||
);
|
||||
steps.push(
|
||||
'C' +
|
||||
relAnchorX +
|
||||
',' +
|
||||
relAnchorY +
|
||||
' ' +
|
||||
(baseX2 + swirlRun) +
|
||||
',' +
|
||||
(baseY2 + swirlRise) +
|
||||
' ' +
|
||||
baseX2 +
|
||||
',' +
|
||||
baseY2
|
||||
);
|
||||
}
|
||||
steps.push('z');
|
||||
this.tail?.setAttribute('d', steps.join(' '));
|
||||
}
|
||||
/**
|
||||
* Move this bubble to the front of the visible workspace.
|
||||
*
|
||||
* @returns Whether or not the bubble has been moved.
|
||||
* @internal
|
||||
*/
|
||||
bringToFront(): boolean {
|
||||
const svgGroup = this.svgRoot?.parentNode;
|
||||
if (this.svgRoot && svgGroup?.lastChild !== this.svgRoot) {
|
||||
svgGroup?.appendChild(this.svgRoot);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getRelativeToSurfaceXY(): Coordinate {
|
||||
return new Coordinate(
|
||||
this.workspace.RTL
|
||||
? -this.relativeLeft + this.anchor.x - this.size.width
|
||||
: this.anchor.x + this.relativeLeft,
|
||||
this.anchor.y + this.relativeTop
|
||||
);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getSvgRoot(): SVGElement {
|
||||
return this.svgRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this bubble during a drag.
|
||||
*
|
||||
* @param newLoc The location to translate to, in workspace coordinates.
|
||||
* @internal
|
||||
*/
|
||||
moveDuringDrag(newLoc: Coordinate) {
|
||||
this.moveTo(newLoc.x, newLoc.y);
|
||||
if (this.workspace.RTL) {
|
||||
this.relativeLeft = this.anchor.x - newLoc.x - this.size.width;
|
||||
} else {
|
||||
this.relativeLeft = newLoc.x - this.anchor.x;
|
||||
}
|
||||
this.relativeTop = newLoc.y - this.anchor.y;
|
||||
this.renderTail();
|
||||
}
|
||||
|
||||
setDragging(_start: boolean) {
|
||||
// NOOP in base class.
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
setDeleteStyle(_enable: boolean) {
|
||||
// NOOP in base class.
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
isDeletable(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
showContextMenu(_e: Event) {
|
||||
// NOOP in base class.
|
||||
}
|
||||
}
|
||||
282
core/bubbles/mini_workspace_bubble.ts
Normal file
282
core/bubbles/mini_workspace_bubble.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {Abstract as AbstractEvent} from '../events/events_abstract.js';
|
||||
import type {BlocklyOptions} from '../blockly_options.js';
|
||||
import {Bubble} from './bubble.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import {Options} from '../options.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import type {Rect} from '../utils/rect.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
|
||||
/**
|
||||
* A bubble that contains a mini-workspace which can hold arbitrary blocks.
|
||||
* Used by the mutator icon.
|
||||
*/
|
||||
export class MiniWorkspaceBubble extends Bubble {
|
||||
/**
|
||||
* The minimum amount of change to the mini workspace view to trigger
|
||||
* resizing the bubble.
|
||||
*/
|
||||
private static readonly MINIMUM_VIEW_CHANGE = 10;
|
||||
|
||||
/**
|
||||
* An arbitrary margin of whitespace to put around the blocks in the
|
||||
* workspace.
|
||||
*/
|
||||
private static readonly MARGIN = Bubble.DOUBLE_BORDER * 3;
|
||||
|
||||
/** The root svg element containing the workspace. */
|
||||
private svgDialog: SVGElement;
|
||||
|
||||
/** The workspace that gets shown within this bubble. */
|
||||
private miniWorkspace: WorkspaceSvg;
|
||||
|
||||
/**
|
||||
* Should this bubble automatically reposition itself when it resizes?
|
||||
* Becomes false after this bubble is first dragged.
|
||||
*/
|
||||
private autoLayout = true;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
workspaceOptions: BlocklyOptions,
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
protected anchor: Coordinate,
|
||||
protected ownerRect?: Rect
|
||||
) {
|
||||
super(workspace, anchor, ownerRect);
|
||||
const options = new Options(workspaceOptions);
|
||||
this.validateWorkspaceOptions(options);
|
||||
|
||||
this.svgDialog = dom.createSvgElement(
|
||||
Svg.SVG,
|
||||
{
|
||||
'x': Bubble.BORDER_WIDTH,
|
||||
'y': Bubble.BORDER_WIDTH,
|
||||
},
|
||||
this.contentContainer
|
||||
);
|
||||
workspaceOptions.parentWorkspace = this.workspace;
|
||||
this.miniWorkspace = this.newWorkspaceSvg(new Options(workspaceOptions));
|
||||
const background = this.miniWorkspace.createDom('blocklyMutatorBackground');
|
||||
this.svgDialog.appendChild(background);
|
||||
if (options.languageTree) {
|
||||
background.insertBefore(
|
||||
this.miniWorkspace.addFlyout(Svg.G),
|
||||
this.miniWorkspace.getCanvas()
|
||||
);
|
||||
const flyout = this.miniWorkspace.getFlyout();
|
||||
flyout?.init(this.miniWorkspace);
|
||||
flyout?.show(options.languageTree);
|
||||
}
|
||||
|
||||
this.miniWorkspace.addChangeListener(this.onWorkspaceChange.bind(this));
|
||||
this.miniWorkspace
|
||||
.getFlyout()
|
||||
?.getWorkspace()
|
||||
?.addChangeListener(this.onWorkspaceChange.bind(this));
|
||||
this.updateBubbleSize();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.miniWorkspace.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getWorkspace(): WorkspaceSvg {
|
||||
return this.miniWorkspace;
|
||||
}
|
||||
|
||||
/** Adds a change listener to the mini workspace. */
|
||||
addWorkspaceChangeListener(listener: (e: AbstractEvent) => void) {
|
||||
this.miniWorkspace.addChangeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the workspace options to make sure folks aren't trying to
|
||||
* enable things the miniworkspace doesn't support.
|
||||
*/
|
||||
private validateWorkspaceOptions(options: Options) {
|
||||
if (options.hasCategories) {
|
||||
throw new Error(
|
||||
'The miniworkspace bubble does not support toolboxes with categories'
|
||||
);
|
||||
}
|
||||
if (options.hasTrashcan) {
|
||||
throw new Error('The miniworkspace bubble does not support trashcans');
|
||||
}
|
||||
if (
|
||||
options.zoomOptions.controls ||
|
||||
options.zoomOptions.wheel ||
|
||||
options.zoomOptions.pinch
|
||||
) {
|
||||
throw new Error('The miniworkspace bubble does not support zooming');
|
||||
}
|
||||
if (
|
||||
options.moveOptions.scrollbars ||
|
||||
options.moveOptions.wheel ||
|
||||
options.moveOptions.drag
|
||||
) {
|
||||
throw new Error(
|
||||
'The miniworkspace bubble does not scrolling/moving the workspace'
|
||||
);
|
||||
}
|
||||
if (options.horizontalLayout) {
|
||||
throw new Error(
|
||||
'The miniworkspace bubble does not support horizontal layouts'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private onWorkspaceChange() {
|
||||
this.bumpBlocksIntoBounds();
|
||||
this.updateBubbleSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bumps blocks that are above the top or outside the start-side of the
|
||||
* workspace back within the workspace.
|
||||
*
|
||||
* Blocks that are below the bottom or outside the end-side of the workspace
|
||||
* are dealt with by resizing the workspace to show them.
|
||||
*/
|
||||
private bumpBlocksIntoBounds() {
|
||||
if (this.miniWorkspace.isDragging()) return;
|
||||
|
||||
const MARGIN = 20;
|
||||
|
||||
for (const block of this.miniWorkspace.getTopBlocks(false)) {
|
||||
const blockXY = block.getRelativeToSurfaceXY();
|
||||
|
||||
// Bump any block that's above the top back inside.
|
||||
if (blockXY.y < MARGIN) {
|
||||
block.moveBy(0, MARGIN - blockXY.y);
|
||||
}
|
||||
// Bump any block overlapping the flyout back inside.
|
||||
if (block.RTL) {
|
||||
let right = -MARGIN;
|
||||
const flyout = this.miniWorkspace.getFlyout();
|
||||
if (flyout) {
|
||||
right -= flyout.getWidth();
|
||||
}
|
||||
if (blockXY.x > right) {
|
||||
block.moveBy(right - blockXY.x, 0);
|
||||
}
|
||||
} else if (blockXY.x < MARGIN) {
|
||||
block.moveBy(MARGIN - blockXY.x, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the size of this bubble to account for the size of the
|
||||
* mini workspace.
|
||||
*/
|
||||
private updateBubbleSize() {
|
||||
if (this.miniWorkspace.isDragging()) return;
|
||||
|
||||
const currSize = this.getSize();
|
||||
const newSize = this.calculateWorkspaceSize();
|
||||
if (
|
||||
Math.abs(currSize.width - newSize.width) <
|
||||
MiniWorkspaceBubble.MINIMUM_VIEW_CHANGE &&
|
||||
Math.abs(currSize.height - newSize.height) <
|
||||
MiniWorkspaceBubble.MINIMUM_VIEW_CHANGE
|
||||
) {
|
||||
// Only resize if the size has noticeably changed.
|
||||
return;
|
||||
}
|
||||
this.svgDialog.setAttribute('width', `${newSize.width}px`);
|
||||
this.svgDialog.setAttribute('height', `${newSize.height}px`);
|
||||
this.miniWorkspace.setCachedParentSvgSize(newSize.width, newSize.height);
|
||||
if (this.miniWorkspace.RTL) {
|
||||
// Scroll the workspace to always left-align.
|
||||
this.miniWorkspace
|
||||
.getCanvas()
|
||||
.setAttribute('transform', `translate(${newSize.width}, 0)`);
|
||||
}
|
||||
this.setSize(
|
||||
new Size(
|
||||
newSize.width + Bubble.DOUBLE_BORDER,
|
||||
newSize.height + Bubble.DOUBLE_BORDER
|
||||
),
|
||||
this.autoLayout
|
||||
);
|
||||
this.miniWorkspace.resize();
|
||||
this.miniWorkspace.recordDragTargets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the size of the mini workspace for use in resizing the bubble.
|
||||
*/
|
||||
private calculateWorkspaceSize(): Size {
|
||||
// TODO (#7104): Clean this up to be more readable and unified for RTL
|
||||
// vs LTR.
|
||||
const canvas = this.miniWorkspace.getCanvas();
|
||||
const workspaceSize = canvas.getBBox();
|
||||
let width = workspaceSize.width + workspaceSize.x;
|
||||
let height = workspaceSize.height + MiniWorkspaceBubble.MARGIN;
|
||||
const flyout = this.miniWorkspace.getFlyout();
|
||||
if (flyout) {
|
||||
const flyoutScrollMetrics = flyout
|
||||
.getWorkspace()
|
||||
.getMetricsManager()
|
||||
.getScrollMetrics();
|
||||
height = Math.max(height, flyoutScrollMetrics.height + 20);
|
||||
width += flyout.getWidth();
|
||||
}
|
||||
if (this.miniWorkspace.RTL) {
|
||||
width = -workspaceSize.x;
|
||||
}
|
||||
width += MiniWorkspaceBubble.MARGIN;
|
||||
return new Size(width, height);
|
||||
}
|
||||
|
||||
/** Reapplies styles to all of the blocks in the mini workspace. */
|
||||
updateBlockStyles() {
|
||||
for (const block of this.miniWorkspace.getAllBlocks(false)) {
|
||||
block.setStyle(block.getStyleName());
|
||||
}
|
||||
|
||||
const flyoutWs = this.miniWorkspace.getFlyout()?.getWorkspace();
|
||||
if (flyoutWs) {
|
||||
for (const block of flyoutWs.getAllBlocks(false)) {
|
||||
block.setStyle(block.getStyleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this bubble during a drag.
|
||||
*
|
||||
* @param newLoc The location to translate to, in workspace coordinates.
|
||||
* @internal
|
||||
*/
|
||||
moveDuringDrag(newLoc: Coordinate): void {
|
||||
super.moveDuringDrag(newLoc);
|
||||
this.autoLayout = false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
moveTo(x: number, y: number): void {
|
||||
super.moveTo(x, y);
|
||||
this.miniWorkspace.recordDragTargets();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
newWorkspaceSvg(options: Options): WorkspaceSvg {
|
||||
throw new Error(
|
||||
'The implementation of newWorkspaceSvg should be ' +
|
||||
'monkey-patched in by blockly.ts'
|
||||
);
|
||||
}
|
||||
}
|
||||
102
core/bubbles/text_bubble.ts
Normal file
102
core/bubbles/text_bubble.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {Bubble} from './bubble.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import {Rect} from '../utils/rect.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
|
||||
/**
|
||||
* A bubble that displays non-editable text. Used by the warning icon.
|
||||
*/
|
||||
export class TextBubble extends Bubble {
|
||||
private paragraph: SVGTextElement;
|
||||
|
||||
constructor(
|
||||
private text: string,
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
protected anchor: Coordinate,
|
||||
protected ownerRect?: Rect
|
||||
) {
|
||||
super(workspace, anchor, ownerRect);
|
||||
this.paragraph = this.stringToSvg(text, this.contentContainer);
|
||||
this.updateBubbleSize();
|
||||
}
|
||||
|
||||
/** @returns the current text of this text bubble. */
|
||||
getText(): string {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
/** Sets the current text of this text bubble, and updates the display. */
|
||||
setText(text: string) {
|
||||
this.text = text;
|
||||
dom.removeNode(this.paragraph);
|
||||
this.paragraph = this.stringToSvg(text, this.contentContainer);
|
||||
this.updateBubbleSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given string into an svg containing that string,
|
||||
* broken up by newlines.
|
||||
*/
|
||||
private stringToSvg(text: string, container: SVGGElement) {
|
||||
const paragraph = this.createParagraph(container);
|
||||
const spans = this.createSpans(paragraph, text);
|
||||
if (this.workspace.RTL)
|
||||
this.rightAlignSpans(paragraph.getBBox().width, spans);
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
/** Creates the paragraph container for this bubble's view's spans. */
|
||||
private createParagraph(container: SVGGElement): SVGTextElement {
|
||||
return dom.createSvgElement(
|
||||
Svg.TEXT,
|
||||
{
|
||||
'class': 'blocklyText blocklyBubbleText blocklyNoPointerEvents',
|
||||
'y': Bubble.BORDER_WIDTH,
|
||||
},
|
||||
container
|
||||
);
|
||||
}
|
||||
|
||||
/** Creates the spans visualizing the text of this bubble. */
|
||||
private createSpans(parent: SVGTextElement, text: string): SVGTSpanElement[] {
|
||||
return text.split('\n').map((line) => {
|
||||
const tspan = dom.createSvgElement(
|
||||
Svg.TSPAN,
|
||||
{'dy': '1em', 'x': Bubble.BORDER_WIDTH},
|
||||
parent
|
||||
);
|
||||
const textNode = document.createTextNode(line);
|
||||
tspan.appendChild(textNode);
|
||||
return tspan;
|
||||
});
|
||||
}
|
||||
|
||||
/** Right aligns the given spans. */
|
||||
private rightAlignSpans(maxWidth: number, spans: SVGTSpanElement[]) {
|
||||
for (const span of spans) {
|
||||
span.setAttribute('text-anchor', 'end');
|
||||
span.setAttribute('x', `${maxWidth + Bubble.BORDER_WIDTH}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the size of this bubble to account for the size of the text. */
|
||||
private updateBubbleSize() {
|
||||
const bbox = this.paragraph.getBBox();
|
||||
this.setSize(
|
||||
new Size(
|
||||
bbox.width + Bubble.BORDER_WIDTH * 2,
|
||||
bbox.height + Bubble.BORDER_WIDTH * 2
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
343
core/bubbles/textinput_bubble.ts
Normal file
343
core/bubbles/textinput_bubble.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {Bubble} from './bubble.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import * as Css from '../css.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import {Rect} from '../utils/rect.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import * as touch from '../touch.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {browserEvents} from '../utils.js';
|
||||
|
||||
/**
|
||||
* A bubble that displays editable text. It can also be resized by the user.
|
||||
* Used by the comment icon.
|
||||
*/
|
||||
export class TextInputBubble extends Bubble {
|
||||
/** The root of the elements specific to the text element. */
|
||||
private inputRoot: SVGForeignObjectElement;
|
||||
|
||||
/** The text input area element. */
|
||||
private textArea: HTMLTextAreaElement;
|
||||
|
||||
/** The group containing the lines indicating the bubble is resizable. */
|
||||
private resizeGroup: SVGGElement;
|
||||
|
||||
/**
|
||||
* Event data associated with the listener for pointer up events on the
|
||||
* resize group.
|
||||
*/
|
||||
private resizePointerUpListener: browserEvents.Data | null = null;
|
||||
|
||||
/**
|
||||
* Event data associated with the listener for pointer move events on the
|
||||
* resize group.
|
||||
*/
|
||||
private resizePointerMoveListener: browserEvents.Data | null = null;
|
||||
|
||||
/** Functions listening for changes to the text of this bubble. */
|
||||
private textChangeListeners: (() => void)[] = [];
|
||||
|
||||
/** Functions listening for changes to the size of this bubble. */
|
||||
private sizeChangeListeners: (() => void)[] = [];
|
||||
|
||||
/** The text of this bubble. */
|
||||
private text = '';
|
||||
|
||||
/** The default size of this bubble, including borders. */
|
||||
private readonly DEFAULT_SIZE = new Size(
|
||||
160 + Bubble.DOUBLE_BORDER,
|
||||
80 + Bubble.DOUBLE_BORDER
|
||||
);
|
||||
|
||||
/** The minimum size of this bubble, including borders. */
|
||||
private readonly MIN_SIZE = new Size(
|
||||
45 + Bubble.DOUBLE_BORDER,
|
||||
20 + Bubble.DOUBLE_BORDER
|
||||
);
|
||||
|
||||
/**
|
||||
* @param workspace The workspace this bubble belongs to.
|
||||
* @param anchor The anchor location of the thing this bubble is attached to.
|
||||
* The tail of the bubble will point to this location.
|
||||
* @param ownerRect An optional rect we don't want the bubble to overlap with
|
||||
* when automatically positioning.
|
||||
*/
|
||||
constructor(
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
protected anchor: Coordinate,
|
||||
protected ownerRect?: Rect
|
||||
) {
|
||||
super(workspace, anchor, ownerRect);
|
||||
({inputRoot: this.inputRoot, textArea: this.textArea} = this.createEditor(
|
||||
this.contentContainer
|
||||
));
|
||||
this.resizeGroup = this.createResizeHandle(this.svgRoot);
|
||||
this.setSize(this.DEFAULT_SIZE, true);
|
||||
}
|
||||
|
||||
/** @returns the text of this bubble. */
|
||||
getText(): string {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
/** Sets the text of this bubble. Calls change listeners. */
|
||||
setText(text: string) {
|
||||
this.text = text;
|
||||
this.textArea.value = text;
|
||||
this.onTextChange();
|
||||
}
|
||||
|
||||
/** Adds a change listener to be notified when this bubble's text changes. */
|
||||
addTextChangeListener(listener: () => void) {
|
||||
this.textChangeListeners.push(listener);
|
||||
}
|
||||
|
||||
/** Adds a change listener to be notified when this bubble's size changes. */
|
||||
addSizeChangeListener(listener: () => void) {
|
||||
this.sizeChangeListeners.push(listener);
|
||||
}
|
||||
|
||||
/** Creates the editor UI for this bubble. */
|
||||
private createEditor(container: SVGGElement): {
|
||||
inputRoot: SVGForeignObjectElement;
|
||||
textArea: HTMLTextAreaElement;
|
||||
} {
|
||||
const inputRoot = dom.createSvgElement(
|
||||
Svg.FOREIGNOBJECT,
|
||||
{
|
||||
'x': Bubble.BORDER_WIDTH,
|
||||
'y': Bubble.BORDER_WIDTH,
|
||||
},
|
||||
container
|
||||
);
|
||||
|
||||
const body = document.createElementNS(dom.HTML_NS, 'body');
|
||||
body.setAttribute('xmlns', dom.HTML_NS);
|
||||
body.className = 'blocklyMinimalBody';
|
||||
|
||||
const textArea = document.createElementNS(
|
||||
dom.HTML_NS,
|
||||
'textarea'
|
||||
) as HTMLTextAreaElement;
|
||||
textArea.className = 'blocklyCommentTextarea';
|
||||
textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR');
|
||||
|
||||
body.appendChild(textArea);
|
||||
inputRoot.appendChild(body);
|
||||
|
||||
this.bindTextAreaEvents(textArea);
|
||||
setTimeout(() => {
|
||||
textArea.focus();
|
||||
}, 0);
|
||||
|
||||
return {inputRoot, textArea};
|
||||
}
|
||||
|
||||
/** Binds events to the text area element. */
|
||||
private bindTextAreaEvents(textArea: HTMLTextAreaElement) {
|
||||
// Don't zoom with mousewheel.
|
||||
browserEvents.conditionalBind(textArea, 'wheel', this, (e: Event) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
browserEvents.conditionalBind(
|
||||
textArea,
|
||||
'focus',
|
||||
this,
|
||||
this.onStartEdit,
|
||||
true
|
||||
);
|
||||
browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange);
|
||||
}
|
||||
|
||||
/** Creates the resize handler elements and binds events to them. */
|
||||
private createResizeHandle(container: SVGGElement): SVGGElement {
|
||||
const resizeGroup = dom.createSvgElement(
|
||||
Svg.G,
|
||||
{
|
||||
'class': this.workspace.RTL ? 'blocklyResizeSW' : 'blocklyResizeSE',
|
||||
},
|
||||
container
|
||||
);
|
||||
const size = 2 * Bubble.BORDER_WIDTH;
|
||||
dom.createSvgElement(
|
||||
Svg.POLYGON,
|
||||
{'points': `0,${size} ${size},${size} ${size},0`},
|
||||
resizeGroup
|
||||
);
|
||||
dom.createSvgElement(
|
||||
Svg.LINE,
|
||||
{
|
||||
'class': 'blocklyResizeLine',
|
||||
'x1': size / 3,
|
||||
'y1': size - 1,
|
||||
'x2': size - 1,
|
||||
'y2': size / 3,
|
||||
},
|
||||
resizeGroup
|
||||
);
|
||||
dom.createSvgElement(
|
||||
Svg.LINE,
|
||||
{
|
||||
'class': 'blocklyResizeLine',
|
||||
'x1': (size * 2) / 3,
|
||||
'y1': size - 1,
|
||||
'x2': size - 1,
|
||||
'y2': (size * 2) / 3,
|
||||
},
|
||||
resizeGroup
|
||||
);
|
||||
|
||||
browserEvents.conditionalBind(
|
||||
resizeGroup,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.onResizePointerDown
|
||||
);
|
||||
|
||||
return resizeGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of this bubble, including the border.
|
||||
*
|
||||
* @param size Sets the size of this bubble, including the border.
|
||||
* @param relayout If true, reposition the bubble from scratch so that it is
|
||||
* optimally visible. If false, reposition it so it maintains the same
|
||||
* position relative to the anchor.
|
||||
*/
|
||||
setSize(size: Size, relayout = false) {
|
||||
size.width = Math.max(size.width, this.MIN_SIZE.width);
|
||||
size.height = Math.max(size.height, this.MIN_SIZE.height);
|
||||
|
||||
const widthMinusBorder = size.width - Bubble.DOUBLE_BORDER;
|
||||
const heightMinusBorder = size.height - Bubble.DOUBLE_BORDER;
|
||||
this.inputRoot.setAttribute('width', `${widthMinusBorder}`);
|
||||
this.inputRoot.setAttribute('height', `${heightMinusBorder}`);
|
||||
this.textArea.style.width = `${widthMinusBorder - 4}px`;
|
||||
this.textArea.style.height = `${heightMinusBorder - 4}px`;
|
||||
|
||||
if (this.workspace.RTL) {
|
||||
this.resizeGroup.setAttribute(
|
||||
'transform',
|
||||
`translate(${Bubble.DOUBLE_BORDER}, ${heightMinusBorder}) scale(-1 1)`
|
||||
);
|
||||
} else {
|
||||
this.resizeGroup.setAttribute(
|
||||
'transform',
|
||||
`translate(${widthMinusBorder}, ${heightMinusBorder})`
|
||||
);
|
||||
}
|
||||
|
||||
super.setSize(size, relayout);
|
||||
this.onSizeChange();
|
||||
}
|
||||
|
||||
/** @returns the size of this bubble. */
|
||||
getSize(): Size {
|
||||
// Overriden to be public.
|
||||
return super.getSize();
|
||||
}
|
||||
|
||||
/** Handles mouse down events on the resize target. */
|
||||
private onResizePointerDown(e: PointerEvent) {
|
||||
this.bringToFront();
|
||||
if (browserEvents.isRightButton(e)) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
this.workspace.startDrag(
|
||||
e,
|
||||
new Coordinate(
|
||||
this.workspace.RTL ? -this.getSize().width : this.getSize().width,
|
||||
this.getSize().height
|
||||
)
|
||||
);
|
||||
|
||||
this.resizePointerUpListener = browserEvents.conditionalBind(
|
||||
document,
|
||||
'pointerup',
|
||||
this,
|
||||
this.onResizePointerUp
|
||||
);
|
||||
this.resizePointerMoveListener = browserEvents.conditionalBind(
|
||||
document,
|
||||
'pointermove',
|
||||
this,
|
||||
this.onResizePointerMove
|
||||
);
|
||||
this.workspace.hideChaff();
|
||||
// This event has been handled. No need to bubble up to the document.
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/** Handles pointer up events on the resize target. */
|
||||
private onResizePointerUp(_e: PointerEvent) {
|
||||
touch.clearTouchIdentifier();
|
||||
if (this.resizePointerUpListener) {
|
||||
browserEvents.unbind(this.resizePointerUpListener);
|
||||
this.resizePointerUpListener = null;
|
||||
}
|
||||
if (this.resizePointerMoveListener) {
|
||||
browserEvents.unbind(this.resizePointerMoveListener);
|
||||
this.resizePointerMoveListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles pointer move events on the resize target. */
|
||||
private onResizePointerMove(e: PointerEvent) {
|
||||
const delta = this.workspace.moveDrag(e);
|
||||
this.setSize(
|
||||
new Size(this.workspace.RTL ? -delta.x : delta.x, delta.y),
|
||||
false
|
||||
);
|
||||
this.onSizeChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles starting an edit of the text area. Brings the bubble to the front.
|
||||
*/
|
||||
private onStartEdit() {
|
||||
if (this.bringToFront()) {
|
||||
// Since the act of moving this node within the DOM causes a loss of
|
||||
// focus, we need to reapply the focus.
|
||||
this.textArea.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles a text change event for the text area. Calls event listeners. */
|
||||
private onTextChange() {
|
||||
this.text = this.textArea.value;
|
||||
for (const listener of this.textChangeListeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles a size change event for the text area. Calls event listeners. */
|
||||
private onSizeChange() {
|
||||
for (const listener of this.sizeChangeListeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Css.register(`
|
||||
.blocklyCommentTextarea {
|
||||
background-color: #fef49c;
|
||||
border: 0;
|
||||
display: block;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
padding: 3px;
|
||||
resize: none;
|
||||
text-overflow: hidden;
|
||||
}
|
||||
`);
|
||||
@@ -21,38 +21,44 @@ import * as mathUtils from './utils/math.js';
|
||||
import type {WorkspaceCommentSvg} from './workspace_comment_svg.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
|
||||
/**
|
||||
* Bumps the given object that has passed out of bounds.
|
||||
*
|
||||
* @param workspace The workspace containing the object.
|
||||
* @param scrollMetrics Scroll metrics
|
||||
* in workspace coordinates.
|
||||
* @param bounds The region to bump an object into. For example, pass
|
||||
* ScrollMetrics to bump a block into the scrollable region of the
|
||||
* workspace, or pass ViewMetrics to bump a block into the visible region of
|
||||
* the workspace. This should be specified in workspace coordinates.
|
||||
* @param object The object to bump.
|
||||
* @returns True if block was bumped.
|
||||
* @returns True if object was bumped.
|
||||
*/
|
||||
function bumpObjectIntoBounds(
|
||||
workspace: WorkspaceSvg, scrollMetrics: ContainerRegion,
|
||||
object: IBoundedElement): boolean {
|
||||
workspace: WorkspaceSvg,
|
||||
bounds: ContainerRegion,
|
||||
object: IBoundedElement
|
||||
): boolean {
|
||||
// Compute new top/left position for object.
|
||||
const objectMetrics = object.getBoundingRectangle();
|
||||
const height = objectMetrics.bottom - objectMetrics.top;
|
||||
const width = objectMetrics.right - objectMetrics.left;
|
||||
|
||||
const topClamp = scrollMetrics.top;
|
||||
const scrollMetricsBottom = scrollMetrics.top + scrollMetrics.height;
|
||||
const bottomClamp = scrollMetricsBottom - height;
|
||||
const topClamp = bounds.top;
|
||||
const boundsBottom = bounds.top + bounds.height;
|
||||
const bottomClamp = boundsBottom - height;
|
||||
// If the object is taller than the workspace we want to
|
||||
// top-align the block
|
||||
const newYPosition =
|
||||
mathUtils.clamp(topClamp, objectMetrics.top, bottomClamp);
|
||||
const newYPosition = mathUtils.clamp(
|
||||
topClamp,
|
||||
objectMetrics.top,
|
||||
bottomClamp
|
||||
);
|
||||
const deltaY = newYPosition - objectMetrics.top;
|
||||
|
||||
// Note: Even in RTL mode the "anchor" of the object is the
|
||||
// top-left corner of the object.
|
||||
let leftClamp = scrollMetrics.left;
|
||||
const scrollMetricsRight = scrollMetrics.left + scrollMetrics.width;
|
||||
let rightClamp = scrollMetricsRight - width;
|
||||
let leftClamp = bounds.left;
|
||||
const boundsRight = bounds.left + bounds.width;
|
||||
let rightClamp = boundsRight - width;
|
||||
if (workspace.RTL) {
|
||||
// If the object is wider than the workspace and we're in RTL
|
||||
// mode we want to right-align the block, which means setting
|
||||
@@ -64,12 +70,15 @@ function bumpObjectIntoBounds(
|
||||
// the right clamp to match.
|
||||
rightClamp = Math.max(leftClamp, rightClamp);
|
||||
}
|
||||
const newXPosition =
|
||||
mathUtils.clamp(leftClamp, objectMetrics.left, rightClamp);
|
||||
const newXPosition = mathUtils.clamp(
|
||||
leftClamp,
|
||||
objectMetrics.left,
|
||||
rightClamp
|
||||
);
|
||||
const deltaX = newXPosition - objectMetrics.left;
|
||||
|
||||
if (deltaX || deltaY) {
|
||||
object.moveBy(deltaX, deltaY);
|
||||
object.moveBy(deltaX, deltaY, ['inbounds']);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -82,8 +91,9 @@ export const bumpIntoBounds = bumpObjectIntoBounds;
|
||||
* @param workspace The workspace to handle.
|
||||
* @returns The event handler.
|
||||
*/
|
||||
export function bumpIntoBoundsHandler(workspace: WorkspaceSvg):
|
||||
(p1: Abstract) => void {
|
||||
export function bumpIntoBoundsHandler(
|
||||
workspace: WorkspaceSvg
|
||||
): (p1: Abstract) => void {
|
||||
return (e) => {
|
||||
const metricsManager = workspace.getMetricsManager();
|
||||
if (!metricsManager.hasFixedEdges() || workspace.isDragging()) {
|
||||
@@ -94,8 +104,10 @@ export function bumpIntoBoundsHandler(workspace: WorkspaceSvg):
|
||||
const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true);
|
||||
|
||||
// Triggered by move/create event
|
||||
const object =
|
||||
extractObjectFromEvent(workspace, e as eventUtils.BumpEvent);
|
||||
const object = extractObjectFromEvent(
|
||||
workspace,
|
||||
e as eventUtils.BumpEvent
|
||||
);
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
@@ -104,18 +116,25 @@ export function bumpIntoBoundsHandler(workspace: WorkspaceSvg):
|
||||
eventUtils.setGroup(e.group);
|
||||
|
||||
const wasBumped = bumpObjectIntoBounds(
|
||||
workspace, scrollMetricsInWsCoords, (object as IBoundedElement));
|
||||
workspace,
|
||||
scrollMetricsInWsCoords,
|
||||
object as IBoundedElement
|
||||
);
|
||||
|
||||
if (wasBumped && !e.group) {
|
||||
console.warn(
|
||||
'Moved object in bounds but there was no' +
|
||||
' event group. This may break undo.');
|
||||
' event group. This may break undo.'
|
||||
);
|
||||
}
|
||||
eventUtils.setGroup(existingGroup);
|
||||
} else if (e.type === eventUtils.VIEWPORT_CHANGE) {
|
||||
const viewportEvent = (e as ViewportChange);
|
||||
if (viewportEvent.scale && viewportEvent.oldScale &&
|
||||
viewportEvent.scale > viewportEvent.oldScale) {
|
||||
const viewportEvent = e as ViewportChange;
|
||||
if (
|
||||
viewportEvent.scale &&
|
||||
viewportEvent.oldScale &&
|
||||
viewportEvent.scale > viewportEvent.oldScale
|
||||
) {
|
||||
bumpTopObjectsIntoBounds(workspace);
|
||||
}
|
||||
}
|
||||
@@ -132,8 +151,9 @@ export function bumpIntoBoundsHandler(workspace: WorkspaceSvg):
|
||||
* object.
|
||||
*/
|
||||
function extractObjectFromEvent(
|
||||
workspace: WorkspaceSvg, e: eventUtils.BumpEvent): BlockSvg|null|
|
||||
WorkspaceCommentSvg {
|
||||
workspace: WorkspaceSvg,
|
||||
e: eventUtils.BumpEvent
|
||||
): BlockSvg | null | WorkspaceCommentSvg {
|
||||
let object = null;
|
||||
switch (e.type) {
|
||||
case eventUtils.BLOCK_CREATE:
|
||||
@@ -145,10 +165,9 @@ function extractObjectFromEvent(
|
||||
break;
|
||||
case eventUtils.COMMENT_CREATE:
|
||||
case eventUtils.COMMENT_MOVE:
|
||||
object =
|
||||
workspace.getCommentById((e as CommentCreate | CommentMove).commentId!
|
||||
) as WorkspaceCommentSvg |
|
||||
null;
|
||||
object = workspace.getCommentById(
|
||||
(e as CommentCreate | CommentMove).commentId!
|
||||
) as WorkspaceCommentSvg | null;
|
||||
break;
|
||||
}
|
||||
return object;
|
||||
@@ -167,7 +186,7 @@ export function bumpTopObjectsIntoBounds(workspace: WorkspaceSvg) {
|
||||
|
||||
const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ goog.declareModuleId('Blockly.clipboard');
|
||||
|
||||
import type {CopyData, ICopyable} from './interfaces/i_copyable.js';
|
||||
|
||||
|
||||
/** Metadata about the object that is currently on the clipboard. */
|
||||
let copyData: CopyData | null = null;
|
||||
|
||||
@@ -46,8 +45,10 @@ export function paste(): ICopyable|null {
|
||||
if (workspace.isFlyout) {
|
||||
workspace = workspace.targetWorkspace!;
|
||||
}
|
||||
if (copyData.typeCounts &&
|
||||
workspace.isCapacityAvailable(copyData.typeCounts)) {
|
||||
if (
|
||||
copyData.typeCounts &&
|
||||
workspace.isCapacityAvailable(copyData.typeCounts)
|
||||
) {
|
||||
return workspace.paste(copyData.saveInfo);
|
||||
}
|
||||
return null;
|
||||
|
||||
369
core/comment.ts
369
core/comment.ts
@@ -1,369 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2011 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Object representing a code comment.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Comment');
|
||||
|
||||
// Unused import preserved for side-effects. Remove if unneeded.
|
||||
import './events/events_block_change.js';
|
||||
// Unused import preserved for side-effects. Remove if unneeded.
|
||||
import './events/events_bubble_open.js';
|
||||
|
||||
import type {CommentModel} from './block.js';
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import {Bubble} from './bubble.js';
|
||||
import * as Css from './css.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {Icon} from './icon.js';
|
||||
import type {Coordinate} from './utils/coordinate.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import type {Size} from './utils/size.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for a comment.
|
||||
*/
|
||||
export class Comment extends Icon {
|
||||
private readonly model: CommentModel;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private cachedText: string|null = '';
|
||||
|
||||
/**
|
||||
* Array holding info needed to unbind events.
|
||||
* Used for disposing.
|
||||
* Ex: [[node, name, func], [node, name, func]].
|
||||
*/
|
||||
private boundEvents: browserEvents.Data[] = [];
|
||||
|
||||
/**
|
||||
* The SVG element that contains the text edit area, or null if not created.
|
||||
*/
|
||||
private foreignObject: SVGForeignObjectElement|null = null;
|
||||
|
||||
/** The editable text area, or null if not created. */
|
||||
private textarea_: HTMLTextAreaElement|null = null;
|
||||
|
||||
/** The top-level node of the comment text, or null if not created. */
|
||||
private paragraphElement_: SVGTextElement|null = null;
|
||||
|
||||
/** @param block The block associated with this comment. */
|
||||
constructor(block: BlockSvg) {
|
||||
super(block);
|
||||
|
||||
/** The model for this comment. */
|
||||
this.model = block.commentModel;
|
||||
// If someone creates the comment directly instead of calling
|
||||
// block.setCommentText we want to make sure the text is non-null;
|
||||
this.model.text = this.model.text ?? '';
|
||||
|
||||
this.createIcon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the comment icon.
|
||||
*
|
||||
* @param group The icon group.
|
||||
*/
|
||||
protected override drawIcon_(group: Element) {
|
||||
// Circle.
|
||||
dom.createSvgElement(
|
||||
Svg.CIRCLE,
|
||||
{'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'}, group);
|
||||
// Can't use a real '?' text character since different browsers and
|
||||
// operating systems render it differently. Body of question mark.
|
||||
dom.createSvgElement(
|
||||
Svg.PATH, {
|
||||
'class': 'blocklyIconSymbol',
|
||||
'd': 'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405' +
|
||||
'0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25' +
|
||||
'-1.201,0.998 -1.201,1.528 -1.204,2.19z',
|
||||
},
|
||||
group);
|
||||
// Dot of question mark.
|
||||
dom.createSvgElement(
|
||||
Svg.RECT, {
|
||||
'class': 'blocklyIconSymbol',
|
||||
'x': '6.8',
|
||||
'y': '10.78',
|
||||
'height': '2',
|
||||
'width': '2',
|
||||
},
|
||||
group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the editor for the comment's bubble.
|
||||
*
|
||||
* @returns The top-level node of the editor.
|
||||
*/
|
||||
private createEditor(): SVGElement {
|
||||
/* Create the editor. Here's the markup that will be generated in
|
||||
* editable mode:
|
||||
<foreignObject x="8" y="8" width="164" height="164">
|
||||
<body xmlns="http://www.w3.org/1999/xhtml"
|
||||
class="blocklyMinimalBody"> <textarea
|
||||
xmlns="http://www.w3.org/1999/xhtml" class="blocklyCommentTextarea"
|
||||
style="height: 164px; width: 164px;"></textarea>
|
||||
</body>
|
||||
</foreignObject>
|
||||
* For non-editable mode see Warning.textToDom_.
|
||||
*/
|
||||
|
||||
this.foreignObject = dom.createSvgElement(
|
||||
Svg.FOREIGNOBJECT,
|
||||
{'x': Bubble.BORDER_WIDTH, 'y': Bubble.BORDER_WIDTH});
|
||||
|
||||
const body = document.createElementNS(dom.HTML_NS, 'body');
|
||||
body.setAttribute('xmlns', dom.HTML_NS);
|
||||
body.className = 'blocklyMinimalBody';
|
||||
|
||||
this.textarea_ = document.createElementNS(dom.HTML_NS, 'textarea') as
|
||||
HTMLTextAreaElement;
|
||||
const textarea = this.textarea_;
|
||||
textarea.className = 'blocklyCommentTextarea';
|
||||
textarea.setAttribute('dir', this.getBlock().RTL ? 'RTL' : 'LTR');
|
||||
textarea.value = this.model.text ?? '';
|
||||
this.resizeTextarea();
|
||||
|
||||
body.appendChild(textarea);
|
||||
this.foreignObject!.appendChild(body);
|
||||
|
||||
this.boundEvents.push(browserEvents.conditionalBind(
|
||||
textarea, 'focus', this, this.startEdit, true));
|
||||
// Don't zoom with mousewheel.
|
||||
this.boundEvents.push(browserEvents.conditionalBind(
|
||||
textarea, 'wheel', this, function(e: Event) {
|
||||
e.stopPropagation();
|
||||
}));
|
||||
this.boundEvents.push(browserEvents.conditionalBind(
|
||||
textarea, 'change', this,
|
||||
/**
|
||||
* @param _e Unused event parameter.
|
||||
*/
|
||||
function(this: Comment, _e: Event) {
|
||||
if (this.cachedText !== this.model.text) {
|
||||
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
|
||||
this.getBlock(), 'comment', null, this.cachedText,
|
||||
this.model.text));
|
||||
}
|
||||
}));
|
||||
this.boundEvents.push(browserEvents.conditionalBind(
|
||||
textarea, 'input', this,
|
||||
/**
|
||||
* @param _e Unused event parameter.
|
||||
*/
|
||||
function(this: Comment, _e: Event) {
|
||||
this.model.text = textarea.value;
|
||||
}));
|
||||
|
||||
setTimeout(textarea.focus.bind(textarea), 0);
|
||||
|
||||
return this.foreignObject;
|
||||
}
|
||||
|
||||
/** Add or remove editability of the comment. */
|
||||
override updateEditable() {
|
||||
super.updateEditable();
|
||||
if (this.isVisible()) {
|
||||
// Recreate the bubble with the correct UI.
|
||||
this.disposeBubble();
|
||||
this.createBubble();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function triggered when the bubble has resized.
|
||||
* Resize the text area accordingly.
|
||||
*/
|
||||
private onBubbleResize() {
|
||||
if (!this.isVisible() || !this.bubble_) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.size = this.bubble_.getBubbleSize();
|
||||
this.resizeTextarea();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the text area to match the size defined on the model (which is
|
||||
* the size of the bubble).
|
||||
*/
|
||||
private resizeTextarea() {
|
||||
if (!this.textarea_ || !this.foreignObject) {
|
||||
return;
|
||||
}
|
||||
const size = this.model.size;
|
||||
const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
|
||||
const widthMinusBorder = size.width - doubleBorderWidth;
|
||||
const heightMinusBorder = size.height - doubleBorderWidth;
|
||||
this.foreignObject.setAttribute('width', `${widthMinusBorder}`);
|
||||
this.foreignObject.setAttribute('height', `${heightMinusBorder}`);
|
||||
this.textarea_.style.width = widthMinusBorder - 4 + 'px';
|
||||
this.textarea_.style.height = heightMinusBorder - 4 + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the comment bubble.
|
||||
*
|
||||
* @param visible True if the bubble should be visible.
|
||||
*/
|
||||
override setVisible(visible: boolean) {
|
||||
if (visible === this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
|
||||
this.getBlock(), visible, 'comment'));
|
||||
this.model.pinned = visible;
|
||||
if (visible) {
|
||||
this.createBubble();
|
||||
} else {
|
||||
this.disposeBubble();
|
||||
}
|
||||
}
|
||||
|
||||
/** Show the bubble. Handles deciding if it should be editable or not. */
|
||||
private createBubble() {
|
||||
if (!this.getBlock().isEditable()) {
|
||||
this.createNonEditableBubble();
|
||||
} else {
|
||||
this.createEditableBubble();
|
||||
}
|
||||
}
|
||||
|
||||
/** Show an editable bubble. */
|
||||
private createEditableBubble() {
|
||||
const block = this.getBlock();
|
||||
this.bubble_ = new Bubble(
|
||||
block.workspace, this.createEditor(), 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.
|
||||
this.bubble_.setSvgId(block.id);
|
||||
this.bubble_.registerResizeEvent(this.onBubbleResize.bind(this));
|
||||
this.applyColour();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a non-editable bubble.
|
||||
*/
|
||||
private createNonEditableBubble() {
|
||||
// TODO (#2917): It would be great if the comment could support line breaks.
|
||||
this.paragraphElement_ = Bubble.textToDom(this.model.text ?? '');
|
||||
this.bubble_ = Bubble.createNonEditableBubble(
|
||||
this.paragraphElement_, this.getBlock(), this.iconXY_ as Coordinate);
|
||||
this.applyColour();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the bubble.
|
||||
*/
|
||||
private disposeBubble() {
|
||||
for (const event of this.boundEvents) {
|
||||
browserEvents.unbind(event);
|
||||
}
|
||||
this.boundEvents.length = 0;
|
||||
if (this.bubble_) {
|
||||
this.bubble_.dispose();
|
||||
this.bubble_ = null;
|
||||
}
|
||||
this.textarea_ = null;
|
||||
this.foreignObject = null;
|
||||
this.paragraphElement_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback fired when an edit starts.
|
||||
*
|
||||
* Bring the comment to the top of the stack when clicked on. Also cache the
|
||||
* current text so it can be used to fire a change event.
|
||||
*
|
||||
* @param _e Mouse up event.
|
||||
*/
|
||||
private startEdit(_e: PointerEvent) {
|
||||
if (this.bubble_?.promote()) {
|
||||
// Since the act of moving this node within the DOM causes a loss of
|
||||
// focus, we need to reapply the focus.
|
||||
this.textarea_!.focus();
|
||||
}
|
||||
|
||||
this.cachedText = this.model.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dimensions of this comment's bubble.
|
||||
*
|
||||
* @returns Object with width and height properties.
|
||||
*/
|
||||
getBubbleSize(): Size {
|
||||
return this.model.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Size this comment's bubble.
|
||||
*
|
||||
* @param width Width of the bubble.
|
||||
* @param height Height of the bubble.
|
||||
*/
|
||||
setBubbleSize(width: number, height: number) {
|
||||
if (this.bubble_) {
|
||||
this.bubble_.setBubbleSize(width, height);
|
||||
} else {
|
||||
this.model.size.width = width;
|
||||
this.model.size.height = height;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the comment's view to match the model.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
updateText() {
|
||||
if (this.textarea_) {
|
||||
this.textarea_.value = this.model.text ?? '';
|
||||
} else if (this.paragraphElement_) {
|
||||
// Non-Editable mode.
|
||||
// TODO (#2917): If 2917 gets added this will probably need to be updated.
|
||||
this.paragraphElement_.firstChild!.textContent = this.model.text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of this comment.
|
||||
*
|
||||
* If you want to receive a comment "delete" event (newValue: null), then this
|
||||
* should not be called directly. Instead call block.setCommentText(null);
|
||||
*/
|
||||
override dispose() {
|
||||
this.getBlock().comment = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/** CSS for block comment. See css.js for use. */
|
||||
Css.register(`
|
||||
.blocklyCommentTextarea {
|
||||
background-color: #fef49c;
|
||||
border: 0;
|
||||
display: block;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
padding: 3px;
|
||||
resize: none;
|
||||
text-overflow: hidden;
|
||||
}
|
||||
`);
|
||||
@@ -15,11 +15,9 @@ import type {ICopyable} from './interfaces/i_copyable.js';
|
||||
import type {Workspace} from './workspace.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
|
||||
/** Database of all workspaces. */
|
||||
const WorkspaceDB_ = Object.create(null);
|
||||
|
||||
|
||||
/**
|
||||
* Find the workspace with the specified ID.
|
||||
*
|
||||
@@ -189,7 +187,9 @@ export const draggingConnections: Connection[] = [];
|
||||
* @returns Map of types to type counts for descendants of the bock.
|
||||
*/
|
||||
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 descendants = block.getDescendants(true);
|
||||
if (opt_stripFollowing) {
|
||||
@@ -199,7 +199,7 @@ export function getBlockTypeCounts(
|
||||
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]) {
|
||||
typeCountsMap[checkBlock.type]++;
|
||||
} else {
|
||||
@@ -249,7 +249,8 @@ function defineBlocksWithJsonArrayInternal(jsonArray: AnyDuringMigration[]) {
|
||||
* definitions created.
|
||||
*/
|
||||
export function createBlockDefinitionsFromJsonArray(
|
||||
jsonArray: AnyDuringMigration[]): {[key: string]: BlockDefinition} {
|
||||
jsonArray: AnyDuringMigration[]
|
||||
): {[key: string]: BlockDefinition} {
|
||||
const blocks: {[key: string]: BlockDefinition} = {};
|
||||
for (let i = 0; i < jsonArray.length; i++) {
|
||||
const elem = jsonArray[i];
|
||||
@@ -261,7 +262,8 @@ export function createBlockDefinitionsFromJsonArray(
|
||||
if (!type) {
|
||||
console.warn(
|
||||
`Block definition #${i} in JSON array is missing a type attribute. ` +
|
||||
'Skipping.');
|
||||
'Skipping.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
blocks[type] = {init: jsonInitFactory(elem)};
|
||||
|
||||
@@ -19,7 +19,6 @@ import type {IDragTarget} from './interfaces/i_drag_target.js';
|
||||
import type {IPositionable} from './interfaces/i_positionable.js';
|
||||
import * as arrayUtils from './utils/array.js';
|
||||
|
||||
|
||||
class Capability<_T> {
|
||||
static POSITIONABLE = new Capability<IPositionable>('positionable');
|
||||
static DRAG_TARGET = new Capability<IDragTarget>('drag_target');
|
||||
@@ -67,8 +66,12 @@ export class ComponentManager {
|
||||
const id = componentInfo.component.id;
|
||||
if (!opt_allowOverrides && this.componentData.has(id)) {
|
||||
throw Error(
|
||||
'Plugin "' + id + '" with capabilities "' +
|
||||
this.componentData.get(id)?.capabilities + '" already added.');
|
||||
'Plugin "' +
|
||||
id +
|
||||
'" with capabilities "' +
|
||||
this.componentData.get(id)?.capabilities +
|
||||
'" already added.'
|
||||
);
|
||||
}
|
||||
this.componentData.set(id, componentInfo);
|
||||
const stringCapabilities = [];
|
||||
@@ -110,12 +113,17 @@ export class ComponentManager {
|
||||
addCapability<T>(id: string, capability: string | Capability<T>) {
|
||||
if (!this.getComponent(id)) {
|
||||
throw Error(
|
||||
'Cannot add capability, "' + capability + '". Plugin "' + id +
|
||||
'" has not been added to the ComponentManager');
|
||||
'Cannot add capability, "' +
|
||||
capability +
|
||||
'". Plugin "' +
|
||||
id +
|
||||
'" has not been added to the ComponentManager'
|
||||
);
|
||||
}
|
||||
if (this.hasCapability(id, capability)) {
|
||||
console.warn(
|
||||
'Plugin "' + id + 'already has capability "' + capability + '"');
|
||||
'Plugin "' + id + 'already has capability "' + capability + '"'
|
||||
);
|
||||
return;
|
||||
}
|
||||
capability = `${capability}`.toLowerCase();
|
||||
@@ -132,13 +140,21 @@ export class ComponentManager {
|
||||
removeCapability<T>(id: string, capability: string | Capability<T>) {
|
||||
if (!this.getComponent(id)) {
|
||||
throw Error(
|
||||
'Cannot remove capability, "' + capability + '". Plugin "' + id +
|
||||
'" has not been added to the ComponentManager');
|
||||
'Cannot remove capability, "' +
|
||||
capability +
|
||||
'". Plugin "' +
|
||||
id +
|
||||
'" has not been added to the ComponentManager'
|
||||
);
|
||||
}
|
||||
if (!this.hasCapability(id, capability)) {
|
||||
console.warn(
|
||||
'Plugin "' + id + 'doesn\'t have capability "' + capability +
|
||||
'" to remove');
|
||||
'Plugin "' +
|
||||
id +
|
||||
'doesn\'t have capability "' +
|
||||
capability +
|
||||
'" to remove'
|
||||
);
|
||||
return;
|
||||
}
|
||||
capability = `${capability}`.toLowerCase();
|
||||
@@ -155,8 +171,10 @@ export class ComponentManager {
|
||||
*/
|
||||
hasCapability<T>(id: string, capability: string | Capability<T>): boolean {
|
||||
capability = `${capability}`.toLowerCase();
|
||||
return this.componentData.has(id) &&
|
||||
this.componentData.get(id)!.capabilities.indexOf(capability) !== -1;
|
||||
return (
|
||||
this.componentData.has(id) &&
|
||||
this.componentData.get(id)!.capabilities.indexOf(capability) !== -1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,7 +195,9 @@ export class ComponentManager {
|
||||
* @returns The components that match the specified capability.
|
||||
*/
|
||||
getComponents<T extends IComponent>(
|
||||
capability: string|Capability<T>, sorted: boolean): T[] {
|
||||
capability: string | Capability<T>,
|
||||
sorted: boolean
|
||||
): T[] {
|
||||
capability = `${capability}`.toLowerCase();
|
||||
const componentIds = this.capabilityToComponentIds.get(capability);
|
||||
if (!componentIds) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.config');
|
||||
|
||||
|
||||
/**
|
||||
* All the values that we expect developers to be able to change
|
||||
* before injecting Blockly.
|
||||
|
||||
@@ -16,13 +16,12 @@ import type {Block} from './block.js';
|
||||
import {ConnectionType} from './connection_type.js';
|
||||
import type {BlockMove} from './events/events_block_move.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import type {Input} from './input.js';
|
||||
import type {Input} from './inputs/input.js';
|
||||
import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js';
|
||||
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
|
||||
import * as blocks from './serialization/blocks.js';
|
||||
import * as Xml from './xml.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for a connection between blocks.
|
||||
*/
|
||||
@@ -51,10 +50,10 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
disposed = false;
|
||||
|
||||
/** 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. */
|
||||
private shadowDom_: Element|null = null;
|
||||
private shadowDom: Element | null = null;
|
||||
|
||||
/**
|
||||
* Horizontal location of this connection.
|
||||
@@ -70,7 +69,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
*/
|
||||
y = 0;
|
||||
|
||||
private shadowState_: blocks.State|null = null;
|
||||
private shadowState: blocks.State | null = null;
|
||||
|
||||
/**
|
||||
* @param source The block establishing this connection.
|
||||
@@ -99,7 +98,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
// Make sure the parentConnection is available.
|
||||
let orphan;
|
||||
if (this.isConnected()) {
|
||||
const shadowState = this.stashShadowState_();
|
||||
const shadowState = this.stashShadowState();
|
||||
const target = this.targetBlock();
|
||||
if (target!.isShadow()) {
|
||||
target!.dispose(false);
|
||||
@@ -107,14 +106,16 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
this.disconnectInternal();
|
||||
orphan = target;
|
||||
}
|
||||
this.applyShadowState_(shadowState);
|
||||
this.applyShadowState(shadowState);
|
||||
}
|
||||
|
||||
// Connect the new connection to the parent.
|
||||
let event;
|
||||
if (eventUtils.isEnabled()) {
|
||||
event =
|
||||
new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock) as BlockMove;
|
||||
event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(
|
||||
childBlock
|
||||
) as BlockMove;
|
||||
event.setReason(['connect']);
|
||||
}
|
||||
connectReciprocally(this, childConnection);
|
||||
childBlock.setParent(parentBlock);
|
||||
@@ -125,11 +126,15 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
|
||||
// Deal with the orphan if it exists.
|
||||
if (orphan) {
|
||||
const orphanConnection = this.type === INPUT ? orphan.outputConnection :
|
||||
orphan.previousConnection;
|
||||
const orphanConnection =
|
||||
this.type === INPUT
|
||||
? orphan.outputConnection
|
||||
: orphan.previousConnection;
|
||||
if (!orphanConnection) return;
|
||||
const connection = Connection.getConnectionForOrphanedConnection(
|
||||
childBlock, orphanConnection);
|
||||
childBlock,
|
||||
orphanConnection
|
||||
);
|
||||
if (connection) {
|
||||
orphanConnection.connect(connection);
|
||||
} else {
|
||||
@@ -147,7 +152,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
// isConnected returns true for shadows and non-shadows.
|
||||
if (this.isConnected()) {
|
||||
// Destroy the attached shadow block & its children (if it exists).
|
||||
this.setShadowStateInternal_();
|
||||
this.setShadowStateInternal();
|
||||
|
||||
const targetBlock = this.targetBlock();
|
||||
if (targetBlock && !targetBlock.isDeadOrDying()) {
|
||||
@@ -175,8 +180,10 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* @returns True if connection faces down or right.
|
||||
*/
|
||||
isSuperior(): boolean {
|
||||
return this.type === ConnectionType.INPUT_VALUE ||
|
||||
this.type === ConnectionType.NEXT_STATEMENT;
|
||||
return (
|
||||
this.type === ConnectionType.INPUT_VALUE ||
|
||||
this.type === ConnectionType.NEXT_STATEMENT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,7 +278,9 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
let event;
|
||||
if (eventUtils.isEnabled()) {
|
||||
event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(
|
||||
childConnection.getSourceBlock()) as BlockMove;
|
||||
childConnection.getSourceBlock()
|
||||
) as BlockMove;
|
||||
event.setReason(['disconnect']);
|
||||
}
|
||||
const otherConnection = this.targetConnection;
|
||||
if (otherConnection) {
|
||||
@@ -299,8 +308,10 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* @returns The parent connection and child connection, given this connection
|
||||
* and the connection it is connected to.
|
||||
*/
|
||||
protected getParentAndChildConnections():
|
||||
{parentConnection?: Connection, childConnection?: Connection} {
|
||||
protected getParentAndChildConnections(): {
|
||||
parentConnection?: Connection;
|
||||
childConnection?: Connection;
|
||||
} {
|
||||
if (!this.targetConnection) return {};
|
||||
if (this.isSuperior()) {
|
||||
return {
|
||||
@@ -319,7 +330,37 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
*/
|
||||
protected respawnShadow_() {
|
||||
// Have to keep respawnShadow_ for backwards compatibility.
|
||||
this.createShadowBlock_(true);
|
||||
this.createShadowBlock(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnects this connection to the input with the given name on the given
|
||||
* block. If there is already a connection connected to that input, that
|
||||
* connection is disconnected.
|
||||
*
|
||||
* @param block The block to connect this connection to.
|
||||
* @param inputName The name of the input to connect this connection to.
|
||||
* @returns True if this connection was able to connect, false otherwise.
|
||||
*/
|
||||
reconnect(block: Block, inputName: string): boolean {
|
||||
// No need to reconnect if this connection's block is deleted.
|
||||
if (this.getSourceBlock().isDeadOrDying()) return false;
|
||||
|
||||
const connectionParent = block.getInput(inputName)?.connection;
|
||||
const currentParent = this.targetBlock();
|
||||
if (
|
||||
(!currentParent || currentParent === block) &&
|
||||
connectionParent &&
|
||||
connectionParent.targetConnection !== this
|
||||
) {
|
||||
if (connectionParent.isConnected()) {
|
||||
// There's already something connected here. Get rid of it.
|
||||
connectionParent.disconnect();
|
||||
}
|
||||
connectionParent.connect(this);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -339,10 +380,15 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
*/
|
||||
protected onCheckChanged_() {
|
||||
// The new value type may not be compatible with the existing connection.
|
||||
if (this.isConnected() &&
|
||||
if (
|
||||
this.isConnected() &&
|
||||
(!this.targetConnection ||
|
||||
!this.getConnectionChecker().canConnect(
|
||||
this, this.targetConnection, false))) {
|
||||
this,
|
||||
this.targetConnection,
|
||||
false
|
||||
))
|
||||
) {
|
||||
const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
|
||||
child!.unplug();
|
||||
}
|
||||
@@ -360,10 +406,10 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
if (!Array.isArray(check)) {
|
||||
check = [check];
|
||||
}
|
||||
this.check_ = check;
|
||||
this.check = check;
|
||||
this.onCheckChanged_();
|
||||
} else {
|
||||
this.check_ = null;
|
||||
this.check = null;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -375,7 +421,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* Null if all types are compatible.
|
||||
*/
|
||||
getCheck(): string[] | null {
|
||||
return this.check_;
|
||||
return this.check;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,7 +430,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* @param shadowDom DOM representation of a block or null.
|
||||
*/
|
||||
setShadowDom(shadowDom: Element | null) {
|
||||
this.setShadowStateInternal_({shadowDom});
|
||||
this.setShadowStateInternal({shadowDom});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -397,9 +443,9 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* @returns Shadow DOM representation of a block or null.
|
||||
*/
|
||||
getShadowDom(returnCurrent?: boolean): Element | null {
|
||||
return returnCurrent && this.targetBlock()!.isShadow() ?
|
||||
Xml.blockToDom((this.targetBlock() as Block)) as Element :
|
||||
this.shadowDom_;
|
||||
return returnCurrent && this.targetBlock()!.isShadow()
|
||||
? (Xml.blockToDom(this.targetBlock() as Block) as Element)
|
||||
: this.shadowDom;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -408,7 +454,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* @param shadowState An state represetation of the block or null.
|
||||
*/
|
||||
setShadowState(shadowState: blocks.State | null) {
|
||||
this.setShadowStateInternal_({shadowState});
|
||||
this.setShadowStateInternal({shadowState});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,7 +471,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
if (returnCurrent && this.targetBlock() && this.targetBlock()!.isShadow()) {
|
||||
return blocks.save(this.targetBlock() as Block);
|
||||
}
|
||||
return this.shadowState_;
|
||||
return this.shadowState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -484,7 +530,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
msg = 'Next Connection of ';
|
||||
} else {
|
||||
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) {
|
||||
parentInput = input;
|
||||
break;
|
||||
@@ -506,13 +552,15 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
*
|
||||
* @returns The state of both the shadowDom_ and shadowState_ properties.
|
||||
*/
|
||||
private stashShadowState_():
|
||||
{shadowDom: Element|null, shadowState: blocks.State|null} {
|
||||
private stashShadowState(): {
|
||||
shadowDom: Element | null;
|
||||
shadowState: blocks.State | null;
|
||||
} {
|
||||
const shadowDom = this.getShadowDom(true);
|
||||
const shadowState = this.getShadowState(true);
|
||||
// Set to null so it doesn't respawn.
|
||||
this.shadowDom_ = null;
|
||||
this.shadowState_ = null;
|
||||
this.shadowDom = null;
|
||||
this.shadowState = null;
|
||||
return {shadowDom, shadowState};
|
||||
}
|
||||
|
||||
@@ -522,12 +570,15 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* @param param0 The state to reapply to the shadowDom_ and shadowState_
|
||||
* properties.
|
||||
*/
|
||||
private applyShadowState_({shadowDom, shadowState}: {
|
||||
shadowDom: Element|null,
|
||||
shadowState: blocks.State|null
|
||||
private applyShadowState({
|
||||
shadowDom,
|
||||
shadowState,
|
||||
}: {
|
||||
shadowDom: Element | null;
|
||||
shadowState: blocks.State | null;
|
||||
}) {
|
||||
this.shadowDom_ = shadowDom;
|
||||
this.shadowState_ = shadowState;
|
||||
this.shadowDom = shadowDom;
|
||||
this.shadowState = shadowState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -535,31 +586,34 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
*
|
||||
* @param param0 The state to set the shadow of this connection to.
|
||||
*/
|
||||
private setShadowStateInternal_({shadowDom = null, shadowState = null}: {
|
||||
shadowDom?: Element|null,
|
||||
shadowState?: blocks.State|null
|
||||
private setShadowStateInternal({
|
||||
shadowDom = null,
|
||||
shadowState = null,
|
||||
}: {
|
||||
shadowDom?: Element | null;
|
||||
shadowState?: blocks.State | null;
|
||||
} = {}) {
|
||||
// One or both of these should always be null.
|
||||
// If neither is null, the shadowState will get priority.
|
||||
this.shadowDom_ = shadowDom;
|
||||
this.shadowState_ = shadowState;
|
||||
this.shadowDom = shadowDom;
|
||||
this.shadowState = shadowState;
|
||||
|
||||
const target = this.targetBlock();
|
||||
if (!target) {
|
||||
this.respawnShadow_();
|
||||
if (this.targetBlock() && this.targetBlock()!.isShadow()) {
|
||||
this.serializeShadow_(this.targetBlock());
|
||||
this.serializeShadow(this.targetBlock());
|
||||
}
|
||||
} else if (target.isShadow()) {
|
||||
target.dispose(false);
|
||||
if (this.getSourceBlock().isDeadOrDying()) return;
|
||||
this.respawnShadow_();
|
||||
if (this.targetBlock() && this.targetBlock()!.isShadow()) {
|
||||
this.serializeShadow_(this.targetBlock());
|
||||
this.serializeShadow(this.targetBlock());
|
||||
}
|
||||
} else {
|
||||
const shadow = this.createShadowBlock_(false);
|
||||
this.serializeShadow_(shadow);
|
||||
const shadow = this.createShadowBlock(false);
|
||||
this.serializeShadow(shadow);
|
||||
if (shadow) {
|
||||
shadow.dispose(false);
|
||||
}
|
||||
@@ -575,11 +629,11 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* @returns The shadow block that was created, or null if both the
|
||||
* shadowState_ and shadowDom_ are null.
|
||||
*/
|
||||
private createShadowBlock_(attemptToConnect: boolean): Block|null {
|
||||
private createShadowBlock(attemptToConnect: boolean): Block | null {
|
||||
const parentBlock = this.getSourceBlock();
|
||||
const shadowState = this.getShadowState();
|
||||
const shadowDom = this.getShadowDom();
|
||||
if (parentBlock.isDeadOrDying() || !shadowState && !shadowDom) {
|
||||
if (parentBlock.isDeadOrDying() || (!shadowState && !shadowDom)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -612,7 +666,8 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
@@ -626,12 +681,12 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
*
|
||||
* @param shadow The shadow to serialize, or null.
|
||||
*/
|
||||
private serializeShadow_(shadow: Block|null) {
|
||||
private serializeShadow(shadow: Block | null) {
|
||||
if (!shadow) {
|
||||
return;
|
||||
}
|
||||
this.shadowDom_ = Xml.blockToDom(shadow) as Element;
|
||||
this.shadowState_ = blocks.save(shadow);
|
||||
this.shadowDom = Xml.blockToDom(shadow) as Element;
|
||||
this.shadowState = blocks.save(shadow);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -644,10 +699,14 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* @returns The suitable connection point on the chain of blocks, or null.
|
||||
*/
|
||||
static getConnectionForOrphanedConnection(
|
||||
startBlock: Block, orphanConnection: Connection): Connection|null {
|
||||
startBlock: Block,
|
||||
orphanConnection: Connection
|
||||
): Connection | null {
|
||||
if (orphanConnection.type === ConnectionType.OUTPUT_VALUE) {
|
||||
return getConnectionForOrphanedOutput(
|
||||
startBlock, orphanConnection.getSourceBlock());
|
||||
startBlock,
|
||||
orphanConnection.getSourceBlock()
|
||||
);
|
||||
}
|
||||
// Otherwise we're dealing with a stack.
|
||||
const connection = startBlock.lastConnectionInStack(true);
|
||||
@@ -682,13 +741,15 @@ function connectReciprocally(first: Connection, second: Connection) {
|
||||
* @param orphanBlock The inferior block.
|
||||
* @returns The suitable connection point on 'block', or null.
|
||||
*/
|
||||
function getSingleConnection(block: Block, orphanBlock: Block): Connection|
|
||||
null {
|
||||
function getSingleConnection(
|
||||
block: Block,
|
||||
orphanBlock: Block
|
||||
): Connection | null {
|
||||
let foundConnection = null;
|
||||
const output = orphanBlock.outputConnection;
|
||||
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;
|
||||
if (connection && typeChecker?.canConnect(output, connection, false)) {
|
||||
if (foundConnection) {
|
||||
@@ -712,10 +773,12 @@ function getSingleConnection(block: Block, orphanBlock: Block): Connection|
|
||||
* @returns The suitable connection point on the chain of blocks, or null.
|
||||
*/
|
||||
function getConnectionForOrphanedOutput(
|
||||
startBlock: Block, orphanBlock: Block): Connection|null {
|
||||
startBlock: Block,
|
||||
orphanBlock: Block
|
||||
): Connection | null {
|
||||
let newBlock: Block | null = startBlock;
|
||||
let connection;
|
||||
while (connection = getSingleConnection(newBlock, orphanBlock)) {
|
||||
while ((connection = getSingleConnection(newBlock, orphanBlock))) {
|
||||
newBlock = connection.targetBlock();
|
||||
if (!newBlock || newBlock.isShadow()) {
|
||||
return connection;
|
||||
|
||||
@@ -21,7 +21,6 @@ import * as internalConstants from './internal_constants.js';
|
||||
import * as registry from './registry.js';
|
||||
import type {RenderedConnection} from './rendered_connection.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for connection type checking logic.
|
||||
*/
|
||||
@@ -38,10 +37,15 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
* @returns Whether the connection is legal.
|
||||
*/
|
||||
canConnect(
|
||||
a: Connection|null, b: Connection|null, isDragging: boolean,
|
||||
opt_distance?: number): boolean {
|
||||
return this.canConnectWithReason(a, b, isDragging, opt_distance) ===
|
||||
Connection.CAN_CONNECT;
|
||||
a: Connection | null,
|
||||
b: Connection | null,
|
||||
isDragging: boolean,
|
||||
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.
|
||||
*/
|
||||
canConnectWithReason(
|
||||
a: Connection|null, b: Connection|null, isDragging: boolean,
|
||||
opt_distance?: number): number {
|
||||
a: Connection | null,
|
||||
b: Connection | null,
|
||||
isDragging: boolean,
|
||||
opt_distance?: number
|
||||
): number {
|
||||
const safety = this.doSafetyChecks(a, b);
|
||||
if (safety !== Connection.CAN_CONNECT) {
|
||||
return safety;
|
||||
@@ -71,10 +78,14 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
return Connection.REASON_CHECKS_FAILED;
|
||||
}
|
||||
|
||||
if (isDragging &&
|
||||
if (
|
||||
isDragging &&
|
||||
!this.doDragChecks(
|
||||
a as RenderedConnection, b as RenderedConnection,
|
||||
opt_distance || 0)) {
|
||||
a as RenderedConnection,
|
||||
b as RenderedConnection,
|
||||
opt_distance || 0
|
||||
)
|
||||
) {
|
||||
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.
|
||||
* @returns A developer-readable error string.
|
||||
*/
|
||||
getErrorMessage(errorCode: number, a: Connection|null, b: Connection|null):
|
||||
string {
|
||||
getErrorMessage(
|
||||
errorCode: number,
|
||||
a: Connection | null,
|
||||
b: Connection | null
|
||||
): string {
|
||||
switch (errorCode) {
|
||||
case Connection.REASON_SELF_CONNECTION:
|
||||
return 'Attempted to connect a block to itself.';
|
||||
@@ -105,7 +119,11 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
const connOne = a!;
|
||||
const connTwo = b!;
|
||||
let msg = 'Connection checks failed. ';
|
||||
msg += connOne + ' expected ' + connOne.getCheck() + ', found ' +
|
||||
msg +=
|
||||
connOne +
|
||||
' expected ' +
|
||||
connOne.getCheck() +
|
||||
', found ' +
|
||||
connTwo.getCheck();
|
||||
return msg;
|
||||
}
|
||||
@@ -151,7 +169,8 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
return Connection.REASON_SELF_CONNECTION;
|
||||
} else if (
|
||||
inferiorConnection.type !==
|
||||
internalConstants.OPPOSITE_TYPE[superiorConnection.type]) {
|
||||
internalConstants.OPPOSITE_TYPE[superiorConnection.type]
|
||||
) {
|
||||
return Connection.REASON_WRONG_TYPE;
|
||||
} else if (superiorBlock.workspace !== inferiorBlock.workspace) {
|
||||
return Connection.REASON_DIFFERENT_WORKSPACES;
|
||||
@@ -160,12 +179,14 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
} else if (
|
||||
inferiorConnection.type === ConnectionType.OUTPUT_VALUE &&
|
||||
inferiorBlock.previousConnection &&
|
||||
inferiorBlock.previousConnection.isConnected()) {
|
||||
inferiorBlock.previousConnection.isConnected()
|
||||
) {
|
||||
return Connection.REASON_PREVIOUS_AND_OUTPUT;
|
||||
} else if (
|
||||
inferiorConnection.type === ConnectionType.PREVIOUS_STATEMENT &&
|
||||
inferiorBlock.outputConnection &&
|
||||
inferiorBlock.outputConnection.isConnected()) {
|
||||
inferiorBlock.outputConnection.isConnected()
|
||||
) {
|
||||
return Connection.REASON_PREVIOUS_AND_OUTPUT;
|
||||
}
|
||||
return Connection.CAN_CONNECT;
|
||||
@@ -206,8 +227,11 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
* @param distance The maximum allowable distance between connections.
|
||||
* @returns True if the connection is allowed during a drag.
|
||||
*/
|
||||
doDragChecks(a: RenderedConnection, b: RenderedConnection, distance: number):
|
||||
boolean {
|
||||
doDragChecks(
|
||||
a: RenderedConnection,
|
||||
b: RenderedConnection,
|
||||
distance: number
|
||||
): boolean {
|
||||
if (a.distanceFrom(b) > distance) {
|
||||
return false;
|
||||
}
|
||||
@@ -223,8 +247,10 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
case ConnectionType.OUTPUT_VALUE: {
|
||||
// Don't offer to connect an already connected left (male) value plug to
|
||||
// an available right (female) value plug.
|
||||
if (b.isConnected() && !b.targetBlock()!.isInsertionMarker() ||
|
||||
a.isConnected()) {
|
||||
if (
|
||||
(b.isConnected() && !b.targetBlock()!.isInsertionMarker()) ||
|
||||
a.isConnected()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@@ -233,8 +259,11 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
// Offering to connect the left (male) of a value block to an already
|
||||
// connected value pair is ok, we'll splice it in.
|
||||
// However, don't offer to splice into an immovable block.
|
||||
if (b.isConnected() && !b.targetBlock()!.isMovable() &&
|
||||
!b.targetBlock()!.isShadow()) {
|
||||
if (
|
||||
b.isConnected() &&
|
||||
!b.targetBlock()!.isMovable() &&
|
||||
!b.targetBlock()!.isShadow()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@@ -244,15 +273,22 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
// the stack. But covering up a shadow block or stack of shadow blocks
|
||||
// is fine. Similarly, replacing a terminal statement with another
|
||||
// terminal statement is allowed.
|
||||
if (b.isConnected() && !a.getSourceBlock().nextConnection &&
|
||||
!b.targetBlock()!.isShadow() && b.targetBlock()!.nextConnection) {
|
||||
if (
|
||||
b.isConnected() &&
|
||||
!a.getSourceBlock().nextConnection &&
|
||||
!b.targetBlock()!.isShadow() &&
|
||||
b.targetBlock()!.nextConnection
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't offer to splice into a stack where the connected block is
|
||||
// immovable, unless the block is a shadow block.
|
||||
if (b.targetBlock() && !b.targetBlock()!.isMovable() &&
|
||||
!b.targetBlock()!.isShadow()) {
|
||||
if (
|
||||
b.targetBlock() &&
|
||||
!b.targetBlock()!.isMovable() &&
|
||||
!b.targetBlock()!.isShadow()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@@ -307,4 +343,7 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
}
|
||||
|
||||
registry.register(
|
||||
registry.Type.CONNECTION_CHECKER, registry.DEFAULT, ConnectionChecker);
|
||||
registry.Type.CONNECTION_CHECKER,
|
||||
registry.DEFAULT,
|
||||
ConnectionChecker
|
||||
);
|
||||
|
||||
@@ -19,7 +19,6 @@ import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
|
||||
import type {RenderedConnection} from './rendered_connection.js';
|
||||
import type {Coordinate} from './utils/coordinate.js';
|
||||
|
||||
|
||||
/**
|
||||
* Database of connections.
|
||||
* Connections are stored in order of their vertical component. This way
|
||||
@@ -27,7 +26,7 @@ import type {Coordinate} from './utils/coordinate.js';
|
||||
*/
|
||||
export class ConnectionDB {
|
||||
/** Array of connections sorted by y position in workspace units. */
|
||||
private readonly connections_: RenderedConnection[] = [];
|
||||
private readonly connections: RenderedConnection[] = [];
|
||||
|
||||
/**
|
||||
* @param connectionChecker The workspace's connection type checker, used to
|
||||
@@ -43,8 +42,8 @@ export class ConnectionDB {
|
||||
* @internal
|
||||
*/
|
||||
addConnection(connection: RenderedConnection, yPos: number) {
|
||||
const index = this.calculateIndexForYPos_(yPos);
|
||||
this.connections_.splice(index, 0, connection);
|
||||
const index = this.calculateIndexForYPos(yPos);
|
||||
this.connections.splice(index, 0, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,14 +57,16 @@ export class ConnectionDB {
|
||||
* @returns The index of the connection, or -1 if the connection was not
|
||||
* found.
|
||||
*/
|
||||
private findIndexOfConnection_(conn: RenderedConnection, yPos: number):
|
||||
number {
|
||||
if (!this.connections_.length) {
|
||||
private findIndexOfConnection(
|
||||
conn: RenderedConnection,
|
||||
yPos: number
|
||||
): number {
|
||||
if (!this.connections.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const bestGuess = this.calculateIndexForYPos_(yPos);
|
||||
if (bestGuess >= this.connections_.length) {
|
||||
const bestGuess = this.calculateIndexForYPos(yPos);
|
||||
if (bestGuess >= this.connections.length) {
|
||||
// Not in list
|
||||
return -1;
|
||||
}
|
||||
@@ -73,17 +74,19 @@ export class ConnectionDB {
|
||||
yPos = conn.y;
|
||||
// Walk forward and back on the y axis looking for the connection.
|
||||
let pointer = bestGuess;
|
||||
while (pointer >= 0 && this.connections_[pointer].y === yPos) {
|
||||
if (this.connections_[pointer] === conn) {
|
||||
while (pointer >= 0 && this.connections[pointer].y === yPos) {
|
||||
if (this.connections[pointer] === conn) {
|
||||
return pointer;
|
||||
}
|
||||
pointer--;
|
||||
}
|
||||
|
||||
pointer = bestGuess;
|
||||
while (pointer < this.connections_.length &&
|
||||
this.connections_[pointer].y === yPos) {
|
||||
if (this.connections_[pointer] === conn) {
|
||||
while (
|
||||
pointer < this.connections.length &&
|
||||
this.connections[pointer].y === yPos
|
||||
) {
|
||||
if (this.connections[pointer] === conn) {
|
||||
return pointer;
|
||||
}
|
||||
pointer++;
|
||||
@@ -97,17 +100,17 @@ export class ConnectionDB {
|
||||
* @param yPos The y position used to decide where to insert the connection.
|
||||
* @returns The candidate index.
|
||||
*/
|
||||
private calculateIndexForYPos_(yPos: number): number {
|
||||
if (!this.connections_.length) {
|
||||
private calculateIndexForYPos(yPos: number): number {
|
||||
if (!this.connections.length) {
|
||||
return 0;
|
||||
}
|
||||
let pointerMin = 0;
|
||||
let pointerMax = this.connections_.length;
|
||||
let pointerMax = this.connections.length;
|
||||
while (pointerMin < pointerMax) {
|
||||
const pointerMid = Math.floor((pointerMin + pointerMax) / 2);
|
||||
if (this.connections_[pointerMid].y < yPos) {
|
||||
if (this.connections[pointerMid].y < yPos) {
|
||||
pointerMin = pointerMid + 1;
|
||||
} else if (this.connections_[pointerMid].y > yPos) {
|
||||
} else if (this.connections[pointerMid].y > yPos) {
|
||||
pointerMax = pointerMid;
|
||||
} else {
|
||||
pointerMin = pointerMid;
|
||||
@@ -125,11 +128,11 @@ export class ConnectionDB {
|
||||
* @throws {Error} If the connection cannot be found in the database.
|
||||
*/
|
||||
removeConnection(connection: RenderedConnection, yPos: number) {
|
||||
const index = this.findIndexOfConnection_(connection, yPos);
|
||||
const index = this.findIndexOfConnection(connection, yPos);
|
||||
if (index === -1) {
|
||||
throw Error('Unable to find connection in connectionDB.');
|
||||
}
|
||||
this.connections_.splice(index, 1);
|
||||
this.connections.splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,9 +143,11 @@ export class ConnectionDB {
|
||||
* @param maxRadius The maximum radius to another connection.
|
||||
* @returns List of connections.
|
||||
*/
|
||||
getNeighbours(connection: RenderedConnection, maxRadius: number):
|
||||
RenderedConnection[] {
|
||||
const db = this.connections_;
|
||||
getNeighbours(
|
||||
connection: RenderedConnection,
|
||||
maxRadius: number
|
||||
): RenderedConnection[] {
|
||||
const db = this.connections;
|
||||
const currentX = connection.x;
|
||||
const currentY = connection.y;
|
||||
|
||||
@@ -169,7 +174,7 @@ export class ConnectionDB {
|
||||
* @returns True if the current connection's vertical distance from the
|
||||
* other connection is less than the allowed radius.
|
||||
*/
|
||||
function checkConnection_(yIndex: number): boolean {
|
||||
function checkConnection(yIndex: number): boolean {
|
||||
const dx = currentX - db[yIndex].x;
|
||||
const dy = currentY - db[yIndex].y;
|
||||
const r = Math.sqrt(dx * dx + dy * dy);
|
||||
@@ -183,12 +188,12 @@ export class ConnectionDB {
|
||||
pointerMin = pointerMid;
|
||||
pointerMax = pointerMid;
|
||||
if (db.length) {
|
||||
while (pointerMin >= 0 && checkConnection_(pointerMin)) {
|
||||
while (pointerMin >= 0 && checkConnection(pointerMin)) {
|
||||
pointerMin--;
|
||||
}
|
||||
do {
|
||||
pointerMax++;
|
||||
} while (pointerMax < db.length && checkConnection_(pointerMax));
|
||||
} while (pointerMax < db.length && checkConnection(pointerMax));
|
||||
}
|
||||
|
||||
return neighbours;
|
||||
@@ -203,9 +208,8 @@ export class ConnectionDB {
|
||||
* @param maxRadius The maximum radius to another connection.
|
||||
* @returns True if connection is in range.
|
||||
*/
|
||||
private isInYRange_(index: number, baseY: number, maxRadius: number):
|
||||
boolean {
|
||||
return Math.abs(this.connections_[index].y - baseY) <= maxRadius;
|
||||
private isInYRange(index: number, baseY: number, maxRadius: number): boolean {
|
||||
return Math.abs(this.connections[index].y - baseY) <= maxRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,9 +223,11 @@ export class ConnectionDB {
|
||||
* connection or null, and 'radius' which is the distance.
|
||||
*/
|
||||
searchForClosest(
|
||||
conn: RenderedConnection, maxRadius: number,
|
||||
dxy: Coordinate): {connection: RenderedConnection|null, radius: number} {
|
||||
if (!this.connections_.length) {
|
||||
conn: RenderedConnection,
|
||||
maxRadius: number,
|
||||
dxy: Coordinate
|
||||
): {connection: RenderedConnection | null; radius: number} {
|
||||
if (!this.connections.length) {
|
||||
// Don't bother.
|
||||
return {connection: null, radius: maxRadius};
|
||||
}
|
||||
@@ -236,7 +242,7 @@ export class ConnectionDB {
|
||||
// calculateIndexForYPos_ finds an index for insertion, which is always
|
||||
// after any block with the same y index. We want to search both forward
|
||||
// and back, so search on both sides of the index.
|
||||
const closestIndex = this.calculateIndexForYPos_(conn.y);
|
||||
const closestIndex = this.calculateIndexForYPos(conn.y);
|
||||
|
||||
let bestConnection = null;
|
||||
let bestRadius = maxRadius;
|
||||
@@ -244,8 +250,8 @@ export class ConnectionDB {
|
||||
|
||||
// Walk forward and back on the y axis looking for the closest x,y point.
|
||||
let pointerMin = closestIndex - 1;
|
||||
while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y, maxRadius)) {
|
||||
temp = this.connections_[pointerMin];
|
||||
while (pointerMin >= 0 && this.isInYRange(pointerMin, conn.y, maxRadius)) {
|
||||
temp = this.connections[pointerMin];
|
||||
if (this.connectionChecker.canConnect(conn, temp, true, bestRadius)) {
|
||||
bestConnection = temp;
|
||||
bestRadius = temp.distanceFrom(conn);
|
||||
@@ -254,9 +260,11 @@ export class ConnectionDB {
|
||||
}
|
||||
|
||||
let pointerMax = closestIndex;
|
||||
while (pointerMax < this.connections_.length &&
|
||||
this.isInYRange_(pointerMax, conn.y, maxRadius)) {
|
||||
temp = this.connections_[pointerMax];
|
||||
while (
|
||||
pointerMax < this.connections.length &&
|
||||
this.isInYRange(pointerMax, conn.y, maxRadius)
|
||||
) {
|
||||
temp = this.connections[pointerMax];
|
||||
if (this.connectionChecker.canConnect(conn, temp, true, bestRadius)) {
|
||||
bestConnection = temp;
|
||||
bestRadius = temp.distanceFrom(conn);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.ConnectionType');
|
||||
|
||||
|
||||
/**
|
||||
* 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'.
|
||||
NEXT_STATEMENT,
|
||||
// An up-facing block stack. E.g. 'break out of loop'.
|
||||
PREVIOUS_STATEMENT
|
||||
PREVIOUS_STATEMENT,
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.constants');
|
||||
|
||||
|
||||
/**
|
||||
* The language-neutral ID given to the collapsed input.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,10 @@ import * as browserEvents from './browser_events.js';
|
||||
import * as clipboard from './clipboard.js';
|
||||
import {config} from './config.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 {Menu} from './menu.js';
|
||||
import {MenuItem} from './menuitem.js';
|
||||
@@ -27,7 +30,6 @@ import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as Xml from './xml.js';
|
||||
|
||||
|
||||
/**
|
||||
* Which block is the context menu attached to?
|
||||
*/
|
||||
@@ -66,8 +68,10 @@ let menu_: Menu|null = null;
|
||||
* @param rtl True if RTL, false if LTR.
|
||||
*/
|
||||
export function show(
|
||||
e: Event, options: (ContextMenuOption|LegacyContextMenuOption)[],
|
||||
rtl: boolean) {
|
||||
e: Event,
|
||||
options: (ContextMenuOption | LegacyContextMenuOption)[],
|
||||
rtl: boolean
|
||||
) {
|
||||
WidgetDiv.show(dummyOwner, rtl, dispose);
|
||||
if (!options.length) {
|
||||
hide();
|
||||
@@ -94,7 +98,8 @@ export function show(
|
||||
*/
|
||||
function populate_(
|
||||
options: (ContextMenuOption | LegacyContextMenuOption)[],
|
||||
rtl: boolean): Menu {
|
||||
rtl: boolean
|
||||
): Menu {
|
||||
/* Here's what one option object looks like:
|
||||
{text: 'Make It So',
|
||||
enabled: true,
|
||||
@@ -146,7 +151,8 @@ function position_(menu: Menu, e: Event, rtl: boolean) {
|
||||
mouseEvent.clientY + viewportBBox.top,
|
||||
mouseEvent.clientY + viewportBBox.top,
|
||||
mouseEvent.clientX + viewportBBox.left,
|
||||
mouseEvent.clientX + viewportBBox.left);
|
||||
mouseEvent.clientX + viewportBBox.left
|
||||
);
|
||||
|
||||
createWidget_(menu);
|
||||
const menuSize = menu.getSize();
|
||||
@@ -179,7 +185,11 @@ function createWidget_(menu: Menu) {
|
||||
dom.addClass(menuDom, 'blocklyContextMenu');
|
||||
// Prevent system context menu when right-clicking a Blockly context menu.
|
||||
browserEvents.conditionalBind(
|
||||
(menuDom as EventTarget), 'contextmenu', null, haltPropagation);
|
||||
menuDom as EventTarget,
|
||||
'contextmenu',
|
||||
null,
|
||||
haltPropagation
|
||||
);
|
||||
// Focus only after the initial render to avoid issue #1329.
|
||||
menu.focus();
|
||||
}
|
||||
@@ -220,7 +230,7 @@ export function dispose() {
|
||||
* @param xml XML representation of new block.
|
||||
* @returns Function that creates a block.
|
||||
*/
|
||||
export function callbackFactory(block: Block, xml: Element): Function {
|
||||
export function callbackFactory(block: Block, xml: Element): () => void {
|
||||
return () => {
|
||||
eventUtils.disable();
|
||||
let newBlock;
|
||||
@@ -256,8 +266,9 @@ export function callbackFactory(block: Block, xml: Element): Function {
|
||||
* containing text, enabled, and a callback.
|
||||
* @internal
|
||||
*/
|
||||
export function commentDeleteOption(comment: WorkspaceCommentSvg):
|
||||
LegacyContextMenuOption {
|
||||
export function commentDeleteOption(
|
||||
comment: WorkspaceCommentSvg
|
||||
): LegacyContextMenuOption {
|
||||
const deleteOption = {
|
||||
text: Msg['REMOVE_COMMENT'],
|
||||
enabled: true,
|
||||
@@ -279,8 +290,9 @@ export function commentDeleteOption(comment: WorkspaceCommentSvg):
|
||||
* containing text, enabled, and a callback.
|
||||
* @internal
|
||||
*/
|
||||
export function commentDuplicateOption(comment: WorkspaceCommentSvg):
|
||||
LegacyContextMenuOption {
|
||||
export function commentDuplicateOption(
|
||||
comment: WorkspaceCommentSvg
|
||||
): LegacyContextMenuOption {
|
||||
const duplicateOption = {
|
||||
text: Msg['DUPLICATE_COMMENT'],
|
||||
enabled: true,
|
||||
@@ -298,20 +310,24 @@ export function commentDuplicateOption(comment: WorkspaceCommentSvg):
|
||||
* originated.
|
||||
* @param e The right-click mouse event.
|
||||
* @returns A menu option, containing text, enabled, and a callback.
|
||||
* @suppress {strictModuleDepCheck,checkTypes} Suppress checks while workspace
|
||||
* comments are not bundled in.
|
||||
* @internal
|
||||
*/
|
||||
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
|
||||
* location of the mouse event.
|
||||
*/
|
||||
function addWsComment() {
|
||||
const comment = new WorkspaceCommentSvg(
|
||||
ws, Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'],
|
||||
WorkspaceCommentSvg.DEFAULT_SIZE, WorkspaceCommentSvg.DEFAULT_SIZE);
|
||||
ws,
|
||||
Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'],
|
||||
WorkspaceCommentSvg.DEFAULT_SIZE,
|
||||
WorkspaceCommentSvg.DEFAULT_SIZE
|
||||
);
|
||||
|
||||
const injectionDiv = ws.getInjectionDiv();
|
||||
// Bounding rect coordinates are in client coordinates, meaning that they
|
||||
@@ -323,7 +339,8 @@ export function workspaceCommentOption(
|
||||
const mouseEvent = e as MouseEvent;
|
||||
const clientOffsetPixels = new Coordinate(
|
||||
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
|
||||
// left corner of the injection div.
|
||||
@@ -331,8 +348,10 @@ export function workspaceCommentOption(
|
||||
|
||||
// The position of the new comment in pixels relative to the origin of the
|
||||
// main workspace.
|
||||
const finalOffset =
|
||||
Coordinate.difference(clientOffsetPixels, mainOffsetPixels);
|
||||
const finalOffset = Coordinate.difference(
|
||||
clientOffsetPixels,
|
||||
mainOffsetPixels
|
||||
);
|
||||
// The position of the new comment in main workspace coordinates.
|
||||
finalOffset.scale(1 / ws.scale);
|
||||
|
||||
|
||||
@@ -9,15 +9,19 @@ goog.declareModuleId('Blockly.ContextMenuItems');
|
||||
|
||||
import type {BlockSvg} from './block_svg.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 Events from './events/events.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {inputTypes} from './input_types.js';
|
||||
import {CommentIcon} from './icons/comment_icon.js';
|
||||
import {Msg} from './msg.js';
|
||||
import {StatementInput} from './renderers/zelos/zelos.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
|
||||
/**
|
||||
* Option to undo previous action.
|
||||
*/
|
||||
@@ -281,12 +285,15 @@ export function registerDeleteAll() {
|
||||
} else {
|
||||
dialog.confirm(
|
||||
Msg['DELETE_ALL_BLOCKS'].replace(
|
||||
'%1', String(deletableBlocks.length)),
|
||||
'%1',
|
||||
String(deletableBlocks.length)
|
||||
),
|
||||
function (ok) {
|
||||
if (ok) {
|
||||
deleteNext_(deletableBlocks);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
|
||||
@@ -341,7 +348,7 @@ export function registerDuplicate() {
|
||||
export function registerComment() {
|
||||
const commentOption: RegistryItem = {
|
||||
displayText(scope: Scope) {
|
||||
if (scope.block!.getCommentIcon()) {
|
||||
if (scope.block!.hasIcon(CommentIcon.TYPE)) {
|
||||
// If there's already a comment, option is to remove.
|
||||
return Msg['REMOVE_COMMENT'];
|
||||
}
|
||||
@@ -350,15 +357,19 @@ export function registerComment() {
|
||||
},
|
||||
preconditionFn(scope: Scope) {
|
||||
const block = scope.block;
|
||||
if (!block!.isInFlyout && block!.workspace.options.comments &&
|
||||
!block!.isCollapsed() && block!.isEditable()) {
|
||||
if (
|
||||
!block!.isInFlyout &&
|
||||
block!.workspace.options.comments &&
|
||||
!block!.isCollapsed() &&
|
||||
block!.isEditable()
|
||||
) {
|
||||
return 'enabled';
|
||||
}
|
||||
return 'hidden';
|
||||
},
|
||||
callback(scope: Scope) {
|
||||
const block = scope.block;
|
||||
if (block!.getCommentIcon()) {
|
||||
if (block!.hasIcon(CommentIcon.TYPE)) {
|
||||
block!.setCommentText(null);
|
||||
} else {
|
||||
block!.setCommentText('');
|
||||
@@ -377,8 +388,9 @@ export function registerComment() {
|
||||
export function registerInline() {
|
||||
const inlineOption: RegistryItem = {
|
||||
displayText(scope: Scope) {
|
||||
return scope.block!.getInputsInline() ? Msg['EXTERNAL_INPUTS'] :
|
||||
Msg['INLINE_INPUTS'];
|
||||
return scope.block!.getInputsInline()
|
||||
? Msg['EXTERNAL_INPUTS']
|
||||
: Msg['INLINE_INPUTS'];
|
||||
},
|
||||
preconditionFn(scope: Scope) {
|
||||
const block = scope.block;
|
||||
@@ -386,8 +398,10 @@ export function registerInline() {
|
||||
for (let i = 1; i < block!.inputList.length; i++) {
|
||||
// Only display this option if there are two value or dummy inputs
|
||||
// next to each other.
|
||||
if (block!.inputList[i - 1].type !== inputTypes.STATEMENT &&
|
||||
block!.inputList[i].type !== inputTypes.STATEMENT) {
|
||||
if (
|
||||
!(block!.inputList[i - 1] instanceof StatementInput) &&
|
||||
!(block!.inputList[i] instanceof StatementInput)
|
||||
) {
|
||||
return 'enabled';
|
||||
}
|
||||
}
|
||||
@@ -410,13 +424,17 @@ export function registerInline() {
|
||||
export function registerCollapseExpandBlock() {
|
||||
const collapseExpandOption: RegistryItem = {
|
||||
displayText(scope: Scope) {
|
||||
return scope.block!.isCollapsed() ? Msg['EXPAND_BLOCK'] :
|
||||
Msg['COLLAPSE_BLOCK'];
|
||||
return scope.block!.isCollapsed()
|
||||
? Msg['EXPAND_BLOCK']
|
||||
: Msg['COLLAPSE_BLOCK'];
|
||||
},
|
||||
preconditionFn(scope: Scope) {
|
||||
const block = scope.block;
|
||||
if (!block!.isInFlyout && block!.isMovable() &&
|
||||
block!.workspace.options.collapse) {
|
||||
if (
|
||||
!block!.isInFlyout &&
|
||||
block!.isMovable() &&
|
||||
block!.workspace.options.collapse
|
||||
) {
|
||||
return 'enabled';
|
||||
}
|
||||
return 'hidden';
|
||||
@@ -437,13 +455,17 @@ export function registerCollapseExpandBlock() {
|
||||
export function registerDisable() {
|
||||
const disableOption: RegistryItem = {
|
||||
displayText(scope: Scope) {
|
||||
return scope.block!.isEnabled() ? Msg['DISABLE_BLOCK'] :
|
||||
Msg['ENABLE_BLOCK'];
|
||||
return scope.block!.isEnabled()
|
||||
? Msg['DISABLE_BLOCK']
|
||||
: Msg['ENABLE_BLOCK'];
|
||||
},
|
||||
preconditionFn(scope: Scope) {
|
||||
const block = scope.block;
|
||||
if (!block!.isInFlyout && block!.workspace.options.disable &&
|
||||
block!.isEditable()) {
|
||||
if (
|
||||
!block!.isInFlyout &&
|
||||
block!.workspace.options.disable &&
|
||||
block!.isEditable()
|
||||
) {
|
||||
if (block!.getInheritedDisabled()) {
|
||||
return 'disabled';
|
||||
}
|
||||
@@ -481,9 +503,9 @@ export function registerDelete() {
|
||||
// Blocks in the current stack would survive this block's deletion.
|
||||
descendantCount -= nextBlock.getDescendants(false).length;
|
||||
}
|
||||
return descendantCount === 1 ?
|
||||
Msg['DELETE_BLOCK'] :
|
||||
Msg['DELETE_X_BLOCKS'].replace('%1', `${descendantCount}`);
|
||||
return descendantCount === 1
|
||||
? Msg['DELETE_BLOCK']
|
||||
: Msg['DELETE_X_BLOCKS'].replace('%1', `${descendantCount}`);
|
||||
},
|
||||
preconditionFn(scope: Scope) {
|
||||
if (!scope.block!.isInFlyout && scope.block!.isDeletable()) {
|
||||
@@ -513,8 +535,10 @@ export function registerHelp() {
|
||||
},
|
||||
preconditionFn(scope: Scope) {
|
||||
const block = scope.block;
|
||||
const url = typeof block!.helpUrl === 'function' ? block!.helpUrl() :
|
||||
block!.helpUrl;
|
||||
const url =
|
||||
typeof block!.helpUrl === 'function'
|
||||
? block!.helpUrl()
|
||||
: block!.helpUrl;
|
||||
if (url) {
|
||||
return 'enabled';
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ goog.declareModuleId('Blockly.ContextMenuRegistry');
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for the registry of context menu items. This is intended to be a
|
||||
* singleton. You should not create a new instance, and only access this class
|
||||
@@ -81,16 +80,19 @@ export class ContextMenuRegistry {
|
||||
* block being clicked on)
|
||||
* @returns the list of ContextMenuOptions
|
||||
*/
|
||||
getContextMenuOptions(scopeType: ScopeType, scope: Scope):
|
||||
ContextMenuOption[] {
|
||||
getContextMenuOptions(
|
||||
scopeType: ScopeType,
|
||||
scope: Scope
|
||||
): ContextMenuOption[] {
|
||||
const menuOptions: ContextMenuOption[] = [];
|
||||
for (const item of this.registry_.values()) {
|
||||
if (scopeType === item.scopeType) {
|
||||
const precondition = item.preconditionFn(scope);
|
||||
if (precondition !== 'hidden') {
|
||||
const displayText = typeof item.displayText === 'function' ?
|
||||
item.displayText(scope) :
|
||||
item.displayText;
|
||||
const displayText =
|
||||
typeof item.displayText === 'function'
|
||||
? item.displayText(scope)
|
||||
: item.displayText;
|
||||
const menuOption: ContextMenuOption = {
|
||||
text: displayText,
|
||||
enabled: precondition === 'enabled',
|
||||
|
||||
37
core/css.ts
37
core/css.ts
@@ -7,7 +7,6 @@
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Css');
|
||||
|
||||
|
||||
/** Has CSS already been injected? */
|
||||
let injected = false;
|
||||
|
||||
@@ -89,31 +88,6 @@ let content = `
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.blocklyWsDragSurface {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Added as a separate rule with multiple classes to make it more specific
|
||||
than a bootstrap rule that selects svg:root. See issue #1275 for context.
|
||||
*/
|
||||
.blocklyWsDragSurface.blocklyOverflowVisible {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.blocklyBlockDragSurface {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: visible !important;
|
||||
z-index: 50; /* Display below toolbox, but above everything else. */
|
||||
}
|
||||
|
||||
.blocklyBlockCanvas.blocklyCanvasTransitioning,
|
||||
.blocklyBubbleCanvas.blocklyCanvasTransitioning {
|
||||
transition: transform .5s;
|
||||
@@ -242,14 +216,6 @@ let content = `
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
/* Change the cursor on the whole drag surface in case the mouse gets
|
||||
ahead of block during a drag. This way the cursor is still a closed hand.
|
||||
*/
|
||||
.blocklyBlockDragSurface .blocklyDraggable {
|
||||
cursor: grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.blocklyDragging.blocklyDraggingDelete {
|
||||
cursor: url("<<<PATH>>>/handdelete.cur"), auto;
|
||||
}
|
||||
@@ -302,8 +268,7 @@ let content = `
|
||||
Don't allow users to select text. It gets annoying when trying to
|
||||
drag a block and selected text moves instead.
|
||||
*/
|
||||
.blocklySvg text,
|
||||
.blocklyBlockDragSurface text {
|
||||
.blocklySvg text {
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
@@ -18,7 +18,6 @@ import {DragTarget} from './drag_target.js';
|
||||
import type {IDeleteArea} from './interfaces/i_delete_area.js';
|
||||
import type {IDraggable} from './interfaces/i_draggable.js';
|
||||
|
||||
|
||||
/**
|
||||
* Abstract class for a component that can delete a block or bubble that is
|
||||
* dropped on top of it.
|
||||
@@ -59,7 +58,7 @@ export class DeleteArea extends DragTarget implements IDeleteArea {
|
||||
*/
|
||||
wouldDelete(element: IDraggable, couldConnect: boolean): boolean {
|
||||
if (element instanceof BlockSvg) {
|
||||
const block = (element);
|
||||
const block = element;
|
||||
const couldDeleteBlock = !block.getParent() && block.isDeletable();
|
||||
this.updateWouldDelete_(couldDeleteBlock && !couldConnect);
|
||||
} else {
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.dialog');
|
||||
|
||||
|
||||
let alertImplementation = function(message: string, opt_callback?: () => void) {
|
||||
let alertImplementation = function (
|
||||
message: string,
|
||||
opt_callback?: () => void
|
||||
) {
|
||||
window.alert(message);
|
||||
if (opt_callback) {
|
||||
opt_callback();
|
||||
@@ -16,13 +18,17 @@ let alertImplementation = function(message: string, opt_callback?: () => void) {
|
||||
};
|
||||
|
||||
let confirmImplementation = function (
|
||||
message: string, callback: (result: boolean) => void) {
|
||||
message: string,
|
||||
callback: (result: boolean) => void
|
||||
) {
|
||||
callback(window.confirm(message));
|
||||
};
|
||||
|
||||
let promptImplementation = function (
|
||||
message: string, defaultValue: string,
|
||||
callback: (result: string|null) => void) {
|
||||
message: string,
|
||||
defaultValue: string,
|
||||
callback: (result: string | null) => void
|
||||
) {
|
||||
callback(window.prompt(message, defaultValue));
|
||||
};
|
||||
|
||||
@@ -65,7 +71,6 @@ function confirmInternal(message: string, callback: (p1: boolean) => void) {
|
||||
confirmImplementation(message, callback);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function setConfirm(
|
||||
confirmFunction: (p1: string, p2: (p1: boolean) => void) => void) {
|
||||
confirmFunction: (p1: string, p2: (p1: boolean) => void) => void
|
||||
) {
|
||||
confirmImplementation = confirmFunction;
|
||||
}
|
||||
|
||||
@@ -88,8 +94,10 @@ export function setConfirm(
|
||||
* @param callback The callback for handling user response.
|
||||
*/
|
||||
export function prompt(
|
||||
message: string, defaultValue: string,
|
||||
callback: (p1: string|null) => void) {
|
||||
message: string,
|
||||
defaultValue: string,
|
||||
callback: (p1: string | null) => void
|
||||
) {
|
||||
promptImplementation(message, defaultValue, callback);
|
||||
}
|
||||
|
||||
@@ -100,8 +108,12 @@ export function prompt(
|
||||
* @see Blockly.dialog.prompt
|
||||
*/
|
||||
export function setPrompt(
|
||||
promptFunction: (p1: string, p2: string, p3: (p1: string|null) => void) =>
|
||||
void) {
|
||||
promptFunction: (
|
||||
p1: string,
|
||||
p2: string,
|
||||
p3: (p1: string | null) => void
|
||||
) => void
|
||||
) {
|
||||
promptImplementation = promptFunction;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import type {IDragTarget} from './interfaces/i_drag_target.js';
|
||||
import type {IDraggable} from './interfaces/i_draggable.js';
|
||||
import type {Rect} from './utils/rect.js';
|
||||
|
||||
|
||||
/**
|
||||
* Abstract class for a component with custom behaviour when a block or bubble
|
||||
* is dragged over or dropped on top of it.
|
||||
|
||||
@@ -23,7 +23,6 @@ import type {Size} from './utils/size.js';
|
||||
import * as style from './utils/style.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
|
||||
/**
|
||||
* Arrow size in px. Should match the value in CSS
|
||||
* (need to position pre-render).
|
||||
@@ -133,8 +132,8 @@ export function createDom() {
|
||||
|
||||
div.style.opacity = '0';
|
||||
// Transition animation for transform: translate() and opacity.
|
||||
div.style.transition = 'transform ' + ANIMATION_TIME + 's, ' +
|
||||
'opacity ' + ANIMATION_TIME + 's';
|
||||
div.style.transition =
|
||||
'transform ' + ANIMATION_TIME + 's, ' + 'opacity ' + ANIMATION_TIME + 's';
|
||||
|
||||
// Handle focusin/out events to add a visual indicator when
|
||||
// a child is focused or blurred.
|
||||
@@ -202,11 +201,17 @@ export function setColour(backgroundColour: string, borderColour: string) {
|
||||
* @returns True if the menu rendered below block; false if above.
|
||||
*/
|
||||
export function showPositionedByBlock<T>(
|
||||
field: Field<T>, block: BlockSvg, opt_onHide?: Function,
|
||||
opt_secondaryYOffset?: number): boolean {
|
||||
field: Field<T>,
|
||||
block: BlockSvg,
|
||||
opt_onHide?: Function,
|
||||
opt_secondaryYOffset?: number
|
||||
): boolean {
|
||||
return showPositionedByRect(
|
||||
getScaledBboxOfBlock(block), field as Field, opt_onHide,
|
||||
opt_secondaryYOffset);
|
||||
getScaledBboxOfBlock(block),
|
||||
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.
|
||||
*/
|
||||
export function showPositionedByField<T>(
|
||||
field: Field<T>, opt_onHide?: Function,
|
||||
opt_secondaryYOffset?: number): boolean {
|
||||
field: Field<T>,
|
||||
opt_onHide?: Function,
|
||||
opt_secondaryYOffset?: number
|
||||
): boolean {
|
||||
positionToField = true;
|
||||
return showPositionedByRect(
|
||||
getScaledBboxOfField(field as Field), field as Field, opt_onHide,
|
||||
opt_secondaryYOffset);
|
||||
getScaledBboxOfField(field as Field),
|
||||
field as Field,
|
||||
opt_onHide,
|
||||
opt_secondaryYOffset
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function showPositionedByRect(
|
||||
bBox: Rect, field: Field, opt_onHide?: Function,
|
||||
opt_secondaryYOffset?: number): boolean {
|
||||
bBox: Rect,
|
||||
field: Field,
|
||||
opt_onHide?: Function,
|
||||
opt_secondaryYOffset?: number
|
||||
): boolean {
|
||||
// If we can fit it, render below the block.
|
||||
const primaryX = bBox.left + (bBox.right - bBox.left) / 2;
|
||||
const primaryY = bBox.bottom;
|
||||
@@ -286,8 +299,14 @@ function showPositionedByRect(
|
||||
}
|
||||
setBoundsElement(workspace.getParentSvg().parentNode as Element | null);
|
||||
return show(
|
||||
field, sourceBlock.RTL, primaryX, primaryY, secondaryX, secondaryY,
|
||||
opt_onHide);
|
||||
field,
|
||||
sourceBlock.RTL,
|
||||
primaryX,
|
||||
primaryY,
|
||||
secondaryX,
|
||||
secondaryY,
|
||||
opt_onHide
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,8 +329,14 @@ function showPositionedByRect(
|
||||
* @internal
|
||||
*/
|
||||
export function show<T>(
|
||||
newOwner: Field<T>, rtl: boolean, primaryX: number, primaryY: number,
|
||||
secondaryX: number, secondaryY: number, opt_onHide?: Function): boolean {
|
||||
newOwner: Field<T>,
|
||||
rtl: boolean,
|
||||
primaryX: number,
|
||||
primaryY: number,
|
||||
secondaryX: number,
|
||||
secondaryY: number,
|
||||
opt_onHide?: Function
|
||||
): boolean {
|
||||
owner = newOwner as Field;
|
||||
onHide = opt_onHide || null;
|
||||
// Set direction.
|
||||
@@ -371,8 +396,11 @@ const internal = {
|
||||
* and arrow.
|
||||
*/
|
||||
getPositionMetrics: function (
|
||||
primaryX: number, primaryY: number, secondaryX: number,
|
||||
secondaryY: number): PositionMetrics {
|
||||
primaryX: number,
|
||||
primaryY: number,
|
||||
secondaryX: number,
|
||||
secondaryY: number
|
||||
): PositionMetrics {
|
||||
const boundsInfo = internal.getBoundsInfo();
|
||||
const divSize = style.getSize(div as Element);
|
||||
|
||||
@@ -383,7 +411,11 @@ const internal = {
|
||||
// Can we fit in-bounds above the target?
|
||||
if (secondaryY - divSize.height > boundsInfo.top) {
|
||||
return getPositionAboveMetrics(
|
||||
secondaryX, secondaryY, boundsInfo, divSize);
|
||||
secondaryX,
|
||||
secondaryY,
|
||||
boundsInfo,
|
||||
divSize
|
||||
);
|
||||
}
|
||||
// Can we fit outside the workspace bounds (but inside the window)
|
||||
// below?
|
||||
@@ -394,7 +426,11 @@ const internal = {
|
||||
// above?
|
||||
if (secondaryY - divSize.height > document.documentElement.clientTop) {
|
||||
return getPositionAboveMetrics(
|
||||
secondaryX, secondaryY, boundsInfo, divSize);
|
||||
secondaryX,
|
||||
secondaryY,
|
||||
boundsInfo,
|
||||
divSize
|
||||
);
|
||||
}
|
||||
|
||||
// Last resort, render at top of page.
|
||||
@@ -415,10 +451,17 @@ const internal = {
|
||||
* and arrow.
|
||||
*/
|
||||
function getPositionBelowMetrics(
|
||||
primaryX: number, primaryY: number, boundsInfo: BoundsInfo,
|
||||
divSize: Size): PositionMetrics {
|
||||
const xCoords =
|
||||
getPositionX(primaryX, boundsInfo.left, boundsInfo.right, divSize.width);
|
||||
primaryX: number,
|
||||
primaryY: number,
|
||||
boundsInfo: BoundsInfo,
|
||||
divSize: Size
|
||||
): PositionMetrics {
|
||||
const xCoords = getPositionX(
|
||||
primaryX,
|
||||
boundsInfo.left,
|
||||
boundsInfo.right,
|
||||
divSize.width
|
||||
);
|
||||
|
||||
const arrowY = -(ARROW_SIZE / 2 + BORDER_SIZE);
|
||||
const finalY = primaryY + PADDING_Y;
|
||||
@@ -448,10 +491,17 @@ function getPositionBelowMetrics(
|
||||
* and arrow.
|
||||
*/
|
||||
function getPositionAboveMetrics(
|
||||
secondaryX: number, secondaryY: number, boundsInfo: BoundsInfo,
|
||||
divSize: Size): PositionMetrics {
|
||||
secondaryX: number,
|
||||
secondaryY: number,
|
||||
boundsInfo: BoundsInfo,
|
||||
divSize: Size
|
||||
): PositionMetrics {
|
||||
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 finalY = secondaryY - divSize.height - PADDING_Y;
|
||||
@@ -481,9 +531,16 @@ function getPositionAboveMetrics(
|
||||
* and arrow.
|
||||
*/
|
||||
function getPositionTopOfPageMetrics(
|
||||
sourceX: number, boundsInfo: BoundsInfo, divSize: Size): PositionMetrics {
|
||||
const xCoords =
|
||||
getPositionX(sourceX, boundsInfo.left, boundsInfo.right, divSize.width);
|
||||
sourceX: number,
|
||||
boundsInfo: BoundsInfo,
|
||||
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.
|
||||
return {
|
||||
@@ -511,8 +568,11 @@ function getPositionTopOfPageMetrics(
|
||||
* @internal
|
||||
*/
|
||||
export function getPositionX(
|
||||
sourceX: number, boundsLeft: number, boundsRight: number,
|
||||
divWidth: number): {divX: number, arrowX: number} {
|
||||
sourceX: number,
|
||||
boundsLeft: number,
|
||||
boundsRight: number,
|
||||
divWidth: number
|
||||
): {divX: number; arrowX: number} {
|
||||
let divX = sourceX;
|
||||
// Offset the topLeft coord so that the dropdowndiv is centered.
|
||||
divX -= divWidth / 2;
|
||||
@@ -527,7 +587,10 @@ export function getPositionX(
|
||||
const horizPadding = ARROW_HORIZONTAL_PADDING;
|
||||
// Clamp the arrow position so that it stays attached to the dropdowndiv.
|
||||
relativeArrowX = math.clamp(
|
||||
horizPadding, relativeArrowX, divWidth - horizPadding - ARROW_SIZE);
|
||||
horizPadding,
|
||||
relativeArrowX,
|
||||
divWidth - horizPadding - ARROW_SIZE
|
||||
);
|
||||
|
||||
return {arrowX: relativeArrowX, divX};
|
||||
}
|
||||
@@ -550,7 +613,9 @@ export function isVisible(): boolean {
|
||||
* @returns True if hidden.
|
||||
*/
|
||||
export function hideIfOwner<T>(
|
||||
divOwner: Field<T>, opt_withoutAnimation?: boolean): boolean {
|
||||
divOwner: Field<T>,
|
||||
opt_withoutAnimation?: boolean
|
||||
): boolean {
|
||||
if (owner === divOwner) {
|
||||
if (opt_withoutAnimation) {
|
||||
hideWithoutAnimation();
|
||||
@@ -625,20 +690,33 @@ export function hideWithoutAnimation() {
|
||||
* @returns True if the menu rendered at the primary origin point.
|
||||
*/
|
||||
function positionInternal(
|
||||
primaryX: number, primaryY: number, secondaryX: number,
|
||||
secondaryY: number): boolean {
|
||||
const metrics =
|
||||
internal.getPositionMetrics(primaryX, primaryY, secondaryX, secondaryY);
|
||||
primaryX: number,
|
||||
primaryY: number,
|
||||
secondaryX: number,
|
||||
secondaryY: number
|
||||
): boolean {
|
||||
const metrics = internal.getPositionMetrics(
|
||||
primaryX,
|
||||
primaryY,
|
||||
secondaryX,
|
||||
secondaryY
|
||||
);
|
||||
|
||||
// Update arrow CSS.
|
||||
if (metrics.arrowVisible) {
|
||||
arrow.style.display = '';
|
||||
arrow.style.transform = 'translate(' + metrics.arrowX + 'px,' +
|
||||
metrics.arrowY + 'px) rotate(45deg)';
|
||||
arrow.style.transform =
|
||||
'translate(' +
|
||||
metrics.arrowX +
|
||||
'px,' +
|
||||
metrics.arrowY +
|
||||
'px) rotate(45deg)';
|
||||
arrow.setAttribute(
|
||||
'class',
|
||||
metrics.arrowAtTop ? 'blocklyDropDownArrow blocklyArrowTop' :
|
||||
'blocklyDropDownArrow blocklyArrowBottom');
|
||||
metrics.arrowAtTop
|
||||
? 'blocklyDropDownArrow blocklyArrowTop'
|
||||
: 'blocklyDropDownArrow blocklyArrowBottom'
|
||||
);
|
||||
} else {
|
||||
arrow.style.display = 'none';
|
||||
}
|
||||
@@ -679,8 +757,9 @@ export function repositionForWindowResize() {
|
||||
// it.
|
||||
if (owner) {
|
||||
const block = owner.getSourceBlock() as BlockSvg;
|
||||
const bBox = positionToField ? getScaledBboxOfField(owner) :
|
||||
getScaledBboxOfBlock(block);
|
||||
const bBox = positionToField
|
||||
? getScaledBboxOfField(owner)
|
||||
: getScaledBboxOfBlock(block);
|
||||
// If we can fit it, render below the block.
|
||||
const primaryX = bBox.left + (bBox.right - bBox.left) / 2;
|
||||
const primaryY = bBox.bottom;
|
||||
|
||||
@@ -7,13 +7,16 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events');
|
||||
|
||||
|
||||
import {Abstract, AbstractEventJson} from './events_abstract.js';
|
||||
import {BlockBase, BlockBaseJson} from './events_block_base.js';
|
||||
import {BlockChange, BlockChangeJson} from './events_block_change.js';
|
||||
import {BlockCreate, BlockCreateJson} from './events_block_create.js';
|
||||
import {BlockDelete, BlockDeleteJson} from './events_block_delete.js';
|
||||
import {BlockDrag, BlockDragJson} from './events_block_drag.js';
|
||||
import {
|
||||
BlockFieldIntermediateChange,
|
||||
BlockFieldIntermediateChangeJson,
|
||||
} from './events_block_field_intermediate_change.js';
|
||||
import {BlockMove, BlockMoveJson} from './events_block_move.js';
|
||||
import {BubbleOpen, BubbleOpenJson, BubbleType} from './events_bubble_open.js';
|
||||
import {Click, ClickJson, ClickTarget} from './events_click.js';
|
||||
@@ -25,9 +28,11 @@ import {CommentMove, CommentMoveJson} from './events_comment_move.js';
|
||||
import {MarkerMove, MarkerMoveJson} from './events_marker_move.js';
|
||||
import {Selected, SelectedJson} from './events_selected.js';
|
||||
import {ThemeChange, ThemeChangeJson} from './events_theme_change.js';
|
||||
import {ToolboxItemSelect, ToolboxItemSelectJson} from './events_toolbox_item_select.js';
|
||||
import {
|
||||
ToolboxItemSelect,
|
||||
ToolboxItemSelectJson,
|
||||
} from './events_toolbox_item_select.js';
|
||||
import {TrashcanOpen, TrashcanOpenJson} from './events_trashcan_open.js';
|
||||
import {Ui} from './events_ui.js';
|
||||
import {UiBase} from './events_ui_base.js';
|
||||
import {VarBase, VarBaseJson} from './events_var_base.js';
|
||||
import {VarCreate, VarCreateJson} from './events_var_create.js';
|
||||
@@ -35,8 +40,7 @@ import {VarDelete, VarDeleteJson} from './events_var_delete.js';
|
||||
import {VarRename, VarRenameJson} from './events_var_rename.js';
|
||||
import {ViewportChange, ViewportChangeJson} from './events_viewport.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import {FinishedLoading, FinishedLoadingJson} from './workspace_events.js';
|
||||
|
||||
import {FinishedLoading} from './workspace_events.js';
|
||||
|
||||
// Events.
|
||||
export {Abstract};
|
||||
@@ -54,6 +58,8 @@ export {BlockDelete};
|
||||
export {BlockDeleteJson};
|
||||
export {BlockDrag};
|
||||
export {BlockDragJson};
|
||||
export {BlockFieldIntermediateChange};
|
||||
export {BlockFieldIntermediateChangeJson};
|
||||
export {BlockMove};
|
||||
export {BlockMoveJson};
|
||||
export {Click};
|
||||
@@ -69,7 +75,6 @@ export {CommentDelete};
|
||||
export {CommentMove};
|
||||
export {CommentMoveJson};
|
||||
export {FinishedLoading};
|
||||
export {FinishedLoadingJson};
|
||||
export {MarkerMove};
|
||||
export {MarkerMoveJson};
|
||||
export {Selected};
|
||||
@@ -80,7 +85,6 @@ export {ToolboxItemSelect};
|
||||
export {ToolboxItemSelectJson};
|
||||
export {TrashcanOpen};
|
||||
export {TrashcanOpenJson};
|
||||
export {Ui};
|
||||
export {UiBase};
|
||||
export {VarBase};
|
||||
export {VarBaseJson};
|
||||
@@ -99,6 +103,8 @@ export const BLOCK_CREATE = eventUtils.BLOCK_CREATE;
|
||||
export const BLOCK_DELETE = eventUtils.BLOCK_DELETE;
|
||||
export const BLOCK_DRAG = eventUtils.BLOCK_DRAG;
|
||||
export const BLOCK_MOVE = eventUtils.BLOCK_MOVE;
|
||||
export const BLOCK_FIELD_INTERMEDIATE_CHANGE =
|
||||
eventUtils.BLOCK_FIELD_INTERMEDIATE_CHANGE;
|
||||
export const BUBBLE_OPEN = eventUtils.BUBBLE_OPEN;
|
||||
export type BumpEvent = eventUtils.BumpEvent;
|
||||
export const BUMP_EVENTS = eventUtils.BUMP_EVENTS;
|
||||
|
||||
@@ -13,13 +13,11 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.Abstract');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as common from '../common.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
import * as eventUtils from './utils.js';
|
||||
|
||||
|
||||
/**
|
||||
* Abstract class for an event.
|
||||
*/
|
||||
@@ -67,19 +65,6 @@ export abstract class Abstract {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
fromJson(json: AbstractEventJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.Abstract.prototype.fromJson', 'version 9', 'version 10',
|
||||
'Blockly.Events.fromJson');
|
||||
this.isBlank = false;
|
||||
this.group = json['group'] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -89,8 +74,11 @@ export abstract class Abstract {
|
||||
* supertypes of parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: AbstractEventJson, workspace: Workspace, event: any):
|
||||
Abstract {
|
||||
static fromJson(
|
||||
json: AbstractEventJson,
|
||||
workspace: Workspace,
|
||||
event: any
|
||||
): Abstract {
|
||||
event.isBlank = false;
|
||||
event.group = json['group'] || '';
|
||||
event.workspaceId = workspace.id;
|
||||
@@ -131,7 +119,8 @@ export abstract class Abstract {
|
||||
if (!workspace) {
|
||||
throw Error(
|
||||
'Workspace is null. Event must have been generated from real' +
|
||||
' Blockly events.');
|
||||
' Blockly events.'
|
||||
);
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@ import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.BlockBase');
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import * as deprecation from '../utils/deprecation.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.
|
||||
@@ -52,25 +53,13 @@ export class BlockBase extends AbstractEvent {
|
||||
if (!this.blockId) {
|
||||
throw new Error(
|
||||
'The block ID is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
json['blockId'] = this.blockId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: BlockBaseJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.BlockBase.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.blockId = json['blockId'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -80,10 +69,16 @@ export class BlockBase extends AbstractEvent {
|
||||
* static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: BlockBaseJson, workspace: Workspace, event?: any):
|
||||
BlockBase {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new BlockBase()) as BlockBase;
|
||||
static fromJson(
|
||||
json: BlockBaseJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): BlockBase {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new BlockBase()
|
||||
) as BlockBase;
|
||||
newEvent.blockId = json['blockId'];
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ goog.declareModuleId('Blockly.Events.BlockChange');
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import type {BlockSvg} from '../block_svg.js';
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import {IconType} from '../icons/icon_types.js';
|
||||
import {hasBubble} from '../interfaces/i_has_bubble.js';
|
||||
import * as registry from '../registry.js';
|
||||
import * as utilsXml from '../utils/xml.js';
|
||||
import {Workspace} from '../workspace.js';
|
||||
@@ -23,7 +24,6 @@ import * as Xml from '../xml.js';
|
||||
import {BlockBase, BlockBaseJson} from './events_block_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners when some element of a block has changed (e.g.
|
||||
* field values, comments, etc).
|
||||
@@ -53,8 +53,12 @@ export class BlockChange extends BlockBase {
|
||||
* @param opt_newValue New value of element.
|
||||
*/
|
||||
constructor(
|
||||
opt_block?: Block, opt_element?: string, opt_name?: string|null,
|
||||
opt_oldValue?: unknown, opt_newValue?: unknown) {
|
||||
opt_block?: Block,
|
||||
opt_element?: string,
|
||||
opt_name?: string | null,
|
||||
opt_oldValue?: unknown,
|
||||
opt_newValue?: unknown
|
||||
) {
|
||||
super(opt_block);
|
||||
|
||||
if (!opt_block) {
|
||||
@@ -76,7 +80,8 @@ export class BlockChange extends BlockBase {
|
||||
if (!this.element) {
|
||||
throw new Error(
|
||||
'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['name'] = this.name;
|
||||
@@ -85,22 +90,6 @@ export class BlockChange extends BlockBase {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: BlockChangeJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.BlockChange.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.element = json['element'];
|
||||
this.name = json['name'];
|
||||
this.oldValue = json['oldValue'];
|
||||
this.newValue = json['newValue'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -110,11 +99,16 @@ export class BlockChange extends BlockBase {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: BlockChangeJson, workspace: Workspace, event?: any):
|
||||
BlockChange {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new BlockChange()) as
|
||||
BlockChange;
|
||||
static fromJson(
|
||||
json: BlockChangeJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): BlockChange {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new BlockChange()
|
||||
) as BlockChange;
|
||||
newEvent.element = json['element'];
|
||||
newEvent.name = json['name'];
|
||||
newEvent.oldValue = json['oldValue'];
|
||||
@@ -141,19 +135,21 @@ export class BlockChange extends BlockBase {
|
||||
if (!this.blockId) {
|
||||
throw new Error(
|
||||
'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);
|
||||
if (!block) {
|
||||
throw new Error(
|
||||
'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.
|
||||
const blockSvg = block as BlockSvg;
|
||||
if (blockSvg.mutator) {
|
||||
const icon = block.getIcon(IconType.MUTATOR);
|
||||
if (icon && hasBubble(icon) && icon.bubbleIsVisible()) {
|
||||
// Close the mutator (if open) since we don't want to update it.
|
||||
blockSvg.mutator.setVisible(false);
|
||||
icon.setBubbleVisible(false);
|
||||
}
|
||||
const value = forward ? this.newValue : this.oldValue;
|
||||
switch (this.element) {
|
||||
@@ -162,12 +158,12 @@ export class BlockChange extends BlockBase {
|
||||
if (field) {
|
||||
field.setValue(value);
|
||||
} else {
|
||||
console.warn('Can\'t set non-existent field: ' + this.name);
|
||||
console.warn("Can't set non-existent field: " + this.name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'comment':
|
||||
block.setCommentText(value as string || null);
|
||||
block.setCommentText((value as string) || null);
|
||||
break;
|
||||
case 'collapsed':
|
||||
block.setCollapsed(!!value);
|
||||
@@ -181,13 +177,15 @@ export class BlockChange extends BlockBase {
|
||||
case 'mutation': {
|
||||
const oldState = BlockChange.getExtraBlockState_(block as BlockSvg);
|
||||
if (block.loadExtraState) {
|
||||
block.loadExtraState(JSON.parse(value as string || '{}'));
|
||||
block.loadExtraState(JSON.parse((value as string) || '{}'));
|
||||
} else if (block.domToMutation) {
|
||||
block.domToMutation(
|
||||
utilsXml.textToDom(value as string || '<mutation/>'));
|
||||
utilsXml.textToDom((value as string) || '<mutation/>')
|
||||
);
|
||||
}
|
||||
eventUtils.fire(
|
||||
new BlockChange(block, 'mutation', null, oldState, value));
|
||||
new BlockChange(block, 'mutation', null, oldState, value)
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -13,7 +13,6 @@ import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.BlockCreate');
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import * as blocks from '../serialization/blocks.js';
|
||||
import * as utilsXml from '../utils/xml.js';
|
||||
@@ -23,7 +22,6 @@ import {BlockBase, BlockBaseJson} from './events_block_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners when a block (or connected stack of blocks) is
|
||||
* created.
|
||||
@@ -69,17 +67,20 @@ export class BlockCreate extends BlockBase {
|
||||
if (!this.xml) {
|
||||
throw new Error(
|
||||
'The block XML is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.ids) {
|
||||
throw new Error(
|
||||
'The block IDs are undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.json) {
|
||||
throw new Error(
|
||||
'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['ids'] = this.ids;
|
||||
@@ -90,24 +91,6 @@ export class BlockCreate extends BlockBase {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: BlockCreateJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.BlockCreate.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.xml = utilsXml.textToDom(json['xml']);
|
||||
this.ids = json['ids'];
|
||||
this.json = json['json'] as blocks.State;
|
||||
if (json['recordUndo'] !== undefined) {
|
||||
this.recordUndo = json['recordUndo'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -117,11 +100,16 @@ export class BlockCreate extends BlockBase {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: BlockCreateJson, workspace: Workspace, event?: any):
|
||||
BlockCreate {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new BlockCreate()) as
|
||||
BlockCreate;
|
||||
static fromJson(
|
||||
json: BlockCreateJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): BlockCreate {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new BlockCreate()
|
||||
) as BlockCreate;
|
||||
newEvent.xml = utilsXml.textToDom(json['xml']);
|
||||
newEvent.ids = json['ids'];
|
||||
newEvent.json = json['json'] as blocks.State;
|
||||
@@ -141,12 +129,14 @@ export class BlockCreate extends BlockBase {
|
||||
if (!this.json) {
|
||||
throw new Error(
|
||||
'The block JSON is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.ids) {
|
||||
throw new Error(
|
||||
'The block IDs are undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (forward) {
|
||||
blocks.append(this.json, workspace);
|
||||
@@ -158,7 +148,7 @@ export class BlockCreate extends BlockBase {
|
||||
block.dispose(false);
|
||||
} else if (id === this.blockId) {
|
||||
// Only complain about root-level block.
|
||||
console.warn('Can\'t uncreate non-existent block: ' + id);
|
||||
console.warn("Can't uncreate non-existent block: " + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.BlockDelete');
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import * as blocks from '../serialization/blocks.js';
|
||||
import * as utilsXml from '../utils/xml.js';
|
||||
@@ -23,7 +22,6 @@ import {BlockBase, BlockBaseJson} from './events_block_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners when a block (or connected stack of blocks) is
|
||||
* deleted.
|
||||
@@ -62,8 +60,9 @@ export class BlockDelete extends BlockBase {
|
||||
this.oldXml = Xml.blockToDomWithXY(opt_block);
|
||||
this.ids = eventUtils.getDescendantIds(opt_block);
|
||||
this.wasShadow = opt_block.isShadow();
|
||||
this.oldJson =
|
||||
blocks.save(opt_block, {addCoordinates: true}) as blocks.State;
|
||||
this.oldJson = blocks.save(opt_block, {
|
||||
addCoordinates: true,
|
||||
}) as blocks.State;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,22 +75,26 @@ export class BlockDelete extends BlockBase {
|
||||
if (!this.oldXml) {
|
||||
throw new Error(
|
||||
'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) {
|
||||
throw new Error(
|
||||
'The block IDs are undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (this.wasShadow === undefined) {
|
||||
throw new Error(
|
||||
'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) {
|
||||
throw new Error(
|
||||
'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['ids'] = this.ids;
|
||||
@@ -103,26 +106,6 @@ export class BlockDelete extends BlockBase {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: BlockDeleteJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.BlockDelete.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.oldXml = utilsXml.textToDom(json['oldXml']);
|
||||
this.ids = json['ids'];
|
||||
this.wasShadow =
|
||||
json['wasShadow'] || this.oldXml.tagName.toLowerCase() === 'shadow';
|
||||
this.oldJson = json['oldJson'];
|
||||
if (json['recordUndo'] !== undefined) {
|
||||
this.recordUndo = json['recordUndo'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -132,11 +115,16 @@ export class BlockDelete extends BlockBase {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: BlockDeleteJson, workspace: Workspace, event?: any):
|
||||
BlockDelete {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new BlockDelete()) as
|
||||
BlockDelete;
|
||||
static fromJson(
|
||||
json: BlockDeleteJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): BlockDelete {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new BlockDelete()
|
||||
) as BlockDelete;
|
||||
newEvent.oldXml = utilsXml.textToDom(json['oldXml']);
|
||||
newEvent.ids = json['ids'];
|
||||
newEvent.wasShadow =
|
||||
@@ -158,12 +146,14 @@ export class BlockDelete extends BlockBase {
|
||||
if (!this.ids) {
|
||||
throw new Error(
|
||||
'The block IDs are undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.oldJson) {
|
||||
throw new Error(
|
||||
'The old block JSON is undefined. Either pass a block ' +
|
||||
'to the constructor, or call fromJson');
|
||||
'to the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (forward) {
|
||||
for (let i = 0; i < this.ids.length; i++) {
|
||||
@@ -173,7 +163,7 @@ export class BlockDelete extends BlockBase {
|
||||
block.dispose(false);
|
||||
} else if (id === this.blockId) {
|
||||
// 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 {
|
||||
|
||||
@@ -13,14 +13,12 @@ import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.BlockDrag');
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {AbstractEventJson} from './events_abstract.js';
|
||||
import {UiBase} from './events_ui_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners when a block is being manually dragged/dropped.
|
||||
*/
|
||||
@@ -67,12 +65,14 @@ export class BlockDrag extends UiBase {
|
||||
if (this.isStart === undefined) {
|
||||
throw new Error(
|
||||
'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) {
|
||||
throw new Error(
|
||||
'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['blockId'] = this.blockId;
|
||||
@@ -82,21 +82,6 @@ export class BlockDrag extends UiBase {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: BlockDragJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.BlockDrag.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.isStart = json['isStart'];
|
||||
this.blockId = json['blockId'];
|
||||
this.blocks = json['blocks'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -106,10 +91,16 @@ export class BlockDrag extends UiBase {
|
||||
* static methods in superclasses..
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: BlockDragJson, workspace: Workspace, event?: any):
|
||||
BlockDrag {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new BlockDrag()) as BlockDrag;
|
||||
static fromJson(
|
||||
json: BlockDragJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): BlockDrag {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new BlockDrag()
|
||||
) as BlockDrag;
|
||||
newEvent.isStart = json['isStart'];
|
||||
newEvent.blockId = json['blockId'];
|
||||
newEvent.blocks = json['blocks'];
|
||||
|
||||
138
core/events/events_block_field_intermediate_change.ts
Normal file
138
core/events/events_block_field_intermediate_change.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for an event representing an intermediate change to a block's field's
|
||||
* value.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.BlockFieldIntermediateChange');
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {Workspace} from '../workspace.js';
|
||||
|
||||
import {BlockBase, BlockBaseJson} from './events_block_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
|
||||
/**
|
||||
* Notifies listeners when the value of a block's field has changed but the
|
||||
* change is not yet complete, and is expected to be followed by a block change
|
||||
* event.
|
||||
*/
|
||||
export class BlockFieldIntermediateChange extends BlockBase {
|
||||
override type = eventUtils.BLOCK_FIELD_INTERMEDIATE_CHANGE;
|
||||
|
||||
// Intermediate events do not undo or redo. They may be fired frequently while
|
||||
// the field editor widget is open. A separate BLOCK_CHANGE event is fired
|
||||
// when the editor is closed, which combines all of the field value changes
|
||||
// into a single change that is recorded in the undo history instead. The
|
||||
// intermediate changes are important for reacting to immediate changes, but
|
||||
// some event handlers would prefer to handle the less frequent final events,
|
||||
// like when triggering workspace serialization. Technically, this method of
|
||||
// grouping changes can result in undo perfoming actions out of order if some
|
||||
// other event occurs between opening and closing the field editor, but such
|
||||
// events are unlikely to cause a broken state.
|
||||
override recordUndo = false;
|
||||
|
||||
/** The name of the field that changed. */
|
||||
name?: string;
|
||||
|
||||
/** The original value of the element. */
|
||||
oldValue: unknown;
|
||||
|
||||
/** The new value of the element. */
|
||||
newValue: unknown;
|
||||
|
||||
/**
|
||||
* @param opt_block The changed block. Undefined for a blank event.
|
||||
* @param opt_name Name of the field affected.
|
||||
* @param opt_oldValue Previous value of element.
|
||||
* @param opt_newValue New value of element.
|
||||
*/
|
||||
constructor(
|
||||
opt_block?: Block,
|
||||
opt_name?: string,
|
||||
opt_oldValue?: unknown,
|
||||
opt_newValue?: unknown
|
||||
) {
|
||||
super(opt_block);
|
||||
if (!opt_block) {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
|
||||
this.name = opt_name;
|
||||
this.oldValue = opt_oldValue;
|
||||
this.newValue = opt_newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the event as JSON.
|
||||
*
|
||||
* @returns JSON representation.
|
||||
*/
|
||||
override toJson(): BlockFieldIntermediateChangeJson {
|
||||
const json = super.toJson() as BlockFieldIntermediateChangeJson;
|
||||
if (!this.name) {
|
||||
throw new Error(
|
||||
'The changed field name is undefined. Either pass a ' +
|
||||
'name to the constructor, or call fromJson.'
|
||||
);
|
||||
}
|
||||
json['name'] = this.name;
|
||||
json['oldValue'] = this.oldValue;
|
||||
json['newValue'] = this.newValue;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
* @param event The event to append new properties to. Should be a subclass
|
||||
* of BlockFieldIntermediateChange, but we can't specify that due to the
|
||||
* fact that parameters to static methods in subclasses must be supertypes
|
||||
* of parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(
|
||||
json: BlockFieldIntermediateChangeJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): BlockFieldIntermediateChange {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new BlockFieldIntermediateChange()
|
||||
) as BlockFieldIntermediateChange;
|
||||
newEvent.name = json['name'];
|
||||
newEvent.oldValue = json['oldValue'];
|
||||
newEvent.newValue = json['newValue'];
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this event record any change of state?
|
||||
*
|
||||
* @returns False if something changed.
|
||||
*/
|
||||
override isNull(): boolean {
|
||||
return this.oldValue === this.newValue;
|
||||
}
|
||||
}
|
||||
|
||||
export interface BlockFieldIntermediateChangeJson extends BlockBaseJson {
|
||||
name: string;
|
||||
newValue: unknown;
|
||||
oldValue: unknown;
|
||||
}
|
||||
|
||||
registry.register(
|
||||
registry.Type.EVENT,
|
||||
eventUtils.BLOCK_FIELD_INTERMEDIATE_CHANGE,
|
||||
BlockFieldIntermediateChange
|
||||
);
|
||||
@@ -14,7 +14,6 @@ goog.declareModuleId('Blockly.Events.BlockMove');
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import {ConnectionType} from '../connection_type.js';
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
|
||||
@@ -22,7 +21,6 @@ import {BlockBase, BlockBaseJson} from './events_block_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
interface BlockLocation {
|
||||
parentId?: string;
|
||||
inputName?: string;
|
||||
@@ -61,11 +59,25 @@ export class BlockMove extends BlockBase {
|
||||
newInputName?: string;
|
||||
|
||||
/**
|
||||
* The new X and Y workspace coordinates of the block if it is a top level
|
||||
* The new X and Y workspace coordinates of the block if it is a top-level
|
||||
* block. Undefined if it is not a top level block.
|
||||
*/
|
||||
newCoordinate?: Coordinate;
|
||||
|
||||
/**
|
||||
* An explanation of what this move is for. Known values include:
|
||||
* 'drag' -- A drag operation completed.
|
||||
* 'bump' -- Block got bumped away from an invalid connection.
|
||||
* 'snap' -- Block got shifted to line up with the grid.
|
||||
* 'inbounds' -- Block got pushed back into a non-scrolling workspace.
|
||||
* 'connect' -- Block got connected to another block.
|
||||
* 'disconnect' -- Block got disconnected from another block.
|
||||
* 'create' -- Block created via XML.
|
||||
* 'cleanup' -- Workspace aligned top-level blocks.
|
||||
* Event merging may create multiple reasons: ['drag', 'bump', 'snap'].
|
||||
*/
|
||||
reason?: string[];
|
||||
|
||||
/** @param opt_block The moved block. Undefined for a blank event. */
|
||||
constructor(opt_block?: Block) {
|
||||
super(opt_block);
|
||||
@@ -95,48 +107,26 @@ export class BlockMove extends BlockBase {
|
||||
json['oldParentId'] = this.oldParentId;
|
||||
json['oldInputName'] = this.oldInputName;
|
||||
if (this.oldCoordinate) {
|
||||
json['oldCoordinate'] = `${Math.round(this.oldCoordinate.x)}, ` +
|
||||
json['oldCoordinate'] =
|
||||
`${Math.round(this.oldCoordinate.x)}, ` +
|
||||
`${Math.round(this.oldCoordinate.y)}`;
|
||||
}
|
||||
json['newParentId'] = this.newParentId;
|
||||
json['newInputName'] = this.newInputName;
|
||||
if (this.newCoordinate) {
|
||||
json['newCoordinate'] = `${Math.round(this.newCoordinate.x)}, ` +
|
||||
json['newCoordinate'] =
|
||||
`${Math.round(this.newCoordinate.x)}, ` +
|
||||
`${Math.round(this.newCoordinate.y)}`;
|
||||
}
|
||||
if (this.reason) {
|
||||
json['reason'] = this.reason;
|
||||
}
|
||||
if (!this.recordUndo) {
|
||||
json['recordUndo'] = this.recordUndo;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: BlockMoveJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.BlockMove.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.oldParentId = json['oldParentId'];
|
||||
this.oldInputName = json['oldInputName'];
|
||||
if (json['oldCoordinate']) {
|
||||
const xy = json['oldCoordinate'].split(',');
|
||||
this.oldCoordinate = new Coordinate(Number(xy[0]), Number(xy[1]));
|
||||
}
|
||||
this.newParentId = json['newParentId'];
|
||||
this.newInputName = json['newInputName'];
|
||||
if (json['newCoordinate']) {
|
||||
const xy = json['newCoordinate'].split(',');
|
||||
this.newCoordinate = new Coordinate(Number(xy[0]), Number(xy[1]));
|
||||
}
|
||||
if (json['recordUndo'] !== undefined) {
|
||||
this.recordUndo = json['recordUndo'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -146,10 +136,16 @@ export class BlockMove extends BlockBase {
|
||||
* static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: BlockMoveJson, workspace: Workspace, event?: any):
|
||||
BlockMove {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new BlockMove()) as BlockMove;
|
||||
static fromJson(
|
||||
json: BlockMoveJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): BlockMove {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new BlockMove()
|
||||
) as BlockMove;
|
||||
newEvent.oldParentId = json['oldParentId'];
|
||||
newEvent.oldInputName = json['oldInputName'];
|
||||
if (json['oldCoordinate']) {
|
||||
@@ -162,6 +158,9 @@ export class BlockMove extends BlockBase {
|
||||
const xy = json['newCoordinate'].split(',');
|
||||
newEvent.newCoordinate = new Coordinate(Number(xy[0]), Number(xy[1]));
|
||||
}
|
||||
if (json['reason'] !== undefined) {
|
||||
newEvent.reason = json['reason'];
|
||||
}
|
||||
if (json['recordUndo'] !== undefined) {
|
||||
newEvent.recordUndo = json['recordUndo'];
|
||||
}
|
||||
@@ -176,6 +175,15 @@ export class BlockMove extends BlockBase {
|
||||
this.newCoordinate = location.coordinate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reason for a move event.
|
||||
*
|
||||
* @param reason Why is this move happening? 'drag', 'bump', 'snap', ...
|
||||
*/
|
||||
setReason(reason: string[]) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parentId and input if the block is connected,
|
||||
* or the XY location if disconnected.
|
||||
@@ -187,13 +195,14 @@ export class BlockMove extends BlockBase {
|
||||
if (!this.blockId) {
|
||||
throw new Error(
|
||||
'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);
|
||||
if (!block) {
|
||||
throw new Error(
|
||||
'The block associated with the block move event ' +
|
||||
'could not be found');
|
||||
'The block associated with the block move event ' + 'could not be found'
|
||||
);
|
||||
}
|
||||
const location = {} as BlockLocation;
|
||||
const parent = block.getParent();
|
||||
@@ -215,9 +224,11 @@ export class BlockMove extends BlockBase {
|
||||
* @returns False if something changed.
|
||||
*/
|
||||
override isNull(): boolean {
|
||||
return this.oldParentId === this.newParentId &&
|
||||
return (
|
||||
this.oldParentId === this.newParentId &&
|
||||
this.oldInputName === this.newInputName &&
|
||||
Coordinate.equals(this.oldCoordinate, this.newCoordinate);
|
||||
Coordinate.equals(this.oldCoordinate, this.newCoordinate)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,11 +241,12 @@ export class BlockMove extends BlockBase {
|
||||
if (!this.blockId) {
|
||||
throw new Error(
|
||||
'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);
|
||||
if (!block) {
|
||||
console.warn('Can\'t move non-existent block: ' + this.blockId);
|
||||
console.warn("Can't move non-existent block: " + this.blockId);
|
||||
return;
|
||||
}
|
||||
const parentId = forward ? this.newParentId : this.oldParentId;
|
||||
@@ -244,7 +256,7 @@ export class BlockMove extends BlockBase {
|
||||
if (parentId) {
|
||||
parentBlock = workspace.getBlockById(parentId);
|
||||
if (!parentBlock) {
|
||||
console.warn('Can\'t connect to non-existent block: ' + parentId);
|
||||
console.warn("Can't connect to non-existent block: " + parentId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -253,11 +265,13 @@ export class BlockMove extends BlockBase {
|
||||
}
|
||||
if (coordinate) {
|
||||
const xy = block.getRelativeToSurfaceXY();
|
||||
block.moveBy(coordinate.x - xy.x, coordinate.y - xy.y);
|
||||
block.moveBy(coordinate.x - xy.x, coordinate.y - xy.y, this.reason);
|
||||
} else {
|
||||
let blockConnection = block.outputConnection;
|
||||
if (!blockConnection ||
|
||||
block.previousConnection && block.previousConnection.isConnected()) {
|
||||
if (
|
||||
!blockConnection ||
|
||||
(block.previousConnection && block.previousConnection.isConnected())
|
||||
) {
|
||||
blockConnection = block.previousConnection;
|
||||
}
|
||||
let parentConnection;
|
||||
@@ -273,7 +287,7 @@ export class BlockMove extends BlockBase {
|
||||
if (parentConnection && blockConnection) {
|
||||
blockConnection.connect(parentConnection);
|
||||
} else {
|
||||
console.warn('Can\'t connect to non-existent input: ' + inputName);
|
||||
console.warn("Can't connect to non-existent input: " + inputName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,6 +300,7 @@ export interface BlockMoveJson extends BlockBaseJson {
|
||||
newParentId?: string;
|
||||
newInputName?: string;
|
||||
newCoordinate?: string;
|
||||
reason?: string[];
|
||||
recordUndo?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,11 @@ goog.declareModuleId('Blockly.Events.BubbleOpen');
|
||||
|
||||
import type {AbstractEventJson} from './events_abstract.js';
|
||||
import type {BlockSvg} from '../block_svg.js';
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {UiBase} from './events_ui_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for a bubble open event.
|
||||
*/
|
||||
@@ -44,7 +42,10 @@ export class BubbleOpen extends UiBase {
|
||||
* 'warning'. Undefined for a blank event.
|
||||
*/
|
||||
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;
|
||||
super(workspaceId);
|
||||
if (!opt_block) return;
|
||||
@@ -64,12 +65,14 @@ export class BubbleOpen extends UiBase {
|
||||
if (this.isOpen === undefined) {
|
||||
throw new Error(
|
||||
'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) {
|
||||
throw new Error(
|
||||
'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['bubbleType'] = this.bubbleType;
|
||||
@@ -77,21 +80,6 @@ export class BubbleOpen extends UiBase {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: BubbleOpenJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.BubbleOpen.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.isOpen = json['isOpen'];
|
||||
this.bubbleType = json['bubbleType'];
|
||||
this.blockId = json['blockId'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -101,11 +89,16 @@ export class BubbleOpen extends UiBase {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: BubbleOpenJson, workspace: Workspace, event?: any):
|
||||
BubbleOpen {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new BubbleOpen()) as
|
||||
BubbleOpen;
|
||||
static fromJson(
|
||||
json: BubbleOpenJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): BubbleOpen {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new BubbleOpen()
|
||||
) as BubbleOpen;
|
||||
newEvent.isOpen = json['isOpen'];
|
||||
newEvent.bubbleType = json['bubbleType'];
|
||||
newEvent.blockId = json['blockId'];
|
||||
|
||||
@@ -13,7 +13,6 @@ import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.Click');
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {AbstractEventJson} from './events_abstract.js';
|
||||
|
||||
@@ -21,7 +20,6 @@ import {UiBase} from './events_ui_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that ome blockly element was clicked.
|
||||
*/
|
||||
@@ -46,8 +44,10 @@ export class Click extends UiBase {
|
||||
* Undefined for a blank event.
|
||||
*/
|
||||
constructor(
|
||||
opt_block?: Block|null, opt_workspaceId?: string|null,
|
||||
opt_targetType?: ClickTarget) {
|
||||
opt_block?: Block | null,
|
||||
opt_workspaceId?: string | null,
|
||||
opt_targetType?: ClickTarget
|
||||
) {
|
||||
let workspaceId = opt_block ? opt_block.workspace.id : opt_workspaceId;
|
||||
if (workspaceId === null) {
|
||||
workspaceId = undefined;
|
||||
@@ -68,27 +68,14 @@ export class Click extends UiBase {
|
||||
if (!this.targetType) {
|
||||
throw new Error(
|
||||
'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['blockId'] = this.blockId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: ClickJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.Click.prototype.fromJson', 'version 9', 'version 10',
|
||||
'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.targetType = json['targetType'];
|
||||
this.blockId = json['blockId'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -99,8 +86,11 @@ export class Click extends UiBase {
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: ClickJson, workspace: Workspace, event?: any): Click {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new Click()) as Click;
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new Click()
|
||||
) as Click;
|
||||
newEvent.targetType = json['targetType'];
|
||||
newEvent.blockId = json['blockId'];
|
||||
return newEvent;
|
||||
|
||||
@@ -12,18 +12,19 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.CommentBase');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as utilsXml from '../utils/xml.js';
|
||||
import type {WorkspaceComment} from '../workspace_comment.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 {CommentDelete} from './events_comment_delete.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Abstract class for a comment event.
|
||||
*/
|
||||
@@ -60,25 +61,13 @@ export class CommentBase extends AbstractEvent {
|
||||
if (!this.commentId) {
|
||||
throw new Error(
|
||||
'The comment ID is undefined. Either pass a comment to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
json['commentId'] = this.commentId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: CommentBaseJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.CommentBase.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.commentId = json['commentId'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -88,11 +77,16 @@ export class CommentBase extends AbstractEvent {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: CommentBaseJson, workspace: Workspace, event?: any):
|
||||
CommentBase {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new CommentBase()) as
|
||||
CommentBase;
|
||||
static fromJson(
|
||||
json: CommentBaseJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): CommentBase {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new CommentBase()
|
||||
) as CommentBase;
|
||||
newEvent.commentId = json['commentId'];
|
||||
return newEvent;
|
||||
}
|
||||
@@ -104,7 +98,9 @@ export class CommentBase extends AbstractEvent {
|
||||
* @param create if True then Create, if False then Delete
|
||||
*/
|
||||
static CommentCreateDeleteHelper(
|
||||
event: CommentCreate|CommentDelete, create: boolean) {
|
||||
event: CommentCreate | CommentDelete,
|
||||
create: boolean
|
||||
) {
|
||||
const workspace = event.getEventWorkspace_();
|
||||
if (create) {
|
||||
const xmlElement = utilsXml.createElement('xml');
|
||||
@@ -117,15 +113,15 @@ export class CommentBase extends AbstractEvent {
|
||||
if (!event.commentId) {
|
||||
throw new Error(
|
||||
'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);
|
||||
if (comment) {
|
||||
comment.dispose();
|
||||
} else {
|
||||
// Only complain about root-level block.
|
||||
console.warn(
|
||||
'Can\'t uncreate non-existent comment: ' + event.commentId);
|
||||
console.warn("Can't uncreate non-existent comment: " + event.commentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.CommentChange');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import type {WorkspaceComment} from '../workspace_comment.js';
|
||||
|
||||
@@ -20,7 +19,6 @@ import {CommentBase, CommentBaseJson} from './events_comment_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that the contents of a workspace comment has changed.
|
||||
*/
|
||||
@@ -41,8 +39,10 @@ export class CommentChange extends CommentBase {
|
||||
* @param opt_newContents New contents of the comment.
|
||||
*/
|
||||
constructor(
|
||||
opt_comment?: WorkspaceComment, opt_oldContents?: string,
|
||||
opt_newContents?: string) {
|
||||
opt_comment?: WorkspaceComment,
|
||||
opt_oldContents?: string,
|
||||
opt_newContents?: string
|
||||
) {
|
||||
super(opt_comment);
|
||||
|
||||
if (!opt_comment) {
|
||||
@@ -65,32 +65,20 @@ export class CommentChange extends CommentBase {
|
||||
if (!this.oldContents_) {
|
||||
throw new Error(
|
||||
'The old contents is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.newContents_) {
|
||||
throw new Error(
|
||||
'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['newContents'] = this.newContents_;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: CommentChangeJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.CommentChange.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.oldContents_ = json['oldContents'];
|
||||
this.newContents_ = json['newContents'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -100,11 +88,16 @@ export class CommentChange extends CommentBase {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: CommentChangeJson, workspace: Workspace, event?: any):
|
||||
CommentChange {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new CommentChange()) as
|
||||
CommentChange;
|
||||
static fromJson(
|
||||
json: CommentChangeJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): CommentChange {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new CommentChange()
|
||||
) as CommentChange;
|
||||
newEvent.oldContents_ = json['oldContents'];
|
||||
newEvent.newContents_ = json['newContents'];
|
||||
return newEvent;
|
||||
@@ -129,11 +122,12 @@ export class CommentChange extends CommentBase {
|
||||
if (!this.commentId) {
|
||||
throw new Error(
|
||||
'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);
|
||||
if (!comment) {
|
||||
console.warn('Can\'t change non-existent comment: ' + this.commentId);
|
||||
console.warn("Can't change non-existent comment: " + this.commentId);
|
||||
return;
|
||||
}
|
||||
const contents = forward ? this.newContents_ : this.oldContents_;
|
||||
@@ -141,11 +135,13 @@ export class CommentChange extends CommentBase {
|
||||
if (forward) {
|
||||
throw new Error(
|
||||
'The new contents is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
'The old contents is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
comment.setContent(contents);
|
||||
}
|
||||
@@ -157,4 +153,7 @@ export interface CommentChangeJson extends CommentBaseJson {
|
||||
}
|
||||
|
||||
registry.register(
|
||||
registry.Type.EVENT, eventUtils.COMMENT_CHANGE, CommentChange);
|
||||
registry.Type.EVENT,
|
||||
eventUtils.COMMENT_CHANGE,
|
||||
CommentChange
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.CommentCreate');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import type {WorkspaceComment} from '../workspace_comment.js';
|
||||
import * as utilsXml from '../utils/xml.js';
|
||||
@@ -22,7 +21,6 @@ import {CommentBase, CommentBaseJson} from './events_comment_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that a workspace comment was created.
|
||||
*/
|
||||
@@ -57,25 +55,13 @@ export class CommentCreate extends CommentBase {
|
||||
if (!this.xml) {
|
||||
throw new Error(
|
||||
'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);
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: CommentCreateJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.CommentCreate.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.xml = utilsXml.textToDom(json['xml']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -85,11 +71,16 @@ export class CommentCreate extends CommentBase {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: CommentCreateJson, workspace: Workspace, event?: any):
|
||||
CommentCreate {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new CommentCreate()) as
|
||||
CommentCreate;
|
||||
static fromJson(
|
||||
json: CommentCreateJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): CommentCreate {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new CommentCreate()
|
||||
) as CommentCreate;
|
||||
newEvent.xml = utilsXml.textToDom(json['xml']);
|
||||
return newEvent;
|
||||
}
|
||||
@@ -109,4 +100,7 @@ export interface CommentCreateJson extends CommentBaseJson {
|
||||
}
|
||||
|
||||
registry.register(
|
||||
registry.Type.EVENT, eventUtils.COMMENT_CREATE, CommentCreate);
|
||||
registry.Type.EVENT,
|
||||
eventUtils.COMMENT_CREATE,
|
||||
CommentCreate
|
||||
);
|
||||
|
||||
@@ -21,7 +21,6 @@ import * as utilsXml from '../utils/xml.js';
|
||||
import * as Xml from '../xml.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that a workspace comment has been deleted.
|
||||
*/
|
||||
@@ -64,7 +63,8 @@ export class CommentDelete extends CommentBase {
|
||||
if (!this.xml) {
|
||||
throw new Error(
|
||||
'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);
|
||||
return json;
|
||||
@@ -79,11 +79,16 @@ export class CommentDelete extends CommentBase {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: CommentDeleteJson, workspace: Workspace, event?: any):
|
||||
CommentDelete {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new CommentDelete()) as
|
||||
CommentDelete;
|
||||
static fromJson(
|
||||
json: CommentDeleteJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): CommentDelete {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new CommentDelete()
|
||||
) as CommentDelete;
|
||||
newEvent.xml = utilsXml.textToDom(json['xml']);
|
||||
return newEvent;
|
||||
}
|
||||
@@ -94,4 +99,7 @@ export interface CommentDeleteJson extends CommentBaseJson {
|
||||
}
|
||||
|
||||
registry.register(
|
||||
registry.Type.EVENT, eventUtils.COMMENT_DELETE, CommentDelete);
|
||||
registry.Type.EVENT,
|
||||
eventUtils.COMMENT_DELETE,
|
||||
CommentDelete
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.CommentMove');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import type {WorkspaceComment} from '../workspace_comment.js';
|
||||
@@ -21,7 +20,6 @@ import {CommentBase, CommentBaseJson} from './events_comment_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that a workspace comment has moved.
|
||||
*/
|
||||
@@ -61,12 +59,14 @@ export class CommentMove extends CommentBase {
|
||||
if (this.newCoordinate_) {
|
||||
throw Error(
|
||||
'Tried to record the new position of a comment on the ' +
|
||||
'same event twice.');
|
||||
'same event twice.'
|
||||
);
|
||||
}
|
||||
if (!this.comment_) {
|
||||
throw new Error(
|
||||
'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();
|
||||
}
|
||||
@@ -92,36 +92,25 @@ export class CommentMove extends CommentBase {
|
||||
if (!this.oldCoordinate_) {
|
||||
throw new Error(
|
||||
'The old comment position is undefined. Either pass a comment to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.newCoordinate_) {
|
||||
throw new Error(
|
||||
'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_.x)}, ` +
|
||||
`${Math.round(this.oldCoordinate_.y)}`;
|
||||
json['newCoordinate'] = Math.round(this.newCoordinate_.x) + ',' +
|
||||
json['newCoordinate'] =
|
||||
Math.round(this.newCoordinate_.x) +
|
||||
',' +
|
||||
Math.round(this.newCoordinate_.y);
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: CommentMoveJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.CommentMove.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
let xy = json['oldCoordinate'].split(',');
|
||||
this.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
|
||||
xy = json['newCoordinate'].split(',');
|
||||
this.newCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -131,11 +120,16 @@ export class CommentMove extends CommentBase {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: CommentMoveJson, workspace: Workspace, event?: any):
|
||||
CommentMove {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new CommentMove()) as
|
||||
CommentMove;
|
||||
static fromJson(
|
||||
json: CommentMoveJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): CommentMove {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new CommentMove()
|
||||
) as CommentMove;
|
||||
let xy = json['oldCoordinate'].split(',');
|
||||
newEvent.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
|
||||
xy = json['newCoordinate'].split(',');
|
||||
@@ -162,11 +156,12 @@ export class CommentMove extends CommentBase {
|
||||
if (!this.commentId) {
|
||||
throw new Error(
|
||||
'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);
|
||||
if (!comment) {
|
||||
console.warn('Can\'t move non-existent comment: ' + this.commentId);
|
||||
console.warn("Can't move non-existent comment: " + this.commentId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -175,7 +170,8 @@ export class CommentMove extends CommentBase {
|
||||
throw new Error(
|
||||
'Either oldCoordinate_ or newCoordinate_ is undefined. ' +
|
||||
'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.
|
||||
const current = comment.getRelativeToSurfaceXY();
|
||||
|
||||
@@ -14,7 +14,6 @@ goog.declareModuleId('Blockly.Events.MarkerMove');
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import {ASTNode} from '../keyboard_nav/ast_node.js';
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
import {AbstractEventJson} from './events_abstract.js';
|
||||
@@ -22,7 +21,6 @@ import {AbstractEventJson} from './events_abstract.js';
|
||||
import {UiBase} from './events_ui_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that a marker (used for keyboard navigation) has
|
||||
* moved.
|
||||
@@ -57,8 +55,11 @@ export class MarkerMove extends UiBase {
|
||||
* Undefined for a blank event.
|
||||
*/
|
||||
constructor(
|
||||
opt_block?: Block|null, isCursor?: boolean, opt_oldNode?: ASTNode|null,
|
||||
opt_newNode?: ASTNode) {
|
||||
opt_block?: Block | null,
|
||||
isCursor?: boolean,
|
||||
opt_oldNode?: ASTNode | null,
|
||||
opt_newNode?: ASTNode
|
||||
) {
|
||||
let workspaceId = opt_block ? opt_block.workspace.id : undefined;
|
||||
if (opt_newNode && opt_newNode.getType() === ASTNode.types.WORKSPACE) {
|
||||
workspaceId = (opt_newNode.getLocation() as Workspace).id;
|
||||
@@ -81,12 +82,14 @@ export class MarkerMove extends UiBase {
|
||||
if (this.isCursor === undefined) {
|
||||
throw new Error(
|
||||
'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) {
|
||||
throw new Error(
|
||||
'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['blockId'] = this.blockId;
|
||||
@@ -95,22 +98,6 @@ export class MarkerMove extends UiBase {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: MarkerMoveJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.MarkerMove.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.isCursor = json['isCursor'];
|
||||
this.blockId = json['blockId'];
|
||||
this.oldNode = json['oldNode'];
|
||||
this.newNode = json['newNode'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -120,11 +107,16 @@ export class MarkerMove extends UiBase {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: MarkerMoveJson, workspace: Workspace, event?: any):
|
||||
MarkerMove {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new MarkerMove()) as
|
||||
MarkerMove;
|
||||
static fromJson(
|
||||
json: MarkerMoveJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): MarkerMove {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new MarkerMove()
|
||||
) as MarkerMove;
|
||||
newEvent.isCursor = json['isCursor'];
|
||||
newEvent.blockId = json['blockId'];
|
||||
newEvent.oldNode = json['oldNode'];
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.Selected');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {AbstractEventJson} from './events_abstract.js';
|
||||
|
||||
@@ -20,7 +19,6 @@ import {UiBase} from './events_ui_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for a selected event.
|
||||
* Notifies listeners that a new element has been selected.
|
||||
@@ -46,8 +44,10 @@ export class Selected extends UiBase {
|
||||
* Null if no element previously selected. Undefined for a blank event.
|
||||
*/
|
||||
constructor(
|
||||
opt_oldElementId?: string|null, opt_newElementId?: string|null,
|
||||
opt_workspaceId?: string) {
|
||||
opt_oldElementId?: string | null,
|
||||
opt_newElementId?: string | null,
|
||||
opt_workspaceId?: string
|
||||
) {
|
||||
super(opt_workspaceId);
|
||||
|
||||
this.oldElementId = opt_oldElementId ?? undefined;
|
||||
@@ -66,20 +66,6 @@ export class Selected extends UiBase {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: SelectedJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.Selected.prototype.fromJson', 'version 9', 'version 10',
|
||||
'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.oldElementId = json['oldElementId'];
|
||||
this.newElementId = json['newElementId'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -89,10 +75,16 @@ export class Selected extends UiBase {
|
||||
* static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: SelectedJson, workspace: Workspace, event?: any):
|
||||
Selected {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new Selected()) as Selected;
|
||||
static fromJson(
|
||||
json: SelectedJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): Selected {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new Selected()
|
||||
) as Selected;
|
||||
newEvent.oldElementId = json['oldElementId'];
|
||||
newEvent.newElementId = json['newElementId'];
|
||||
return newEvent;
|
||||
|
||||
@@ -12,14 +12,12 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.ThemeChange');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {AbstractEventJson} from './events_abstract.js';
|
||||
import {UiBase} from './events_ui_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that the workspace theme has changed.
|
||||
*/
|
||||
@@ -49,25 +47,13 @@ export class ThemeChange extends UiBase {
|
||||
if (!this.themeName) {
|
||||
throw new Error(
|
||||
'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;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: ThemeChangeJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.ThemeChange.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.themeName = json['themeName'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -77,11 +63,16 @@ export class ThemeChange extends UiBase {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: ThemeChangeJson, workspace: Workspace, event?: any):
|
||||
ThemeChange {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new ThemeChange()) as
|
||||
ThemeChange;
|
||||
static fromJson(
|
||||
json: ThemeChangeJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): ThemeChange {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new ThemeChange()
|
||||
) as ThemeChange;
|
||||
newEvent.themeName = json['themeName'];
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
@@ -12,14 +12,12 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.ToolboxItemSelect');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {AbstractEventJson} from './events_abstract.js';
|
||||
import {UiBase} from './events_ui_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that a toolbox item has been selected.
|
||||
*/
|
||||
@@ -41,8 +39,10 @@ export class ToolboxItemSelect extends UiBase {
|
||||
* Undefined for a blank event.
|
||||
*/
|
||||
constructor(
|
||||
opt_oldItem?: string|null, opt_newItem?: string|null,
|
||||
opt_workspaceId?: string) {
|
||||
opt_oldItem?: string | null,
|
||||
opt_newItem?: string | null,
|
||||
opt_workspaceId?: string
|
||||
) {
|
||||
super(opt_workspaceId);
|
||||
this.oldItem = opt_oldItem ?? undefined;
|
||||
this.newItem = opt_newItem ?? undefined;
|
||||
@@ -60,20 +60,6 @@ export class ToolboxItemSelect extends UiBase {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: ToolboxItemSelectJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.ToolboxItemSelect.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.oldItem = json['oldItem'];
|
||||
this.newItem = json['newItem'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -84,11 +70,15 @@ export class ToolboxItemSelect extends UiBase {
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(
|
||||
json: ToolboxItemSelectJson, workspace: Workspace,
|
||||
event?: any): ToolboxItemSelect {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new ToolboxItemSelect()) as
|
||||
ToolboxItemSelect;
|
||||
json: ToolboxItemSelectJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): ToolboxItemSelect {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new ToolboxItemSelect()
|
||||
) as ToolboxItemSelect;
|
||||
newEvent.oldItem = json['oldItem'];
|
||||
newEvent.newItem = json['newItem'];
|
||||
return newEvent;
|
||||
@@ -101,4 +91,7 @@ export interface ToolboxItemSelectJson extends AbstractEventJson {
|
||||
}
|
||||
|
||||
registry.register(
|
||||
registry.Type.EVENT, eventUtils.TOOLBOX_ITEM_SELECT, ToolboxItemSelect);
|
||||
registry.Type.EVENT,
|
||||
eventUtils.TOOLBOX_ITEM_SELECT,
|
||||
ToolboxItemSelect
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.TrashcanOpen');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {AbstractEventJson} from './events_abstract.js';
|
||||
|
||||
@@ -20,7 +19,6 @@ import {UiBase} from './events_ui_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners when the trashcan is opening or closing.
|
||||
*/
|
||||
@@ -53,25 +51,13 @@ export class TrashcanOpen extends UiBase {
|
||||
if (this.isOpen === undefined) {
|
||||
throw new Error(
|
||||
'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;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: TrashcanOpenJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.TrashcanOpen.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.isOpen = json['isOpen'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -81,11 +67,16 @@ export class TrashcanOpen extends UiBase {
|
||||
* parameters to static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: TrashcanOpenJson, workspace: Workspace, event?: any):
|
||||
TrashcanOpen {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new TrashcanOpen()) as
|
||||
TrashcanOpen;
|
||||
static fromJson(
|
||||
json: TrashcanOpenJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): TrashcanOpen {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new TrashcanOpen()
|
||||
) as TrashcanOpen;
|
||||
newEvent.isOpen = json['isOpen'];
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2018 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* (Deprecated) Events fired as a result of UI actions in
|
||||
* Blockly's editor.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.Ui');
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {UiBase} from './events_ui_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for a UI event.
|
||||
*
|
||||
* @deprecated December 2020. Instead use a more specific UI event.
|
||||
*/
|
||||
export class Ui extends UiBase {
|
||||
blockId: AnyDuringMigration;
|
||||
element: AnyDuringMigration;
|
||||
oldValue: AnyDuringMigration;
|
||||
newValue: AnyDuringMigration;
|
||||
override type = eventUtils.UI;
|
||||
|
||||
/**
|
||||
* @param opt_block The affected block. Null for UI events that do not have
|
||||
* an associated block. Undefined for a blank event.
|
||||
* @param opt_element One of 'selected', 'comment', 'mutatorOpen', etc.
|
||||
* @param opt_oldValue Previous value of element.
|
||||
* @param opt_newValue New value of element.
|
||||
*/
|
||||
constructor(
|
||||
opt_block?: Block|null, opt_element?: string,
|
||||
opt_oldValue?: AnyDuringMigration, opt_newValue?: AnyDuringMigration) {
|
||||
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
|
||||
super(workspaceId);
|
||||
|
||||
this.blockId = opt_block ? opt_block.id : null;
|
||||
this.element = typeof opt_element === 'undefined' ? '' : opt_element;
|
||||
this.oldValue = typeof opt_oldValue === 'undefined' ? '' : opt_oldValue;
|
||||
this.newValue = typeof opt_newValue === 'undefined' ? '' : opt_newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the event as JSON.
|
||||
*
|
||||
* @returns JSON representation.
|
||||
*/
|
||||
override toJson(): AnyDuringMigration {
|
||||
const json = super.toJson() as AnyDuringMigration;
|
||||
json['element'] = this.element;
|
||||
if (this.newValue !== undefined) {
|
||||
json['newValue'] = this.newValue;
|
||||
}
|
||||
if (this.blockId) {
|
||||
json['blockId'] = this.blockId;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: AnyDuringMigration) {
|
||||
super.fromJson(json);
|
||||
this.element = json['element'];
|
||||
this.newValue = json['newValue'];
|
||||
this.blockId = json['blockId'];
|
||||
}
|
||||
}
|
||||
|
||||
registry.register(registry.Type.EVENT, eventUtils.UI, Ui);
|
||||
@@ -15,7 +15,6 @@ goog.declareModuleId('Blockly.Events.UiBase');
|
||||
|
||||
import {Abstract as AbstractEvent} from './events_abstract.js';
|
||||
|
||||
|
||||
/**
|
||||
* Base class for a UI event.
|
||||
* UI events are events that don't need to be sent over the wire for multi-user
|
||||
|
||||
@@ -12,13 +12,14 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.VarBase');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.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';
|
||||
|
||||
|
||||
/**
|
||||
* Abstract class for a variable event.
|
||||
*/
|
||||
@@ -50,25 +51,13 @@ export class VarBase extends AbstractEvent {
|
||||
if (!this.varId) {
|
||||
throw new Error(
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
json['varId'] = this.varId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: VarBaseJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.VarBase.prototype.fromJson', 'version 9', 'version 10',
|
||||
'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.varId = json['varId'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -78,10 +67,16 @@ export class VarBase extends AbstractEvent {
|
||||
* static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: VarBaseJson, workspace: Workspace, event?: any):
|
||||
VarBase {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new VarBase()) as VarBase;
|
||||
static fromJson(
|
||||
json: VarBaseJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): VarBase {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new VarBase()
|
||||
) as VarBase;
|
||||
newEvent.varId = json['varId'];
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.VarCreate');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import type {VariableModel} from '../variable_model.js';
|
||||
|
||||
@@ -20,7 +19,6 @@ import {VarBase, VarBaseJson} from './events_var_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that a variable model has been created.
|
||||
*/
|
||||
@@ -56,32 +54,20 @@ export class VarCreate extends VarBase {
|
||||
if (this.varType === undefined) {
|
||||
throw new Error(
|
||||
'The var type is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.varName) {
|
||||
throw new Error(
|
||||
'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['varName'] = this.varName;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: VarCreateJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.VarCreate.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.varType = json['varType'];
|
||||
this.varName = json['varName'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -91,10 +77,16 @@ export class VarCreate extends VarBase {
|
||||
* static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: VarCreateJson, workspace: Workspace, event?: any):
|
||||
VarCreate {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new VarCreate()) as VarCreate;
|
||||
static fromJson(
|
||||
json: VarCreateJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): VarCreate {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new VarCreate()
|
||||
) as VarCreate;
|
||||
newEvent.varType = json['varType'];
|
||||
newEvent.varName = json['varName'];
|
||||
return newEvent;
|
||||
@@ -110,12 +102,14 @@ export class VarCreate extends VarBase {
|
||||
if (!this.varId) {
|
||||
throw new Error(
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.varName) {
|
||||
throw new Error(
|
||||
'The var name is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (forward) {
|
||||
workspace.createVariable(this.varName, this.varType, this.varId);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.VarDelete');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import type {VariableModel} from '../variable_model.js';
|
||||
|
||||
@@ -15,7 +14,6 @@ import {VarBase, VarBaseJson} from './events_var_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that a variable model has been deleted.
|
||||
*
|
||||
@@ -51,32 +49,20 @@ export class VarDelete extends VarBase {
|
||||
if (this.varType === undefined) {
|
||||
throw new Error(
|
||||
'The var type is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.varName) {
|
||||
throw new Error(
|
||||
'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['varName'] = this.varName;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: VarDeleteJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.VarDelete.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.varType = json['varType'];
|
||||
this.varName = json['varName'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -86,10 +72,16 @@ export class VarDelete extends VarBase {
|
||||
* static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: VarDeleteJson, workspace: Workspace, event?: any):
|
||||
VarDelete {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new VarDelete()) as VarDelete;
|
||||
static fromJson(
|
||||
json: VarDeleteJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): VarDelete {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new VarDelete()
|
||||
) as VarDelete;
|
||||
newEvent.varType = json['varType'];
|
||||
newEvent.varName = json['varName'];
|
||||
return newEvent;
|
||||
@@ -105,12 +97,14 @@ export class VarDelete extends VarBase {
|
||||
if (!this.varId) {
|
||||
throw new Error(
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.varName) {
|
||||
throw new Error(
|
||||
'The var name is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (forward) {
|
||||
workspace.deleteVariableById(this.varId);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.VarRename');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import type {VariableModel} from '../variable_model.js';
|
||||
|
||||
@@ -15,7 +14,6 @@ import {VarBase, VarBaseJson} from './events_var_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that a variable model was renamed.
|
||||
*
|
||||
@@ -54,32 +52,20 @@ export class VarRename extends VarBase {
|
||||
if (!this.oldName) {
|
||||
throw new Error(
|
||||
'The old var name is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.newName) {
|
||||
throw new Error(
|
||||
'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['newName'] = this.newName;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: VarRenameJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.VarRename.prototype.fromJson', 'version 9',
|
||||
'version 10', 'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.oldName = json['oldName'];
|
||||
this.newName = json['newName'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -89,10 +75,16 @@ export class VarRename extends VarBase {
|
||||
* static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: VarRenameJson, workspace: Workspace, event?: any):
|
||||
VarRename {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new VarRename()) as VarRename;
|
||||
static fromJson(
|
||||
json: VarRenameJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): VarRename {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new VarRename()
|
||||
) as VarRename;
|
||||
newEvent.oldName = json['oldName'];
|
||||
newEvent.newName = json['newName'];
|
||||
return newEvent;
|
||||
@@ -108,17 +100,20 @@ export class VarRename extends VarBase {
|
||||
if (!this.varId) {
|
||||
throw new Error(
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.oldName) {
|
||||
throw new Error(
|
||||
'The old var name is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.newName) {
|
||||
throw new Error(
|
||||
'The new var name is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (forward) {
|
||||
workspace.renameVariableById(this.varId, this.newName);
|
||||
|
||||
@@ -12,14 +12,12 @@
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.Events.ViewportChange');
|
||||
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {AbstractEventJson} from './events_abstract.js';
|
||||
import {UiBase} from './events_ui_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners that the workspace surface's position or scale has
|
||||
* changed.
|
||||
@@ -59,8 +57,12 @@ export class ViewportChange extends UiBase {
|
||||
* event.
|
||||
*/
|
||||
constructor(
|
||||
opt_top?: number, opt_left?: number, opt_scale?: number,
|
||||
opt_workspaceId?: string, opt_oldScale?: number) {
|
||||
opt_top?: number,
|
||||
opt_left?: number,
|
||||
opt_scale?: number,
|
||||
opt_workspaceId?: string,
|
||||
opt_oldScale?: number
|
||||
) {
|
||||
super(opt_workspaceId);
|
||||
|
||||
this.viewTop = opt_top;
|
||||
@@ -79,22 +81,26 @@ export class ViewportChange extends UiBase {
|
||||
if (this.viewTop === undefined) {
|
||||
throw new Error(
|
||||
'The view top is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (this.viewLeft === undefined) {
|
||||
throw new Error(
|
||||
'The view left is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (this.scale === undefined) {
|
||||
throw new Error(
|
||||
'The scale is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (this.oldScale === undefined) {
|
||||
throw new Error(
|
||||
'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['viewLeft'] = this.viewLeft;
|
||||
@@ -103,22 +109,6 @@ export class ViewportChange extends UiBase {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: ViewportChangeJson) {
|
||||
deprecation.warn(
|
||||
'Blockly.Events.Viewport.prototype.fromJson', 'version 9', 'version 10',
|
||||
'Blockly.Events.fromJson');
|
||||
super.fromJson(json);
|
||||
this.viewTop = json['viewTop'];
|
||||
this.viewLeft = json['viewLeft'];
|
||||
this.scale = json['scale'];
|
||||
this.oldScale = json['oldScale'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
@@ -128,11 +118,16 @@ export class ViewportChange extends UiBase {
|
||||
* static methods in superclasses.
|
||||
* @internal
|
||||
*/
|
||||
static fromJson(json: ViewportChangeJson, workspace: Workspace, event?: any):
|
||||
ViewportChange {
|
||||
const newEvent =
|
||||
super.fromJson(json, workspace, event ?? new ViewportChange()) as
|
||||
ViewportChange;
|
||||
static fromJson(
|
||||
json: ViewportChangeJson,
|
||||
workspace: Workspace,
|
||||
event?: any
|
||||
): ViewportChange {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new ViewportChange()
|
||||
) as ViewportChange;
|
||||
newEvent.viewTop = json['viewTop'];
|
||||
newEvent.viewLeft = json['viewLeft'];
|
||||
newEvent.scale = json['scale'];
|
||||
@@ -149,4 +144,7 @@ export interface ViewportChangeJson extends AbstractEventJson {
|
||||
}
|
||||
|
||||
registry.register(
|
||||
registry.Type.EVENT, eventUtils.VIEWPORT_CHANGE, ViewportChange);
|
||||
registry.Type.EVENT,
|
||||
eventUtils.VIEWPORT_CHANGE,
|
||||
ViewportChange
|
||||
);
|
||||
|
||||
@@ -22,7 +22,6 @@ import type {CommentCreate} from './events_comment_create.js';
|
||||
import type {CommentMove} from './events_comment_move.js';
|
||||
import type {ViewportChange} from './events_viewport.js';
|
||||
|
||||
|
||||
/** Group ID for new events. Grouped events are indivisible. */
|
||||
let group = '';
|
||||
|
||||
@@ -80,6 +79,13 @@ export const CHANGE = 'change';
|
||||
*/
|
||||
export const BLOCK_CHANGE = CHANGE;
|
||||
|
||||
/**
|
||||
* Name of event representing an in-progress change to a field of a block, which
|
||||
* is expected to be followed by a block change event.
|
||||
*/
|
||||
export const BLOCK_FIELD_INTERMEDIATE_CHANGE =
|
||||
'block_field_intermediate_change';
|
||||
|
||||
/**
|
||||
* Name of event that moves a block. Will be deprecated for BLOCK_MOVE.
|
||||
*/
|
||||
@@ -196,8 +202,12 @@ export type BumpEvent = BlockCreate|BlockMove|CommentCreate|CommentMove;
|
||||
* Not to be confused with bumping so that disconnected connections do not
|
||||
* appear connected.
|
||||
*/
|
||||
export const BUMP_EVENTS: string[] =
|
||||
[BLOCK_CREATE, BLOCK_MOVE, COMMENT_CREATE, COMMENT_MOVE];
|
||||
export const BUMP_EVENTS: string[] = [
|
||||
BLOCK_CREATE,
|
||||
BLOCK_MOVE,
|
||||
COMMENT_CREATE,
|
||||
COMMENT_MOVE,
|
||||
];
|
||||
|
||||
/** List of events queued for firing. */
|
||||
const FIRE_QUEUE: Abstract[] = [];
|
||||
@@ -235,12 +245,11 @@ function fireInternal(event: Abstract) {
|
||||
FIRE_QUEUE.push(event);
|
||||
}
|
||||
|
||||
|
||||
/** Fire all queued events. */
|
||||
function fireNow() {
|
||||
const queue = filter(FIRE_QUEUE, true);
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
@@ -249,6 +258,46 @@ function fireNow() {
|
||||
eventWorkspace.fireChangeListener(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Post-filter the undo stack to squash and remove any events that result in
|
||||
// a null event
|
||||
|
||||
// 1. Determine which workspaces will need to have their undo stacks validated
|
||||
const workspaceIds = new Set(queue.map((e) => e.workspaceId));
|
||||
for (const workspaceId of workspaceIds) {
|
||||
// Only process valid workspaces
|
||||
if (!workspaceId) {
|
||||
continue;
|
||||
}
|
||||
const eventWorkspace = common.getWorkspaceById(workspaceId);
|
||||
if (!eventWorkspace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the last contiguous group of events on the stack
|
||||
const undoStack = eventWorkspace.getUndoStack();
|
||||
let i;
|
||||
let group: string | undefined = undefined;
|
||||
for (i = undoStack.length; i > 0; i--) {
|
||||
const event = undoStack[i - 1];
|
||||
if (event.group === '') {
|
||||
break;
|
||||
} else if (group === undefined) {
|
||||
group = event.group;
|
||||
} else if (event.group !== group) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!group || i == undoStack.length - 1) {
|
||||
// Need a group of two or more events on the stack. Nothing to do here.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract the event group, filter, and add back to the undo stack
|
||||
let events = undoStack.splice(i, undoStack.length - i);
|
||||
events = filter(events, true);
|
||||
undoStack.push(...events);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,7 +317,7 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
|
||||
const mergedQueue = [];
|
||||
const hash = Object.create(null);
|
||||
// Merge duplicates.
|
||||
for (let i = 0, event; event = queue[i]; i++) {
|
||||
for (let i = 0, event; (event = queue[i]); i++) {
|
||||
if (!event.isNull()) {
|
||||
// Treat all UI events as the same type in hash table.
|
||||
const eventType = event.isUiEvent ? UI : event.type;
|
||||
@@ -290,11 +339,23 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
|
||||
lastEvent.newParentId = moveEvent.newParentId;
|
||||
lastEvent.newInputName = moveEvent.newInputName;
|
||||
lastEvent.newCoordinate = moveEvent.newCoordinate;
|
||||
if (moveEvent.reason) {
|
||||
if (lastEvent.reason) {
|
||||
// Concatenate reasons without duplicates.
|
||||
const reasonSet = new Set(
|
||||
moveEvent.reason.concat(lastEvent.reason)
|
||||
);
|
||||
lastEvent.reason = Array.from(reasonSet);
|
||||
} else {
|
||||
lastEvent.reason = moveEvent.reason;
|
||||
}
|
||||
}
|
||||
lastEntry.index = i;
|
||||
} else if (
|
||||
event.type === CHANGE &&
|
||||
(event as BlockChange).element === lastEvent.element &&
|
||||
(event as BlockChange).name === lastEvent.name) {
|
||||
(event as BlockChange).name === lastEvent.name
|
||||
) {
|
||||
const changeEvent = event as BlockChange;
|
||||
// Merge change events.
|
||||
lastEvent.newValue = changeEvent.newValue;
|
||||
@@ -325,11 +386,13 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
|
||||
}
|
||||
// Move mutation events to the top of the queue.
|
||||
// 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
|
||||
// 'Abstract'.
|
||||
if (event.type === CHANGE &&
|
||||
(event as AnyDuringMigration).element === 'mutation') {
|
||||
if (
|
||||
event.type === CHANGE &&
|
||||
(event as AnyDuringMigration).element === 'mutation'
|
||||
) {
|
||||
queue.unshift(queue.splice(i, 1)[0]);
|
||||
}
|
||||
}
|
||||
@@ -341,7 +404,7 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
|
||||
* in the undo stack. Called by Workspace.clearUndo.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -410,7 +473,7 @@ function setGroupInternal(state: boolean|string) {
|
||||
export function getDescendantIds(block: Block): string[] {
|
||||
const ids = [];
|
||||
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;
|
||||
}
|
||||
return ids;
|
||||
@@ -425,43 +488,24 @@ export function getDescendantIds(block: Block): string[] {
|
||||
* @throws {Error} if an event type is not found in the registry.
|
||||
*/
|
||||
export function fromJson(
|
||||
json: AnyDuringMigration, workspace: Workspace): Abstract {
|
||||
json: AnyDuringMigration,
|
||||
workspace: Workspace
|
||||
): Abstract {
|
||||
const eventClass = get(json['type']);
|
||||
if (!eventClass) throw Error('Unknown event type.');
|
||||
|
||||
if (eventClassHasStaticFromJson(eventClass)) {
|
||||
return (eventClass as any).fromJson(json, workspace);
|
||||
}
|
||||
|
||||
// Fallback to the old deserialization method.
|
||||
const event = new eventClass();
|
||||
event.fromJson(json);
|
||||
event.workspaceId = workspace.id;
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given event constructor has /its own/ static fromJson
|
||||
* method.
|
||||
*
|
||||
* Returns false if no static fromJson method exists on the contructor, or if
|
||||
* the static fromJson method is inheritted.
|
||||
*/
|
||||
function eventClassHasStaticFromJson(eventClass: new (...p: any[]) => Abstract):
|
||||
boolean {
|
||||
const untypedEventClass = eventClass as any;
|
||||
return Object.getOwnPropertyDescriptors(untypedEventClass).fromJson &&
|
||||
typeof untypedEventClass.fromJson === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the class for a specific event type from the registry.
|
||||
*
|
||||
* @param eventType The type of the event to get.
|
||||
* @returns The event class with the given type.
|
||||
*/
|
||||
export function get(eventType: string):
|
||||
(new (...p1: AnyDuringMigration[]) => Abstract) {
|
||||
export function get(
|
||||
eventType: string
|
||||
): new (...p1: AnyDuringMigration[]) => Abstract {
|
||||
const event = registry.getClass(registry.Type.EVENT, eventType);
|
||||
if (!event) {
|
||||
throw new Error(`Event type ${eventType} not found in registry.`);
|
||||
@@ -483,8 +527,9 @@ export function disableOrphans(event: Abstract) {
|
||||
if (!blockEvent.workspaceId) {
|
||||
return;
|
||||
}
|
||||
const eventWorkspace =
|
||||
common.getWorkspaceById(blockEvent.workspaceId) as WorkspaceSvg;
|
||||
const eventWorkspace = common.getWorkspaceById(
|
||||
blockEvent.workspaceId
|
||||
) as WorkspaceSvg;
|
||||
if (!blockEvent.blockId) {
|
||||
throw new Error('Encountered a blockEvent without a proper blockId');
|
||||
}
|
||||
@@ -497,12 +542,13 @@ export function disableOrphans(event: Abstract) {
|
||||
const parent = block.getParent();
|
||||
if (parent && parent.isEnabled()) {
|
||||
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);
|
||||
}
|
||||
} else if (
|
||||
(block.outputConnection || block.previousConnection) &&
|
||||
!eventWorkspace.isDragging()) {
|
||||
!eventWorkspace.isDragging()
|
||||
) {
|
||||
do {
|
||||
block.setEnabled(false);
|
||||
block = block.getNextBlock();
|
||||
|
||||
@@ -14,10 +14,9 @@ goog.declareModuleId('Blockly.Events.FinishedLoading');
|
||||
|
||||
import * as registry from '../registry.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js';
|
||||
import {Abstract as AbstractEvent} from './events_abstract.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
|
||||
|
||||
/**
|
||||
* Notifies listeners when the workspace has finished deserializing from
|
||||
* JSON/XML.
|
||||
@@ -39,37 +38,10 @@ export class FinishedLoading extends AbstractEvent {
|
||||
|
||||
this.workspaceId = opt_workspace.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the event as JSON.
|
||||
*
|
||||
* @returns JSON representation.
|
||||
*/
|
||||
override toJson(): FinishedLoadingJson {
|
||||
const json = super.toJson() as FinishedLoadingJson;
|
||||
if (!this.workspaceId) {
|
||||
throw new Error(
|
||||
'The workspace ID is undefined. Either pass a workspace to ' +
|
||||
'the constructor, or call fromJson');
|
||||
}
|
||||
json['workspaceId'] = this.workspaceId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON event.
|
||||
*
|
||||
* @param json JSON representation.
|
||||
*/
|
||||
override fromJson(json: FinishedLoadingJson) {
|
||||
super.fromJson(json);
|
||||
this.workspaceId = json['workspaceId'];
|
||||
}
|
||||
}
|
||||
|
||||
export interface FinishedLoadingJson extends AbstractEventJson {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
registry.register(
|
||||
registry.Type.EVENT, eventUtils.FINISHED_LOADING, FinishedLoading);
|
||||
registry.Type.EVENT,
|
||||
eventUtils.FINISHED_LOADING,
|
||||
FinishedLoading
|
||||
);
|
||||
|
||||
@@ -10,10 +10,9 @@ goog.declareModuleId('Blockly.Extensions');
|
||||
import type {Block} from './block.js';
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import {FieldDropdown} from './field_dropdown.js';
|
||||
import {Mutator} from './mutator.js';
|
||||
import {MutatorIcon} from './icons/mutator_icon.js';
|
||||
import * as parsing from './utils/parsing.js';
|
||||
|
||||
|
||||
/** The set of all registered extensions, keyed by extension name/id. */
|
||||
const allExtensions = Object.create(null);
|
||||
export const TEST_ONLY = {allExtensions};
|
||||
@@ -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.
|
||||
*/
|
||||
export function registerMutator(
|
||||
name: string, mixinObj: AnyDuringMigration,
|
||||
opt_helperFn?: () => AnyDuringMigration, opt_blockList?: string[]) {
|
||||
name: string,
|
||||
mixinObj: AnyDuringMigration,
|
||||
opt_helperFn?: () => AnyDuringMigration,
|
||||
opt_blockList?: string[]
|
||||
) {
|
||||
const errorPrefix = 'Error when registering mutator "' + name + '": ';
|
||||
|
||||
checkHasMutatorProperties(errorPrefix, mixinObj);
|
||||
@@ -87,7 +89,7 @@ export function registerMutator(
|
||||
// Sanity checks passed.
|
||||
register(name, function (this: Block) {
|
||||
if (hasMutatorDialog) {
|
||||
this.setMutator(new Mutator(opt_blockList || [], this as BlockSvg));
|
||||
this.setMutator(new MutatorIcon(opt_blockList || [], this as BlockSvg));
|
||||
}
|
||||
// Mixin the object.
|
||||
this.mixin(mixinObj);
|
||||
@@ -108,7 +110,8 @@ export function unregister(name: string) {
|
||||
delete allExtensions[name];
|
||||
} else {
|
||||
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 + '": ';
|
||||
checkHasMutatorProperties(errorPrefix, block);
|
||||
} else {
|
||||
if (!mutatorPropertiesMatch(
|
||||
mutatorProperties as AnyDuringMigration[], block)) {
|
||||
if (
|
||||
!mutatorPropertiesMatch(mutatorProperties as AnyDuringMigration[], block)
|
||||
) {
|
||||
throw Error(
|
||||
'Error when applying extension "' + name + '": ' +
|
||||
'mutation properties changed when applying a non-mutator extension.');
|
||||
'Error when applying 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);
|
||||
if (properties.length) {
|
||||
throw Error(
|
||||
'Error: tried to apply mutation "' + mutationName +
|
||||
'Error: tried to apply mutation "' +
|
||||
mutationName +
|
||||
'" 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.
|
||||
*/
|
||||
function checkXmlHooks(
|
||||
object: AnyDuringMigration, errorPrefix: string): boolean {
|
||||
object: AnyDuringMigration,
|
||||
errorPrefix: string
|
||||
): boolean {
|
||||
return checkHasFunctionPair(
|
||||
object.mutationToDom, object.domToMutation,
|
||||
errorPrefix + ' mutationToDom/domToMutation');
|
||||
object.mutationToDom,
|
||||
object.domToMutation,
|
||||
errorPrefix + ' mutationToDom/domToMutation'
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Checks if the given object has both the 'saveExtraState' and 'loadExtraState'
|
||||
@@ -208,10 +222,14 @@ function checkXmlHooks(
|
||||
* actually a function.
|
||||
*/
|
||||
function checkJsonHooks(
|
||||
object: AnyDuringMigration, errorPrefix: string): boolean {
|
||||
object: AnyDuringMigration,
|
||||
errorPrefix: string
|
||||
): boolean {
|
||||
return checkHasFunctionPair(
|
||||
object.saveExtraState, object.loadExtraState,
|
||||
errorPrefix + ' saveExtraState/loadExtraState');
|
||||
object.saveExtraState,
|
||||
object.loadExtraState,
|
||||
errorPrefix + ' saveExtraState/loadExtraState'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,9 +243,14 @@ function checkJsonHooks(
|
||||
* actually a function.
|
||||
*/
|
||||
function checkMutatorDialog(
|
||||
object: AnyDuringMigration, errorPrefix: string): boolean {
|
||||
object: AnyDuringMigration,
|
||||
errorPrefix: string
|
||||
): boolean {
|
||||
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.
|
||||
*/
|
||||
function checkHasFunctionPair(
|
||||
func1: AnyDuringMigration, func2: AnyDuringMigration,
|
||||
errorPrefix: string): boolean {
|
||||
func1: AnyDuringMigration,
|
||||
func2: AnyDuringMigration,
|
||||
errorPrefix: string
|
||||
): boolean {
|
||||
if (func1 && func2) {
|
||||
if (typeof func1 !== 'function' || typeof func2 !== 'function') {
|
||||
throw Error(errorPrefix + ' must be a function');
|
||||
@@ -263,13 +288,16 @@ function checkHasFunctionPair(
|
||||
* @param object The object to inspect.
|
||||
*/
|
||||
function checkHasMutatorProperties(
|
||||
errorPrefix: string, object: AnyDuringMigration) {
|
||||
errorPrefix: string,
|
||||
object: AnyDuringMigration
|
||||
) {
|
||||
const hasXmlHooks = checkXmlHooks(object, errorPrefix);
|
||||
const hasJsonHooks = checkJsonHooks(object, errorPrefix);
|
||||
if (!hasXmlHooks && !hasJsonHooks) {
|
||||
throw Error(
|
||||
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
|
||||
// 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.
|
||||
*/
|
||||
function mutatorPropertiesMatch(
|
||||
oldProperties: AnyDuringMigration[], block: Block): boolean {
|
||||
oldProperties: AnyDuringMigration[],
|
||||
block: Block
|
||||
): boolean {
|
||||
const newProperties = getMutatorProperties(block);
|
||||
if (newProperties.length !== oldProperties.length) {
|
||||
return false;
|
||||
@@ -375,7 +405,9 @@ export function runAfterPageLoad(fn: () => void) {
|
||||
* @returns The extension function.
|
||||
*/
|
||||
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.
|
||||
const blockTypesChecked: AnyDuringMigration[] = [];
|
||||
|
||||
@@ -383,7 +415,8 @@ export function buildTooltipForDropdown(
|
||||
// Wait for load, in case Blockly.Msg is not yet populated.
|
||||
// runAfterPageLoad() does not run in a Node.js environment due to lack
|
||||
// of document object, in which case skip the validation.
|
||||
if (typeof document === 'object') { // Relies on document.readyState
|
||||
if (typeof document === 'object') {
|
||||
// Relies on document.readyState
|
||||
runAfterPageLoad(function () {
|
||||
for (const key in lookupTable) {
|
||||
// Will print warnings if reference is missing.
|
||||
@@ -399,13 +432,17 @@ export function buildTooltipForDropdown(
|
||||
blockTypesChecked.push(this.type);
|
||||
}
|
||||
|
||||
this.setTooltip(function(this: Block) {
|
||||
this.setTooltip(
|
||||
function (this: Block) {
|
||||
const value = String(this.getFieldValue(dropdownName));
|
||||
let tooltip = lookupTable[value];
|
||||
if (tooltip === null) {
|
||||
if (blockTypesChecked.indexOf(this.type) === -1) {
|
||||
// Warn for missing values on generated tooltips.
|
||||
let warning = 'No tooltip mapping for value ' + value + ' of field ' +
|
||||
let warning =
|
||||
'No tooltip mapping for value ' +
|
||||
value +
|
||||
' of field ' +
|
||||
dropdownName;
|
||||
if (this.type !== null) {
|
||||
warning += ' of block type ' + this.type;
|
||||
@@ -416,7 +453,8 @@ export function buildTooltipForDropdown(
|
||||
tooltip = parsing.replaceMessageReferences(tooltip);
|
||||
}
|
||||
return tooltip;
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
return extensionFn;
|
||||
}
|
||||
@@ -430,7 +468,10 @@ export function buildTooltipForDropdown(
|
||||
* @param lookupTable The string lookup table
|
||||
*/
|
||||
function checkDropdownOptionsInTable(
|
||||
block: Block, dropdownName: string, lookupTable: {[key: string]: string}) {
|
||||
block: Block,
|
||||
dropdownName: string,
|
||||
lookupTable: {[key: string]: string}
|
||||
) {
|
||||
// Validate all dropdown options have values.
|
||||
const dropdown = block.getField(dropdownName);
|
||||
if (dropdown instanceof FieldDropdown && !dropdown.isOptionListDynamic()) {
|
||||
@@ -439,8 +480,13 @@ function checkDropdownOptionsInTable(
|
||||
const optionKey = options[i][1]; // label, then value
|
||||
if (lookupTable[optionKey] === null) {
|
||||
console.warn(
|
||||
'No tooltip mapping for value ' + optionKey + ' of field ' +
|
||||
dropdownName + ' of block type ' + block.type);
|
||||
'No tooltip mapping for value ' +
|
||||
optionKey +
|
||||
' of field ' +
|
||||
dropdownName +
|
||||
' of block type ' +
|
||||
block.type
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -457,12 +503,15 @@ function checkDropdownOptionsInTable(
|
||||
* @returns The extension function.
|
||||
*/
|
||||
export function buildTooltipWithFieldText(
|
||||
msgTemplate: string, fieldName: string): Function {
|
||||
msgTemplate: string,
|
||||
fieldName: string
|
||||
): Function {
|
||||
// Check the tooltip string messages for invalid references.
|
||||
// Wait for load, in case Blockly.Msg is not yet populated.
|
||||
// runAfterPageLoad() does not run in a Node.js environment due to lack
|
||||
// of document object, in which case skip the validation.
|
||||
if (typeof document === 'object') { // Relies on document.readyState
|
||||
if (typeof document === 'object') {
|
||||
// Relies on document.readyState
|
||||
runAfterPageLoad(function () {
|
||||
// Will print warnings if reference is missing.
|
||||
parsing.checkMessageReferences(msgTemplate);
|
||||
@@ -471,11 +520,14 @@ export function buildTooltipWithFieldText(
|
||||
|
||||
/** The actual extension. */
|
||||
function extensionFn(this: Block) {
|
||||
this.setTooltip(function(this: Block) {
|
||||
this.setTooltip(
|
||||
function (this: Block) {
|
||||
const field = this.getField(fieldName);
|
||||
return parsing.replaceMessageReferences(msgTemplate)
|
||||
return parsing
|
||||
.replaceMessageReferences(msgTemplate)
|
||||
.replace('%1', field ? field.getText() : '');
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
return extensionFn;
|
||||
}
|
||||
@@ -488,10 +540,14 @@ export function buildTooltipWithFieldText(
|
||||
*/
|
||||
function extensionParentTooltip(this: Block) {
|
||||
const tooltipWhenNotConnected = this.tooltip;
|
||||
this.setTooltip(function(this: Block) {
|
||||
this.setTooltip(
|
||||
function (this: Block) {
|
||||
const parent = this.getParent();
|
||||
return parent && parent.getInputsInline() && parent.tooltip ||
|
||||
tooltipWhenNotConnected;
|
||||
}.bind(this));
|
||||
return (
|
||||
(parent && parent.getInputsInline() && parent.tooltip) ||
|
||||
tooltipWhenNotConnected
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
register('parent_tooltip_when_inline', extensionParentTooltip);
|
||||
|
||||
205
core/field.ts
205
core/field.ts
@@ -22,7 +22,7 @@ import type {BlockSvg} from './block_svg.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import * as dropDownDiv from './dropdowndiv.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import type {Input} from './input.js';
|
||||
import type {Input} from './inputs/input.js';
|
||||
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
|
||||
import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js';
|
||||
import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
|
||||
@@ -42,6 +42,7 @@ import * as userAgent from './utils/useragent.js';
|
||||
import * as utilsXml from './utils/xml.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import {ISerializable} from './interfaces/i_serializable.js';
|
||||
|
||||
/**
|
||||
* A function that is called to validate changes to the field's value before
|
||||
@@ -65,10 +66,14 @@ export type FieldValidator<T = any> = (newValue: T) => T|null|undefined;
|
||||
*
|
||||
* @typeParam T - The value stored on the field.
|
||||
*/
|
||||
export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
export abstract class Field<T = any>
|
||||
implements
|
||||
IASTNodeLocationSvg,
|
||||
IASTNodeLocationWithBlock,
|
||||
IKeyboardAccessible,
|
||||
IRegistrable {
|
||||
IRegistrable,
|
||||
ISerializable
|
||||
{
|
||||
/**
|
||||
* To overwrite the default value which is set in **Field**, directly update
|
||||
* the prototype.
|
||||
@@ -207,15 +212,18 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
value: T|typeof Field.SKIP_SETUP, validator?: FieldValidator<T>|null,
|
||||
config?: FieldConfig) {
|
||||
value: T | typeof Field.SKIP_SETUP,
|
||||
validator?: FieldValidator<T> | null,
|
||||
config?: FieldConfig
|
||||
) {
|
||||
/**
|
||||
* A generic value possessed by the field.
|
||||
* Should generally be non-null, only null when the field is created.
|
||||
*/
|
||||
this.value_ = 'DEFAULT_VALUE' in new.target.prototype ?
|
||||
new.target.prototype.DEFAULT_VALUE :
|
||||
this.DEFAULT_VALUE;
|
||||
this.value_ =
|
||||
'DEFAULT_VALUE' in new.target.prototype
|
||||
? new.target.prototype.DEFAULT_VALUE
|
||||
: this.DEFAULT_VALUE;
|
||||
|
||||
/** The size of the area rendered by the field. */
|
||||
this.size_ = new Size(0, 0);
|
||||
@@ -263,9 +271,12 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* @returns The renderer constant provider.
|
||||
*/
|
||||
getConstants(): ConstantProvider | null {
|
||||
if (!this.constants_ && this.sourceBlock_ &&
|
||||
if (
|
||||
!this.constants_ &&
|
||||
this.sourceBlock_ &&
|
||||
!this.sourceBlock_.isDeadOrDying() &&
|
||||
this.sourceBlock_.workspace.rendered) {
|
||||
this.sourceBlock_.workspace.rendered
|
||||
) {
|
||||
this.constants_ = (this.sourceBlock_.workspace as WorkspaceSvg)
|
||||
.getRenderer()
|
||||
.getConstants();
|
||||
@@ -333,7 +344,8 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*/
|
||||
protected createBorderRect_() {
|
||||
this.borderRect_ = dom.createSvgElement(
|
||||
Svg.RECT, {
|
||||
Svg.RECT,
|
||||
{
|
||||
'rx': this.getConstants()!.FIELD_BORDER_RECT_RADIUS,
|
||||
'ry': this.getConstants()!.FIELD_BORDER_RECT_RADIUS,
|
||||
'x': 0,
|
||||
@@ -342,7 +354,8 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
'width': this.size_.width,
|
||||
'class': 'blocklyFieldRect',
|
||||
},
|
||||
this.fieldGroup_);
|
||||
this.fieldGroup_
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,10 +365,12 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*/
|
||||
protected createTextElement_() {
|
||||
this.textElement_ = dom.createSvgElement(
|
||||
Svg.TEXT, {
|
||||
Svg.TEXT,
|
||||
{
|
||||
'class': 'blocklyText',
|
||||
},
|
||||
this.fieldGroup_);
|
||||
this.fieldGroup_
|
||||
);
|
||||
if (this.getConstants()!.FIELD_TEXT_BASELINE_CENTER) {
|
||||
this.textElement_.setAttribute('dominant-baseline', 'central');
|
||||
}
|
||||
@@ -372,7 +387,11 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
if (!clickTarget) throw new Error('A click target has not been set.');
|
||||
Tooltip.bindMouseEvents(clickTarget);
|
||||
this.mouseDownWrapper_ = browserEvents.conditionalBind(
|
||||
clickTarget, 'pointerdown', this, this.onMouseDown_);
|
||||
clickTarget,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.onMouseDown_
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -443,13 +462,17 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* @returns The stringified version of the XML state, or null.
|
||||
*/
|
||||
protected saveLegacyState(callingClass: FieldProto): string | null {
|
||||
if (callingClass.prototype.saveState === this.saveState &&
|
||||
callingClass.prototype.toXml !== this.toXml) {
|
||||
if (
|
||||
callingClass.prototype.saveState === this.saveState &&
|
||||
callingClass.prototype.toXml !== this.toXml
|
||||
) {
|
||||
const elem = utilsXml.createElement('field');
|
||||
elem.setAttribute('name', this.name || '');
|
||||
const text = utilsXml.domToText(this.toXml(elem));
|
||||
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
|
||||
// no implementations of either hook. Just do our thing.
|
||||
@@ -465,10 +488,14 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* @param state The state to apply to the field.
|
||||
* @returns Whether the state was applied or not.
|
||||
*/
|
||||
loadLegacyState(callingClass: FieldProto, state: AnyDuringMigration):
|
||||
boolean {
|
||||
if (callingClass.prototype.loadState === this.loadState &&
|
||||
callingClass.prototype.fromXml !== this.fromXml) {
|
||||
loadLegacyState(
|
||||
callingClass: FieldProto,
|
||||
state: AnyDuringMigration
|
||||
): boolean {
|
||||
if (
|
||||
callingClass.prototype.loadState === this.loadState &&
|
||||
callingClass.prototype.fromXml !== this.fromXml
|
||||
) {
|
||||
this.fromXml(utilsXml.textToDom(state as string));
|
||||
return true;
|
||||
}
|
||||
@@ -538,9 +565,12 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* @returns Whether this field is clickable.
|
||||
*/
|
||||
isClickable(): boolean {
|
||||
return this.enabled_ && !!this.sourceBlock_ &&
|
||||
return (
|
||||
this.enabled_ &&
|
||||
!!this.sourceBlock_ &&
|
||||
this.sourceBlock_.isEditable() &&
|
||||
this.showEditor_ !== Field.prototype.showEditor_;
|
||||
this.showEditor_ !== Field.prototype.showEditor_
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -552,8 +582,12 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* editable block.
|
||||
*/
|
||||
isCurrentlyEditable(): boolean {
|
||||
return this.enabled_ && this.EDITABLE && !!this.sourceBlock_ &&
|
||||
this.sourceBlock_.isEditable();
|
||||
return (
|
||||
this.enabled_ &&
|
||||
this.EDITABLE &&
|
||||
!!this.sourceBlock_ &&
|
||||
this.sourceBlock_.isEditable()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,7 +605,8 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
console.warn(
|
||||
'Detected an editable field that was not serializable.' +
|
||||
' Please define SERIALIZABLE property as true on all editable custom' +
|
||||
' fields. Proceeding with serialization.');
|
||||
' fields. Proceeding with serialization.'
|
||||
);
|
||||
isSerializable = true;
|
||||
}
|
||||
}
|
||||
@@ -683,13 +718,18 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the field to match the colour/style of the block. Should only be
|
||||
* called by BlockSvg.applyColour().
|
||||
* Updates the field to match the colour/style of the block.
|
||||
*
|
||||
* @internal
|
||||
* Non-abstract sub-classes may wish to implement this if the colour of the
|
||||
* field depends on the colour of the block. It will automatically be called
|
||||
* at relevant times, such as when the parent block or renderer changes.
|
||||
*
|
||||
* See {@link
|
||||
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/customizing-fields/creating#matching_block_colours
|
||||
* | the field documentation} for more information, or FieldDropdown for an
|
||||
* example.
|
||||
*/
|
||||
applyColour() {}
|
||||
// Non-abstract sub-classes may wish to implement this. See FieldDropdown.
|
||||
|
||||
/**
|
||||
* Used by getSize() to move/resize any DOM elements, and get the new size.
|
||||
@@ -729,6 +769,28 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
protected showEditor_(_e?: Event): void {}
|
||||
// NOP
|
||||
|
||||
/**
|
||||
* A developer hook to reposition the WidgetDiv during a window resize. You
|
||||
* need to define this hook if your field has a WidgetDiv that needs to
|
||||
* reposition itself when the window is resized. For example, text input
|
||||
* fields define this hook so that the input WidgetDiv can reposition itself
|
||||
* on a window resize event. This is especially important when modal inputs
|
||||
* have been disabled, as Android devices will fire a window resize event when
|
||||
* the soft keyboard opens.
|
||||
*
|
||||
* If you want the WidgetDiv to hide itself instead of repositioning, return
|
||||
* false. This is the default behavior.
|
||||
*
|
||||
* DropdownDivs already handle their own positioning logic, so you do not need
|
||||
* to override this function if your field only has a DropdownDiv.
|
||||
*
|
||||
* @returns True if the field should be repositioned,
|
||||
* false if the WidgetDiv should hide itself instead.
|
||||
*/
|
||||
repositionForWindowResize(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the size of the field based on the text.
|
||||
*
|
||||
@@ -736,17 +798,23 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*/
|
||||
protected updateSize_(margin?: number) {
|
||||
const constants = this.getConstants();
|
||||
const xOffset = margin !== undefined ? margin :
|
||||
this.borderRect_ ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING :
|
||||
0;
|
||||
const xOffset =
|
||||
margin !== undefined
|
||||
? margin
|
||||
: this.borderRect_
|
||||
? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING
|
||||
: 0;
|
||||
let totalWidth = xOffset * 2;
|
||||
let totalHeight = constants!.FIELD_TEXT_HEIGHT;
|
||||
|
||||
let contentWidth = 0;
|
||||
if (this.textElement_) {
|
||||
contentWidth = dom.getFastTextWidth(
|
||||
this.textElement_, constants!.FIELD_TEXT_FONTSIZE,
|
||||
constants!.FIELD_TEXT_FONTWEIGHT, constants!.FIELD_TEXT_FONTFAMILY);
|
||||
this.textElement_,
|
||||
constants!.FIELD_TEXT_FONTSIZE,
|
||||
constants!.FIELD_TEXT_FONTWEIGHT,
|
||||
constants!.FIELD_TEXT_FONTFAMILY
|
||||
);
|
||||
totalWidth += contentWidth;
|
||||
}
|
||||
if (this.borderRect_) {
|
||||
@@ -777,16 +845,21 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
this.textElement_.setAttribute(
|
||||
'x',
|
||||
String(
|
||||
this.getSourceBlock()?.RTL ?
|
||||
this.size_.width - contentWidth - xOffset :
|
||||
xOffset));
|
||||
this.getSourceBlock()?.RTL
|
||||
? this.size_.width - contentWidth - xOffset
|
||||
: xOffset
|
||||
)
|
||||
);
|
||||
this.textElement_.setAttribute(
|
||||
'y',
|
||||
String(
|
||||
constants!.FIELD_TEXT_BASELINE_CENTER ?
|
||||
halfHeight :
|
||||
halfHeight - constants!.FIELD_TEXT_HEIGHT / 2 +
|
||||
constants!.FIELD_TEXT_BASELINE));
|
||||
constants!.FIELD_TEXT_BASELINE_CENTER
|
||||
? halfHeight
|
||||
: halfHeight -
|
||||
constants!.FIELD_TEXT_HEIGHT / 2 +
|
||||
constants!.FIELD_TEXT_BASELINE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** Position a field's border rect after a size change. */
|
||||
@@ -797,9 +870,13 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
this.borderRect_.setAttribute('width', String(this.size_.width));
|
||||
this.borderRect_.setAttribute('height', String(this.size_.height));
|
||||
this.borderRect_.setAttribute(
|
||||
'rx', String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS));
|
||||
'rx',
|
||||
String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS)
|
||||
);
|
||||
this.borderRect_.setAttribute(
|
||||
'ry', String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS));
|
||||
'ry',
|
||||
String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -825,7 +902,8 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
if (this.size_.width !== 0) {
|
||||
console.warn(
|
||||
'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_;
|
||||
@@ -964,9 +1042,12 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* than this method.
|
||||
*
|
||||
* @param newValue New value.
|
||||
* @param fireChangeEvent Whether to fire a change event. Defaults to true.
|
||||
* Should usually be true unless the change will be reported some other
|
||||
* way, e.g. an intermediate field change event.
|
||||
* @sealed
|
||||
*/
|
||||
setValue(newValue: AnyDuringMigration) {
|
||||
setValue(newValue: AnyDuringMigration, fireChangeEvent = true) {
|
||||
const doLogging = false;
|
||||
if (newValue === null) {
|
||||
doLogging && console.log('null, return');
|
||||
@@ -1002,9 +1083,16 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
}
|
||||
|
||||
this.doValueUpdate_(localValue);
|
||||
if (source && eventUtils.isEnabled()) {
|
||||
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
|
||||
source, 'field', this.name || null, oldValue, localValue));
|
||||
if (fireChangeEvent && source && eventUtils.isEnabled()) {
|
||||
eventUtils.fire(
|
||||
new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
|
||||
source,
|
||||
'field',
|
||||
this.name || null,
|
||||
oldValue,
|
||||
localValue
|
||||
)
|
||||
);
|
||||
}
|
||||
if (this.isDirty_) {
|
||||
this.forceRerender();
|
||||
@@ -1020,7 +1108,9 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* @returns New value, or an Error object.
|
||||
*/
|
||||
private processValidation_(
|
||||
newValue: AnyDuringMigration, validatedValue: T|null|undefined): T|Error {
|
||||
newValue: AnyDuringMigration,
|
||||
validatedValue: T | null | undefined
|
||||
): T | Error {
|
||||
if (validatedValue === null) {
|
||||
this.doValueInvalid_(newValue);
|
||||
if (this.isDirty_) {
|
||||
@@ -1028,7 +1118,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
}
|
||||
return Error();
|
||||
}
|
||||
return validatedValue === undefined ? newValue as T : validatedValue;
|
||||
return validatedValue === undefined ? (newValue as T) : validatedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1062,8 +1152,9 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*/
|
||||
protected doClassValidation_(newValue: T): T | null | undefined;
|
||||
protected doClassValidation_(newValue?: AnyDuringMigration): T | null;
|
||||
protected doClassValidation_(newValue?: T|AnyDuringMigration): T|null
|
||||
|undefined {
|
||||
protected doClassValidation_(
|
||||
newValue?: T | AnyDuringMigration
|
||||
): T | null | undefined {
|
||||
if (newValue === null || newValue === undefined) {
|
||||
return null;
|
||||
}
|
||||
@@ -1116,7 +1207,8 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* the empty string.
|
||||
*/
|
||||
setTooltip(newTip: Tooltip.TipInfo | null) {
|
||||
if (!newTip && newTip !== '') { // If null or undefined.
|
||||
if (!newTip && newTip !== '') {
|
||||
// If null or undefined.
|
||||
newTip = this.sourceBlock_;
|
||||
}
|
||||
const clickTarget = this.getClickTarget_();
|
||||
@@ -1324,6 +1416,7 @@ export class UnattachedFieldError extends Error {
|
||||
constructor() {
|
||||
super(
|
||||
'The field has not yet been attached to its input. ' +
|
||||
'Call appendField to attach it.');
|
||||
'Call appendField to attach it.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,14 @@ import {BlockSvg} from './block_svg.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import * as Css from './css.js';
|
||||
import * as dropDownDiv from './dropdowndiv.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {Field, UnattachedFieldError} from './field.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 math from './utils/math.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
@@ -115,7 +120,9 @@ export class FieldAngle extends FieldInput<number> {
|
||||
*/
|
||||
constructor(
|
||||
value?: string | number | typeof Field.SKIP_SETUP,
|
||||
validator?: FieldAngleValidator, config?: FieldAngleConfig) {
|
||||
validator?: FieldAngleValidator,
|
||||
config?: FieldAngleConfig
|
||||
) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
if (value === Field.SKIP_SETUP) return;
|
||||
@@ -193,7 +200,8 @@ export class FieldAngle extends FieldInput<number> {
|
||||
if (this.sourceBlock_ instanceof BlockSvg) {
|
||||
dropDownDiv.setColour(
|
||||
this.sourceBlock_.style.colourPrimary,
|
||||
this.sourceBlock_.style.colourTertiary);
|
||||
this.sourceBlock_.style.colourTertiary
|
||||
);
|
||||
}
|
||||
|
||||
dropDownDiv.showPositionedByField(this, this.dropdownDispose.bind(this));
|
||||
@@ -217,50 +225,80 @@ export class FieldAngle extends FieldInput<number> {
|
||||
'style': 'touch-action: none',
|
||||
});
|
||||
const circle = dom.createSvgElement(
|
||||
Svg.CIRCLE, {
|
||||
Svg.CIRCLE,
|
||||
{
|
||||
'cx': FieldAngle.HALF,
|
||||
'cy': FieldAngle.HALF,
|
||||
'r': FieldAngle.RADIUS,
|
||||
'class': 'blocklyAngleCircle',
|
||||
},
|
||||
svg);
|
||||
this.gauge =
|
||||
dom.createSvgElement(Svg.PATH, {'class': 'blocklyAngleGauge'}, svg);
|
||||
svg
|
||||
);
|
||||
this.gauge = dom.createSvgElement(
|
||||
Svg.PATH,
|
||||
{'class': 'blocklyAngleGauge'},
|
||||
svg
|
||||
);
|
||||
this.line = dom.createSvgElement(
|
||||
Svg.LINE, {
|
||||
Svg.LINE,
|
||||
{
|
||||
'x1': FieldAngle.HALF,
|
||||
'y1': FieldAngle.HALF,
|
||||
'class': 'blocklyAngleLine',
|
||||
},
|
||||
svg);
|
||||
svg
|
||||
);
|
||||
// Draw markers around the edge.
|
||||
for (let angle = 0; angle < 360; angle += 15) {
|
||||
dom.createSvgElement(
|
||||
Svg.LINE, {
|
||||
Svg.LINE,
|
||||
{
|
||||
'x1': FieldAngle.HALF + FieldAngle.RADIUS,
|
||||
'y1': FieldAngle.HALF,
|
||||
'x2': FieldAngle.HALF + FieldAngle.RADIUS -
|
||||
(angle % 45 === 0 ? 10 : 5),
|
||||
'x2':
|
||||
FieldAngle.HALF + FieldAngle.RADIUS - (angle % 45 === 0 ? 10 : 5),
|
||||
'y2': FieldAngle.HALF,
|
||||
'class': 'blocklyAngleMarks',
|
||||
'transform': 'rotate(' + angle + ',' + FieldAngle.HALF + ',' +
|
||||
FieldAngle.HALF + ')',
|
||||
'transform':
|
||||
'rotate(' +
|
||||
angle +
|
||||
',' +
|
||||
FieldAngle.HALF +
|
||||
',' +
|
||||
FieldAngle.HALF +
|
||||
')',
|
||||
},
|
||||
svg);
|
||||
svg
|
||||
);
|
||||
}
|
||||
|
||||
// The angle picker is different from other fields in that it updates on
|
||||
// mousemove even if it's not in the middle of a drag. In future we may
|
||||
// change this behaviour.
|
||||
this.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
|
||||
// a click handler on the drag surface to update the value if the surface
|
||||
// is clicked.
|
||||
this.boundEvents.push(browserEvents.conditionalBind(
|
||||
circle, 'pointerdown', this, this.onMouseMove_, true));
|
||||
this.boundEvents.push(browserEvents.conditionalBind(
|
||||
circle, 'pointermove', this, this.onMouseMove_, true));
|
||||
this.boundEvents.push(
|
||||
browserEvents.conditionalBind(
|
||||
circle,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.onMouseMove_,
|
||||
true
|
||||
)
|
||||
);
|
||||
this.boundEvents.push(
|
||||
browserEvents.conditionalBind(
|
||||
circle,
|
||||
'pointermove',
|
||||
this,
|
||||
this.onMouseMove_,
|
||||
true
|
||||
)
|
||||
);
|
||||
return svg;
|
||||
}
|
||||
|
||||
@@ -326,7 +364,26 @@ export class FieldAngle extends FieldInput<number> {
|
||||
}
|
||||
angle = this.wrapValue(angle);
|
||||
if (angle !== this.value_) {
|
||||
this.setEditorValue_(angle);
|
||||
// Intermediate value changes from user input are not confirmed until the
|
||||
// user closes the editor, and may be numerous. Inhibit reporting these as
|
||||
// normal block change events, and instead report them as special
|
||||
// intermediate changes that do not get recorded in undo history.
|
||||
const oldValue = this.value_;
|
||||
this.setEditorValue_(angle, false);
|
||||
if (
|
||||
this.sourceBlock_ &&
|
||||
eventUtils.isEnabled() &&
|
||||
this.value_ !== oldValue
|
||||
) {
|
||||
eventUtils.fire(
|
||||
new (eventUtils.get(eventUtils.BLOCK_FIELD_INTERMEDIATE_CHANGE))(
|
||||
this.sourceBlock_,
|
||||
this.name || null,
|
||||
oldValue,
|
||||
this.value_
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,14 +410,31 @@ export class FieldAngle extends FieldInput<number> {
|
||||
x2 += Math.cos(angleRadians) * FieldAngle.RADIUS;
|
||||
y2 -= Math.sin(angleRadians) * FieldAngle.RADIUS;
|
||||
// Don't ask how the flag calculations work. They just do.
|
||||
let largeFlag =
|
||||
Math.abs(Math.floor((angleRadians - angle1) / Math.PI) % 2);
|
||||
let largeFlag = Math.abs(
|
||||
Math.floor((angleRadians - angle1) / Math.PI) % 2
|
||||
);
|
||||
if (clockwiseFlag) {
|
||||
largeFlag = 1 - largeFlag;
|
||||
}
|
||||
path.push(
|
||||
' l ', x1, ',', y1, ' A ', FieldAngle.RADIUS, ',', FieldAngle.RADIUS,
|
||||
' 0 ', largeFlag, ' ', clockwiseFlag, ' ', x2, ',', y2, ' z');
|
||||
' l ',
|
||||
x1,
|
||||
',',
|
||||
y1,
|
||||
' A ',
|
||||
FieldAngle.RADIUS,
|
||||
',',
|
||||
FieldAngle.RADIUS,
|
||||
' 0 ',
|
||||
largeFlag,
|
||||
' ',
|
||||
clockwiseFlag,
|
||||
' ',
|
||||
x2,
|
||||
',',
|
||||
y2,
|
||||
' z'
|
||||
);
|
||||
}
|
||||
this.gauge.setAttribute('d', path.join(''));
|
||||
this.line.setAttribute('x2', `${x2}`);
|
||||
@@ -456,7 +530,6 @@ fieldRegistry.register('field_angle', FieldAngle);
|
||||
|
||||
FieldAngle.prototype.DEFAULT_VALUE = 0;
|
||||
|
||||
|
||||
/**
|
||||
* CSS for angle field.
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,7 @@ type CheckboxBool = BoolString|boolean;
|
||||
export class FieldCheckbox extends Field<CheckboxBool> {
|
||||
/** Default character for the checkmark. */
|
||||
static readonly CHECK_CHAR = '✓';
|
||||
private checkChar_: string;
|
||||
private checkChar: string;
|
||||
|
||||
/**
|
||||
* Serializable fields are saved by the serializer, non-serializable fields
|
||||
@@ -63,14 +63,16 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
||||
*/
|
||||
constructor(
|
||||
value?: CheckboxBool | typeof Field.SKIP_SETUP,
|
||||
validator?: FieldCheckboxValidator, config?: FieldCheckboxConfig) {
|
||||
validator?: FieldCheckboxValidator,
|
||||
config?: FieldCheckboxConfig
|
||||
) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
/**
|
||||
* Character for the check mark. Used to apply a different check mark
|
||||
* character to individual fields.
|
||||
*/
|
||||
this.checkChar_ = FieldCheckbox.CHECK_CHAR;
|
||||
this.checkChar = FieldCheckbox.CHECK_CHAR;
|
||||
|
||||
if (value === Field.SKIP_SETUP) return;
|
||||
if (config) {
|
||||
@@ -89,7 +91,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
||||
*/
|
||||
protected override configure_(config: FieldCheckboxConfig) {
|
||||
super.configure_(config);
|
||||
if (config.checkCharacter) this.checkChar_ = config.checkCharacter;
|
||||
if (config.checkCharacter) this.checkChar = config.checkCharacter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +129,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
||||
}
|
||||
|
||||
override getDisplayText_() {
|
||||
return this.checkChar_;
|
||||
return this.checkChar;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,7 +139,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
||||
* the default.
|
||||
*/
|
||||
setCheckCharacter(character: string | null) {
|
||||
this.checkChar_ = character || FieldCheckbox.CHECK_CHAR;
|
||||
this.checkChar = character || FieldCheckbox.CHECK_CHAR;
|
||||
this.forceRerender();
|
||||
}
|
||||
|
||||
@@ -152,8 +154,9 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
||||
* @param newValue The input value.
|
||||
* @returns A valid value ('TRUE' or 'FALSE), or null if invalid.
|
||||
*/
|
||||
protected override doClassValidation_(newValue?: AnyDuringMigration):
|
||||
BoolString|null {
|
||||
protected override doClassValidation_(
|
||||
newValue?: AnyDuringMigration
|
||||
): BoolString | null {
|
||||
if (newValue === true || newValue === 'TRUE') {
|
||||
return 'TRUE';
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user