mirror of
https://github.com/google/blockly.git
synced 2025-12-16 06:10: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',
|
||||
}),
|
||||
{
|
||||
|
||||
12
.github/CONTRIBUTING.md
vendored
12
.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/)
|
||||
|
||||
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).
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -30,7 +30,7 @@ body:
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
3.
|
||||
- type: textarea
|
||||
id: stack-trace
|
||||
attributes:
|
||||
|
||||
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:
|
||||
|
||||
44
.github/workflows/assign_reviewers.yml
vendored
44
.github/workflows/assign_reviewers.yml
vendored
@@ -16,26 +16,26 @@ jobs:
|
||||
requested-reviewer:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Assign requested reviewer
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
if (context.payload.pull_request === undefined) {
|
||||
throw new Error("Can't get pull_request payload. " +
|
||||
'Check a request reviewer event was triggered.');
|
||||
- name: Assign requested reviewer
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
if (context.payload.pull_request === undefined) {
|
||||
throw new Error("Can't get pull_request payload. " +
|
||||
'Check a request reviewer event was triggered.');
|
||||
}
|
||||
const reviewers = context.payload.pull_request.requested_reviewers;
|
||||
// Assignees takes in a list of logins rather than the
|
||||
// reviewer object.
|
||||
const reviewerNames = reviewers.map(reviewer => reviewer.login);
|
||||
const {number:issue_number} = context.payload.pull_request;
|
||||
github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
assignees: reviewerNames
|
||||
});
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
const reviewers = context.payload.pull_request.requested_reviewers;
|
||||
// Assignees takes in a list of logins rather than the
|
||||
// reviewer object.
|
||||
const reviewerNames = reviewers.map(reviewer => reviewer.login);
|
||||
const {number:issue_number} = context.payload.pull_request;
|
||||
github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
assignees: reviewerNames
|
||||
});
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
|
||||
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
|
||||
88
.github/workflows/build.yml
vendored
88
.github/workflows/build.yml
vendored
@@ -18,70 +18,72 @@ 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/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- 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: 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: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Npm Install
|
||||
run: npm install
|
||||
- name: Npm Install
|
||||
run: npm install
|
||||
|
||||
- name: Linux Test Setup
|
||||
if: runner.os == 'Linux'
|
||||
run: source ./tests/scripts/setup_linux_env.sh
|
||||
- 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: MacOS Test Setup
|
||||
if: runner.os == 'macOS'
|
||||
run: source ./tests/scripts/setup_osx_env.sh
|
||||
|
||||
- name: Run
|
||||
run: npm run test
|
||||
- name: Run
|
||||
run: npm run test
|
||||
|
||||
env:
|
||||
CI: true
|
||||
env:
|
||||
CI: true
|
||||
|
||||
lint:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Npm Install
|
||||
run: npm install
|
||||
- name: Npm Install
|
||||
run: npm install
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
clang-formatter:
|
||||
format:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: DoozyX/clang-format-lint-action@v0.15
|
||||
with:
|
||||
source: 'core'
|
||||
extensions: 'js,ts'
|
||||
# This should be as close as possible to the version that the npm
|
||||
# package supports. This can be found by running:
|
||||
# npx clang-format --version.
|
||||
clangFormatVersion: 15
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
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,
|
||||
};
|
||||
22
README.md
22
README.md
@@ -1,6 +1,6 @@
|
||||
# Blockly
|
||||
|
||||
Google's Blockly is a library that adds a visual code editor to web and mobile apps. The Blockly editor uses interlocking, graphical blocks to represent code concepts like variables, logical expressions, loops, and more. It allows users to apply programming principles without having to worry about syntax or the intimidation of a blinking cursor on the command line. All code is free and open source.
|
||||
Google's Blockly is a library that adds a visual code editor to web and mobile apps. The Blockly editor uses interlocking, graphical blocks to represent code concepts like variables, logical expressions, loops, and more. It allows users to apply programming principles without having to worry about syntax or the intimidation of a blinking cursor on the command line. All code is free and open source.
|
||||
|
||||

|
||||
|
||||
@@ -8,13 +8,13 @@ Google's Blockly is a library that adds a visual code editor to web and mobile a
|
||||
|
||||
Blockly has many resources for learning how to use the library. Start at our [Google Developers Site](https://developers.google.com/blockly) to read the documentation on how to get started, configure Blockly, and integrate it into your application. The developers site also contains links to:
|
||||
|
||||
* [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
|
||||
Blockly](https://developers.google.com/blockly/registration). The questionnaire only takes
|
||||
a few minutes and will help us better support the Blockly community.
|
||||
|
||||
### Installing Blockly
|
||||
@@ -28,8 +28,9 @@ npm install blockly
|
||||
For more information on installing and using Blockly, see the [Getting Started article](https://developers.google.com/blockly/guides/get-started/web).
|
||||
|
||||
### 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.
|
||||
*/
|
||||
@@ -52,7 +49,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([
|
||||
{
|
||||
'type': 'colour_rgb',
|
||||
'message0':
|
||||
'%{BKY_COLOUR_RGB_TITLE} %{BKY_COLOUR_RGB_RED} %1 %{BKY_COLOUR_RGB_GREEN} %2 %{BKY_COLOUR_RGB_BLUE} %3',
|
||||
'%{BKY_COLOUR_RGB_TITLE} %{BKY_COLOUR_RGB_RED} %1 %{BKY_COLOUR_RGB_GREEN} %2 %{BKY_COLOUR_RGB_BLUE} %3',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_value',
|
||||
@@ -82,8 +79,9 @@ export const blocks = createBlockDefinitionsFromJsonArray([
|
||||
// Block for blending two colours together.
|
||||
{
|
||||
'type': 'colour_blend',
|
||||
'message0': '%{BKY_COLOUR_BLEND_TITLE} %{BKY_COLOUR_BLEND_COLOUR1} ' +
|
||||
'%1 %{BKY_COLOUR_BLEND_COLOUR2} %2 %{BKY_COLOUR_BLEND_RATIO} %3',
|
||||
'message0':
|
||||
'%{BKY_COLOUR_BLEND_TITLE} %{BKY_COLOUR_BLEND_COLOUR1} ' +
|
||||
'%1 %{BKY_COLOUR_BLEND_COLOUR2} %2 %{BKY_COLOUR_BLEND_RATIO} %3',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_value',
|
||||
|
||||
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
|
||||
@@ -289,14 +275,33 @@ const TOOLTIPS_BY_OP = {
|
||||
};
|
||||
|
||||
Extensions.register(
|
||||
'logic_op_tooltip',
|
||||
Extensions.buildTooltipForDropdown('OP', TOOLTIPS_BY_OP));
|
||||
'logic_op_tooltip',
|
||||
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,86 +519,107 @@ 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) {
|
||||
reconnectChildBlocks_: function (
|
||||
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() {
|
||||
if (!this.elseifCount_ && !this.elseCount_) {
|
||||
return Msg['CONTROLS_IF_TOOLTIP_1'];
|
||||
} else if (!this.elseifCount_ && this.elseCount_) {
|
||||
return Msg['CONTROLS_IF_TOOLTIP_2'];
|
||||
} else if (this.elseifCount_ && !this.elseCount_) {
|
||||
return Msg['CONTROLS_IF_TOOLTIP_3'];
|
||||
} else if (this.elseifCount_ && this.elseCount_) {
|
||||
return Msg['CONTROLS_IF_TOOLTIP_4'];
|
||||
}
|
||||
return '';
|
||||
}.bind(this));
|
||||
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_) {
|
||||
return Msg['CONTROLS_IF_TOOLTIP_2'];
|
||||
} else if (this.elseifCount_ && !this.elseCount_) {
|
||||
return Msg['CONTROLS_IF_TOOLTIP_3'];
|
||||
} else if (this.elseifCount_ && this.elseCount_) {
|
||||
return Msg['CONTROLS_IF_TOOLTIP_4'];
|
||||
}
|
||||
return '';
|
||||
}.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 &&
|
||||
!this.workspace.connectionChecker.doTypeChecks(
|
||||
blockA.outputConnection, blockB.outputConnection)) {
|
||||
// Disconnect blocks that existed prior to this change if they don't
|
||||
// match.
|
||||
if (
|
||||
blockA &&
|
||||
blockB &&
|
||||
!this.workspace.connectionChecker.doTypeChecks(
|
||||
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 &&
|
||||
!block.workspace.connectionChecker.doTypeChecks(
|
||||
block.outputConnection, parentConnection)) {
|
||||
// Ensure that any disconnections are grouped with the causing event.
|
||||
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.
|
||||
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': [{
|
||||
'type': 'input_value',
|
||||
'name': 'TIMES',
|
||||
'check': 'Number',
|
||||
}],
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'TIMES',
|
||||
'check': 'Number',
|
||||
},
|
||||
],
|
||||
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
|
||||
'args1': [{
|
||||
'type': 'input_statement',
|
||||
'name': 'DO',
|
||||
}],
|
||||
'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': [{
|
||||
'type': 'field_number',
|
||||
'name': 'TIMES',
|
||||
'value': 10,
|
||||
'min': 0,
|
||||
'precision': 1,
|
||||
}],
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_number',
|
||||
'name': 'TIMES',
|
||||
'value': 10,
|
||||
'min': 0,
|
||||
'precision': 1,
|
||||
},
|
||||
],
|
||||
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
|
||||
'args1': [{
|
||||
'type': 'input_statement',
|
||||
'name': 'DO',
|
||||
}],
|
||||
'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': [{
|
||||
'type': 'input_statement',
|
||||
'name': 'DO',
|
||||
}],
|
||||
'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': [{
|
||||
'type': 'input_statement',
|
||||
'name': 'DO',
|
||||
}],
|
||||
'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': [{
|
||||
'type': 'input_statement',
|
||||
'name': 'DO',
|
||||
}],
|
||||
'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': [{
|
||||
'type': 'field_dropdown',
|
||||
'name': 'FLOW',
|
||||
'options': [
|
||||
['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}', 'BREAK'],
|
||||
['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}', 'CONTINUE'],
|
||||
],
|
||||
}],
|
||||
'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}',
|
||||
@@ -226,13 +226,14 @@ const WHILE_UNTIL_TOOLTIPS = {
|
||||
};
|
||||
|
||||
Extensions.register(
|
||||
'controls_whileUntil_tooltip',
|
||||
Extensions.buildTooltipForDropdown('MODE', WHILE_UNTIL_TOOLTIPS));
|
||||
'controls_whileUntil_tooltip',
|
||||
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}',
|
||||
@@ -240,55 +241,66 @@ const BREAK_CONTINUE_TOOLTIPS = {
|
||||
};
|
||||
|
||||
Extensions.register(
|
||||
'controls_flow_tooltip',
|
||||
Extensions.buildTooltipForDropdown('FLOW', BREAK_CONTINUE_TOOLTIPS));
|
||||
'controls_flow_tooltip',
|
||||
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);
|
||||
'contextMenu_newGetVariableBlock',
|
||||
CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN
|
||||
);
|
||||
|
||||
Extensions.register(
|
||||
'controls_for_tooltip',
|
||||
Extensions.buildTooltipWithFieldText('%{BKY_CONTROLS_FOR_TOOLTIP}', 'VAR'));
|
||||
'controls_for_tooltip',
|
||||
Extensions.buildTooltipWithFieldText('%{BKY_CONTROLS_FOR_TOOLTIP}', 'VAR')
|
||||
);
|
||||
|
||||
Extensions.register(
|
||||
'controls_forEach_tooltip',
|
||||
Extensions.buildTooltipWithFieldText(
|
||||
'%{BKY_CONTROLS_FOREACH_TOOLTIP}', 'VAR'));
|
||||
'controls_forEach_tooltip',
|
||||
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': [{
|
||||
'type': 'field_number',
|
||||
'name': 'NUM',
|
||||
'value': 0,
|
||||
}],
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_number',
|
||||
'name': 'NUM',
|
||||
'value': 0,
|
||||
},
|
||||
],
|
||||
'output': 'Number',
|
||||
'helpUrl': '%{BKY_MATH_NUMBER_HELPURL}',
|
||||
'style': 'math_blocks',
|
||||
@@ -428,12 +426,13 @@ const TOOLTIPS_BY_OP = {
|
||||
};
|
||||
|
||||
Extensions.register(
|
||||
'math_op_tooltip',
|
||||
Extensions.buildTooltipForDropdown('OP', TOOLTIPS_BY_OP));
|
||||
'math_op_tooltip',
|
||||
Extensions.buildTooltipForDropdown('OP', TOOLTIPS_BY_OP)
|
||||
);
|
||||
|
||||
/** Type of a block that has IS_DIVISBLEBY_MUTATOR_MIXIN */
|
||||
type DivisiblebyBlock = Block&DivisiblebyMixin;
|
||||
interface DivisiblebyMixin extends DivisiblebyMixinType {};
|
||||
type DivisiblebyBlock = Block & DivisiblebyMixin;
|
||||
interface DivisiblebyMixin extends DivisiblebyMixinType {}
|
||||
type DivisiblebyMixinType = typeof IS_DIVISIBLEBY_MUTATOR_MIXIN;
|
||||
|
||||
/**
|
||||
@@ -451,9 +450,9 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
|
||||
*
|
||||
* @returns XML storage element.
|
||||
*/
|
||||
mutationToDom: function(this: DivisiblebyBlock): Element {
|
||||
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;
|
||||
},
|
||||
@@ -463,8 +462,8 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
|
||||
*
|
||||
* @param xmlElement XML storage element.
|
||||
*/
|
||||
domToMutation: function(this: DivisiblebyBlock, xmlElement: Element) {
|
||||
const divisorInput = (xmlElement.getAttribute('divisor_input') === 'true');
|
||||
domToMutation: function (this: DivisiblebyBlock, xmlElement: Element) {
|
||||
const divisorInput = xmlElement.getAttribute('divisor_input') === 'true';
|
||||
this.updateShape_(divisorInput);
|
||||
},
|
||||
|
||||
@@ -478,7 +477,7 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
|
||||
*
|
||||
* @param divisorInput True if this block has a divisor input.
|
||||
*/
|
||||
updateShape_: function(this: DivisiblebyBlock, divisorInput: boolean) {
|
||||
updateShape_: function (this: DivisiblebyBlock, divisorInput: boolean) {
|
||||
// Add or remove a Value Input.
|
||||
const inputExists = this.getInput('DIVISOR');
|
||||
if (divisorInput) {
|
||||
@@ -496,28 +495,32 @@ const IS_DIVISIBLEBY_MUTATOR_MIXIN = {
|
||||
* can update the block shape (add/remove divisor input) based on whether
|
||||
* property is "divisible by".
|
||||
*/
|
||||
const IS_DIVISIBLE_MUTATOR_EXTENSION = function(this: DivisiblebyBlock) {
|
||||
const IS_DIVISIBLE_MUTATOR_EXTENSION = function (this: DivisiblebyBlock) {
|
||||
this.getField('PROPERTY')!.setValidator(
|
||||
/** @param option The selected dropdown option. */
|
||||
function(this: FieldDropdown, option: string) {
|
||||
const divisorInput = (option === 'DIVISIBLE_BY');
|
||||
(this.getSourceBlock() as DivisiblebyBlock).updateShape_(divisorInput);
|
||||
return undefined; // FieldValidators can't be void. Use option as-is.
|
||||
});
|
||||
/** @param option The selected dropdown option. */
|
||||
function (this: FieldDropdown, option: string) {
|
||||
const divisorInput = option === 'DIVISIBLE_BY';
|
||||
(this.getSourceBlock() as DivisiblebyBlock).updateShape_(divisorInput);
|
||||
return undefined; // FieldValidators can't be void. Use option as-is.
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
Extensions.registerMutator(
|
||||
'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'));
|
||||
'math_change_tooltip',
|
||||
Extensions.buildTooltipWithFieldText('%{BKY_MATH_CHANGE_TOOLTIP}', 'VAR')
|
||||
);
|
||||
|
||||
/** Type of a block that has LIST_MODES_MUTATOR_MIXIN */
|
||||
type ListModesBlock = Block&ListModesMixin;
|
||||
interface ListModesMixin extends ListModesMixinType {};
|
||||
type ListModesBlock = Block & ListModesMixin;
|
||||
interface ListModesMixin extends ListModesMixinType {}
|
||||
type ListModesMixinType = typeof LIST_MODES_MUTATOR_MIXIN;
|
||||
|
||||
/**
|
||||
@@ -530,7 +533,7 @@ const LIST_MODES_MUTATOR_MIXIN = {
|
||||
*
|
||||
* @param newOp Either 'MODE' or some op than returns a number.
|
||||
*/
|
||||
updateType_: function(this: ListModesBlock, newOp: string) {
|
||||
updateType_: function (this: ListModesBlock, newOp: string) {
|
||||
if (newOp === 'MODE') {
|
||||
this.outputConnection!.setCheck('Array');
|
||||
} else {
|
||||
@@ -543,7 +546,7 @@ const LIST_MODES_MUTATOR_MIXIN = {
|
||||
*
|
||||
* @returns XML storage element.
|
||||
*/
|
||||
mutationToDom: function(this: ListModesBlock): Element {
|
||||
mutationToDom: function (this: ListModesBlock): Element {
|
||||
const container = xmlUtils.createElement('mutation');
|
||||
container.setAttribute('op', this.getFieldValue('OP'));
|
||||
return container;
|
||||
@@ -554,7 +557,7 @@ const LIST_MODES_MUTATOR_MIXIN = {
|
||||
*
|
||||
* @param xmlElement XML storage element.
|
||||
*/
|
||||
domToMutation: function(this: ListModesBlock, xmlElement: Element) {
|
||||
domToMutation: function (this: ListModesBlock, xmlElement: Element) {
|
||||
const op = xmlElement.getAttribute('op');
|
||||
if (op === null) throw new TypeError('xmlElement had no op attribute');
|
||||
this.updateType_(op);
|
||||
@@ -570,17 +573,20 @@ const LIST_MODES_MUTATOR_MIXIN = {
|
||||
* Extension to 'math_on_list' blocks that allows support of
|
||||
* modes operation (outputs a list of numbers).
|
||||
*/
|
||||
const LIST_MODES_MUTATOR_EXTENSION = function(this: ListModesBlock) {
|
||||
const LIST_MODES_MUTATOR_EXTENSION = function (this: ListModesBlock) {
|
||||
this.getField('OP')!.setValidator(
|
||||
function(this: ListModesBlock, newOp: string) {
|
||||
this.updateType_(newOp);
|
||||
return undefined;
|
||||
}.bind(this));
|
||||
function (this: ListModesBlock, newOp: string) {
|
||||
this.updateType_(newOp);
|
||||
return undefined;
|
||||
}.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) {
|
||||
return function() {
|
||||
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) {
|
||||
return function() {
|
||||
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);
|
||||
'contextMenu_variableSetterGetter',
|
||||
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': [{
|
||||
'type': 'field_variable',
|
||||
'name': 'VAR',
|
||||
'variable': '%{BKY_VARIABLES_DEFAULT_NAME}',
|
||||
}],
|
||||
'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) {
|
||||
return function() {
|
||||
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) {
|
||||
return function() {
|
||||
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);
|
||||
'contextMenu_variableDynamicSetterGetter',
|
||||
CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN
|
||||
);
|
||||
|
||||
// Register provided blocks.
|
||||
defineBlocks(blocks);
|
||||
678
core/block.ts
678
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;
|
||||
@@ -21,11 +20,10 @@ interface CloneRect {
|
||||
}
|
||||
|
||||
/** PID of disconnect UI animation. There can only be one at a time. */
|
||||
let disconnectPid: ReturnType<typeof setTimeout>|null = null;
|
||||
let disconnectPid: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/** The wobbling block. There can only be one at a time. */
|
||||
let wobblingBlock: BlockSvg|null = null;
|
||||
|
||||
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 + ')');
|
||||
'transform',
|
||||
'translate(' + x + ',' + y + ')' + ' scale(' + scale + ')'
|
||||
);
|
||||
setTimeout(disposeUiStep, 10, clone, rect, rtl, start, workspaceScale);
|
||||
}
|
||||
}
|
||||
@@ -92,7 +98,7 @@ export function connectionUiEffect(block: BlockSvg) {
|
||||
const scale = workspace.scale;
|
||||
workspace.getAudioManager().play('click');
|
||||
if (scale < 1) {
|
||||
return; // Too small to care about visual effects.
|
||||
return; // Too small to care about visual effects.
|
||||
}
|
||||
// Determine the absolute coordinates of the inferior block.
|
||||
const xy = workspace.getSvgXY(block.getSvgRoot());
|
||||
@@ -105,15 +111,17 @@ export function connectionUiEffect(block: BlockSvg) {
|
||||
xy.y += 3 * scale;
|
||||
}
|
||||
const ripple = dom.createSvgElement(
|
||||
Svg.CIRCLE, {
|
||||
'cx': xy.x,
|
||||
'cy': xy.y,
|
||||
'r': 0,
|
||||
'fill': 'none',
|
||||
'stroke': '#888',
|
||||
'stroke-width': 10,
|
||||
},
|
||||
workspace.getParentSvg());
|
||||
Svg.CIRCLE,
|
||||
{
|
||||
'cx': xy.x,
|
||||
'cy': xy.y,
|
||||
'r': 0,
|
||||
'fill': 'none',
|
||||
'stroke': '#888',
|
||||
'stroke-width': 10,
|
||||
},
|
||||
workspace.getParentSvg()
|
||||
);
|
||||
// Start the animation.
|
||||
connectionUiStep(ripple, new Date(), scale);
|
||||
}
|
||||
@@ -147,13 +155,13 @@ export function disconnectUiEffect(block: BlockSvg) {
|
||||
disconnectUiStop();
|
||||
block.workspace.getAudioManager().play('disconnect');
|
||||
if (block.workspace.scale < 1) {
|
||||
return; // Too small to care about visual effects.
|
||||
return; // Too small to care about visual effects.
|
||||
}
|
||||
// Horizontal distance for bottom of block to wiggle.
|
||||
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;
|
||||
}
|
||||
@@ -170,8 +178,8 @@ export function disconnectUiEffect(block: BlockSvg) {
|
||||
* @param start Date of animation's start.
|
||||
*/
|
||||
function disconnectUiStep(block: BlockSvg, magnitude: number, start: Date) {
|
||||
const DURATION = 200; // Milliseconds.
|
||||
const WIGGLES = 3; // Half oscillations.
|
||||
const DURATION = 200; // Milliseconds.
|
||||
const WIGGLES = 3; // Half oscillations.
|
||||
|
||||
const ms = new Date().getTime() - start.getTime();
|
||||
const percent = ms / DURATION;
|
||||
@@ -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, {
|
||||
'xmlns': dom.SVG_NS,
|
||||
'xmlns:html': dom.HTML_NS,
|
||||
'xmlns:xlink': dom.XLINK_NS,
|
||||
'version': '1.1',
|
||||
'class': 'blocklyBlockDragSurface',
|
||||
},
|
||||
this.container);
|
||||
Svg.SVG,
|
||||
{
|
||||
'xmlns': dom.SVG_NS,
|
||||
'xmlns:html': dom.HTML_NS,
|
||||
'xmlns:xlink': dom.XLINK_NS,
|
||||
'version': '1.1',
|
||||
'class': 'blocklyBlockDragSurface',
|
||||
},
|
||||
this.container
|
||||
);
|
||||
|
||||
this.dragGroup = dom.createSvgElement(Svg.G, {}, this.svg);
|
||||
}
|
||||
@@ -120,8 +121,9 @@ export class BlockDragSurfaceSvg {
|
||||
this.childSurfaceXY.x = roundX;
|
||||
this.childSurfaceXY.y = roundY;
|
||||
this.dragGroup.setAttribute(
|
||||
'transform',
|
||||
'translate(' + roundX + ',' + roundY + ') scale(' + scale + ')');
|
||||
'transform',
|
||||
'translate(' + roundX + ',' + roundY + ') scale(' + scale + ')'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,7 +202,7 @@ export class BlockDragSurfaceSvg {
|
||||
*
|
||||
* @returns Drag surface block DOM element, or null if no blocks exist.
|
||||
*/
|
||||
getCurrentBlock(): Element|null {
|
||||
getCurrentBlock(): Element | null {
|
||||
return this.dragGroup.firstChild as Element;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -44,7 +44,7 @@ export class BlockDragger implements IBlockDragger {
|
||||
protected workspace_: WorkspaceSvg;
|
||||
|
||||
/** Which drag area the mouse pointer is over, if any. */
|
||||
private dragTarget_: IDragTarget|null = null;
|
||||
private dragTarget_: IDragTarget | null = null;
|
||||
|
||||
/** Whether the block would be deleted if dropped immediately. */
|
||||
protected wouldDeleteBlock_ = false;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,9 +133,11 @@ export class BlockDragger implements IBlockDragger {
|
||||
*/
|
||||
protected shouldDisconnect_(healStack: boolean): boolean {
|
||||
return !!(
|
||||
this.draggingBlock_.getParent() ||
|
||||
healStack && this.draggingBlock_.nextConnection &&
|
||||
this.draggingBlock_.nextConnection.targetBlock());
|
||||
this.draggingBlock_.getParent() ||
|
||||
(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_ &&
|
||||
this.dragTarget_.shouldPreventMove(this.draggingBlock_);
|
||||
let newLoc: Coordinate;
|
||||
let delta: Coordinate|null = null;
|
||||
if (preventMove) {
|
||||
newLoc = this.startXY_;
|
||||
} else {
|
||||
const preventMove =
|
||||
!!this.dragTarget_ &&
|
||||
this.dragTarget_.shouldPreventMove(this.draggingBlock_);
|
||||
let delta: Coordinate | null = null;
|
||||
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,15 +238,17 @@ 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
|
||||
// bounds.
|
||||
bumpObjects.bumpIntoBounds(
|
||||
this.draggingBlock_.workspace,
|
||||
this.workspace_.getMetricsManager().getScrollMetrics(true),
|
||||
this.draggingBlock_);
|
||||
this.draggingBlock_.workspace,
|
||||
this.workspace_.getMetricsManager().getScrollMetrics(true),
|
||||
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,21 +329,25 @@ 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);
|
||||
// AnyDuringMigration because: Property 'addStyle' does not exist on
|
||||
// type 'IToolbox'.
|
||||
} else if (
|
||||
!isEnd &&
|
||||
typeof (toolbox as AnyDuringMigration).addStyle === 'function') {
|
||||
!isEnd &&
|
||||
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);
|
||||
@@ -373,8 +386,9 @@ 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.x / this.workspace_.scale,
|
||||
pixelCoord.y / this.workspace_.scale
|
||||
);
|
||||
if (this.workspace_.isMutator) {
|
||||
// If we're in a mutator, its scale is always 1, purely because of some
|
||||
// oddities in our rendering optimizations. The actual scale is the same
|
||||
@@ -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[]'.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
371
core/blockly.ts
371
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;
|
||||
@@ -491,7 +396,7 @@ export const VARIABLE_CATEGORY_NAME: string = Variables.CATEGORY_NAME;
|
||||
* variable blocks.
|
||||
*/
|
||||
export const VARIABLE_DYNAMIC_CATEGORY_NAME: string =
|
||||
VariablesDynamic.CATEGORY_NAME;
|
||||
VariablesDynamic.CATEGORY_NAME;
|
||||
/**
|
||||
* String for use in the "custom" attribute of a category in toolbox XML.
|
||||
* This string indicates that the category should be dynamically populated with
|
||||
@@ -499,58 +404,64 @@ 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 {
|
||||
return new Block(this, prototypeName, opt_id);
|
||||
};
|
||||
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 {
|
||||
return new BlockSvg(this, prototypeName, opt_id);
|
||||
};
|
||||
WorkspaceSvg.prototype.newBlock = function (
|
||||
prototypeName: string,
|
||||
opt_id?: string
|
||||
): BlockSvg {
|
||||
return new BlockSvg(this, prototypeName, opt_id);
|
||||
};
|
||||
|
||||
WorkspaceSvg.newTrashcan = function(workspace: WorkspaceSvg): Trashcan {
|
||||
WorkspaceSvg.newTrashcan = function (workspace: WorkspaceSvg): Trashcan {
|
||||
return new Trashcan(workspace);
|
||||
};
|
||||
|
||||
WorkspaceCommentSvg.prototype.showContextMenu =
|
||||
function(this: WorkspaceCommentSvg, e: Event) {
|
||||
if (this.workspace.options.readOnly) {
|
||||
return;
|
||||
}
|
||||
const menuOptions = [];
|
||||
WorkspaceCommentSvg.prototype.showContextMenu = function (
|
||||
this: WorkspaceCommentSvg,
|
||||
e: Event
|
||||
) {
|
||||
if (this.workspace.options.readOnly) {
|
||||
return;
|
||||
}
|
||||
const menuOptions = [];
|
||||
|
||||
if (this.isDeletable() && this.isMovable()) {
|
||||
menuOptions.push(ContextMenu.commentDuplicateOption(this));
|
||||
menuOptions.push(ContextMenu.commentDeleteOption(this));
|
||||
}
|
||||
if (this.isDeletable() && this.isMovable()) {
|
||||
menuOptions.push(ContextMenu.commentDuplicateOption(this));
|
||||
menuOptions.push(ContextMenu.commentDeleteOption(this));
|
||||
}
|
||||
|
||||
ContextMenu.show(e, menuOptions, this.RTL);
|
||||
};
|
||||
ContextMenu.show(e, menuOptions, this.RTL);
|
||||
};
|
||||
|
||||
Mutator.prototype.newWorkspaceSvg =
|
||||
function(options: Options): WorkspaceSvg {
|
||||
return new WorkspaceSvg(options);
|
||||
};
|
||||
MiniWorkspaceBubble.prototype.newWorkspaceSvg = function (
|
||||
options: Options
|
||||
): WorkspaceSvg {
|
||||
return new WorkspaceSvg(options);
|
||||
};
|
||||
|
||||
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]);
|
||||
for (let i = 0; i < flattenedProcedures.length; i++) {
|
||||
this.getName(flattenedProcedures[i][0], Names.NameType.PROCEDURE);
|
||||
}
|
||||
};
|
||||
Names.prototype.populateProcedures = function (
|
||||
this: Names,
|
||||
workspace: Workspace
|
||||
) {
|
||||
const procedures = Procedures.allProcedures(workspace);
|
||||
// Flatten the return vs no-return procedure lists.
|
||||
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};
|
||||
@@ -664,9 +575,8 @@ export {Flyout};
|
||||
export {FlyoutButton};
|
||||
export {FlyoutMetricsManager};
|
||||
export {CodeGenerator};
|
||||
export {CodeGenerator as Generator}; // Deprecated name, October 2022.
|
||||
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.
|
||||
*/
|
||||
@@ -32,14 +31,14 @@ export interface BlocklyOptions {
|
||||
renderer?: string;
|
||||
rendererOverrides?: {[rendererConstant: string]: any};
|
||||
rtl?: boolean;
|
||||
scrollbars?: ScrollbarOptions|boolean;
|
||||
scrollbars?: ScrollbarOptions | boolean;
|
||||
sounds?: boolean;
|
||||
theme?: Theme|string|ITheme;
|
||||
toolbox?: string|ToolboxDefinition|Element;
|
||||
theme?: Theme | string | ITheme;
|
||||
toolbox?: string | ToolboxDefinition | Element;
|
||||
toolboxPosition?: string;
|
||||
trashcan?: boolean;
|
||||
maxTrashcanContents?: number;
|
||||
plugins?: {[key: string]: (new(...p1: any[]) => any)|string};
|
||||
plugins?: {[key: string]: (new (...p1: any[]) => any) | string};
|
||||
zoom?: ZoomOptions;
|
||||
parentWorkspace?: WorkspaceSvg;
|
||||
}
|
||||
@@ -53,7 +52,7 @@ export interface GridOptions {
|
||||
|
||||
export interface MoveOptions {
|
||||
drag?: boolean;
|
||||
scrollbars?: boolean|ScrollbarOptions;
|
||||
scrollbars?: boolean | ScrollbarOptions;
|
||||
wheel?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,17 +226,17 @@ 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.
|
||||
case 0x00: // Pixel mode.
|
||||
default:
|
||||
return {x: e.deltaX, y: e.deltaY};
|
||||
case 0x01: // Line mode.
|
||||
case 0x01: // Line mode.
|
||||
return {
|
||||
x: e.deltaX * LINE_MODE_MULTIPLIER,
|
||||
y: e.deltaY * LINE_MODE_MULTIPLIER,
|
||||
};
|
||||
case 0x02: // Page mode.
|
||||
case 0x02: // Page mode.
|
||||
return {
|
||||
x: e.deltaX * PAGE_MODE_MULTIPLIER,
|
||||
y: e.deltaY * PAGE_MODE_MULTIPLIER,
|
||||
|
||||
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
|
||||
@@ -31,12 +29,11 @@ import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
*/
|
||||
export class BubbleDragger {
|
||||
/** Which drag target the mouse pointer is over, if any. */
|
||||
private dragTarget_: IDragTarget|null = null;
|
||||
private dragTarget_: IDragTarget | null = null;
|
||||
|
||||
/** Whether the bubble would be deleted if dropped immediately. */
|
||||
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);
|
||||
@@ -116,11 +103,13 @@ export class BubbleDragger {
|
||||
* @param dragTarget The drag target that the bubblee is currently over.
|
||||
* @returns Whether dropping the bubble immediately would delete the block.
|
||||
*/
|
||||
private shouldDelete_(dragTarget: IDragTarget|null): boolean {
|
||||
private shouldDelete_(dragTarget: IDragTarget | null): boolean {
|
||||
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);
|
||||
}
|
||||
@@ -149,7 +138,7 @@ export class BubbleDragger {
|
||||
this.dragBubble(e, currentDragDeltaXY);
|
||||
|
||||
const preventMove =
|
||||
this.dragTarget_ && this.dragTarget_.shouldPreventMove(this.bubble);
|
||||
this.dragTarget_ && this.dragTarget_.shouldPreventMove(this.bubble);
|
||||
let newLoc;
|
||||
if (preventMove) {
|
||||
newLoc = this.startXY_;
|
||||
@@ -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);
|
||||
@@ -207,8 +194,9 @@ export class BubbleDragger {
|
||||
*/
|
||||
private pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate {
|
||||
const result = new Coordinate(
|
||||
pixelCoord.x / this.workspace.scale,
|
||||
pixelCoord.y / this.workspace.scale);
|
||||
pixelCoord.x / this.workspace.scale,
|
||||
pixelCoord.y / this.workspace.scale
|
||||
);
|
||||
if (this.workspace.isMutator) {
|
||||
// If we're in a mutator, its scale is always 1, purely because of some
|
||||
// oddities in our rendering optimizations. The actual scale is the same
|
||||
|
||||
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.');
|
||||
'Moved object in bounds but there was no' +
|
||||
' 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,9 +9,8 @@ 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;
|
||||
let copyData: CopyData | null = null;
|
||||
|
||||
/**
|
||||
* Copy a block or workspace comment onto the local clipboard.
|
||||
@@ -36,7 +35,7 @@ function copyInternal(toCopy: ICopyable) {
|
||||
* @returns The pasted thing if the paste was successful, null otherwise.
|
||||
* @internal
|
||||
*/
|
||||
export function paste(): ICopyable|null {
|
||||
export function paste(): ICopyable | null {
|
||||
if (!copyData) {
|
||||
return 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;
|
||||
@@ -61,18 +62,18 @@ export function paste(): ICopyable|null {
|
||||
* duplication failed.
|
||||
* @internal
|
||||
*/
|
||||
export function duplicate(toDuplicate: ICopyable): ICopyable|null {
|
||||
export function duplicate(toDuplicate: ICopyable): ICopyable | null {
|
||||
return TEST_ONLY.duplicateInternal(toDuplicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private version of duplicate for stubbing in tests.
|
||||
*/
|
||||
function duplicateInternal(toDuplicate: ICopyable): ICopyable|null {
|
||||
function duplicateInternal(toDuplicate: ICopyable): ICopyable | null {
|
||||
const oldCopyData = copyData;
|
||||
copy(toDuplicate);
|
||||
const pastedThing =
|
||||
toDuplicate.toCopyData()?.source?.paste(copyData!.saveInfo) ?? null;
|
||||
toDuplicate.toCopyData()?.source?.paste(copyData!.saveInfo) ?? null;
|
||||
copyData = oldCopyData;
|
||||
return pastedThing;
|
||||
}
|
||||
|
||||
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,18 +15,16 @@ 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.
|
||||
*
|
||||
* @param id ID of workspace to find.
|
||||
* @returns The sought after workspace or null if not found.
|
||||
*/
|
||||
export function getWorkspaceById(id: string): Workspace|null {
|
||||
export function getWorkspaceById(id: string): Workspace | null {
|
||||
return WorkspaceDB_[id] || null;
|
||||
}
|
||||
|
||||
@@ -90,12 +88,12 @@ export function setMainWorkspace(workspace: Workspace) {
|
||||
/**
|
||||
* Currently selected copyable object.
|
||||
*/
|
||||
let selected: ICopyable|null = null;
|
||||
let selected: ICopyable | null = null;
|
||||
|
||||
/**
|
||||
* Returns the currently selected copyable object.
|
||||
*/
|
||||
export function getSelected(): ICopyable|null {
|
||||
export function getSelected(): ICopyable | null {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@@ -107,14 +105,14 @@ export function getSelected(): ICopyable|null {
|
||||
* @param newSelection The newly selected block.
|
||||
* @internal
|
||||
*/
|
||||
export function setSelected(newSelection: ICopyable|null) {
|
||||
export function setSelected(newSelection: ICopyable | null) {
|
||||
selected = newSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container element in which to render the WidgetDiv, DropDownDiv and Tooltip.
|
||||
*/
|
||||
let parentContainer: Element|null;
|
||||
let parentContainer: Element | null;
|
||||
|
||||
/**
|
||||
* Get the container element in which to render the WidgetDiv, DropDownDiv and
|
||||
@@ -122,7 +120,7 @@ let parentContainer: Element|null;
|
||||
*
|
||||
* @returns The parent container.
|
||||
*/
|
||||
export function getParentContainer(): Element|null {
|
||||
export function getParentContainer(): Element | null {
|
||||
return parentContainer;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -218,7 +218,7 @@ export function getBlockTypeCounts(
|
||||
* of jsonDef.
|
||||
*/
|
||||
function jsonInitFactory(jsonDef: AnyDuringMigration): () => void {
|
||||
return function(this: Block) {
|
||||
return function (this: Block) {
|
||||
this.jsonInit(jsonDef);
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
@@ -260,8 +261,9 @@ export function createBlockDefinitionsFromJsonArray(
|
||||
const type = elem['type'];
|
||||
if (!type) {
|
||||
console.warn(
|
||||
`Block definition #${i} in JSON array is missing a type attribute. ` +
|
||||
'Skipping.');
|
||||
`Block definition #${i} in JSON array is missing a type attribute. ` +
|
||||
'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 = [];
|
||||
@@ -107,15 +110,20 @@ export class ComponentManager {
|
||||
* @param id The ID of the component to add the capability to.
|
||||
* @param capability The capability to add.
|
||||
*/
|
||||
addCapability<T>(id: string, capability: string|Capability<T>) {
|
||||
addCapability<T>(id: string, capability: string | Capability<T>) {
|
||||
if (!this.getComponent(id)) {
|
||||
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();
|
||||
@@ -129,16 +137,24 @@ export class ComponentManager {
|
||||
* @param id The ID of the component to remove the capability from.
|
||||
* @param capability The capability to remove.
|
||||
*/
|
||||
removeCapability<T>(id: string, capability: string|Capability<T>) {
|
||||
removeCapability<T>(id: string, capability: string | Capability<T>) {
|
||||
if (!this.getComponent(id)) {
|
||||
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();
|
||||
@@ -153,10 +169,12 @@ export class ComponentManager {
|
||||
* @param capability The capability to check for.
|
||||
* @returns Whether the component has the capability.
|
||||
*/
|
||||
hasCapability<T>(id: string, capability: string|Capability<T>): boolean {
|
||||
hasCapability<T>(id: string, capability: string | Capability<T>): boolean {
|
||||
capability = `${capability}`.toLowerCase();
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,7 +183,7 @@ export class ComponentManager {
|
||||
* @param id The ID of the component to get.
|
||||
* @returns The component with the given name or undefined if not found.
|
||||
*/
|
||||
getComponent(id: string): IComponent|undefined {
|
||||
getComponent(id: string): IComponent | undefined {
|
||||
return this.componentData.get(id)?.component;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -189,10 +209,10 @@ export class ComponentManager {
|
||||
componentIds.forEach((id) => {
|
||||
componentDataList.push(this.componentData.get(id)!);
|
||||
});
|
||||
componentDataList.sort(function(a, b) {
|
||||
componentDataList.sort(function (a, b) {
|
||||
return a.weight - b.weight;
|
||||
});
|
||||
componentDataList.forEach(function(componentDatum) {
|
||||
componentDataList.forEach(function (componentDatum) {
|
||||
components.push(componentDatum.component as T);
|
||||
});
|
||||
} else {
|
||||
@@ -208,7 +228,7 @@ export namespace ComponentManager {
|
||||
/** An object storing component information. */
|
||||
export interface ComponentDatum {
|
||||
component: IComponent;
|
||||
capabilities: Array<string|Capability<IComponent>>;
|
||||
capabilities: Array<string | Capability<IComponent>>;
|
||||
weight: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -41,7 +40,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
protected sourceBlock_: Block;
|
||||
|
||||
/** Connection this connection connects to. Null if not connected. */
|
||||
targetConnection: Connection|null = null;
|
||||
targetConnection: Connection | null = null;
|
||||
|
||||
/**
|
||||
* Has this connection been disposed of?
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,7 +265,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
*/
|
||||
protected disconnectInternal(setParent = true) {
|
||||
const {parentConnection, childConnection} =
|
||||
this.getParentAndChildConnections();
|
||||
this.getParentAndChildConnections();
|
||||
if (!parentConnection || !childConnection) {
|
||||
throw Error('Source connection not connected.');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,7 +368,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
*
|
||||
* @returns The connected block or null if none is connected.
|
||||
*/
|
||||
targetBlock(): Block|null {
|
||||
targetBlock(): Block | null {
|
||||
if (this.isConnected()) {
|
||||
return this.targetConnection?.getSourceBlock() ?? null;
|
||||
}
|
||||
@@ -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() &&
|
||||
(!this.targetConnection ||
|
||||
!this.getConnectionChecker().canConnect(
|
||||
this, this.targetConnection, false))) {
|
||||
if (
|
||||
this.isConnected() &&
|
||||
(!this.targetConnection ||
|
||||
!this.getConnectionChecker().canConnect(
|
||||
this,
|
||||
this.targetConnection,
|
||||
false
|
||||
))
|
||||
) {
|
||||
const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
|
||||
child!.unplug();
|
||||
}
|
||||
@@ -355,15 +401,15 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* types are compatible.
|
||||
* @returns The connection being modified (to allow chaining).
|
||||
*/
|
||||
setCheck(check: string|string[]|null): Connection {
|
||||
setCheck(check: string | string[] | null): Connection {
|
||||
if (check) {
|
||||
if (!Array.isArray(check)) {
|
||||
check = [check];
|
||||
}
|
||||
this.check_ = check;
|
||||
this.check = check;
|
||||
this.onCheckChanged_();
|
||||
} else {
|
||||
this.check_ = null;
|
||||
this.check = null;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -374,8 +420,8 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* @returns List of compatible value types.
|
||||
* Null if all types are compatible.
|
||||
*/
|
||||
getCheck(): string[]|null {
|
||||
return this.check_;
|
||||
getCheck(): string[] | null {
|
||||
return this.check;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,8 +429,8 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
*
|
||||
* @param shadowDom DOM representation of a block or null.
|
||||
*/
|
||||
setShadowDom(shadowDom: Element|null) {
|
||||
this.setShadowStateInternal_({shadowDom});
|
||||
setShadowDom(shadowDom: Element | null) {
|
||||
this.setShadowStateInternal({shadowDom});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,10 +442,10 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* just returned.
|
||||
* @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_;
|
||||
getShadowDom(returnCurrent?: boolean): Element | null {
|
||||
return returnCurrent && this.targetBlock()!.isShadow()
|
||||
? (Xml.blockToDom(this.targetBlock() as Block) as Element)
|
||||
: this.shadowDom;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,8 +453,8 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
*
|
||||
* @param shadowState An state represetation of the block or null.
|
||||
*/
|
||||
setShadowState(shadowState: blocks.State|null) {
|
||||
this.setShadowStateInternal_({shadowState});
|
||||
setShadowState(shadowState: blocks.State | null) {
|
||||
this.setShadowStateInternal({shadowState});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -421,11 +467,11 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* returned.
|
||||
* @returns Serialized object representation of the block, or null.
|
||||
*/
|
||||
getShadowState(returnCurrent?: boolean): blocks.State|null {
|
||||
getShadowState(returnCurrent?: boolean): blocks.State | null {
|
||||
if (returnCurrent && this.targetBlock() && this.targetBlock()!.isShadow()) {
|
||||
return blocks.save(this.targetBlock() as Block);
|
||||
}
|
||||
return this.shadowState_;
|
||||
return this.shadowState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -452,7 +498,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
* exists.
|
||||
* @internal
|
||||
*/
|
||||
getParentInput(): Input|null {
|
||||
getParentInput(): Input | null {
|
||||
let parentInput = null;
|
||||
const inputs = this.sourceBlock_.inputList;
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
@@ -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,17 +741,19 @@ 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) {
|
||||
return null; // More than one connection.
|
||||
return null; // More than one connection.
|
||||
}
|
||||
foundConnection = connection;
|
||||
}
|
||||
@@ -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 {
|
||||
let newBlock: Block|null = startBlock;
|
||||
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 &&
|
||||
!this.doDragChecks(
|
||||
a as RenderedConnection, b as RenderedConnection,
|
||||
opt_distance || 0)) {
|
||||
if (
|
||||
isDragging &&
|
||||
!this.doDragChecks(
|
||||
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,8 +119,12 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
const connOne = a!;
|
||||
const connTwo = b!;
|
||||
let msg = 'Connection checks failed. ';
|
||||
msg += connOne + ' expected ' + connOne.getCheck() + ', found ' +
|
||||
connTwo.getCheck();
|
||||
msg +=
|
||||
connOne +
|
||||
' expected ' +
|
||||
connOne.getCheck() +
|
||||
', found ' +
|
||||
connTwo.getCheck();
|
||||
return msg;
|
||||
}
|
||||
case Connection.REASON_SHADOW_PARENT:
|
||||
@@ -128,7 +146,7 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
* @param b The second of the connections to check.
|
||||
* @returns An enum with the reason this connection is safe or unsafe.
|
||||
*/
|
||||
doSafetyChecks(a: Connection|null, b: Connection|null): number {
|
||||
doSafetyChecks(a: Connection | null, b: Connection | null): number {
|
||||
if (!a || !b) {
|
||||
return Connection.REASON_TARGET_NULL;
|
||||
}
|
||||
@@ -150,22 +168,25 @@ export class ConnectionChecker implements IConnectionChecker {
|
||||
if (superiorBlock === inferiorBlock) {
|
||||
return Connection.REASON_SELF_CONNECTION;
|
||||
} else if (
|
||||
inferiorConnection.type !==
|
||||
internalConstants.OPPOSITE_TYPE[superiorConnection.type]) {
|
||||
inferiorConnection.type !==
|
||||
internalConstants.OPPOSITE_TYPE[superiorConnection.type]
|
||||
) {
|
||||
return Connection.REASON_WRONG_TYPE;
|
||||
} else if (superiorBlock.workspace !== inferiorBlock.workspace) {
|
||||
return Connection.REASON_DIFFERENT_WORKSPACES;
|
||||
} else if (superiorBlock.isShadow() && !inferiorBlock.isShadow()) {
|
||||
return Connection.REASON_SHADOW_PARENT;
|
||||
} else if (
|
||||
inferiorConnection.type === ConnectionType.OUTPUT_VALUE &&
|
||||
inferiorBlock.previousConnection &&
|
||||
inferiorBlock.previousConnection.isConnected()) {
|
||||
inferiorConnection.type === ConnectionType.OUTPUT_VALUE &&
|
||||
inferiorBlock.previousConnection &&
|
||||
inferiorBlock.previousConnection.isConnected()
|
||||
) {
|
||||
return Connection.REASON_PREVIOUS_AND_OUTPUT;
|
||||
} else if (
|
||||
inferiorConnection.type === ConnectionType.PREVIOUS_STATEMENT &&
|
||||
inferiorBlock.outputConnection &&
|
||||
inferiorBlock.outputConnection.isConnected()) {
|
||||
inferiorConnection.type === ConnectionType.PREVIOUS_STATEMENT &&
|
||||
inferiorBlock.outputConnection &&
|
||||
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,11 +30,10 @@ 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?
|
||||
*/
|
||||
let currentBlock: Block|null = null;
|
||||
let currentBlock: Block | null = null;
|
||||
|
||||
const dummyOwner = {};
|
||||
|
||||
@@ -40,7 +42,7 @@ const dummyOwner = {};
|
||||
*
|
||||
* @returns The block the context menu is attached to.
|
||||
*/
|
||||
export function getCurrentBlock(): Block|null {
|
||||
export function getCurrentBlock(): Block | null {
|
||||
return currentBlock;
|
||||
}
|
||||
|
||||
@@ -49,14 +51,14 @@ export function getCurrentBlock(): Block|null {
|
||||
*
|
||||
* @param block The block the context menu is attached to.
|
||||
*/
|
||||
export function setCurrentBlock(block: Block|null) {
|
||||
export function setCurrentBlock(block: Block | null) {
|
||||
currentBlock = block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu object.
|
||||
*/
|
||||
let menu_: Menu|null = null;
|
||||
let menu_: Menu | null = null;
|
||||
|
||||
/**
|
||||
* Construct the menu based on the list of options and show the menu.
|
||||
@@ -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();
|
||||
@@ -79,10 +83,10 @@ export function show(
|
||||
position_(menu, e, rtl);
|
||||
// 1ms delay is required for focusing on context menus because some other
|
||||
// mouse event is still waiting in the queue and clears focus.
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
menu.focus();
|
||||
}, 1);
|
||||
currentBlock = null; // May be set by Blockly.Block.
|
||||
currentBlock = null; // May be set by Blockly.Block.
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +97,9 @@ export function show(
|
||||
* @returns The menu that will be shown on right click.
|
||||
*/
|
||||
function populate_(
|
||||
options: (ContextMenuOption|LegacyContextMenuOption)[],
|
||||
rtl: boolean): Menu {
|
||||
options: (ContextMenuOption | LegacyContextMenuOption)[],
|
||||
rtl: boolean
|
||||
): Menu {
|
||||
/* Here's what one option object looks like:
|
||||
{text: 'Make It So',
|
||||
enabled: true,
|
||||
@@ -110,7 +115,7 @@ function populate_(
|
||||
menu.addChild(menuItem);
|
||||
menuItem.setEnabled(option.enabled);
|
||||
if (option.enabled) {
|
||||
const actionHandler = function() {
|
||||
const actionHandler = function () {
|
||||
hide();
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
@@ -143,10 +148,11 @@ function position_(menu: Menu, e: Event, rtl: boolean) {
|
||||
// This one is just a point, but we'll pretend that it's a rect so we can use
|
||||
// some helper functions.
|
||||
const anchorBBox = new Rect(
|
||||
mouseEvent.clientY + viewportBBox.top,
|
||||
mouseEvent.clientY + viewportBBox.top,
|
||||
mouseEvent.clientX + viewportBBox.left,
|
||||
mouseEvent.clientX + viewportBBox.left);
|
||||
mouseEvent.clientY + viewportBBox.top,
|
||||
mouseEvent.clientY + viewportBBox.top,
|
||||
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,12 +266,13 @@ 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,
|
||||
callback: function() {
|
||||
callback: function () {
|
||||
eventUtils.setGroup(true);
|
||||
comment.dispose();
|
||||
eventUtils.setGroup(false);
|
||||
@@ -279,12 +290,13 @@ 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,
|
||||
callback: function() {
|
||||
callback: function () {
|
||||
clipboard.duplicate(comment);
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
@@ -322,8 +338,9 @@ export function workspaceCommentOption(
|
||||
// The client coordinates offset by the injection div's upper left corner.
|
||||
const mouseEvent = e as MouseEvent;
|
||||
const clientOffsetPixels = new Coordinate(
|
||||
mouseEvent.clientX - boundingRect.left,
|
||||
mouseEvent.clientY - boundingRect.top);
|
||||
mouseEvent.clientX - boundingRect.left,
|
||||
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);
|
||||
|
||||
@@ -350,7 +369,7 @@ export function workspaceCommentOption(
|
||||
enabled: true,
|
||||
} as ContextMenuOption;
|
||||
wsCommentOption.text = Msg['ADD_COMMENT'];
|
||||
wsCommentOption.callback = function() {
|
||||
wsCommentOption.callback = function () {
|
||||
addWsComment();
|
||||
};
|
||||
return wsCommentOption;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -111,7 +115,7 @@ function toggleOption_(shouldCollapse: boolean, topBlocks: BlockSvg[]) {
|
||||
}
|
||||
Events.setGroup(true);
|
||||
for (let i = 0; i < topBlocks.length; i++) {
|
||||
let block: BlockSvg|null = topBlocks[i];
|
||||
let block: BlockSvg | null = topBlocks[i];
|
||||
while (block) {
|
||||
timeoutCounter++;
|
||||
setTimeout(timeoutFn.bind(null, block), ms);
|
||||
@@ -133,7 +137,7 @@ export function registerCollapse() {
|
||||
if (scope.workspace!.options.collapse) {
|
||||
const topBlocks = scope.workspace!.getTopBlocks(false);
|
||||
for (let i = 0; i < topBlocks.length; i++) {
|
||||
let block: BlockSvg|null = topBlocks[i];
|
||||
let block: BlockSvg | null = topBlocks[i];
|
||||
while (block) {
|
||||
if (!block.isCollapsed()) {
|
||||
return 'enabled';
|
||||
@@ -167,7 +171,7 @@ export function registerExpand() {
|
||||
if (scope.workspace!.options.collapse) {
|
||||
const topBlocks = scope.workspace!.getTopBlocks(false);
|
||||
for (let i = 0; i < topBlocks.length; i++) {
|
||||
let block: BlockSvg|null = topBlocks[i];
|
||||
let block: BlockSvg | null = topBlocks[i];
|
||||
while (block) {
|
||||
if (block.isCollapsed()) {
|
||||
return 'enabled';
|
||||
@@ -280,13 +284,16 @@ export function registerDeleteAll() {
|
||||
deleteNext_(deletableBlocks);
|
||||
} else {
|
||||
dialog.confirm(
|
||||
Msg['DELETE_ALL_BLOCKS'].replace(
|
||||
'%1', String(deletableBlocks.length)),
|
||||
function(ok) {
|
||||
if (ok) {
|
||||
deleteNext_(deletableBlocks);
|
||||
}
|
||||
});
|
||||
Msg['DELETE_ALL_BLOCKS'].replace(
|
||||
'%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
|
||||
@@ -66,7 +65,7 @@ export class ContextMenuRegistry {
|
||||
* @param id The ID of the RegistryItem to get.
|
||||
* @returns RegistryItem or null if not found
|
||||
*/
|
||||
getItem(id: string): RegistryItem|null {
|
||||
getItem(id: string): RegistryItem | null {
|
||||
return this.registry_.get(id) ?? null;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
@@ -102,7 +104,7 @@ export class ContextMenuRegistry {
|
||||
}
|
||||
}
|
||||
}
|
||||
menuOptions.sort(function(a, b) {
|
||||
menuOptions.sort(function (a, b) {
|
||||
return a.weight - b.weight;
|
||||
});
|
||||
return menuOptions;
|
||||
@@ -135,7 +137,7 @@ export namespace ContextMenuRegistry {
|
||||
export interface RegistryItem {
|
||||
callback: (p1: Scope) => void;
|
||||
scopeType: ScopeType;
|
||||
displayText: ((p1: Scope) => string)|string;
|
||||
displayText: ((p1: Scope) => string) | string;
|
||||
preconditionFn: (p1: Scope) => string;
|
||||
weight: number;
|
||||
id: string;
|
||||
@@ -175,4 +177,4 @@ export type Scope = ContextMenuRegistry.Scope;
|
||||
export type RegistryItem = ContextMenuRegistry.RegistryItem;
|
||||
export type ContextMenuOption = ContextMenuRegistry.ContextMenuOption;
|
||||
export type LegacyContextMenuOption =
|
||||
ContextMenuRegistry.LegacyContextMenuOption;
|
||||
ContextMenuRegistry.LegacyContextMenuOption;
|
||||
|
||||
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,22 +7,28 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
let confirmImplementation = function(
|
||||
message: string, callback: (result: boolean) => void) {
|
||||
let confirmImplementation = function (
|
||||
message: string,
|
||||
callback: (result: boolean) => void
|
||||
) {
|
||||
callback(window.confirm(message));
|
||||
};
|
||||
|
||||
let promptImplementation = function(
|
||||
message: string, defaultValue: string,
|
||||
callback: (result: string|null) => void) {
|
||||
let promptImplementation = function (
|
||||
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.
|
||||
@@ -76,7 +75,7 @@ export class DragTarget implements IDragTarget {
|
||||
* @returns The component's bounding box. Null if drag target area should be
|
||||
* ignored.
|
||||
*/
|
||||
getClientRect(): Rect|null {
|
||||
getClientRect(): Rect | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -52,10 +51,10 @@ export const ANIMATION_TIME = 0.25;
|
||||
* Timer for animation out, to be cleared if we need to immediately hide
|
||||
* without disrupting new shows.
|
||||
*/
|
||||
let animateOutTimer: ReturnType<typeof setTimeout>|null = null;
|
||||
let animateOutTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/** Callback for when the drop-down is hidden. */
|
||||
let onHide: Function|null = null;
|
||||
let onHide: Function | null = null;
|
||||
|
||||
/** A class name representing the current owner's workspace renderer. */
|
||||
let renderedClassName = '';
|
||||
@@ -76,13 +75,13 @@ let arrow: HTMLDivElement;
|
||||
* Drop-downs will appear within the bounds of this element if possible.
|
||||
* Set in setBoundsElement.
|
||||
*/
|
||||
let boundsElement: Element|null = null;
|
||||
let boundsElement: Element | null = null;
|
||||
|
||||
/** The object currently using the drop-down. */
|
||||
let owner: Field|null = null;
|
||||
let owner: Field | null = null;
|
||||
|
||||
/** Whether the dropdown was positioned to a field or the source block. */
|
||||
let positionToField: boolean|null = null;
|
||||
let positionToField: boolean | null = null;
|
||||
|
||||
/**
|
||||
* Dropdown bounds info object used to encapsulate sizing information about a
|
||||
@@ -103,9 +102,9 @@ export interface PositionMetrics {
|
||||
initialY: number;
|
||||
finalX: number;
|
||||
finalY: number;
|
||||
arrowX: number|null;
|
||||
arrowY: number|null;
|
||||
arrowAtTop: boolean|null;
|
||||
arrowX: number | null;
|
||||
arrowY: number | null;
|
||||
arrowAtTop: boolean | null;
|
||||
arrowVisible: boolean;
|
||||
}
|
||||
|
||||
@@ -116,7 +115,7 @@ export interface PositionMetrics {
|
||||
*/
|
||||
export function createDom() {
|
||||
if (div) {
|
||||
return; // Already created.
|
||||
return; // Already created.
|
||||
}
|
||||
div = document.createElement('div');
|
||||
div.className = 'blocklyDropDownDiv';
|
||||
@@ -133,15 +132,15 @@ 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.
|
||||
div.addEventListener('focusin', function() {
|
||||
div.addEventListener('focusin', function () {
|
||||
dom.addClass(div, 'blocklyFocused');
|
||||
});
|
||||
div.addEventListener('focusout', function() {
|
||||
div.addEventListener('focusout', function () {
|
||||
dom.removeClass(div, 'blocklyFocused');
|
||||
});
|
||||
}
|
||||
@@ -152,14 +151,14 @@ export function createDom() {
|
||||
*
|
||||
* @param boundsElem Element to bind drop-down to.
|
||||
*/
|
||||
export function setBoundsElement(boundsElem: Element|null) {
|
||||
export function setBoundsElement(boundsElem: Element | null) {
|
||||
boundsElement = boundsElem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The field that currently owns this, or null.
|
||||
*/
|
||||
export function getOwner(): Field|null {
|
||||
export function getOwner(): Field | null {
|
||||
return owner;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -345,7 +370,7 @@ const internal = {
|
||||
* @returns An object containing size information about the bounding element
|
||||
* (bounding box and width/height).
|
||||
*/
|
||||
getBoundsInfo: function(): BoundsInfo {
|
||||
getBoundsInfo: function (): BoundsInfo {
|
||||
const boundPosition = style.getPageOffset(boundsElement as Element);
|
||||
const boundSize = style.getSize(boundsElement as Element);
|
||||
|
||||
@@ -370,9 +395,12 @@ const internal = {
|
||||
* @returns Various final metrics, including rendered positions for drop-down
|
||||
* and arrow.
|
||||
*/
|
||||
getPositionMetrics: function(
|
||||
primaryX: number, primaryY: number, secondaryX: number,
|
||||
secondaryY: number): PositionMetrics {
|
||||
getPositionMetrics: function (
|
||||
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;
|
||||
@@ -426,7 +469,7 @@ function getPositionBelowMetrics(
|
||||
return {
|
||||
initialX: xCoords.divX,
|
||||
initialY: primaryY,
|
||||
finalX: xCoords.divX, // X position remains constant during animation.
|
||||
finalX: xCoords.divX, // X position remains constant during animation.
|
||||
finalY,
|
||||
arrowX: xCoords.arrowX,
|
||||
arrowY,
|
||||
@@ -448,19 +491,26 @@ 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;
|
||||
const initialY = secondaryY - divSize.height; // No padding on Y.
|
||||
const initialY = secondaryY - divSize.height; // No padding on Y.
|
||||
|
||||
return {
|
||||
initialX: xCoords.divX,
|
||||
initialY,
|
||||
finalX: xCoords.divX, // X position remains constant during animation.
|
||||
finalX: xCoords.divX, // X position remains constant during animation.
|
||||
finalY,
|
||||
arrowX: xCoords.arrowX,
|
||||
arrowY,
|
||||
@@ -481,16 +531,23 @@ 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 {
|
||||
initialX: xCoords.divX,
|
||||
initialY: 0,
|
||||
finalX: xCoords.divX, // X position remains constant during animation.
|
||||
finalY: 0, // Y position remains constant during animation.
|
||||
finalX: xCoords.divX, // X position remains constant during animation.
|
||||
finalY: 0, // Y position remains constant during animation.
|
||||
arrowAtTop: null,
|
||||
arrowX: null,
|
||||
arrowY: null,
|
||||
@@ -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();
|
||||
@@ -569,7 +634,7 @@ export function hide() {
|
||||
div.style.transform = 'translate(0, 0)';
|
||||
div.style.opacity = '0';
|
||||
// Finish animation - reset all values to default.
|
||||
animateOutTimer = setTimeout(function() {
|
||||
animateOutTimer = setTimeout(function () {
|
||||
hideWithoutAnimation();
|
||||
}, ANIMATION_TIME * 1000);
|
||||
if (onHide) {
|
||||
@@ -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');
|
||||
'class',
|
||||
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;
|
||||
@@ -130,8 +118,9 @@ export abstract class Abstract {
|
||||
}
|
||||
if (!workspace) {
|
||||
throw Error(
|
||||
'Workspace is null. Event must have been generated from real' +
|
||||
' Blockly events.');
|
||||
'Workspace is null. Event must have been generated from real' +
|
||||
' 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.
|
||||
@@ -51,26 +52,14 @@ export class BlockBase extends AbstractEvent {
|
||||
const json = super.toJson() as BlockBaseJson;
|
||||
if (!this.blockId) {
|
||||
throw new Error(
|
||||
'The block ID is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The block ID is undefined. Either pass a block to ' +
|
||||
'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,12 +53,16 @@ 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) {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
this.element = opt_element;
|
||||
this.name = opt_name || undefined;
|
||||
@@ -75,8 +79,9 @@ export class BlockChange extends BlockBase {
|
||||
const json = super.toJson() as BlockChangeJson;
|
||||
if (!this.element) {
|
||||
throw new Error(
|
||||
'The changed element is undefined. Either pass an ' +
|
||||
'element to the constructor, or call fromJson');
|
||||
'The changed element is undefined. Either pass an ' +
|
||||
'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'];
|
||||
@@ -140,20 +134,22 @@ export class BlockChange extends BlockBase {
|
||||
const workspace = this.getEventWorkspace_();
|
||||
if (!this.blockId) {
|
||||
throw new Error(
|
||||
'The block ID is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The block ID is undefined. Either pass a block to ' +
|
||||
'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');
|
||||
'The associated block is undefined. Either pass a ' +
|
||||
'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.
|
||||
@@ -32,7 +30,7 @@ export class BlockCreate extends BlockBase {
|
||||
override type = eventUtils.BLOCK_CREATE;
|
||||
|
||||
/** The XML representation of the created block(s). */
|
||||
xml?: Element|DocumentFragment;
|
||||
xml?: Element | DocumentFragment;
|
||||
|
||||
/** The JSON respresentation of the created block(s). */
|
||||
json?: blocks.State;
|
||||
@@ -45,7 +43,7 @@ export class BlockCreate extends BlockBase {
|
||||
super(opt_block);
|
||||
|
||||
if (!opt_block) {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
|
||||
if (opt_block.isShadow()) {
|
||||
@@ -68,18 +66,21 @@ export class BlockCreate extends BlockBase {
|
||||
const json = super.toJson() as BlockCreateJson;
|
||||
if (!this.xml) {
|
||||
throw new Error(
|
||||
'The block XML is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The block XML is undefined. Either pass a block 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 block IDs are undefined. Either pass a block to ' +
|
||||
'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 block JSON is undefined. Either pass a block to ' +
|
||||
'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;
|
||||
@@ -140,13 +128,15 @@ export class BlockCreate extends BlockBase {
|
||||
const workspace = this.getEventWorkspace_();
|
||||
if (!this.json) {
|
||||
throw new Error(
|
||||
'The block JSON is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The block JSON is undefined. Either pass a block 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 block IDs are undefined. Either pass a block to ' +
|
||||
'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,14 +22,13 @@ 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.
|
||||
*/
|
||||
export class BlockDelete extends BlockBase {
|
||||
/** The XML representation of the deleted block(s). */
|
||||
oldXml?: Element|DocumentFragment;
|
||||
oldXml?: Element | DocumentFragment;
|
||||
|
||||
/** The JSON respresentation of the deleted block(s). */
|
||||
oldJson?: blocks.State;
|
||||
@@ -48,7 +46,7 @@ export class BlockDelete extends BlockBase {
|
||||
super(opt_block);
|
||||
|
||||
if (!opt_block) {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
|
||||
if (opt_block.getParent()) {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,23 +74,27 @@ export class BlockDelete extends BlockBase {
|
||||
const json = super.toJson() as BlockDeleteJson;
|
||||
if (!this.oldXml) {
|
||||
throw new Error(
|
||||
'The old block XML is undefined. Either pass a block ' +
|
||||
'to the constructor, or call fromJson');
|
||||
'The old block XML is undefined. Either pass a block ' +
|
||||
'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 block IDs are undefined. Either pass a block to ' +
|
||||
'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');
|
||||
'Whether the block was a shadow is undefined. Either ' +
|
||||
'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');
|
||||
'The old block JSON is undefined. Either pass a block ' +
|
||||
'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,15 +115,20 @@ 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 =
|
||||
json['wasShadow'] || newEvent.oldXml.tagName.toLowerCase() === 'shadow';
|
||||
json['wasShadow'] || newEvent.oldXml.tagName.toLowerCase() === 'shadow';
|
||||
newEvent.oldJson = json['oldJson'];
|
||||
if (json['recordUndo'] !== undefined) {
|
||||
newEvent.recordUndo = json['recordUndo'];
|
||||
@@ -157,13 +145,15 @@ export class BlockDelete extends BlockBase {
|
||||
const workspace = this.getEventWorkspace_();
|
||||
if (!this.ids) {
|
||||
throw new Error(
|
||||
'The block IDs are undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The block IDs are undefined. Either 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');
|
||||
'The old block JSON is undefined. Either pass a block ' +
|
||||
'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.
|
||||
*/
|
||||
@@ -66,13 +64,15 @@ export class BlockDrag extends UiBase {
|
||||
const json = super.toJson() as BlockDragJson;
|
||||
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');
|
||||
'Whether this event is the start of a drag is undefined. ' +
|
||||
'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 block ID is undefined. Either pass a block to ' +
|
||||
'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,14 +107,19 @@ export class BlockMove extends BlockBase {
|
||||
json['oldParentId'] = this.oldParentId;
|
||||
json['oldInputName'] = this.oldInputName;
|
||||
if (this.oldCoordinate) {
|
||||
json['oldCoordinate'] = `${Math.round(this.oldCoordinate.x)}, ` +
|
||||
`${Math.round(this.oldCoordinate.y)}`;
|
||||
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)}, ` +
|
||||
`${Math.round(this.newCoordinate.y)}`;
|
||||
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;
|
||||
@@ -110,33 +127,6 @@ export class BlockMove extends BlockBase {
|
||||
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.
|
||||
@@ -186,14 +194,15 @@ export class BlockMove extends BlockBase {
|
||||
const workspace = this.getEventWorkspace_();
|
||||
if (!this.blockId) {
|
||||
throw new Error(
|
||||
'The block ID is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The block ID is undefined. Either pass a block to ' +
|
||||
'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 &&
|
||||
this.oldInputName === this.newInputName &&
|
||||
Coordinate.equals(this.oldCoordinate, this.newCoordinate);
|
||||
return (
|
||||
this.oldParentId === this.newParentId &&
|
||||
this.oldInputName === this.newInputName &&
|
||||
Coordinate.equals(this.oldCoordinate, this.newCoordinate)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,22 +240,23 @@ export class BlockMove extends BlockBase {
|
||||
const workspace = this.getEventWorkspace_();
|
||||
if (!this.blockId) {
|
||||
throw new Error(
|
||||
'The block ID is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The block ID is undefined. Either pass a block to ' +
|
||||
'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;
|
||||
const inputName = forward ? this.newInputName : this.oldInputName;
|
||||
const coordinate = forward ? this.newCoordinate : this.oldCoordinate;
|
||||
let parentBlock: Block|null;
|
||||
let parentBlock: Block | null;
|
||||
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;
|
||||
@@ -63,13 +64,15 @@ export class BubbleOpen extends UiBase {
|
||||
const json = super.toJson() as BubbleOpenJson;
|
||||
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');
|
||||
'Whether this event is for opening the bubble is undefined. ' +
|
||||
'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');
|
||||
'The type of bubble is undefined. Either pass the ' +
|
||||
'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;
|
||||
@@ -67,28 +67,15 @@ export class Click extends UiBase {
|
||||
const json = super.toJson() as ClickJson;
|
||||
if (!this.targetType) {
|
||||
throw new Error(
|
||||
'The click target type is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The click target type is undefined. Either pass a block to ' +
|
||||
'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.
|
||||
*/
|
||||
@@ -59,26 +60,14 @@ export class CommentBase extends AbstractEvent {
|
||||
const json = super.toJson() as CommentBaseJson;
|
||||
if (!this.commentId) {
|
||||
throw new Error(
|
||||
'The comment ID is undefined. Either pass a comment to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The comment ID is undefined. Either pass a comment to ' +
|
||||
'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');
|
||||
@@ -116,16 +112,16 @@ export class CommentBase extends AbstractEvent {
|
||||
} else {
|
||||
if (!event.commentId) {
|
||||
throw new Error(
|
||||
'The comment ID is undefined. Either pass a comment to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The comment ID is undefined. Either pass a comment to ' +
|
||||
'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,18 +39,20 @@ 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) {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
|
||||
this.oldContents_ =
|
||||
typeof opt_oldContents === 'undefined' ? '' : opt_oldContents;
|
||||
typeof opt_oldContents === 'undefined' ? '' : opt_oldContents;
|
||||
this.newContents_ =
|
||||
typeof opt_newContents === 'undefined' ? '' : opt_newContents;
|
||||
typeof opt_newContents === 'undefined' ? '' : opt_newContents;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,33 +64,21 @@ export class CommentChange extends CommentBase {
|
||||
const json = super.toJson() as CommentChangeJson;
|
||||
if (!this.oldContents_) {
|
||||
throw new Error(
|
||||
'The old contents is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The old contents is undefined. Either pass a value to ' +
|
||||
'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 new contents is undefined. Either pass a value to ' +
|
||||
'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;
|
||||
@@ -128,24 +121,27 @@ export class CommentChange extends CommentBase {
|
||||
const workspace = this.getEventWorkspace_();
|
||||
if (!this.commentId) {
|
||||
throw new Error(
|
||||
'The comment ID is undefined. Either pass a comment to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The comment ID is undefined. Either pass a comment to ' +
|
||||
'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_;
|
||||
if (!contents) {
|
||||
if (forward) {
|
||||
throw new Error(
|
||||
'The new contents is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The new contents is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
'The old contents is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The old contents is undefined. Either pass a value to ' +
|
||||
'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.
|
||||
*/
|
||||
@@ -30,7 +28,7 @@ export class CommentCreate extends CommentBase {
|
||||
override type = eventUtils.COMMENT_CREATE;
|
||||
|
||||
/** The XML representation of the created workspace comment. */
|
||||
xml?: Element|DocumentFragment;
|
||||
xml?: Element | DocumentFragment;
|
||||
|
||||
/**
|
||||
* @param opt_comment The created comment.
|
||||
@@ -56,26 +54,14 @@ export class CommentCreate extends CommentBase {
|
||||
const json = super.toJson() as CommentCreateJson;
|
||||
if (!this.xml) {
|
||||
throw new Error(
|
||||
'The comment XML is undefined. Either pass a comment to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The comment XML is undefined. Either pass a comment to ' +
|
||||
'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.
|
||||
*/
|
||||
@@ -39,7 +38,7 @@ export class CommentDelete extends CommentBase {
|
||||
super(opt_comment);
|
||||
|
||||
if (!opt_comment) {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
|
||||
this.xml = opt_comment.toXmlWithXY();
|
||||
@@ -63,8 +62,9 @@ export class CommentDelete extends CommentBase {
|
||||
const json = super.toJson() as CommentDeleteJson;
|
||||
if (!this.xml) {
|
||||
throw new Error(
|
||||
'The comment XML is undefined. Either pass a comment to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The comment XML is undefined. Either pass a comment to ' +
|
||||
'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.
|
||||
*/
|
||||
@@ -46,7 +44,7 @@ export class CommentMove extends CommentBase {
|
||||
super(opt_comment);
|
||||
|
||||
if (!opt_comment) {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
|
||||
this.comment_ = opt_comment;
|
||||
@@ -60,13 +58,15 @@ export class CommentMove extends CommentBase {
|
||||
recordNew() {
|
||||
if (this.newCoordinate_) {
|
||||
throw Error(
|
||||
'Tried to record the new position of a comment on the ' +
|
||||
'same event twice.');
|
||||
'Tried to record the new position of a comment on the ' +
|
||||
'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 comment is undefined. Pass a comment to ' +
|
||||
'the constructor if you want to use the record functionality'
|
||||
);
|
||||
}
|
||||
this.newCoordinate_ = this.comment_.getRelativeToSurfaceXY();
|
||||
}
|
||||
@@ -91,37 +91,26 @@ export class CommentMove extends CommentBase {
|
||||
const json = super.toJson() as CommentMoveJson;
|
||||
if (!this.oldCoordinate_) {
|
||||
throw new Error(
|
||||
'The old comment position is undefined. Either pass a comment to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The old comment position is undefined. Either pass a comment to ' +
|
||||
'the constructor, or call fromJson'
|
||||
);
|
||||
}
|
||||
if (!this.newCoordinate_) {
|
||||
throw new Error(
|
||||
'The new comment position is undefined. Either call recordNew, or ' +
|
||||
'call fromJson');
|
||||
'The new comment position is undefined. Either call recordNew, or ' +
|
||||
'call fromJson'
|
||||
);
|
||||
}
|
||||
json['oldCoordinate'] = `${Math.round(this.oldCoordinate_.x)}, ` +
|
||||
`${Math.round(this.oldCoordinate_.y)}`;
|
||||
json['newCoordinate'] = Math.round(this.newCoordinate_.x) + ',' +
|
||||
Math.round(this.newCoordinate_.y);
|
||||
json['oldCoordinate'] =
|
||||
`${Math.round(this.oldCoordinate_.x)}, ` +
|
||||
`${Math.round(this.oldCoordinate_.y)}`;
|
||||
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(',');
|
||||
@@ -161,21 +155,23 @@ export class CommentMove extends CommentBase {
|
||||
const workspace = this.getEventWorkspace_();
|
||||
if (!this.commentId) {
|
||||
throw new Error(
|
||||
'The comment ID is undefined. Either pass a comment to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The comment ID is undefined. Either pass a comment to ' +
|
||||
'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;
|
||||
}
|
||||
|
||||
const target = forward ? this.newCoordinate_ : this.oldCoordinate_;
|
||||
if (!target) {
|
||||
throw new Error(
|
||||
'Either oldCoordinate_ or newCoordinate_ is undefined. ' +
|
||||
'Either oldCoordinate_ or newCoordinate_ is undefined. ' +
|
||||
'Either pass a comment to the constructor and call recordNew, ' +
|
||||
'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;
|
||||
@@ -80,13 +81,15 @@ export class MarkerMove extends UiBase {
|
||||
const json = super.toJson() as MarkerMoveJson;
|
||||
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');
|
||||
'Whether this is a cursor event or not is undefined. Either pass ' +
|
||||
'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 new node is undefined. Either pass a node to ' +
|
||||
'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.
|
||||
*/
|
||||
@@ -48,26 +46,14 @@ export class ThemeChange extends UiBase {
|
||||
const json = super.toJson() as ThemeChangeJson;
|
||||
if (!this.themeName) {
|
||||
throw new Error(
|
||||
'The theme name is undefined. Either pass a theme name to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The theme name is undefined. Either pass a theme name to ' +
|
||||
'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.
|
||||
*/
|
||||
@@ -52,26 +50,14 @@ export class TrashcanOpen extends UiBase {
|
||||
const json = super.toJson() as TrashcanOpenJson;
|
||||
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');
|
||||
'Whether this is already open or not is undefined. Either pass ' +
|
||||
'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.
|
||||
*/
|
||||
@@ -49,26 +50,14 @@ export class VarBase extends AbstractEvent {
|
||||
const json = super.toJson() as VarBaseJson;
|
||||
if (!this.varId) {
|
||||
throw new Error(
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'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.
|
||||
*/
|
||||
@@ -40,7 +38,7 @@ export class VarCreate extends VarBase {
|
||||
super(opt_variable);
|
||||
|
||||
if (!opt_variable) {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
this.varType = opt_variable.type;
|
||||
this.varName = opt_variable.name;
|
||||
@@ -55,33 +53,21 @@ export class VarCreate extends VarBase {
|
||||
const json = super.toJson() as VarCreateJson;
|
||||
if (this.varType === undefined) {
|
||||
throw new Error(
|
||||
'The var type is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The var type is undefined. Either pass a variable to ' +
|
||||
'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 var name is undefined. Either pass a variable to ' +
|
||||
'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;
|
||||
@@ -109,13 +101,15 @@ export class VarCreate extends VarBase {
|
||||
const workspace = this.getEventWorkspace_();
|
||||
if (!this.varId) {
|
||||
throw new Error(
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'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 var name is undefined. Either pass a variable to ' +
|
||||
'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.
|
||||
*
|
||||
@@ -35,7 +33,7 @@ export class VarDelete extends VarBase {
|
||||
super(opt_variable);
|
||||
|
||||
if (!opt_variable) {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
this.varType = opt_variable.type;
|
||||
this.varName = opt_variable.name;
|
||||
@@ -50,33 +48,21 @@ export class VarDelete extends VarBase {
|
||||
const json = super.toJson() as VarDeleteJson;
|
||||
if (this.varType === undefined) {
|
||||
throw new Error(
|
||||
'The var type is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The var type is undefined. Either pass a variable to ' +
|
||||
'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 var name is undefined. Either pass a variable to ' +
|
||||
'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;
|
||||
@@ -104,13 +96,15 @@ export class VarDelete extends VarBase {
|
||||
const workspace = this.getEventWorkspace_();
|
||||
if (!this.varId) {
|
||||
throw new Error(
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'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 var name is undefined. Either pass a variable to ' +
|
||||
'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.
|
||||
*
|
||||
@@ -38,7 +36,7 @@ export class VarRename extends VarBase {
|
||||
super(opt_variable);
|
||||
|
||||
if (!opt_variable) {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
this.oldName = opt_variable.name;
|
||||
this.newName = typeof newName === 'undefined' ? '' : newName;
|
||||
@@ -53,33 +51,21 @@ export class VarRename extends VarBase {
|
||||
const json = super.toJson() as VarRenameJson;
|
||||
if (!this.oldName) {
|
||||
throw new Error(
|
||||
'The old var name is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The old var name is undefined. Either pass a variable to ' +
|
||||
'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 new var name is undefined. Either pass a value to ' +
|
||||
'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;
|
||||
@@ -107,18 +99,21 @@ export class VarRename extends VarBase {
|
||||
const workspace = this.getEventWorkspace_();
|
||||
if (!this.varId) {
|
||||
throw new Error(
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The var ID is undefined. Either pass a variable to ' +
|
||||
'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 old var name is undefined. Either pass a variable to ' +
|
||||
'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 new var name is undefined. Either pass a value to ' +
|
||||
'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;
|
||||
@@ -78,23 +80,27 @@ export class ViewportChange extends UiBase {
|
||||
const json = super.toJson() as ViewportChangeJson;
|
||||
if (this.viewTop === undefined) {
|
||||
throw new Error(
|
||||
'The view top is undefined. Either pass a value to ' +
|
||||
'the constructor, or call fromJson');
|
||||
'The view top is undefined. Either pass a value to ' +
|
||||
'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 view left is undefined. Either pass a value to ' +
|
||||
'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 scale is undefined. Either pass a value to ' +
|
||||
'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 old scale is undefined. Either pass a value to ' +
|
||||
'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.
|
||||
*/
|
||||
@@ -187,7 +193,7 @@ export const FINISHED_LOADING = 'finished_loading';
|
||||
* Not to be confused with bumping so that disconnected connections do not
|
||||
* appear connected.
|
||||
*/
|
||||
export type BumpEvent = BlockCreate|BlockMove|CommentCreate|CommentMove;
|
||||
export type BumpEvent = BlockCreate | BlockMove | CommentCreate | CommentMove;
|
||||
|
||||
/**
|
||||
* List of events that cause objects to be bumped back into the visible
|
||||
@@ -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.type === CHANGE &&
|
||||
(event as BlockChange).element === lastEvent.element &&
|
||||
(event as BlockChange).name === lastEvent.name
|
||||
) {
|
||||
const changeEvent = event as BlockChange;
|
||||
// Merge change events.
|
||||
lastEvent.newValue = changeEvent.newValue;
|
||||
@@ -316,7 +377,7 @@ export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
|
||||
}
|
||||
}
|
||||
// Filter out any events that have become null due to merging.
|
||||
queue = mergedQueue.filter(function(e) {
|
||||
queue = mergedQueue.filter(function (e) {
|
||||
return !e.isNull();
|
||||
});
|
||||
if (!forward) {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -385,14 +448,14 @@ export function getGroup(): string {
|
||||
* @param state True to start new group, false to end group.
|
||||
* String to set group explicitly.
|
||||
*/
|
||||
export function setGroup(state: boolean|string) {
|
||||
export function setGroup(state: boolean | string) {
|
||||
TEST_ONLY.setGroupInternal(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private version of setGroup for stubbing in tests.
|
||||
*/
|
||||
function setGroupInternal(state: boolean|string) {
|
||||
function setGroupInternal(state: boolean | string) {
|
||||
if (typeof state === 'boolean') {
|
||||
group = state ? idGenerator.genUid() : '';
|
||||
} else {
|
||||
@@ -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,33 +488,13 @@ 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';
|
||||
return (eventClass as any).fromJson(json, workspace);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -460,8 +503,9 @@ function eventClassHasStaticFromJson(eventClass: new (...p: any[]) => Abstract):
|
||||
* @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()) {
|
||||
(block.outputConnection || block.previousConnection) &&
|
||||
!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};
|
||||
@@ -54,7 +53,7 @@ export function registerMixin(name: string, mixinObj: AnyDuringMigration) {
|
||||
if (!mixinObj || typeof mixinObj !== 'object') {
|
||||
throw Error('Error: Mixin "' + name + '" must be a object');
|
||||
}
|
||||
register(name, function(this: Block) {
|
||||
register(name, function (this: Block) {
|
||||
this.mixin(mixinObj);
|
||||
});
|
||||
}
|
||||
@@ -73,8 +72,11 @@ export function registerMixin(name: string, mixinObj: AnyDuringMigration) {
|
||||
* @throws {Error} if the mutation is invalid or can't be applied to the block.
|
||||
*/
|
||||
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);
|
||||
@@ -85,9 +87,9 @@ export function registerMutator(
|
||||
}
|
||||
|
||||
// Sanity checks passed.
|
||||
register(name, function(this: Block) {
|
||||
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');
|
||||
errorPrefix +
|
||||
'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;
|
||||
@@ -343,10 +373,10 @@ export function runAfterPageLoad(fn: () => void) {
|
||||
throw Error('runAfterPageLoad() requires browser document.');
|
||||
}
|
||||
if (document.readyState === 'complete') {
|
||||
fn(); // Page has already loaded. Call immediately.
|
||||
fn(); // Page has already loaded. Call immediately.
|
||||
} else {
|
||||
// Poll readyState.
|
||||
const readyStateCheckInterval = setInterval(function() {
|
||||
const readyStateCheckInterval = setInterval(function () {
|
||||
if (document.readyState === 'complete') {
|
||||
clearInterval(readyStateCheckInterval);
|
||||
fn();
|
||||
@@ -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,8 +415,9 @@ 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
|
||||
runAfterPageLoad(function() {
|
||||
if (typeof document === 'object') {
|
||||
// Relies on document.readyState
|
||||
runAfterPageLoad(function () {
|
||||
for (const key in lookupTable) {
|
||||
// Will print warnings if reference is missing.
|
||||
parsing.checkMessageReferences(lookupTable[key]);
|
||||
@@ -399,24 +432,29 @@ export function buildTooltipForDropdown(
|
||||
blockTypesChecked.push(this.type);
|
||||
}
|
||||
|
||||
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 ' +
|
||||
this.setTooltip(
|
||||
function (this: Block) {
|
||||
const value = String(this.getFieldValue(dropdownName));
|
||||
let tooltip = lookupTable[value];
|
||||
if (tooltip === null) {
|
||||
if (blockTypesChecked.indexOf(this.type) === -1) {
|
||||
// Warn for missing values on generated tooltips.
|
||||
let warning =
|
||||
'No tooltip mapping for value ' +
|
||||
value +
|
||||
' of field ' +
|
||||
dropdownName;
|
||||
if (this.type !== null) {
|
||||
warning += ' of block type ' + this.type;
|
||||
if (this.type !== null) {
|
||||
warning += ' of block type ' + this.type;
|
||||
}
|
||||
console.warn(warning + '.');
|
||||
}
|
||||
console.warn(warning + '.');
|
||||
} else {
|
||||
tooltip = parsing.replaceMessageReferences(tooltip);
|
||||
}
|
||||
} else {
|
||||
tooltip = parsing.replaceMessageReferences(tooltip);
|
||||
}
|
||||
return tooltip;
|
||||
}.bind(this));
|
||||
return tooltip;
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
return extensionFn;
|
||||
}
|
||||
@@ -430,17 +468,25 @@ 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()) {
|
||||
const options = dropdown.getOptions();
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const optionKey = options[i][1]; // label, then value
|
||||
const optionKey = options[i][1]; // label, then value
|
||||
if (lookupTable[optionKey] === null) {
|
||||
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,13 +503,16 @@ 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
|
||||
runAfterPageLoad(function() {
|
||||
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) {
|
||||
const field = this.getField(fieldName);
|
||||
return parsing.replaceMessageReferences(msgTemplate)
|
||||
this.setTooltip(
|
||||
function (this: Block) {
|
||||
const field = this.getField(fieldName);
|
||||
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) {
|
||||
const parent = this.getParent();
|
||||
return parent && parent.getInputsInline() && parent.tooltip ||
|
||||
tooltipWhenNotConnected;
|
||||
}.bind(this));
|
||||
this.setTooltip(
|
||||
function (this: Block) {
|
||||
const parent = this.getParent();
|
||||
return (
|
||||
(parent && parent.getInputsInline() && parent.tooltip) ||
|
||||
tooltipWhenNotConnected
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
register('parent_tooltip_when_inline', extensionParentTooltip);
|
||||
|
||||
307
core/field.ts
307
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
|
||||
@@ -58,17 +59,21 @@ import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
*
|
||||
* - `undefined` to set `newValue` as is.
|
||||
*/
|
||||
export type FieldValidator<T = any> = (newValue: T) => T|null|undefined;
|
||||
export type FieldValidator<T = any> = (newValue: T) => T | null | undefined;
|
||||
|
||||
/**
|
||||
* Abstract class for an editable field.
|
||||
*
|
||||
* @typeParam T - The value stored on the field.
|
||||
*/
|
||||
export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
IASTNodeLocationWithBlock,
|
||||
IKeyboardAccessible,
|
||||
IRegistrable {
|
||||
export abstract class Field<T = any>
|
||||
implements
|
||||
IASTNodeLocationSvg,
|
||||
IASTNodeLocationWithBlock,
|
||||
IKeyboardAccessible,
|
||||
IRegistrable,
|
||||
ISerializable
|
||||
{
|
||||
/**
|
||||
* To overwrite the default value which is set in **Field**, directly update
|
||||
* the prototype.
|
||||
@@ -78,7 +83,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* FieldImage.prototype.DEFAULT_VALUE = null;
|
||||
* ```
|
||||
*/
|
||||
DEFAULT_VALUE: T|null = null;
|
||||
DEFAULT_VALUE: T | null = null;
|
||||
|
||||
/** Non-breaking space. */
|
||||
static readonly NBSP = '\u00A0';
|
||||
@@ -95,47 +100,47 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* Static labels are usually unnamed.
|
||||
*/
|
||||
name?: string = undefined;
|
||||
protected value_: T|null;
|
||||
protected value_: T | null;
|
||||
|
||||
/** Validation function called when user edits an editable field. */
|
||||
protected validator_: FieldValidator<T>|null = null;
|
||||
protected validator_: FieldValidator<T> | null = null;
|
||||
|
||||
/**
|
||||
* Used to cache the field's tooltip value if setTooltip is called when the
|
||||
* field is not yet initialized. Is *not* guaranteed to be accurate.
|
||||
*/
|
||||
private tooltip_: Tooltip.TipInfo|null = null;
|
||||
private tooltip_: Tooltip.TipInfo | null = null;
|
||||
protected size_: Size;
|
||||
|
||||
/**
|
||||
* Holds the cursors svg element when the cursor is attached to the field.
|
||||
* This is null if there is no cursor on the field.
|
||||
*/
|
||||
private cursorSvg_: SVGElement|null = null;
|
||||
private cursorSvg_: SVGElement | null = null;
|
||||
|
||||
/**
|
||||
* Holds the markers svg element when the marker is attached to the field.
|
||||
* This is null if there is no marker on the field.
|
||||
*/
|
||||
private markerSvg_: SVGElement|null = null;
|
||||
private markerSvg_: SVGElement | null = null;
|
||||
|
||||
/** The rendered field's SVG group element. */
|
||||
protected fieldGroup_: SVGGElement|null = null;
|
||||
protected fieldGroup_: SVGGElement | null = null;
|
||||
|
||||
/** The rendered field's SVG border element. */
|
||||
protected borderRect_: SVGRectElement|null = null;
|
||||
protected borderRect_: SVGRectElement | null = null;
|
||||
|
||||
/** The rendered field's SVG text element. */
|
||||
protected textElement_: SVGTextElement|null = null;
|
||||
protected textElement_: SVGTextElement | null = null;
|
||||
|
||||
/** The rendered field's text content element. */
|
||||
protected textContent_: Text|null = null;
|
||||
protected textContent_: Text | null = null;
|
||||
|
||||
/** Mouse down event listener data. */
|
||||
private mouseDownWrapper_: browserEvents.Data|null = null;
|
||||
private mouseDownWrapper_: browserEvents.Data | null = null;
|
||||
|
||||
/** Constants associated with the source block's renderer. */
|
||||
protected constants_: ConstantProvider|null = null;
|
||||
protected constants_: ConstantProvider | null = null;
|
||||
|
||||
/**
|
||||
* Has this field been disposed of?
|
||||
@@ -148,7 +153,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
maxDisplayLength = 50;
|
||||
|
||||
/** Block this field is attached to. Starts as null, then set in init. */
|
||||
protected sourceBlock_: Block|null = null;
|
||||
protected sourceBlock_: Block | null = null;
|
||||
|
||||
/** Does this block need to be re-rendered? */
|
||||
protected isDirty_ = true;
|
||||
@@ -162,21 +167,21 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
protected enabled_ = true;
|
||||
|
||||
/** The element the click handler is bound to. */
|
||||
protected clickTarget_: Element|null = null;
|
||||
protected clickTarget_: Element | null = null;
|
||||
|
||||
/**
|
||||
* The prefix field.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
prefixField: string|null = null;
|
||||
prefixField: string | null = null;
|
||||
|
||||
/**
|
||||
* The suffix field.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
suffixField: string|null = null;
|
||||
suffixField: string | null = null;
|
||||
|
||||
/**
|
||||
* Editable fields usually show some sort of UI indicating they are
|
||||
@@ -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);
|
||||
@@ -262,13 +270,16 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*
|
||||
* @returns The renderer constant provider.
|
||||
*/
|
||||
getConstants(): ConstantProvider|null {
|
||||
if (!this.constants_ && this.sourceBlock_ &&
|
||||
!this.sourceBlock_.isDeadOrDying() &&
|
||||
this.sourceBlock_.workspace.rendered) {
|
||||
getConstants(): ConstantProvider | null {
|
||||
if (
|
||||
!this.constants_ &&
|
||||
this.sourceBlock_ &&
|
||||
!this.sourceBlock_.isDeadOrDying() &&
|
||||
this.sourceBlock_.workspace.rendered
|
||||
) {
|
||||
this.constants_ = (this.sourceBlock_.workspace as WorkspaceSvg)
|
||||
.getRenderer()
|
||||
.getConstants();
|
||||
.getRenderer()
|
||||
.getConstants();
|
||||
}
|
||||
return this.constants_;
|
||||
}
|
||||
@@ -279,7 +290,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* @returns The block containing this field.
|
||||
* @throws An error if the source block is not defined.
|
||||
*/
|
||||
getSourceBlock(): Block|null {
|
||||
getSourceBlock(): Block | null {
|
||||
return this.sourceBlock_;
|
||||
}
|
||||
|
||||
@@ -333,16 +344,18 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*/
|
||||
protected createBorderRect_() {
|
||||
this.borderRect_ = dom.createSvgElement(
|
||||
Svg.RECT, {
|
||||
'rx': this.getConstants()!.FIELD_BORDER_RECT_RADIUS,
|
||||
'ry': this.getConstants()!.FIELD_BORDER_RECT_RADIUS,
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'height': this.size_.height,
|
||||
'width': this.size_.width,
|
||||
'class': 'blocklyFieldRect',
|
||||
},
|
||||
this.fieldGroup_);
|
||||
Svg.RECT,
|
||||
{
|
||||
'rx': this.getConstants()!.FIELD_BORDER_RECT_RADIUS,
|
||||
'ry': this.getConstants()!.FIELD_BORDER_RECT_RADIUS,
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'height': this.size_.height,
|
||||
'width': this.size_.width,
|
||||
'class': 'blocklyFieldRect',
|
||||
},
|
||||
this.fieldGroup_
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,10 +365,12 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*/
|
||||
protected createTextElement_() {
|
||||
this.textElement_ = dom.createSvgElement(
|
||||
Svg.TEXT, {
|
||||
'class': 'blocklyText',
|
||||
},
|
||||
this.fieldGroup_);
|
||||
Svg.TEXT,
|
||||
{
|
||||
'class': 'blocklyText',
|
||||
},
|
||||
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_
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,14 +461,18 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* Used to see if `this` has overridden any relevant hooks.
|
||||
* @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) {
|
||||
protected saveLegacyState(callingClass: FieldProto): string | null {
|
||||
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_ &&
|
||||
this.sourceBlock_.isEditable() &&
|
||||
this.showEditor_ !== Field.prototype.showEditor_;
|
||||
return (
|
||||
this.enabled_ &&
|
||||
!!this.sourceBlock_ &&
|
||||
this.sourceBlock_.isEditable() &&
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -569,9 +603,10 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
isSerializable = true;
|
||||
} else if (this.EDITABLE) {
|
||||
console.warn(
|
||||
'Detected an editable field that was not serializable.' +
|
||||
'Detected an editable field that was not serializable.' +
|
||||
' Please define SERIALIZABLE property as true on all editable custom' +
|
||||
' fields. Proceeding with serialization.');
|
||||
' fields. Proceeding with serialization.'
|
||||
);
|
||||
isSerializable = true;
|
||||
}
|
||||
}
|
||||
@@ -629,7 +664,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*
|
||||
* @returns Validation function, or null.
|
||||
*/
|
||||
getValidator(): FieldValidator<T>|null {
|
||||
getValidator(): FieldValidator<T> | null {
|
||||
return this.validator_;
|
||||
}
|
||||
|
||||
@@ -639,7 +674,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*
|
||||
* @returns The group element.
|
||||
*/
|
||||
getSvgRoot(): SVGGElement|null {
|
||||
getSvgRoot(): SVGGElement | null {
|
||||
return this.fieldGroup_;
|
||||
}
|
||||
|
||||
@@ -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_) {
|
||||
@@ -775,18 +843,23 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
const halfHeight = this.size_.height / 2;
|
||||
|
||||
this.textElement_.setAttribute(
|
||||
'x',
|
||||
String(
|
||||
this.getSourceBlock()?.RTL ?
|
||||
this.size_.width - contentWidth - xOffset :
|
||||
xOffset));
|
||||
'x',
|
||||
String(
|
||||
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));
|
||||
'y',
|
||||
String(
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -824,8 +901,9 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
// Don't issue a warning if the field is actually zero width.
|
||||
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.');
|
||||
'Deprecated use of setting size_.width to 0 to rerender a' +
|
||||
' field. Set field.isDirty_ to true instead.'
|
||||
);
|
||||
}
|
||||
}
|
||||
return this.size_;
|
||||
@@ -925,7 +1003,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*
|
||||
* @returns Current text or null.
|
||||
*/
|
||||
protected getText_(): string|null {
|
||||
protected getText_(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1036,7 +1126,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*
|
||||
* @returns Current value.
|
||||
*/
|
||||
getValue(): T|null {
|
||||
getValue(): T | null {
|
||||
return this.value_;
|
||||
}
|
||||
|
||||
@@ -1060,10 +1150,11 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*
|
||||
* - `undefined` to set `newValue` as is.
|
||||
*/
|
||||
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): T | null | undefined;
|
||||
protected doClassValidation_(newValue?: AnyDuringMigration): T | null;
|
||||
protected doClassValidation_(
|
||||
newValue?: T | AnyDuringMigration
|
||||
): T | null | undefined {
|
||||
if (newValue === null || newValue === undefined) {
|
||||
return null;
|
||||
}
|
||||
@@ -1115,8 +1206,9 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
* display the tooltip of the parent block. To not display a tooltip pass
|
||||
* the empty string.
|
||||
*/
|
||||
setTooltip(newTip: Tooltip.TipInfo|null) {
|
||||
if (!newTip && newTip !== '') { // If null or undefined.
|
||||
setTooltip(newTip: Tooltip.TipInfo | null) {
|
||||
if (!newTip && newTip !== '') {
|
||||
// If null or undefined.
|
||||
newTip = this.sourceBlock_;
|
||||
}
|
||||
const clickTarget = this.getClickTarget_();
|
||||
@@ -1149,7 +1241,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
|
||||
*
|
||||
* @returns Element to bind click handler to.
|
||||
*/
|
||||
protected getClickTarget_(): Element|null {
|
||||
protected getClickTarget_(): Element | null {
|
||||
return this.clickTarget_ || this.getSvgRoot();
|
||||
}
|
||||
|
||||
@@ -1323,7 +1415,8 @@ export class UnattachedFieldError extends Error {
|
||||
/** @internal */
|
||||
constructor() {
|
||||
super(
|
||||
'The field has not yet been attached to its input. ' +
|
||||
'Call appendField to attach it.');
|
||||
'The field has not yet been attached to its input. ' +
|
||||
'Call appendField to attach it.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user