diff --git a/.eslintignore b/.eslintignore index 373bf7a871d..1430463fea6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,21 +1,28 @@ -*_compressed*.js -*_uncompressed*.js -gulpfile.js +# Build Artifacts /msg/* +/build/* /dist/* -/core/utils/global.js +/typings/* +/docs/* + +# Tests other than mocha unit tests /tests/blocks/* /tests/themes/* /tests/compile/* /tests/jsunit/* /tests/generators/* -/tests/mocha/run_mocha_tests_in_browser.js +/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 \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000000..14d27ca1af9 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,187 @@ +const rules = { + 'no-unused-vars': [ + 'error', + { + 'args': 'after-used', + // Ignore vars starting with an underscore. + 'varsIgnorePattern': '^_', + // Ignore arguments starting with an underscore. + 'argsIgnorePattern': '^_', + }, + ], + // Blockly uses for exporting symbols. no-self-assign added in eslint 5. + 'no-self-assign': ['off'], + // Blockly uses single quotes except for JSON blobs, which must use double + // quotes. + 'quotes': ['off'], + // Blockly uses 'use strict' in files. + 'strict': ['off'], + // Closure style allows redeclarations. + 'no-redeclare': ['off'], + 'valid-jsdoc': ['error'], + 'no-console': ['off'], + 'spaced-comment': [ + 'error', + 'always', + { + 'block': { + 'balanced': true, + }, + 'exceptions': ['*'], + }, + ], + // Blockly uses prefixes for optional arguments and test-only functions. + 'camelcase': [ + 'error', + { + 'properties': 'never', + 'allow': ['^opt_', '^_opt_', '^testOnly_'], + }, + ], + // Blockly uses capital letters for some non-constructor namespaces. + // Keep them for legacy reasons. + 'new-cap': ['off'], + // Blockly uses objects as maps, but uses Object.create(null) to + // instantiate them. + 'guard-for-in': ['off'], + 'prefer-spread': ['off'], +}; + +/** + * Build shared settings for TS linting and add in the config differences. + * @return {Object} The override TS linting for given files and a given + * tsconfig. + */ +function buildTSOverride({files, tsconfig}) { + return { + 'files': files, + 'plugins': ['@typescript-eslint/eslint-plugin', 'jsdoc'], + 'settings': { + 'jsdoc': { + 'mode': 'typescript', + }, + }, + 'parser': '@typescript-eslint/parser', + 'parserOptions': { + 'project': tsconfig, + 'tsconfigRootDir': '.', + 'ecmaVersion': 2020, + 'sourceType': 'module', + }, + 'extends': [ + 'plugin:@typescript-eslint/recommended', + 'plugin:jsdoc/recommended', + 'prettier', // Extend again so that these rules are applied last + ], + 'rules': { + // TS rules + // Blockly uses namespaces to do declaration merging in some cases. + '@typescript-eslint/no-namespace': ['off'], + // Use the updated TypeScript-specific rule. + 'no-invalid-this': ['off'], + '@typescript-eslint/no-invalid-this': ['error'], + // Needs decision. 601 problems. + '@typescript-eslint/no-non-null-assertion': ['off'], + // Use TS-specific rule. + 'no-unused-vars': ['off'], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + 'argsIgnorePattern': '^_', + 'varsIgnorePattern': '^_', + }, + ], + // Temporarily disable. 23 problems. + '@typescript-eslint/no-explicit-any': ['off'], + // Temporarily disable. 128 problems. + 'require-jsdoc': ['off'], + // Temporarily disable. 55 problems. + '@typescript-eslint/ban-types': ['off'], + // Temporarily disable. 33 problems. + '@typescript-eslint/no-empty-function': ['off'], + // Temporarily disable. 3 problems. + '@typescript-eslint/no-empty-interface': ['off'], + + // TsDoc rules (using JsDoc plugin) + // Disable built-in jsdoc verifier. + 'valid-jsdoc': ['off'], + // Don't require types in params and returns docs. + 'jsdoc/require-param-type': ['off'], + 'jsdoc/require-returns-type': ['off'], + // params and returns docs are optional. + 'jsdoc/require-param-description': ['off'], + 'jsdoc/require-returns': ['off'], + // Disable for now (breaks on `this` which is not really a param). + 'jsdoc/require-param': ['off'], + // Don't auto-add missing jsdoc. Only required on exported items. + 'jsdoc/require-jsdoc': [ + 'warn', + { + 'enableFixer': false, + 'publicOnly': true, + }, + ], + '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} + 'jsdoc/no-undefined-types': ['off'], + 'jsdoc/valid-types': ['off'], + // Disabled due to not handling `this`. If re-enabled, + // checkDestructured option + // should be left as false. + 'jsdoc/check-param-names': ['off', {'checkDestructured': false}], + // Allow any text in the license tag. Other checks are not relevant. + 'jsdoc/check-values': ['off'], + // 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}], + }, + }; +} + +// NOTE: When this output is put directly in `module.exports`, the formatter +// does not align with the linter. +const eslintJSON = { + 'rules': rules, + 'env': { + 'es2020': true, + 'browser': true, + }, + 'globals': { + 'goog': true, + 'exports': true, + }, + 'extends': ['eslint:recommended', 'google', 'prettier'], + // TypeScript-specific config. Uses above rules plus these. + 'overrides': [ + buildTSOverride({ + files: ['./**/*.ts', './**/*.tsx'], + tsconfig: './tsconfig.json', + }), + buildTSOverride({ + files: ['./tests/typescript/**/*.ts', './tests/typescript/**/*.tsx'], + tsconfig: './tests/typescript/tsconfig.json', + }), + { + 'files': ['./.eslintrc.js'], + 'env': { + 'node': true, + }, + }, + ], +}; + +module.exports = eslintJSON; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 2cfe544cd27..00000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "rules": { - "curly": ["error"], - "eol-last": ["error"], - // Blockly/Google use 2-space indents. - // Blockly/Google uses +4 space indents for line continuations. - // Ignore default rules for ternary expressions. - "indent": [ - "error", 2, - { - "SwitchCase": 1, - "MemberExpression": 2, - "ObjectExpression": 1, - "FunctionDeclaration": { - "body": 1, - "parameters": 2 - }, - "FunctionExpression": { - "body": 1, - "parameters": 2 - }, - "CallExpression": { - "arguments": 2 - }, - "ignoredNodes": ["ConditionalExpression"] - } - ], - "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": [ - "error", - { - "args": "after-used", - // Ignore vars starting with an underscore. - "varsIgnorePattern": "^_", - // Ignore arguments starting with an underscore. - "argsIgnorePattern": "^_" - } - ], - "no-use-before-define": ["error"], - // Blockly uses for exporting symbols. no-self-assign added in eslint 5. - "no-self-assign": ["off"], - // Blockly uses single quotes except for JSON blobs, which must use double quotes. - "quotes": ["off"], - "semi": ["error", "always"], - // Blockly doesn't have space before function paren when defining functions. - "space-before-function-paren": ["error", "never"], - // Blockly doesn't have space before function paren when calling functions. - "func-call-spacing": ["error", "never"], - "space-infix-ops": ["error"], - // Blockly uses 'use strict' in files. - "strict": ["off"], - // Closure style allows redeclarations. - "no-redeclare": ["off"], - "valid-jsdoc": ["error", {"requireReturn": false}], - "no-console": ["off"], - "no-multi-spaces": ["error", { "ignoreEOLComments": true }], - "operator-linebreak": ["error", "after"], - "spaced-comment": ["error", "always", { - "block": { - "balanced": true - }, - "exceptions": ["*"] - }], - "es5/no-es6-methods": ["warn"] - }, - "env": { - "browser": true - }, - "globals": { - "Blockly": true, - "goog": true - }, - "extends": [ - "eslint:recommended", - "plugin:es5/no-es2015" - ] -} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..176a458f94e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..d52d27df155 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @google/blockly-eng \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fcd866043f5..634b59bfad4 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -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 2017 we -support IE 10 and 11, 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). diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index bf67c2781ba..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -name: Bug Report -about: Create a report to help us improve -labels: 'type: bug, triage' -assignees: '' - ---- - - - -**Describe the bug** - - - -**To Reproduce** - - -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** - - - -**Screenshots** - - - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Stack Traces** - - - -``` -Replace with error stack trace. -``` - -**Additional context** - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000000..d346d87afe1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,58 @@ +name: Report a bug 🐛 +description: Report bugs in Blockly, so we can fix them. +labels: 'issue: bug, issue: triage' +body: + - type: markdown + attributes: + value: > + Thank you for taking the time to fill out a bug report! + If you have a question about how to use Blockly in your application, + please ask on the [forum](https://groups.google.com/forum/#!forum/blockly) instead of filing an issue. + - type: checkboxes + id: duplicates + attributes: + label: Check for duplicates + options: + - label: I have searched for similar issues before opening a new one. + - type: textarea + id: description + attributes: + label: Description + description: Please provide a clear and concise description of the bug. + placeholder: What happened? What did you expect to happen? + validations: + required: true + - type: textarea + id: repro + attributes: + label: Reproduction steps + description: What steps should we take to reproduce the issue? + value: | + 1. + 2. + 3. + - type: textarea + id: stack-trace + attributes: + label: Stack trace + description: If you saw an error message or stack trace, please include it here. + placeholder: The text in this section will be formatted automatically; no need to include backticks. + render: shell + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Screenshots can help us see the behavior you're describing. Please add a screenshot or gif, especially if you are describing a rendering or visual bug. + placeholder: Paste or drag-and-drop an image to upload it. + - type: dropdown + id: browsers + attributes: + label: Browsers + description: Please select all browsers you've observed this behavior on. If the bug isn't browser-specific, you can leave this blank. + multiple: true + options: + - Chrome desktop + - Safari desktop + - Firefox desktop + - Android mobile + - iOS mobile diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 17ed2a34d18..aa4bd749d99 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ contact_links: - - name: Blockly Forum + - name: Ask a question ❓ url: https://groups.google.com/forum/#!forum/blockly - about: The Blockly developer forum, where you can ask and answer questions. - - name: Plugins and examples + about: Go to the Blockly developer forum, where you can ask and answer questions. + - name: Report issues with plugins and examples 🧩 url: https://github.com/google/blockly-samples/issues/new/choose about: File bugs or feature requests about plugins and samples in our blockly-samples repository. diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index 56ea9fb6488..00000000000 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: Documentation -about: Report an issue with our documentation -labels: 'type: documentation, triage' -assignees: '' - ---- - - - -**Where** - - - -**What** - - - -- [ ] Text -- [ ] Image or Gif -- [ ] Other - -**Old content** - - - -**Suggested content** - - - -**Additional context** - - diff --git a/.github/ISSUE_TEMPLATE/documentation.yaml b/.github/ISSUE_TEMPLATE/documentation.yaml new file mode 100644 index 00000000000..e3a5b118821 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yaml @@ -0,0 +1,38 @@ +name: Report a documentation problem 📖 +description: Could our documentation be better? Tell us how. +labels: 'issue: docs, issue: triage' +body: + - type: markdown + attributes: + value: > + Thanks for helping us improve our developer site documentation! + Use this template to describe issues with the content on our + [developer site](https://developers.google.com/blockly/guides). + - type: input + id: link + attributes: + label: Location + description: > + A link to the page with the documentation you want us to be updated. + If no page exists, describe what the page should be, and where. + - type: checkboxes + id: type + attributes: + label: Type + description: What kind of content is it? + options: + - label: Text + - label: Image or Gif + - label: Other + - type: textarea + id: content + attributes: + label: Suggested content + description: Your suggestion for improved documentation. If it's helpful, also include the old content for comparison. + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem. If this is related to a specific pull request, link to it. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 5b85b635c6e..00000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -labels: 'type: feature request, triage' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** - - - -**Describe the solution you'd like** - - - -**Describe alternatives you've considered** - - - -**Additional context** - - diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 00000000000..04c3fdef6e2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,39 @@ +name: Make a feature request ✨ +description: Suggest an idea to make Blockly better. +labels: 'issue: feature request, issue: triage' +body: + - type: markdown + attributes: + value: > + Thank you for taking the time to fill out a feature request! + If you have a question about how to use Blockly in your application, + please ask on the [forum](https://groups.google.com/forum/#!forum/blockly) instead of filing an issue. + - type: checkboxes + id: duplicates + attributes: + label: Check for duplicates + options: + - label: I have searched for similar issues before opening a new one. + - type: textarea + id: problem + attributes: + label: Problem + description: Is your feature request related to a problem? Please describe. + placeholder: I'm always frustrated when... + - type: textarea + id: request + attributes: + label: Request + description: Describe your feature request and how it solves your problem. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Describe any alternative solutions or features you've considered. + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 211fff419a3..6a62a4d9d92 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,14 +7,13 @@ -- [ ] I branched from develop -- [ ] My pull request is against develop -- [ ] My code follows the [style guide](https://developers.google.com/blockly/guides/modify/web/style-guide) +- [ ] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves +Fixes ### Proposed Changes @@ -26,27 +25,11 @@ ### Test Coverage - - -Tested on: - - - - - - - - - + ### Documentation diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3ae6170df4e..42f0d297aea 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,8 +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)' + labels: + - 'PR: chore' + - 'PR: dependencies' + - package-ecosystem: 'github-actions' # See documentation for possible values + directory: '/' + target-branch: 'develop' + schedule: + interval: 'weekly' + commit-message: + prefix: 'chore(deps)' + labels: + - 'PR: chore' + - 'PR: dependencies' diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000000..a6b8dc3ef36 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,32 @@ +# release.yml + +changelog: + exclude: + labels: + - ignore-for-release + - 'PR: chore' + authors: + - dependabot + categories: + - title: Breaking changes 🛠 + labels: + - breaking change + - title: Deprecations 🧹 - APIs that may be removed in future releases + labels: + - deprecation + - title: New features ✨ + labels: + - 'PR: feature' + - title: Bug fixes 🐛 + labels: + - 'PR: fix' + - title: Cleanup ♻️ + labels: + - 'PR: docs' + - 'PR: refactor' + - title: Reverted changes ⎌ + labels: + - 'PR: revert' + - title: Other changes + labels: + - '*' diff --git a/.github/workflows/appengine_deploy.yml b/.github/workflows/appengine_deploy.yml new file mode 100644 index 00000000000..f7098d53a7e --- /dev/null +++ b/.github/workflows/appengine_deploy.yml @@ -0,0 +1,54 @@ +# Workflow that prepares files and deploys to appengine + +name: Deploy to App Engine + +# Controls when the workflow will run +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + prepare: + name: Prepare + runs-on: ubuntu-latest + + steps: + # Checks-out the repository under $GITHUB_WORKSPACE. + # When running manually this checks out the master branch. + - uses: actions/checkout@v4 + + - name: Prepare demo files + # Install all dependencies, then copy all the files needed for demos. + run: | + npm install + npm run prepareDemos + + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: appengine_files + path: _deploy/ + + deploy: + name: Deploy + runs-on: ubuntu-latest + # The prepare step must succeed for this step to run. + needs: prepare + steps: + - name: Download prepared files + uses: actions/download-artifact@v4 + with: + name: appengine_files + path: _deploy/ + + - name: Deploy to App Engine + uses: google-github-actions/deploy-appengine@v2.1.0 + # For parameters see: + # https://github.com/google-github-actions/deploy-appengine#inputs + with: + working_directory: _deploy/ + deliverables: app.yaml + project_id: ${{ secrets.GCP_PROJECT }} + credentials: ${{ secrets.GCP_SA_KEY }} + promote: false + version: vtest diff --git a/.github/workflows/assign_reviewers.yml b/.github/workflows/assign_reviewers.yml new file mode 100644 index 00000000000..33bd9e778a9 --- /dev/null +++ b/.github/workflows/assign_reviewers.yml @@ -0,0 +1,41 @@ +name: Assign requested reviewers + +# This workflow adds requested reviewers as assignees. If you remove a +# requested reviewer, it will not remove them as an assignee. +# +# See https://github.com/google/blockly/issues/5643 for more +# information on why this was added. +# +# N.B.: Runs with a read-write repo token. Do not check out the +# submitted branch! +on: + pull_request_target: + types: [review_requested] + +jobs: + requested-reviewer: + runs-on: ubuntu-latest + steps: + - name: Assign requested reviewer + uses: actions/github-script@v7 + 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); + } diff --git a/.github/workflows/browser_test.yml b/.github/workflows/browser_test.yml new file mode 100644 index 00000000000..3675af7b042 --- /dev/null +++ b/.github/workflows/browser_test.yml @@ -0,0 +1,55 @@ +# 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: [18.x, 20.x] + # See supported Node.js release schedule at + # https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + 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@v4 + 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: Run Build + run: npm run build + + - name: Run Test + run: npm run test:browser + + env: + CI: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000000..ad62722e1f7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,85 @@ +# This workflow will do a clean install, start the selenium server, and run +# all of our tests. + +name: Node.js CI + +on: [pull_request] + +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: [ubuntu-latest] + node-version: [18.x, 20.x] + # See supported Node.js release schedule at + # https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + 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@v4 + 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: Run + run: npm run test + + env: + CI: true + + lint: + timeout-minutes: 5 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - name: Npm Install + run: npm install + + - name: Lint + run: npm run lint + + format: + timeout-minutes: 5 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - name: Npm Install + run: npm install + + - name: Check Format + run: npm run format:check diff --git a/.github/workflows/conventional-label.yml b/.github/workflows/conventional-label.yml new file mode 100644 index 00000000000..64289d98723 --- /dev/null +++ b/.github/workflows/conventional-label.yml @@ -0,0 +1,17 @@ +on: + pull_request_target: + types: + - opened + - edited +name: conventional-release-labels +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: bcoe/conventional-release-labels@v1 + with: + 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: '[]' diff --git a/.github/workflows/develop_freeze.yml b/.github/workflows/develop_freeze.yml new file mode 100644 index 00000000000..395a34434dd --- /dev/null +++ b/.github/workflows/develop_freeze.yml @@ -0,0 +1,26 @@ +# This workflow will comment on pull requests that are submitted while develop +# is frozen during the week of release. Skips any pull requests that have the +# label 'ignore-freeze'. +# This workflow should be enabled only while develop is frozen. + +name: Develop Freeze PR Comment + +on: + # Trigger the workflow on pull request on develop branch + pull_request: + types: + - opened + - reopened + branches: + - develop + +jobs: + freeze-comment: + if: ${{ !contains(github.event.pull_request.labels.*.name, 'ignore-freeze') }} + runs-on: ubuntu-latest + steps: + - name: PR Comment + 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.' diff --git a/.github/workflows/tag_module_cleanup.yml b/.github/workflows/tag_module_cleanup.yml new file mode 100644 index 00000000000..d83d0e9371a --- /dev/null +++ b/.github/workflows/tag_module_cleanup.yml @@ -0,0 +1,37 @@ +# For new pull requests against the goog_module branch, adds the 'type: cleanup' +# label and sets the milestone to q3 2021 release. + +name: Tag module cleanup + +# Trigger on pull requests against goog_module branch only +# Uses pull_request_target to get write permissions so that it can write labels. +on: + pull_request_target: + branches: + - goog_module + +jobs: + tag-module-cleanup: + # Add the type: cleanup label + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + // Note that pull requests are considered issues and "shared" + // actions for both features, like manipulating labels and + // milestones are provided within the issues API. + await github.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + // 2021 q3 release milestone. + // https://github.com/google/blockly/milestone/18 + milestone: 18 + }) + await github.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['type: cleanup'] + }) diff --git a/.github/workflows/welcome_new_contributors.yml b/.github/workflows/welcome_new_contributors.yml new file mode 100644 index 00000000000..37ca9ef89df --- /dev/null +++ b/.github/workflows/welcome_new_contributors.yml @@ -0,0 +1,36 @@ +on: + pull_request_target: + types: + - opened +name: Welcome new contributors +jobs: + welcome: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + pr-message: > + Welcome! It looks like this is your first pull request in Blockly, + so here are a couple of tips: + + - You can find tips about contributing to Blockly and how to + validate your changes on our + [developer site](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change). + + - All contributors must sign the Google Contributor License + Agreement (CLA). If the google-cla bot leaves a comment on this + PR, make sure you follow the instructions. + + - We use conventional commits to make versioning the package easier. Make sure your commit + message is in the [proper format](https://developers.google.com/blockly/guides/contribute/get-started/commits) + or [learn how to fix it](https://developers.google.com/blockly/guides/contribute/get-started/commits#fixing_non-conventional_commits). + + - If any of the other checks on this PR fail, you can click on + them to learn why. It might be that your change caused a test + failure, or that you need to double-check the + [style guide](https://developers.google.com/blockly/guides/contribute/core/style_guide). + + Thank you for opening this PR! A member of the Blockly team will review it soon. diff --git a/.gitignore b/.gitignore index 1e7a1e198b1..3c1938f17d9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ build-debug.log *.pyc *.komodoproject /nbproject/private/ +tsdoc-metadata.json tests/compile/main_compressed.js tests/compile/main_compressed.js.map @@ -16,5 +17,6 @@ tests/screenshot/outputs/* local_build/*compiler*.jar local_build/local_*_compressed.js chromedriver -typings/tmp/* +build/ dist/ +temp/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..9d52f19fe6a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,30 @@ +# 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/* +/demos/* +/appengine/* +/externs/* +/closure/* +/scripts/gulpfiles/* +CHANGELOG.md +PULL_REQUEST_TEMPLATE.md + +# Don't bother formatting JavaScript files we're about to migrate: +/generators/**/*.js diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000000..bbfb466399d --- /dev/null +++ b/.prettierrc.js @@ -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, +}; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8384ecd53b1..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: node_js -os: linux -dist: xenial -node_js: - - 10 - - 12 - - 14 -addons: - chrome: stable - firefox: latest - # TODO (#2114): re-enable osx build. - # - os: osx - # node_js: stable - # osx_image: xcode8.3 - # addons: - # firefox: latest -env: - - TRAVIS_CI=true -before_script: - - export DISPLAY=:99.0 - - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then ( tests/scripts/setup_linux_env.sh ) fi - - if [ "${TRAVIS_OS_NAME}" == "osx" ]; then ( tests/scripts/setup_osx_env.sh ) fi - - sleep 2 -script: - - npm run test:run diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000000..cbe1c7ee290 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,77 @@ +# Changelog + +## [8.0.0](https://github.com/google/blockly/compare/blockly-v7.20211209.0...blockly-v8.0.0) (2022-03-31) + + +### ⚠ BREAKING CHANGES + +* change paste to return the pasted thing to support keyboard nav (#5996) +* **blocks:** ...and rename Blockly.blocks.all (blocks/all.js) to Blockly.libraryBlocks (blocks/blocks.js +* * refactor(blocks): Make loopTypes a Set +* allows previously internal constants to be configurable (#5897) +* * refactor(blocks): Make loopTypes a Set +* remove unused constants from internalConstants (#5889) + +### Features + +* add mocha failure messages to console output ([#5984](https://github.com/google/blockly/issues/5984)) ([7d250fa](https://github.com/google/blockly/commit/7d250fa9cfb30f95e7af523720b66c8b001df15c)) +* Allow developers to set a custom tooltip rendering function. ([#5956](https://github.com/google/blockly/issues/5956)) ([6841ccc](https://github.com/google/blockly/commit/6841ccc99fdbcc5f6d5a63bb36cb3b6ebd2be246)) +* **blocks:** Export block definitions ([#5908](https://github.com/google/blockly/issues/5908)) ([ffb8907](https://github.com/google/blockly/commit/ffb8907db8d0f11609c1fe14b2a450d3e639a871)) +* make mocha fail if it encounters 0 tests ([#5981](https://github.com/google/blockly/issues/5981)) ([0b2bf3a](https://github.com/google/blockly/commit/0b2bf3ae9d0c777f4d13d47692f5ae224dff1ec8)) +* **tests:** Add a test to validate `scripts/migration/renamings.js` ([#5980](https://github.com/google/blockly/issues/5980)) ([3c723f0](https://github.com/google/blockly/commit/3c723f0199b1f3b5eaac58f064b02d52b60d0ddb)) +* **tests:** Use official semver.org RegExp ([#5990](https://github.com/google/blockly/issues/5990)) ([afc4088](https://github.com/google/blockly/commit/afc4088ce278f97585f9ff5e65a921f7c4c65531)) + + +### Bug Fixes + +* Adds check for changedTouches ([#5869](https://github.com/google/blockly/issues/5869)) ([3f4f505](https://github.com/google/blockly/commit/3f4f5057919fdb4a329e9d2b15378c5c5831ae3b)) +* advanced playground and playground to work when hosted ([#6021](https://github.com/google/blockly/issues/6021)) ([364bf14](https://github.com/google/blockly/commit/364bf14ce6932f426591e3f53c1d066771ddcb8e)) +* always rename caller to legal name ([#6014](https://github.com/google/blockly/issues/6014)) ([c430800](https://github.com/google/blockly/commit/c4308007bc4b58d51adf1fda7b51ffa9f1d3f093)) +* **blocks:** correct the callType_ of procedures_defreturn ([#5974](https://github.com/google/blockly/issues/5974)) ([b34db5b](https://github.com/google/blockly/commit/b34db5bd01f7b532ebabc80264ca9fc804a76c75)) +* **build:** Correctly handle deep export paths in UMD wrapper ([#5945](https://github.com/google/blockly/issues/5945)) ([71ab146](https://github.com/google/blockly/commit/71ab146bc21aef9bdd6b2385c1df5f51e3ff5b58)) +* bumping a block after duplicate breaking undo ([#5844](https://github.com/google/blockly/issues/5844)) ([5204569](https://github.com/google/blockly/commit/5204569cff58c1ead7c15165a1351fa6a2ba2ad3)) +* change getCandidate_ and showInsertionMarker_ to be more dynamic ([#5722](https://github.com/google/blockly/issues/5722)) ([68d8113](https://github.com/google/blockly/commit/68d81132b851d20884ee9da41719fa62cdfce0ee)) +* change paste to return the pasted thing to support keyboard nav ([#5996](https://github.com/google/blockly/issues/5996)) ([20f1475](https://github.com/google/blockly/commit/20f1475afc1abf4b5e600219c2981150fc621ba5)) +* Change the truthy tests of width and height in WorkspaceSvg.setCachedParentSvgSize to actual comparisons with null so that zero value can be saved into the cache ([#5997](https://github.com/google/blockly/issues/5997)) ([fec44d9](https://github.com/google/blockly/commit/fec44d917e4b8475beba28e4769a50982425e887)) +* comments not being restored when dragging ([#6011](https://github.com/google/blockly/issues/6011)) ([85ce3b8](https://github.com/google/blockly/commit/85ce3b82c6c32e8a2a1608c6d604262ea0e5c38d)) +* convert the common renderer to an ES6 class ([#5978](https://github.com/google/blockly/issues/5978)) ([c1004be](https://github.com/google/blockly/commit/c1004be1f24debe1df1566e6067cf2f6769d51aa)) +* convert the Workspace class to an ES6 class ([#5977](https://github.com/google/blockly/issues/5977)) ([e2eaebe](https://github.com/google/blockly/commit/e2eaebec47b08a83eb36d0d04cefa254d1c5d666)) +* custom block context menus ([#5976](https://github.com/google/blockly/issues/5976)) ([8058df2](https://github.com/google/blockly/commit/8058df2a71dcecdc1190ae1d6f5dcccfafc980e8)) +* Don't throw if drag surface is empty. ([#5695](https://github.com/google/blockly/issues/5695)) ([769a25f](https://github.com/google/blockly/commit/769a25f4badffd2409ce19535344c98f5d8b01c9)) +* export Blockly.Names.NameType and Blockly.Input.Align correctly ([#6030](https://github.com/google/blockly/issues/6030)) ([2c15d00](https://github.com/google/blockly/commit/2c15d002ababcba7f34c526c05f231735e3e0169)) +* Export loopTypes from Blockly.blocks.loops ([#5900](https://github.com/google/blockly/issues/5900)) ([4f74210](https://github.com/google/blockly/commit/4f74210e74ef0b06216ab0f288268192674d69c9)) +* Export loopTypes from Blockly.blocks.loops ([#5900](https://github.com/google/blockly/issues/5900)) ([74ef1cb](https://github.com/google/blockly/commit/74ef1cbf521f7c6447ea9672ddbfe861d2292e5f)) +* Fix bug where workspace comments could not be created. ([#6024](https://github.com/google/blockly/issues/6024)) ([2cf8eb8](https://github.com/google/blockly/commit/2cf8eb87dcb029ba152b63b01ee7e4df431d1bb6)) +* Fix downloading screenshots on the playground. ([#6025](https://github.com/google/blockly/issues/6025)) ([ca6e590](https://github.com/google/blockly/commit/ca6e590101d511a8d98a5c5438af32ff6749e020)) +* fix keycodes type ([#5805](https://github.com/google/blockly/issues/5805)) ([0a96543](https://github.com/google/blockly/commit/0a96543a1179636e4efeb3c654c075952aca0c9f)) +* Fixed the label closure on demo/blockfactory ([#5833](https://github.com/google/blockly/issues/5833)) ([e8ea2e9](https://github.com/google/blockly/commit/e8ea2e9902fb9f642459e7341c3d59e19f359fca)) +* **generators:** Fix an operator precedence issue in the math_number_property generators to remove extra parentheses ([#5685](https://github.com/google/blockly/issues/5685)) ([a31003f](https://github.com/google/blockly/commit/a31003fab964e529152389029ec3126a3802851b)) +* incorrect module for event data in renamings database ([#6012](https://github.com/google/blockly/issues/6012)) ([e502eaa](https://github.com/google/blockly/commit/e502eaa6e1c88b2bb34e9a87917a15098b81cfa3)) +* Move [@alias](https://github.com/alias) onto classes instead of constructors ([#6003](https://github.com/google/blockly/issues/6003)) ([1647a32](https://github.com/google/blockly/commit/1647a3299ac48b5924f987015d8f3c47593922af)) +* move test helpers from samples into core ([#5969](https://github.com/google/blockly/issues/5969)) ([2edd228](https://github.com/google/blockly/commit/2edd22811752f05e16c68d593e5d1b809e24ed25)) +* move the dropdown div to a namespace instead of a class with only static properties ([#5979](https://github.com/google/blockly/issues/5979)) ([543cb8e](https://github.com/google/blockly/commit/543cb8e1b1c1a7fca5a1629f42f71c9b18e1a255)) +* msg imports in type definitions ([#5858](https://github.com/google/blockly/issues/5858)) ([07a75de](https://github.com/google/blockly/commit/07a75dee8de13b6c5a02959325a0155d413d6712)) +* opening/closing the mutators ([#6000](https://github.com/google/blockly/issues/6000)) ([243fc52](https://github.com/google/blockly/commit/243fc52a96e1089aad89ff6b642c6605d8f71afd)) +* playground access to Blockly ([9e1cda8](https://github.com/google/blockly/commit/9e1cda8f45cea1707c5a228d5ce79b4cd81566f8)) +* playground test blocks, text area listeners, and show/hide buttons ([#6015](https://github.com/google/blockly/issues/6015)) ([7abf3de](https://github.com/google/blockly/commit/7abf3de910a35e1a6086a3243570627a41e73339)) +* procedure param edits breaking undo ([#5845](https://github.com/google/blockly/issues/5845)) ([8a71f87](https://github.com/google/blockly/commit/8a71f879504503f4aec1140fe653d93602c664df)) +* re-expose HSV_VALUE and HSV_SATURATION as settable properties on Blockly ([#5821](https://github.com/google/blockly/issues/5821)) ([0e5f3ce](https://github.com/google/blockly/commit/0e5f3ce6074fbbb2923e9a62bffefeae0a813be8)) +* re-expose HSV_VALUE and HSV_SATURATION as settable properties on Blockly ([#5821](https://github.com/google/blockly/issues/5821)) ([6fc3316](https://github.com/google/blockly/commit/6fc3316309534270106050f0e1fecb7a09b8e62c)) +* revert "Delete events should animate when played ([#5919](https://github.com/google/blockly/issues/5919))" ([#6031](https://github.com/google/blockly/issues/6031)) ([c4a25eb](https://github.com/google/blockly/commit/c4a25eb3c432b0e8a9a18aae42839d163a177c48)) +* revert converting test helpers to es modules ([#5982](https://github.com/google/blockly/issues/5982)) ([01d4597](https://github.com/google/blockly/commit/01d45972d4df8b5e4afa4a19d93defb8961fea57)) +* setting null for a font style on a theme ([#5831](https://github.com/google/blockly/issues/5831)) ([835fb02](https://github.com/google/blockly/commit/835fb02343df0a4b9dab7704a4b3d8be8e9a497c)) +* **tests:** Enable --debug for test:compile:advanced; fix some errors ([#5959](https://github.com/google/blockly/issues/5959)) ([88334be](https://github.com/google/blockly/commit/88334bea80aa26c08705f16aba5e79dd708158f9)) +* **tests:** Enable `--debug` for `test:compile:advanced`; fix some errors (and demote the rest to warnings) ([#5983](https://github.com/google/blockly/issues/5983)) ([e11b583](https://github.com/google/blockly/commit/e11b5834e5e4e8fe991be32afb08eafa7c8adffd)) +* TypeScript exporting of the serialization functions ([#5890](https://github.com/google/blockly/issues/5890)) ([5d7c890](https://github.com/google/blockly/commit/5d7c890243ba7d0501514ba48778715097ce5a3b)) +* undo/redo for auto disabling if-return blocks ([#6018](https://github.com/google/blockly/issues/6018)) ([c7a359a](https://github.com/google/blockly/commit/c7a359a8424287f139752573a27a8a6eb97cb7b3)) +* update the playground to load compressed when hosted ([#5835](https://github.com/google/blockly/issues/5835)) ([2adf326](https://github.com/google/blockly/commit/2adf326c230589800880faa9599eca2ecc94d283)) +* Update typings for q1 2022 release ([#6051](https://github.com/google/blockly/issues/6051)) ([69f3d4a](https://github.com/google/blockly/commit/69f3d4ae89ce16a558443dd0a772e35b62c096d3)) +* Use correct namespace for svgMath functions ([#5813](https://github.com/google/blockly/issues/5813)) ([b8cc983](https://github.com/google/blockly/commit/b8cc983324338b2cbd536425c93ff3e7d512751e)) +* Use correct namespace for svgMath functions ([#5813](https://github.com/google/blockly/issues/5813)) ([025bab6](https://github.com/google/blockly/commit/025bab656669f99ebdb8b95bea39ebae296f1495)) + + +### Code Refactoring + +* allows previously internal constants to be configurable ([#5897](https://github.com/google/blockly/issues/5897)) ([4b5733e](https://github.com/google/blockly/commit/4b5733e7c85f2e196719550a3cfdcbcbd61739df)) +* **blocks:** Rename Blockly.blocks.* modules to Blockly.libraryBlocks.* ([#5953](https://github.com/google/blockly/issues/5953)) ([5078dcb](https://github.com/google/blockly/commit/5078dcbc6d4d48422313732e87191b29569b5eed)) +* remove unused constants from internalConstants ([#5889](https://github.com/google/blockly/issues/5889)) ([f0b1077](https://github.com/google/blockly/commit/f0b10776eb0657a5446adcfc62ad13f419c14271)) diff --git a/README.md b/README.md index 0551dda930a..5a0f3b8f27b 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,58 @@ -# Blockly [![Build Status]( https://travis-ci.org/google/blockly.svg?branch=master)](https://travis-ci.org/google/blockly) +# Blockly +Google's Blockly is a library that adds a visual code editor to web and mobile apps. The Blockly editor uses interlocking, graphical blocks to represent code concepts like variables, logical expressions, loops, and more. It allows users to apply programming principles without having to worry about syntax or the intimidation of a blinking cursor on the command line. All code is free and open source. -Google's Blockly is a web-based, visual programming editor. Users can drag -blocks together to build programs. All code is free and open source. +![](https://developers.google.com/blockly/images/sample.png) -**The project page is https://developers.google.com/blockly/** +## Getting Started with Blockly -![](https://developers.google.com/blockly/images/sample.png) +Blockly has many resources for learning how to use the library. Start at our [Google Developers Site](https://developers.google.com/blockly) to read the documentation on how to get started, configure Blockly, and integrate it into your application. The developers site also contains links to: -Blockly has an active [developer forum](https://groups.google.com/forum/#!forum/blockly). Please drop by and say hello. Show us your prototypes early; 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. +- [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. -Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com) +### Installing Blockly + +Blockly is [available on npm](https://www.npmjs.com/package/blockly). + +```bash +npm install blockly +``` -We support IE11 and test it using [BrowserStack](https://browserstack.com) +For more information on installing and using Blockly, see the [Getting Started article](https://developers.google.com/blockly/guides/get-started/web). -Want to contribute? Great! First, read [our guidelines for contributors](https://developers.google.com/blockly/guides/modify/contributing). +### 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. + +### blockly-samples + +We have a number of resources such as example code, demos, and plugins in another repository called [blockly-samples](https://github.com/google/blockly-samples/). A plugin is a self-contained piece of code that adds functionality to Blockly. Plugins can add fields, define themes, create renderers, and much more. For more information, see the [Plugins documentation](https://developers.google.com/blockly/guides/plugins/overview). + +## Contributing to Blockly + +Want to make Blockly better? We welcome contributions to Blockly in the form of pull requests, bug reports, documentation, answers on the forum, and more! Check out our [Contributing Guidelines](https://developers.google.com/blockly/guides/modify/contributing) for more information. You might also want to look for issues tagged "[Help Wanted](https://github.com/google/blockly/labels/help%20wanted)" which are issues we think would be great for external contributors to help with. ## Releases -The next major release will be **September 25th, 2020**. +We release by pushing the latest code to the master branch, followed by updating the npm package, our [docs](https://developers.google.com/blockly), and [demo pages](https://google.github.io/blockly-samples/). If there are breaking bugs, such as a crash when performing a standard action or a rendering issue that makes Blockly unusable, we will cherry-pick fixes to master between releases to fix them. The [releases page](https://github.com/google/blockly/releases) has a list of all releases. + +We use [semantic versioning](https://semver.org/). Releases that have breaking changes or are otherwise not backwards compatible will have a new major version. Patch versions are reserved for bug-fix patches between scheduled releases. -We release by pushing the latest code to the master branch, followed by updating our [docs](https://developers.google.com/blockly) and [demo pages](https://blockly-demo.appspot.com). We typically release a new version of Blockly once a quarter (every 3 months). If there are breaking bugs, such as a crash when performing a standard action or a rendering issue that makes Blockly unusable, we will cherry-pick fixes to master between releases to fix them. The [releases page](https://github.com/google/blockly/releases) has a list of all releases. +We now have a [beta release on npm](https://www.npmjs.com/package/blockly?activeTab=versions). If you'd like to test the upcoming release, or try out a not-yet-released new API, you can use the beta channel with: -Releases are tagged by the release date (YYYYMMDD) with a leading '2.' and a trailing '.0' in case we ever need a major or minor version (such as [2.20190722.1](https://github.com/google/blockly/tree/2.20190722.1)). If you're using npm, you can install the ``blockly`` package on npm: ```bash -npm install blockly +npm install blockly@beta ``` -### New APIs - -Once a new API is merged into master it is considered beta until the following release. We generally try to avoid changing an API after it has been merged to master, but sometimes we need to make changes after seeing how an API is used. If an API has been around for at least two releases we'll do our best to avoid breaking it. - -Unreleased APIs may change radically. Anything that is in `develop` but not `master` is subject to change without warning. +As it is a beta channel, it may be less stable, and the APIs there are subject to change. ### Branches @@ -47,14 +64,17 @@ There are two main branches for Blockly. **other branches:** - Larger changes may have their own branches until they are good enough for people to try out. These will be developed separately until we think they are almost ready for release. These branches typically get merged into develop immediately after a release to allow extra time for testing. -## Issues and Milestones +### New APIs + +Once a new API is merged into master it is considered beta until the following release. We generally try to avoid changing an API after it has been merged to master, but sometimes we need to make changes after seeing how an API is used. If an API has been around for at least two releases we'll do our best to avoid breaking it. -We typically triage all bugs within 2 working days, which includes adding any appropriate labels and assigning it to a milestone. Please keep in mind, we are a small team so even feature requests that everyone agrees on may not be prioritized. +Unreleased APIs may change radically. Anything that is in `develop` but not `master` is subject to change without warning. -### Milestones +## Issues and Milestones -**Upcoming release** - The upcoming release milestone is for all bugs we plan on fixing before the next release. This typically has the form of `year_quarter_release` (such as `2019_q2_release`). Some bugs will be added to this release when they are triaged, others may be added closer to a release. +We typically triage all bugs within 1 week, which includes adding any appropriate labels and assigning it to a milestone. Please keep in mind, we are a small team so even feature requests that everyone agrees on may not be prioritized. -**Bug Bash Backlog** - These are bugs that we're still prioritizing. They haven't been added to a specific release yet, but we'll consider them for each release depending on relative priority and available time. +## Good to Know -**Icebox** - These are bugs that we do not intend to spend time on. They are either too much work or minor enough that we don't expect them to ever take priority. We are still happy to accept pull requests for these bugs. +- Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com) +- We test browsers using [BrowserStack](https://browserstack.com) diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000000..bd053b36b40 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +exclude: [] diff --git a/api-extractor.json b/api-extractor.json new file mode 100644 index 00000000000..6c599d255b4 --- /dev/null +++ b/api-extractor.json @@ -0,0 +1,385 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + // "projectFolder": "..", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + "mainEntryPointFilePath": "dist/index.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we can specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + */ + "bundledPackages": [], + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + // "tsconfigFilePath": "/tsconfig.json", + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": false + + /** + * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce + * a full file path. + * + * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: ".api.md" + */ + // "reportFileName": ".api.md", + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportFolder": "/temp/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportTempFolder": "/temp/" + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": true + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json" + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": true, + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + "untrimmedFilePath": "/dist/_rollup.d.ts" + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release. + * This file will include only declarations that are marked as "@public", "@beta", or "@alpha". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "alphaTrimmedFilePath": "/dist/-alpha.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "publicTrimmedFilePath": "/dist/-public.d.ts", + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + // "enabled": true, + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning" + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + } + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + }, + + // We don't use `@public`, that's just the default. + "ae-missing-release-tag": { + "logLevel": "none" + }, + + // Needs investigation. + "ae-forgotten-export": { + "logLevel": "none" + } + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + }, + + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + }, + + // These two are due to "type-like" tags in JsDoc like + // `@suppress {warningName}`. The braces are unexpected in TsDoc. + "tsdoc-malformed-inline-tag": { + "logLevel": "none" + }, + "tsdoc-escape-right-brace": { + "logLevel": "none" + } + } + } +} diff --git a/appengine/.gcloudignore b/appengine/.gcloudignore index 8e8c010a308..00d3868b177 100644 --- a/appengine/.gcloudignore +++ b/appengine/.gcloudignore @@ -2,18 +2,15 @@ .* *.soy *.komodoproject -/deploy +deploy /static/appengine/ -/static/closure/ /static/demos/plane/soy/*.jar /static/demos/plane/xlf/ /static/externs/ /static/msg/json/ -/static/node_modules/ /static/scripts/ /static/typings/ -/static/build.py /static/eslintrc.json /static/gulpfile.js /static/jsconfig.json diff --git a/appengine/add_timestamps.py b/appengine/add_timestamps.py index 8cf90b522aa..05ff7b92814 100644 --- a/appengine/add_timestamps.py +++ b/appengine/add_timestamps.py @@ -33,7 +33,7 @@ Start a venv: `python3 -m venv venv && source venv/bin/activate` Inside your vm run `pip install google-cloud-ndb` -Run the script: `python add_timestamps.py` +Run the script: `python3 add_timestamps.py` """ __author__ = "fenichel@google.com (Rachel Fenichel)" @@ -62,8 +62,8 @@ def run_query(): while more: results, cursor, more = query.fetch_page(PAGE_SIZE, start_cursor=cursor) handle_results(results) - page_count = page_count + 1 - result_count = result_count + len(results) + page_count += 1 + result_count += len(results) print(f'{datetime.datetime.now().strftime("%I:%M:%S %p")} : page {page_count} : {result_count}') run_query() diff --git a/appengine/app.yaml b/appengine/app.yaml index 2fd93c49440..9c93fb6873d 100644 --- a/appengine/app.yaml +++ b/appengine/app.yaml @@ -1,4 +1,4 @@ -runtime: python37 +runtime: python312 handlers: # Redirect obsolete URLs. @@ -14,6 +14,58 @@ handlers: - url: /static/apps/.* static_files: redirect.html upload: redirect.html +# Certain demos were moved on 25 Nov 2022. +- url: /static/demos/fixed/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/resizable/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/toolbox/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/maxBlocks/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/generator/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/headless/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/interpreter/step-execution.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/interpreter/async-execution.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/graph/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/rtl/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/custom-dialogs/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/custom-fields/turtle/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/custom-fields/pitch/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/mirror/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/plane/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/keyboard_nav/.* + static_files: redirect.html + upload: redirect.html +- url: /static/demos/custom-fields/.* + static_files: redirect.html + upload: redirect.html # Blockly files. - url: /static @@ -50,4 +102,3 @@ handlers: - url: /.* script: auto secure: always - \ No newline at end of file diff --git a/appengine/blockly_compressed.js b/appengine/blockly_compressed.js new file mode 100644 index 00000000000..dc118dfbe20 --- /dev/null +++ b/appengine/blockly_compressed.js @@ -0,0 +1,11 @@ +// Added November 2022 after discovering that a number of orgs were hot-linking +// their Blockly applications to https://blockly-demo.appspot.com/ +// Delete this file in early 2024. +var msg = 'Compiled Blockly files should be loaded from https://unpkg.com/blockly/\n' + + 'For help, contact https://groups.google.com/g/blockly'; +console.log(msg); +try { + alert(msg); +} catch (_e) { + // Can't alert? Probably node.js. +} diff --git a/appengine/deploy b/appengine/deploy deleted file mode 100755 index 7fb5e6c0c07..00000000000 --- a/appengine/deploy +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -# Script to deploy on GAE. - -APP=./app.yaml -if ! [ -f $APP ] ; then - echo $APP not found 1>&2 - exit 1 -fi - -PROJECT=blockly-demo -VERSION=37 - -echo 'Beginning deployment...' -gcloud app deploy --project $PROJECT --version $VERSION --no-promote -echo 'Deployment finished.' diff --git a/appengine/expiration.py b/appengine/expiration.py index 19142c30ab4..89d4a7a8d5d 100644 --- a/appengine/expiration.py +++ b/appengine/expiration.py @@ -39,13 +39,14 @@ def delete_expired(): results = query.fetch(limit=QUERY_LIMIT, keys_only=True) for x in results: x.delete() + return len(results) def app(environ, start_response): - out = "" headers = [ ("Content-Type", "text/plain") ] start_response("200 OK", headers) - delete_expired() + n = delete_expired() + out = "%d records deleted." % n return [out.encode("utf-8")] diff --git a/appengine/redirect.html b/appengine/redirect.html index 9c29bac59f9..cc7cf301848 100644 --- a/appengine/redirect.html +++ b/appengine/redirect.html @@ -47,20 +47,59 @@ } } -if (loc.match('/apps/puzzle/')) { +if (loc.includes('/apps/puzzle/')) { // Puzzle moved to Blockly Games on 15 Oct 2014. - loc = 'https://blockly-games.appspot.com/puzzle'; -} else if (loc.match('/apps/maze/')) { + loc = 'https://blockly.games/puzzle'; +} else if (loc.includes('/apps/maze/')) { // Maze moved to Blockly Games on 10 Nov 2014. - loc = 'https://blockly-games.appspot.com/maze'; -} else if (loc.match('/apps/turtle/')) { + loc = 'https://blockly.games/maze'; +} else if (loc.includes('/apps/turtle/')) { // Turtle moved to Blockly Games on 10 Nov 2014. - loc = 'https://blockly-games.appspot.com/turtle'; -} else if (loc.match('/apps/')) { + loc = 'https://blockly.games/turtle'; +} else if (loc.includes('/apps/')) { // Remaining apps moved to demos on 20 Nov 2014. loc = loc.replace('/apps/', '/demos/'); } +// Demos without saved data were moved to Blockly Samples in 2021. +if (loc.includes('/demos/fixed/')) { + loc = 'https://google.github.io/blockly-samples/examples/fixed-demo/'; +} else if (loc.includes('/demos/resizable/overlay')) { + loc = 'https://google.github.io/blockly-samples/examples/resizable-demo/overlay.html'; +} else if (loc.includes('/demos/resizable/')) { + loc = 'https://google.github.io/blockly-samples/examples/resizable-demo/'; +} else if (loc.includes('/demos/toolbox/')) { + loc = 'https://google.github.io/blockly-samples/examples/toolbox-demo/'; +} else if (loc.includes('/demos/maxBlocks/')) { + loc = 'https://google.github.io/blockly-samples/examples/max-blocks-demo/'; +} else if (loc.includes('/demos/generator/')) { + loc = 'https://google.github.io/blockly-samples/examples/generator-demo/'; +} else if (loc.includes('/demos/headless/')) { + loc = 'https://google.github.io/blockly-samples/examples/headless-demo/'; +} else if (loc.includes('/demos/interpreter/step-execution')) { + loc = 'https://google.github.io/blockly-samples/examples/interpreter-demo/step-execution.html'; +} else if (loc.includes('/demos/interpreter/async-execution')) { + loc = 'https://google.github.io/blockly-samples/examples/interpreter-demo/async-execution.html'; +} else if (loc.includes('/demos/graph/')) { + loc = 'https://google.github.io/blockly-samples/examples/graph-demo/'; +} else if (loc.includes('/demos/rtl/')) { + loc = 'https://google.github.io/blockly-samples/examples/rtl-demo/'; +} else if (loc.includes('/demos/custom-dialogs/')) { + loc = 'https://google.github.io/blockly-samples/examples/custom-dialogs-demo'; +} else if (loc.includes('/demos/custom-fields/turtle/')) { + loc = 'https://google.github.io/blockly-samples/examples/turtle-field-demo/'; +} else if (loc.includes('/demos/custom-fields/pitch/')) { + loc = 'https://google.github.io/blockly-samples/examples/pitch-field-demo/'; +} else if (loc.includes('/demos/mirror/')) { + loc = 'https://google.github.io/blockly-samples/examples/mirror-demo/'; +} else if (loc.includes('/demos/plane/')) { + loc = 'https://google.github.io/blockly-samples/examples/plane-demo/'; +} else if (loc.includes('/demos/keyboard_nav/')) { + loc = 'https://google.github.io/blockly-samples/plugins/keyboard-navigation/test/'; +} else if (loc.includes('/demos/custom-fields/')) { + loc = 'https://google.github.io/blockly-samples/examples/pitch-field-demo/'; +} + location = loc; diff --git a/appengine/storage.js b/appengine/storage.js index 82a74052acd..c1157177133 100644 --- a/appengine/storage.js +++ b/appengine/storage.js @@ -6,7 +6,6 @@ /** * @fileoverview Loading and saving blocks with localStorage and cloud storage. - * @author q.neutron@gmail.com (Quynh Neutron) */ 'use strict'; @@ -45,7 +44,7 @@ BlocklyStorage.restoreBlocks = function(opt_workspace) { var url = window.location.href.split('#')[0]; if ('localStorage' in window && window.localStorage[url]) { var workspace = opt_workspace || Blockly.getMainWorkspace(); - var xml = Blockly.Xml.textToDom(window.localStorage[url]); + var xml = Blockly.utils.xml.textToDom(window.localStorage[url]); Blockly.Xml.domToWorkspace(xml, workspace); } }; @@ -59,7 +58,7 @@ BlocklyStorage.link = function(opt_workspace) { var xml = Blockly.Xml.workspaceToDom(workspace, true); // Remove x/y coordinates from XML if there's only one block stack. // There's no reason to store this, removing it helps with anonymity. - if (workspace.getTopBlocks(false).length == 1 && xml.querySelector) { + if (workspace.getTopBlocks(false).length === 1 && xml.querySelector) { var block = xml.querySelector('block'); if (block) { block.removeAttribute('x'); @@ -116,21 +115,23 @@ BlocklyStorage.makeRequest_ = function(url, name, content, workspace) { * @private */ BlocklyStorage.handleRequest_ = function() { - if (BlocklyStorage.httpRequest_.readyState == 4) { - if (BlocklyStorage.httpRequest_.status != 200) { + if (BlocklyStorage.httpRequest_.readyState === 4) { + if (BlocklyStorage.httpRequest_.status !== 200) { BlocklyStorage.alert(BlocklyStorage.HTTPREQUEST_ERROR + '\n' + 'httpRequest_.status: ' + BlocklyStorage.httpRequest_.status); } else { var data = BlocklyStorage.httpRequest_.responseText.trim(); - if (BlocklyStorage.httpRequest_.name == 'xml') { + if (BlocklyStorage.httpRequest_.name === 'xml') { window.location.hash = data; BlocklyStorage.alert(BlocklyStorage.LINK_ALERT.replace('%1', window.location.href)); - } else if (BlocklyStorage.httpRequest_.name == 'key') { + } else if (BlocklyStorage.httpRequest_.name === 'key') { if (!data.length) { BlocklyStorage.alert(BlocklyStorage.HASH_ERROR.replace('%1', window.location.hash)); } else { + // Remove poison line to prevent raw content from being served. + data = data.replace(/^\{\[\(\< UNTRUSTED CONTENT \>\)\]\}\n/, ''); BlocklyStorage.loadXml_(data, BlocklyStorage.httpRequest_.workspace); } } @@ -153,7 +154,7 @@ BlocklyStorage.monitorChanges_ = function(workspace) { function change() { var xmlDom = Blockly.Xml.workspaceToDom(workspace); var xmlText = Blockly.Xml.domToText(xmlDom); - if (startXmlText != xmlText) { + if (startXmlText !== xmlText) { window.location.hash = ''; workspace.removeChangeListener(change); } @@ -169,7 +170,7 @@ BlocklyStorage.monitorChanges_ = function(workspace) { */ BlocklyStorage.loadXml_ = function(xml, workspace) { try { - xml = Blockly.Xml.textToDom(xml); + xml = Blockly.utils.xml.textToDom(xml); } catch (e) { BlocklyStorage.alert(BlocklyStorage.XML_ERROR + '\nXML: ' + xml); return; diff --git a/appengine/storage.py b/appengine/storage.py index e90375bf1e7..34db68b29ac 100644 --- a/appengine/storage.py +++ b/appengine/storage.py @@ -15,15 +15,15 @@ limitations under the License. """ -"""Store and retrieve XML with App Engine. +"""Store and retrieve Blockly XML/JSON with App Engine. """ __author__ = "q.neutron@gmail.com (Quynh Neutron)" -import cgi import hashlib -from random import randint from google.cloud import ndb +from random import randint +from urllib.parse import unquote class Xml(ndb.Model): @@ -32,6 +32,7 @@ class Xml(ndb.Model): xml_content = ndb.TextProperty() last_accessed = ndb.DateTimeProperty(auto_now=True) + def keyGen(): # Generate a random string of length KEY_LEN. KEY_LEN = 6 @@ -40,13 +41,28 @@ def keyGen(): return "".join([CHARS[randint(0, max_index)] for x in range(KEY_LEN)]) +# Parse POST data (e.g. a=1&b=2) into a dictionary (e.g. {"a": 1, "b": 2}). +# Very minimal parser. Does not combine repeated names (a=1&a=2), ignores +# valueless names (a&b), does not support isindex or multipart/form-data. +def parse_post(environ): + fp = environ["wsgi.input"] + data = fp.read().decode() + parts = data.split("&") + dict = {} + for part in parts: + tuple = part.split("=", 1) + if len(tuple) == 2: + dict[tuple[0]] = unquote(tuple[1]) + return dict + + def xmlToKey(xml_content): - # Store XML and return a generated key. + # Store XML/JSON and return a generated key. xml_hash = int(hashlib.sha1(xml_content.encode("utf-8")).hexdigest(), 16) xml_hash = int(xml_hash % (2 ** 64) - (2 ** 63)) - lookup_query = Xml.query(Xml.xml_hash == xml_hash) client = ndb.Client() with client.context(): + lookup_query = Xml.query(Xml.xml_hash == xml_hash) lookup_result = lookup_query.get() if lookup_result: xml_key = lookup_result.key.string_id() @@ -65,7 +81,7 @@ def xmlToKey(xml_content): def keyToXml(key_provided): - # Retrieve stored XML based on the provided key. + # Retrieve stored XML/JSON based on the provided key. # Normalize the string. key_provided = key_provided.lower().strip() # Check datastore for a match. @@ -80,20 +96,30 @@ def keyToXml(key_provided): with client.context(): result.put() xml = result.xml_content + # Add a poison line to prevent raw content from being served. + xml = "{[(< UNTRUSTED CONTENT >)]}\n" + xml return xml def app(environ, start_response): - forms = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ) + headers = [ + ("Content-Type", "text/plain") + ] + if environ["REQUEST_METHOD"] != "POST": + start_response("405 Method Not Allowed", headers) + return ["Storage only accepts POST".encode("utf-8")] + if ("CONTENT_TYPE" in environ and + environ["CONTENT_TYPE"] != "application/x-www-form-urlencoded"): + start_response("405 Method Not Allowed", headers) + return ["Storage only accepts application/x-www-form-urlencoded".encode("utf-8")] + + forms = parse_post(environ) if "xml" in forms: - out = xmlToKey(forms["xml"].value) + out = xmlToKey(forms["xml"]) elif "key" in forms: - out = keyToXml(forms["key"].value) + out = keyToXml(forms["key"]) else: out = "" - headers = [ - ("Content-Type", "text/plain") - ] start_response("200 OK", headers) return [out.encode("utf-8")] diff --git a/blockly_compressed.js b/blockly_compressed.js deleted file mode 100644 index 44cdda8d4b9..00000000000 --- a/blockly_compressed.js +++ /dev/null @@ -1,1455 +0,0 @@ -// Do not edit this file; automatically generated by gulp. - -/* eslint-disable */ -;(function(root, factory) { - if (typeof define === 'function' && define.amd) { // AMD - define([], factory); - } else if (typeof exports === 'object') { // Node.js - module.exports = factory(); - } else { // Browser - root.Blockly = factory(); - } -}(this, function() { - 'use strict';var Blockly={constants:{},LINE_MODE_MULTIPLIER:40,PAGE_MODE_MULTIPLIER:125,DRAG_RADIUS:5,FLYOUT_DRAG_RADIUS:10,SNAP_RADIUS:28};Blockly.CONNECTING_SNAP_RADIUS=Blockly.SNAP_RADIUS;Blockly.CURRENT_CONNECTION_PREFERENCE=8;Blockly.BUMP_DELAY=250;Blockly.BUMP_RANDOMNESS=10;Blockly.COLLAPSE_CHARS=30;Blockly.LONGPRESS=750;Blockly.SOUND_LIMIT=100;Blockly.DRAG_STACK=!0;Blockly.HSV_SATURATION=.45;Blockly.HSV_VALUE=.65;Blockly.SPRITE={width:96,height:124,url:"sprites.png"};Blockly.INPUT_VALUE=1; -Blockly.OUTPUT_VALUE=2;Blockly.NEXT_STATEMENT=3;Blockly.PREVIOUS_STATEMENT=4;Blockly.DUMMY_INPUT=5;Blockly.ALIGN_LEFT=-1;Blockly.ALIGN_CENTRE=0;Blockly.ALIGN_RIGHT=1;Blockly.DRAG_NONE=0;Blockly.DRAG_STICKY=1;Blockly.DRAG_BEGIN=1;Blockly.DRAG_FREE=2;Blockly.OPPOSITE_TYPE=[];Blockly.OPPOSITE_TYPE[Blockly.INPUT_VALUE]=Blockly.OUTPUT_VALUE;Blockly.OPPOSITE_TYPE[Blockly.OUTPUT_VALUE]=Blockly.INPUT_VALUE;Blockly.OPPOSITE_TYPE[Blockly.NEXT_STATEMENT]=Blockly.PREVIOUS_STATEMENT; -Blockly.OPPOSITE_TYPE[Blockly.PREVIOUS_STATEMENT]=Blockly.NEXT_STATEMENT;Blockly.TOOLBOX_AT_TOP=0;Blockly.TOOLBOX_AT_BOTTOM=1;Blockly.TOOLBOX_AT_LEFT=2;Blockly.TOOLBOX_AT_RIGHT=3;Blockly.DELETE_AREA_NONE=null;Blockly.DELETE_AREA_TRASH=1;Blockly.DELETE_AREA_TOOLBOX=2;Blockly.VARIABLE_CATEGORY_NAME="VARIABLE";Blockly.VARIABLE_DYNAMIC_CATEGORY_NAME="VARIABLE_DYNAMIC";Blockly.PROCEDURE_CATEGORY_NAME="PROCEDURE";Blockly.RENAME_VARIABLE_ID="RENAME_VARIABLE_ID";Blockly.DELETE_VARIABLE_ID="DELETE_VARIABLE_ID";Blockly.registry={};Blockly.registry.typeMap_={};Blockly.registry.DEFAULT="default";Blockly.registry.Type=function(a){this.name_=a};Blockly.registry.Type.prototype.toString=function(){return this.name_};Blockly.registry.Type.CONNECTION_CHECKER=new Blockly.registry.Type("connectionChecker");Blockly.registry.Type.EVENT=new Blockly.registry.Type("event");Blockly.registry.Type.FIELD=new Blockly.registry.Type("field");Blockly.registry.Type.RENDERER=new Blockly.registry.Type("renderer"); -Blockly.registry.Type.TOOLBOX=new Blockly.registry.Type("toolbox");Blockly.registry.Type.THEME=new Blockly.registry.Type("theme");Blockly.registry.Type.TOOLBOX_ITEM=new Blockly.registry.Type("toolboxItem");Blockly.registry.Type.FLYOUTS_VERTICAL_TOOLBOX=new Blockly.registry.Type("flyoutsVerticalToolbox");Blockly.registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX=new Blockly.registry.Type("flyoutsHorizontalToolbox"); -Blockly.registry.register=function(a,b,c,d){if(!(a instanceof Blockly.registry.Type)&&"string"!=typeof a||""==String(a).trim())throw Error('Invalid type "'+a+'". The type must be a non-empty string or a Blockly.registry.Type.');a=String(a).toLowerCase();if("string"!=typeof b||""==b.trim())throw Error('Invalid name "'+b+'". The name must be a non-empty string.');b=b.toLowerCase();if(!c)throw Error("Can not register a null value");var e=Blockly.registry.typeMap_[a];e||(e=Blockly.registry.typeMap_[a]= -{});Blockly.registry.validate_(a,c);if(!d&&e[b])throw Error('Name "'+b+'" with type "'+a+'" already registered.');e[b]=c};Blockly.registry.validate_=function(a,b){switch(a){case String(Blockly.registry.Type.FIELD):if("function"!=typeof b.fromJson)throw Error('Type "'+a+'" must have a fromJson function');}}; -Blockly.registry.unregister=function(a,b){a=String(a).toLowerCase();b=b.toLowerCase();var c=Blockly.registry.typeMap_[a];c?c[b]?delete Blockly.registry.typeMap_[a][b]:console.warn('No name "'+b+'" with type "'+a+'" found'):console.warn('No type "'+a+'" found')}; -Blockly.registry.getItem_=function(a,b){a=String(a).toLowerCase();b=b.toLowerCase();var c=Blockly.registry.typeMap_[a];return c?c[b]?c[b]:(console.warn('No name "'+b+'" with type "'+a+'" found'),null):(console.warn('No type "'+a+'" found'),null)};Blockly.registry.hasItem=function(a,b){a=String(a).toLowerCase();b=b.toLowerCase();return(a=Blockly.registry.typeMap_[a])?!!a[b]:!1};Blockly.registry.getClass=function(a,b){return Blockly.registry.getItem_(a,b)}; -Blockly.registry.getObject=function(a,b){return Blockly.registry.getItem_(a,b)};Blockly.registry.getClassFromOptions=function(a,b){b=b.plugins[a.toString()]||Blockly.registry.DEFAULT;return"function"==typeof b?b:Blockly.registry.getClass(a,b)};Blockly.utils={};Blockly.utils.global=function(){return"object"===typeof self?self:"object"===typeof window?window:"object"===typeof global?global:this}();Blockly.Msg={};Blockly.utils.global.Blockly||(Blockly.utils.global.Blockly={});Blockly.utils.global.Blockly.Msg||(Blockly.utils.global.Blockly.Msg=Blockly.Msg);Blockly.utils.colour={}; -Blockly.utils.colour.parse=function(a){a=String(a).toLowerCase().trim();var b=Blockly.utils.colour.names[a];if(b)return b;b="0x"==a.substring(0,2)?"#"+a.substring(2):a;b="#"==b[0]?b:"#"+b;if(/^#[0-9a-f]{6}$/.test(b))return b;if(/^#[0-9a-f]{3}$/.test(b))return["#",b[1],b[1],b[2],b[2],b[3],b[3]].join("");var c=a.match(/^(?:rgb)?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);return c&&(a=Number(c[1]),b=Number(c[2]),c=Number(c[3]),0<=a&&256>a&&0<=b&&256>b&&0<=c&&256>c)?Blockly.utils.colour.rgbToHex(a,b, -c):null};Blockly.utils.colour.rgbToHex=function(a,b,c){b=a<<16|b<<8|c;return 16>a?"#"+(16777216|b).toString(16).substr(1):"#"+b.toString(16)};Blockly.utils.colour.hexToRgb=function(a){a=Blockly.utils.colour.parse(a);if(!a)return[0,0,0];a=parseInt(a.substr(1),16);return[a>>16,a>>8&255,a&255]}; -Blockly.utils.colour.hsvToHex=function(a,b,c){var d=0,e=0,f=0;if(0==b)f=e=d=c;else{var g=Math.floor(a/60),h=a/60-g;a=c*(1-b);var k=c*(1-b*h);b=c*(1-b*(1-h));switch(g){case 1:d=k;e=c;f=a;break;case 2:d=a;e=c;f=b;break;case 3:d=a;e=k;f=c;break;case 4:d=b;e=a;f=c;break;case 5:d=c;e=a;f=k;break;case 6:case 0:d=c,e=b,f=a}}return Blockly.utils.colour.rgbToHex(Math.floor(d),Math.floor(e),Math.floor(f))}; -Blockly.utils.colour.blend=function(a,b,c){a=Blockly.utils.colour.parse(a);if(!a)return null;b=Blockly.utils.colour.parse(b);if(!b)return null;a=Blockly.utils.colour.hexToRgb(a);b=Blockly.utils.colour.hexToRgb(b);return Blockly.utils.colour.rgbToHex(Math.round(b[0]+c*(a[0]-b[0])),Math.round(b[1]+c*(a[1]-b[1])),Math.round(b[2]+c*(a[2]-b[2])))}; -Blockly.utils.colour.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00"};Blockly.utils.Coordinate=function(a,b){this.x=a;this.y=b};Blockly.utils.Coordinate.equals=function(a,b){return a==b?!0:a&&b?a.x==b.x&&a.y==b.y:!1};Blockly.utils.Coordinate.distance=function(a,b){var c=a.x-b.x;a=a.y-b.y;return Math.sqrt(c*c+a*a)};Blockly.utils.Coordinate.magnitude=function(a){return Math.sqrt(a.x*a.x+a.y*a.y)};Blockly.utils.Coordinate.difference=function(a,b){return new Blockly.utils.Coordinate(a.x-b.x,a.y-b.y)}; -Blockly.utils.Coordinate.sum=function(a,b){return new Blockly.utils.Coordinate(a.x+b.x,a.y+b.y)};Blockly.utils.Coordinate.prototype.scale=function(a){this.x*=a;this.y*=a;return this};Blockly.utils.Coordinate.prototype.translate=function(a,b){this.x+=a;this.y+=b;return this};Blockly.utils.Rect=function(a,b,c,d){this.top=a;this.bottom=b;this.left=c;this.right=d};Blockly.utils.Rect.prototype.contains=function(a,b){return a>=this.left&&a<=this.right&&b>=this.top&&b<=this.bottom};Blockly.utils.string={};Blockly.utils.string.startsWith=function(a,b){return 0==a.lastIndexOf(b,0)};Blockly.utils.string.shortestStringLength=function(a){return a.length?a.reduce(function(b,c){return b.lengthb&&(b=c[d].length);d=-Infinity;var e=1;do{var f=d;var g=a;a=[];var h=c.length/e,k=1;for(d=0;df);return g}; -Blockly.utils.string.wrapScore_=function(a,b,c){for(var d=[0],e=[],f=0;fd&&(d=h,e=g)}return e?Blockly.utils.string.wrapMutate_(a,e,c):b};Blockly.utils.string.wrapToText_=function(a,b){for(var c=[],d=0;d=h?(e=2,f=h,(h=a.join(""))&&c.push(h),a.length=0):"{"==h?e=3:(a.push("%",h),e=0):2==e?"0"<=h&&"9">=h?f+=h:(c.push(parseInt(f,10)),g--,e=0):3==e&&(""==h?(a.splice(0,0,"%{"),g--,e=0):"}"!=h?a.push(h):(e=a.join(""),/[A-Z]\w*/i.test(e)?(h=e.toUpperCase(), -(h=Blockly.utils.string.startsWith(h,"BKY_")?h.substring(4):null)&&h in Blockly.Msg?(e=Blockly.Msg[h],"string"==typeof e?Array.prototype.push.apply(c,Blockly.utils.tokenizeInterpolation_(e,b)):b?c.push(String(e)):c.push(e)):c.push("%{"+e+"}")):c.push("%{"+e+"}"),e=a.length=0))}(h=a.join(""))&&c.push(h);b=[];for(g=a.length=0;gc;c++)b[c]=Blockly.utils.genUid.soup_.charAt(Math.random()*a);return b.join("")};Blockly.utils.genUid.soup_="!#$%()*+,-./:;=?@[]^_`{|}~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; -Blockly.utils.is3dSupported=function(){if(void 0!==Blockly.utils.is3dSupported.cached_)return Blockly.utils.is3dSupported.cached_;if(!Blockly.utils.global.getComputedStyle)return!1;var a=document.createElement("p"),b="none",c={webkitTransform:"-webkit-transform",OTransform:"-o-transform",msTransform:"-ms-transform",MozTransform:"-moz-transform",transform:"transform"};document.body.insertBefore(a,null);for(var d in c)if(void 0!==a.style[d]){a.style[d]="translate3d(1px,1px,1px)";b=Blockly.utils.global.getComputedStyle(a); -if(!b)return document.body.removeChild(a),!1;b=b.getPropertyValue(c[d])}document.body.removeChild(a);Blockly.utils.is3dSupported.cached_="none"!==b;return Blockly.utils.is3dSupported.cached_};Blockly.utils.runAfterPageLoad=function(a){if("object"!=typeof document)throw Error("Blockly.utils.runAfterPageLoad() requires browser document.");if("complete"==document.readyState)a();else var b=setInterval(function(){"complete"==document.readyState&&(clearInterval(b),a())},10)}; -Blockly.utils.getViewportBBox=function(){var a=Blockly.utils.style.getViewportPageOffset();return new Blockly.utils.Rect(a.y,document.documentElement.clientHeight+a.y,a.x,document.documentElement.clientWidth+a.x)};Blockly.utils.arrayRemove=function(a,b){b=a.indexOf(b);if(-1==b)return!1;a.splice(b,1);return!0}; -Blockly.utils.getDocumentScroll=function(){var a=document.documentElement,b=window;return Blockly.utils.userAgent.IE&&b.pageYOffset!=a.scrollTop?new Blockly.utils.Coordinate(a.scrollLeft,a.scrollTop):new Blockly.utils.Coordinate(b.pageXOffset||a.scrollLeft,b.pageYOffset||a.scrollTop)};Blockly.utils.getBlockTypeCounts=function(a,b){var c=Object.create(null),d=a.getDescendants(!0);b&&(a=a.getNextBlock())&&(a=d.indexOf(a),d.splice(a,d.length-a));for(a=0;b=d[a];a++)c[b.type]?c[b.type]++:c[b.type]=1;return c}; -Blockly.utils.screenToWsCoordinates=function(a,b){var c=b.x;b=b.y;var d=a.getInjectionDiv().getBoundingClientRect();c=new Blockly.utils.Coordinate(c-d.left,b-d.top);b=a.getOriginOffsetInPixels();return Blockly.utils.Coordinate.difference(c,b).scale(1/a.scale)}; -Blockly.utils.parseBlockColour=function(a){var b="string"==typeof a?Blockly.utils.replaceMessageReferences(a):a,c=Number(b);if(!isNaN(c)&&0<=c&&360>=c)return{hue:c,hex:Blockly.utils.colour.hsvToHex(c,Blockly.HSV_SATURATION,255*Blockly.HSV_VALUE)};if(c=Blockly.utils.colour.parse(b))return{hue:null,hex:c};c='Invalid colour: "'+b+'"';a!=b&&(c+=' (from "'+a+'")');throw Error(c);};Blockly.Events={};Blockly.Events.group_="";Blockly.Events.recordUndo=!0;Blockly.Events.disabled_=0;Blockly.Events.CREATE="create";Blockly.Events.BLOCK_CREATE=Blockly.Events.CREATE;Blockly.Events.DELETE="delete";Blockly.Events.BLOCK_DELETE=Blockly.Events.DELETE;Blockly.Events.CHANGE="change";Blockly.Events.BLOCK_CHANGE=Blockly.Events.CHANGE;Blockly.Events.MOVE="move";Blockly.Events.BLOCK_MOVE=Blockly.Events.MOVE;Blockly.Events.VAR_CREATE="var_create";Blockly.Events.VAR_DELETE="var_delete"; -Blockly.Events.VAR_RENAME="var_rename";Blockly.Events.UI="ui";Blockly.Events.COMMENT_CREATE="comment_create";Blockly.Events.COMMENT_DELETE="comment_delete";Blockly.Events.COMMENT_CHANGE="comment_change";Blockly.Events.COMMENT_MOVE="comment_move";Blockly.Events.FINISHED_LOADING="finished_loading";Blockly.Events.BUMP_EVENTS=[Blockly.Events.BLOCK_CREATE,Blockly.Events.BLOCK_MOVE,Blockly.Events.COMMENT_CREATE,Blockly.Events.COMMENT_MOVE];Blockly.Events.FIRE_QUEUE_=[]; -Blockly.Events.fire=function(a){Blockly.Events.isEnabled()&&(Blockly.Events.FIRE_QUEUE_.length||setTimeout(Blockly.Events.fireNow_,0),Blockly.Events.FIRE_QUEUE_.push(a))};Blockly.Events.fireNow_=function(){for(var a=Blockly.Events.filter(Blockly.Events.FIRE_QUEUE_,!0),b=Blockly.Events.FIRE_QUEUE_.length=0,c;c=a[b];b++)if(c.workspaceId){var d=Blockly.Workspace.getById(c.workspaceId);d&&d.fireChangeListener(c)}}; -Blockly.Events.filter=function(a,b){a=a.slice();b||a.reverse();for(var c=[],d=Object.create(null),e=0,f;f=a[e];e++)if(!f.isNull()){var g=[f.type,f.blockId,f.workspaceId].join(" "),h=d[g],k=h?h.event:null;if(!h)d[g]={event:f,index:e},c.push(f);else if(f.type==Blockly.Events.MOVE&&h.index==e-1)k.newParentId=f.newParentId,k.newInputName=f.newInputName,k.newCoordinate=f.newCoordinate,h.index=e;else if(f.type==Blockly.Events.CHANGE&&f.element==k.element&&f.name==k.name)k.newValue=f.newValue;else if(f.type!= -Blockly.Events.UI||"click"!=f.element||"commentOpen"!=k.element&&"mutatorOpen"!=k.element&&"warningOpen"!=k.element)d[g]={event:f,index:1},c.push(f)}a=c.filter(function(l){return!l.isNull()});b||a.reverse();for(e=1;f=a[e];e++)f.type==Blockly.Events.CHANGE&&"mutation"==f.element&&a.unshift(a.splice(e,1)[0]);return a};Blockly.Events.clearPendingUndo=function(){for(var a=0,b;b=Blockly.Events.FIRE_QUEUE_[a];a++)b.recordUndo=!1};Blockly.Events.disable=function(){Blockly.Events.disabled_++}; -Blockly.Events.enable=function(){Blockly.Events.disabled_--};Blockly.Events.isEnabled=function(){return 0==Blockly.Events.disabled_};Blockly.Events.getGroup=function(){return Blockly.Events.group_};Blockly.Events.setGroup=function(a){Blockly.Events.group_="boolean"==typeof a?a?Blockly.utils.genUid():"":a};Blockly.Events.getDescendantIds=function(a){var b=[];a=a.getDescendants(!1);for(var c=0,d;d=a[c];c++)b[c]=d.id;return b}; -Blockly.Events.fromJson=function(a,b){var c=Blockly.registry.getClass(Blockly.registry.Type.EVENT,a.type);if(!c)throw Error("Unknown event type.");c=new c;c.fromJson(a);c.workspaceId=b.id;return c}; -Blockly.Events.disableOrphans=function(a){if((a.type==Blockly.Events.MOVE||a.type==Blockly.Events.CREATE)&&a.workspaceId){var b=Blockly.Workspace.getById(a.workspaceId);if(a=b.getBlockById(a.blockId)){var c=a.getParent();if(c&&c.isEnabled())for(b=a.getDescendants(!1),a=0;c=b[a];a++)c.setEnabled(!0);else if((a.outputConnection||a.previousConnection)&&!b.isDragging()){do a.setEnabled(!1),a=a.getNextBlock();while(a)}}}};Blockly.Events.Abstract=function(){this.isBlank=null;this.workspaceId=void 0;this.group=Blockly.Events.getGroup();this.recordUndo=Blockly.Events.recordUndo};Blockly.Events.Abstract.prototype.toJson=function(){var a={type:this.type};this.group&&(a.group=this.group);return a};Blockly.Events.Abstract.prototype.fromJson=function(a){this.isBlank=!1;this.group=a.group};Blockly.Events.Abstract.prototype.isNull=function(){return!1};Blockly.Events.Abstract.prototype.run=function(a){}; -Blockly.Events.Abstract.prototype.getEventWorkspace_=function(){if(this.workspaceId)var a=Blockly.Workspace.getById(this.workspaceId);if(!a)throw Error("Workspace is null. Event must have been generated from real Blockly events.");return a};Blockly.utils.object={};Blockly.utils.object.inherits=function(a,b){a.superClass_=b.prototype;a.prototype=Object.create(b.prototype);a.prototype.constructor=a};Blockly.utils.object.mixin=function(a,b){for(var c in b)a[c]=b[c]};Blockly.utils.object.deepMerge=function(a,b){for(var c in b)a[c]=null!=b[c]&&"object"===typeof b[c]?Blockly.utils.object.deepMerge(a[c]||Object.create(null),b[c]):b[c];return a};Blockly.utils.object.values=function(a){return Object.values?Object.values(a):Object.keys(a).map(function(b){return a[b]})};Blockly.Events.Ui=function(a,b,c,d){Blockly.Events.Ui.superClass_.constructor.call(this);this.isBlank="undefined"==typeof a;this.blockId=a?a.id:null;this.workspaceId=a?a.workspace.id:void 0;this.element="undefined"==typeof b?"":b;this.oldValue="undefined"==typeof c?"":c;this.newValue="undefined"==typeof d?"":d;this.recordUndo=!1};Blockly.utils.object.inherits(Blockly.Events.Ui,Blockly.Events.Abstract);Blockly.Events.Ui.prototype.type=Blockly.Events.UI; -Blockly.Events.Ui.prototype.toJson=function(){var a=Blockly.Events.Ui.superClass_.toJson.call(this);a.element=this.element;void 0!==this.newValue&&(a.newValue=this.newValue);this.blockId&&(a.blockId=this.blockId);return a};Blockly.Events.Ui.prototype.fromJson=function(a){Blockly.Events.Ui.superClass_.fromJson.call(this,a);this.element=a.element;this.newValue=a.newValue;this.blockId=a.blockId};Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.UI,Blockly.Events.Ui);Blockly.utils.Svg=function(a){this.tagName_=a};Blockly.utils.Svg.prototype.toString=function(){return this.tagName_};Blockly.utils.Svg.ANIMATE=new Blockly.utils.Svg("animate");Blockly.utils.Svg.CIRCLE=new Blockly.utils.Svg("circle");Blockly.utils.Svg.CLIPPATH=new Blockly.utils.Svg("clipPath");Blockly.utils.Svg.DEFS=new Blockly.utils.Svg("defs");Blockly.utils.Svg.FECOMPOSITE=new Blockly.utils.Svg("feComposite");Blockly.utils.Svg.FECOMPONENTTRANSFER=new Blockly.utils.Svg("feComponentTransfer"); -Blockly.utils.Svg.FEFLOOD=new Blockly.utils.Svg("feFlood");Blockly.utils.Svg.FEFUNCA=new Blockly.utils.Svg("feFuncA");Blockly.utils.Svg.FEGAUSSIANBLUR=new Blockly.utils.Svg("feGaussianBlur");Blockly.utils.Svg.FEPOINTLIGHT=new Blockly.utils.Svg("fePointLight");Blockly.utils.Svg.FESPECULARLIGHTING=new Blockly.utils.Svg("feSpecularLighting");Blockly.utils.Svg.FILTER=new Blockly.utils.Svg("filter");Blockly.utils.Svg.FOREIGNOBJECT=new Blockly.utils.Svg("foreignObject");Blockly.utils.Svg.G=new Blockly.utils.Svg("g"); -Blockly.utils.Svg.IMAGE=new Blockly.utils.Svg("image");Blockly.utils.Svg.LINE=new Blockly.utils.Svg("line");Blockly.utils.Svg.PATH=new Blockly.utils.Svg("path");Blockly.utils.Svg.PATTERN=new Blockly.utils.Svg("pattern");Blockly.utils.Svg.POLYGON=new Blockly.utils.Svg("polygon");Blockly.utils.Svg.RECT=new Blockly.utils.Svg("rect");Blockly.utils.Svg.SVG=new Blockly.utils.Svg("svg");Blockly.utils.Svg.TEXT=new Blockly.utils.Svg("text");Blockly.utils.Svg.TSPAN=new Blockly.utils.Svg("tspan");Blockly.utils.dom={};Blockly.utils.dom.SVG_NS="http://www.w3.org/2000/svg";Blockly.utils.dom.HTML_NS="http://www.w3.org/1999/xhtml";Blockly.utils.dom.XLINK_NS="http://www.w3.org/1999/xlink";Blockly.utils.dom.NodeType={ELEMENT_NODE:1,TEXT_NODE:3,COMMENT_NODE:8,DOCUMENT_POSITION_CONTAINED_BY:16};Blockly.utils.dom.cacheWidths_=null;Blockly.utils.dom.cacheReference_=0;Blockly.utils.dom.canvasContext_=null; -Blockly.utils.dom.createSvgElement=function(a,b,c){a=document.createElementNS(Blockly.utils.dom.SVG_NS,String(a));for(var d in b)a.setAttribute(d,b[d]);document.body.runtimeStyle&&(a.runtimeStyle=a.currentStyle=a.style);c&&c.appendChild(a);return a};Blockly.utils.dom.addClass=function(a,b){var c=a.getAttribute("class")||"";if(-1!=(" "+c+" ").indexOf(" "+b+" "))return!1;c&&(c+=" ");a.setAttribute("class",c+b);return!0}; -Blockly.utils.dom.removeClasses=function(a,b){b=b.split(" ");for(var c=0;cb||b>this.getChildCount())throw Error(Blockly.Component.Error.CHILD_INDEX_OUT_OF_BOUNDS);this.childIndex_[a.getId()]=a;if(a.getParent()==this){var d=this.children_.indexOf(a);-1>>/g,a),a=document.createElement("style"),a.id="blockly-common-style",c=document.createTextNode(c),a.appendChild(c),document.head.insertBefore(a,document.head.firstChild))}}; -Blockly.Css.CONTENT=[".blocklySvg {","background-color: #fff;","outline: none;","overflow: hidden;","position: absolute;","display: block;","}",".blocklyWidgetDiv {","display: none;","position: absolute;","z-index: 99999;","}",".injectionDiv {","height: 100%;","position: relative;","overflow: hidden;","touch-action: none;","}",".blocklyNonSelectable {","user-select: none;","-ms-user-select: none;","-webkit-user-select: none;","}",".blocklyWsDragSurface {","display: none;","position: absolute;","top: 0;", -"left: 0;","}",".blocklyWsDragSurface.blocklyOverflowVisible {","overflow: visible;","}",".blocklyBlockDragSurface {","display: none;","position: absolute;","top: 0;","left: 0;","right: 0;","bottom: 0;","overflow: visible !important;","z-index: 50;","}",".blocklyBlockCanvas.blocklyCanvasTransitioning,",".blocklyBubbleCanvas.blocklyCanvasTransitioning {","transition: transform .5s;","}",".blocklyTooltipDiv {","background-color: #ffffc7;","border: 1px solid #ddc;","box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);", -"color: #000;","display: none;","font: 9pt sans-serif;","opacity: .9;","padding: 2px;","position: absolute;","z-index: 100000;","}",".blocklyDropDownDiv {","position: absolute;","left: 0;","top: 0;","z-index: 1000;","display: none;","border: 1px solid;","border-color: #dadce0;","background-color: #fff;","border-radius: 2px;","padding: 4px;","box-shadow: 0 0 3px 1px rgba(0,0,0,.3);","}",".blocklyDropDownDiv.blocklyFocused {","box-shadow: 0 0 6px 1px rgba(0,0,0,.3);","}",".blocklyDropDownContent {", -"max-height: 300px;","overflow: auto;","overflow-x: hidden;","position: relative;","}",".blocklyDropDownArrow {","position: absolute;","left: 0;","top: 0;","width: 16px;","height: 16px;","z-index: -1;","background-color: inherit;","border-color: inherit;","}",".blocklyDropDownButton {","display: inline-block;","float: left;","padding: 0;","margin: 4px;","border-radius: 4px;","outline: none;","border: 1px solid;","transition: box-shadow .1s;","cursor: pointer;","}",".blocklyArrowTop {","border-top: 1px solid;", -"border-left: 1px solid;","border-top-left-radius: 4px;","border-color: inherit;","}",".blocklyArrowBottom {","border-bottom: 1px solid;","border-right: 1px solid;","border-bottom-right-radius: 4px;","border-color: inherit;","}",".blocklyResizeSE {","cursor: se-resize;","fill: #aaa;","}",".blocklyResizeSW {","cursor: sw-resize;","fill: #aaa;","}",".blocklyResizeLine {","stroke: #515A5A;","stroke-width: 1;","}",".blocklyHighlightedConnectionPath {","fill: none;","stroke: #fc3;","stroke-width: 4px;", -"}",".blocklyPathLight {","fill: none;","stroke-linecap: round;","stroke-width: 1;","}",".blocklySelected>.blocklyPathLight {","display: none;","}",".blocklyDraggable {",'cursor: url("<<>>/handopen.cur"), auto;',"cursor: grab;","cursor: -webkit-grab;","}",".blocklyDragging {",'cursor: url("<<>>/handclosed.cur"), auto;',"cursor: grabbing;","cursor: -webkit-grabbing;","}",".blocklyDraggable:active {",'cursor: url("<<>>/handclosed.cur"), auto;',"cursor: grabbing;","cursor: -webkit-grabbing;", -"}",".blocklyBlockDragSurface .blocklyDraggable {",'cursor: url("<<>>/handclosed.cur"), auto;',"cursor: grabbing;","cursor: -webkit-grabbing;","}",".blocklyDragging.blocklyDraggingDelete {",'cursor: url("<<>>/handdelete.cur"), auto;',"}",".blocklyDragging>.blocklyPath,",".blocklyDragging>.blocklyPathLight {","fill-opacity: .8;","stroke-opacity: .8;","}",".blocklyDragging>.blocklyPathDark {","display: none;","}",".blocklyDisabled>.blocklyPath {","fill-opacity: .5;","stroke-opacity: .5;", -"}",".blocklyDisabled>.blocklyPathLight,",".blocklyDisabled>.blocklyPathDark {","display: none;","}",".blocklyInsertionMarker>.blocklyPath,",".blocklyInsertionMarker>.blocklyPathLight,",".blocklyInsertionMarker>.blocklyPathDark {","fill-opacity: .2;","stroke: none;","}",".blocklyMultilineText {","font-family: monospace;","}",".blocklyNonEditableText>text {","pointer-events: none;","}",".blocklyFlyout {","position: absolute;","z-index: 20;","}",".blocklyText text {","cursor: default;","}",".blocklySvg text,", -".blocklyBlockDragSurface text {","user-select: none;","-ms-user-select: none;","-webkit-user-select: none;","cursor: inherit;","}",".blocklyHidden {","display: none;","}",".blocklyFieldDropdown:not(.blocklyHidden) {","display: block;","}",".blocklyIconGroup {","cursor: default;","}",".blocklyIconGroup:not(:hover),",".blocklyIconGroupReadonly {","opacity: .6;","}",".blocklyIconShape {","fill: #00f;","stroke: #fff;","stroke-width: 1px;","}",".blocklyIconSymbol {","fill: #fff;","}",".blocklyMinimalBody {", -"margin: 0;","padding: 0;","}",".blocklyHtmlInput {","border: none;","border-radius: 4px;","height: 100%;","margin: 0;","outline: none;","padding: 0;","width: 100%;","text-align: center;","display: block;","box-sizing: border-box;","}",".blocklyHtmlInput::-ms-clear {","display: none;","}",".blocklyMainBackground {","stroke-width: 1;","stroke: #c6c6c6;","}",".blocklyMutatorBackground {","fill: #fff;","stroke: #ddd;","stroke-width: 1;","}",".blocklyFlyoutBackground {","fill: #ddd;","fill-opacity: .8;", -"}",".blocklyMainWorkspaceScrollbar {","z-index: 20;","}",".blocklyFlyoutScrollbar {","z-index: 30;","}",".blocklyScrollbarHorizontal,",".blocklyScrollbarVertical {","position: absolute;","outline: none;","}",".blocklyScrollbarBackground {","opacity: 0;","}",".blocklyScrollbarHandle {","fill: #ccc;","}",".blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,",".blocklyScrollbarHandle:hover {","fill: #bbb;","}",".blocklyFlyout .blocklyScrollbarHandle {","fill: #bbb;","}",".blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,", -".blocklyFlyout .blocklyScrollbarHandle:hover {","fill: #aaa;","}",".blocklyInvalidInput {","background: #faa;","}",".blocklyVerticalMarker {","stroke-width: 3px;","fill: rgba(255,255,255,.5);","pointer-events: none;","}",".blocklyComputeCanvas {","position: absolute;","width: 0;","height: 0;","}",".blocklyNoPointerEvents {","pointer-events: none;","}",".blocklyContextMenu {","border-radius: 4px;","max-height: 100%;","}",".blocklyDropdownMenu {","border-radius: 2px;","padding: 0 !important;","}", -".blocklyDropdownMenu .blocklyMenuItem {","padding-left: 28px;","}",".blocklyDropdownMenu .blocklyMenuItemRtl {","padding-left: 5px;","padding-right: 28px;","}",".blocklyWidgetDiv .blocklyMenu {","background: #fff;","border: 1px solid transparent;","box-shadow: 0 0 3px 1px rgba(0,0,0,.3);","font: normal 13px Arial, sans-serif;","margin: 0;","outline: none;","padding: 4px 0;","position: absolute;","overflow-y: auto;","overflow-x: hidden;","max-height: 100%;","z-index: 20000;","}",".blocklyWidgetDiv .blocklyMenu.blocklyFocused {", -"box-shadow: 0 0 6px 1px rgba(0,0,0,.3);","}",".blocklyDropDownDiv .blocklyMenu {",'font: normal 13px "Helvetica Neue", Helvetica, sans-serif;',"outline: none;","z-index: 20000;","}",".blocklyMenuItem {","border: none;","color: #000;","cursor: pointer;","list-style: none;","margin: 0;","min-width: 7em;","padding: 6px 15px;","white-space: nowrap;","}",".blocklyMenuItemDisabled {","color: #ccc;","cursor: inherit;","}",".blocklyMenuItemHighlight {","background-color: rgba(0,0,0,.1);","}",".blocklyMenuItemCheckbox {", -"height: 16px;","position: absolute;","width: 16px;","}",".blocklyMenuItemSelected .blocklyMenuItemCheckbox {","background: url(<<>>/sprites.png) no-repeat -48px -16px;","float: left;","margin-left: -24px;","position: static;","}",".blocklyMenuItemRtl .blocklyMenuItemCheckbox {","float: right;","margin-right: -24px;","}"];Blockly.utils.math={};Blockly.utils.math.toRadians=function(a){return a*Math.PI/180};Blockly.utils.math.toDegrees=function(a){return 180*a/Math.PI};Blockly.utils.math.clamp=function(a,b,c){if(ce.top?Blockly.DropDownDiv.getPositionAboveMetrics_(c,d,e,f):b+f.heightdocument.documentElement.clientTop?Blockly.DropDownDiv.getPositionAboveMetrics_(c, -d,e,f):Blockly.DropDownDiv.getPositionTopOfPageMetrics_(a,e,f)};Blockly.DropDownDiv.getPositionBelowMetrics_=function(a,b,c,d){a=Blockly.DropDownDiv.getPositionX(a,c.left,c.right,d.width);return{initialX:a.divX,initialY:b,finalX:a.divX,finalY:b+Blockly.DropDownDiv.PADDING_Y,arrowX:a.arrowX,arrowY:-(Blockly.DropDownDiv.ARROW_SIZE/2+Blockly.DropDownDiv.BORDER_SIZE),arrowAtTop:!0,arrowVisible:!0}}; -Blockly.DropDownDiv.getPositionAboveMetrics_=function(a,b,c,d){a=Blockly.DropDownDiv.getPositionX(a,c.left,c.right,d.width);return{initialX:a.divX,initialY:b-d.height,finalX:a.divX,finalY:b-d.height-Blockly.DropDownDiv.PADDING_Y,arrowX:a.arrowX,arrowY:d.height-2*Blockly.DropDownDiv.BORDER_SIZE-Blockly.DropDownDiv.ARROW_SIZE/2,arrowAtTop:!1,arrowVisible:!0}}; -Blockly.DropDownDiv.getPositionTopOfPageMetrics_=function(a,b,c){a=Blockly.DropDownDiv.getPositionX(a,b.left,b.right,c.width);return{initialX:a.divX,initialY:0,finalX:a.divX,finalY:0,arrowAtTop:null,arrowX:null,arrowY:null,arrowVisible:!1}}; -Blockly.DropDownDiv.getPositionX=function(a,b,c,d){var e=a;a=Blockly.utils.math.clamp(b,a-d/2,c-d);e-=Blockly.DropDownDiv.ARROW_SIZE/2;b=Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING;d=Blockly.utils.math.clamp(b,e-a,d-b-Blockly.DropDownDiv.ARROW_SIZE);return{arrowX:d,divX:a}};Blockly.DropDownDiv.isVisible=function(){return!!Blockly.DropDownDiv.owner_}; -Blockly.DropDownDiv.hideIfOwner=function(a,b){return Blockly.DropDownDiv.owner_===a?(b?Blockly.DropDownDiv.hideWithoutAnimation():Blockly.DropDownDiv.hide(),!0):!1}; -Blockly.DropDownDiv.hide=function(){var a=Blockly.DropDownDiv.DIV_;a.style.transform="translate(0, 0)";a.style.opacity=0;Blockly.DropDownDiv.animateOutTimer_=setTimeout(function(){Blockly.DropDownDiv.hideWithoutAnimation()},1E3*Blockly.DropDownDiv.ANIMATION_TIME);Blockly.DropDownDiv.onHide_&&(Blockly.DropDownDiv.onHide_(),Blockly.DropDownDiv.onHide_=null)}; -Blockly.DropDownDiv.hideWithoutAnimation=function(){if(Blockly.DropDownDiv.isVisible()){Blockly.DropDownDiv.animateOutTimer_&&clearTimeout(Blockly.DropDownDiv.animateOutTimer_);var a=Blockly.DropDownDiv.DIV_;a.style.transform="";a.style.left="";a.style.top="";a.style.opacity=0;a.style.display="none";a.style.backgroundColor="";a.style.borderColor="";Blockly.DropDownDiv.onHide_&&(Blockly.DropDownDiv.onHide_(),Blockly.DropDownDiv.onHide_=null);Blockly.DropDownDiv.clearContent();Blockly.DropDownDiv.owner_= -null;Blockly.DropDownDiv.rendererClassName_&&(Blockly.utils.dom.removeClass(a,Blockly.DropDownDiv.rendererClassName_),Blockly.DropDownDiv.rendererClassName_="");Blockly.DropDownDiv.themeClassName_&&(Blockly.utils.dom.removeClass(a,Blockly.DropDownDiv.themeClassName_),Blockly.DropDownDiv.themeClassName_="");Blockly.getMainWorkspace().markFocused()}}; -Blockly.DropDownDiv.positionInternal_=function(a,b,c,d){a=Blockly.DropDownDiv.getPositionMetrics_(a,b,c,d);a.arrowVisible?(Blockly.DropDownDiv.arrow_.style.display="",Blockly.DropDownDiv.arrow_.style.transform="translate("+a.arrowX+"px,"+a.arrowY+"px) rotate(45deg)",Blockly.DropDownDiv.arrow_.setAttribute("class",a.arrowAtTop?"blocklyDropDownArrow blocklyArrowTop":"blocklyDropDownArrow blocklyArrowBottom")):Blockly.DropDownDiv.arrow_.style.display="none";b=Math.floor(a.initialX);c=Math.floor(a.initialY); -d=Math.floor(a.finalX);var e=Math.floor(a.finalY),f=Blockly.DropDownDiv.DIV_;f.style.left=b+"px";f.style.top=c+"px";f.style.display="block";f.style.opacity=1;f.style.transform="translate("+(d-b)+"px,"+(e-c)+"px)";return!!a.arrowAtTop}; -Blockly.DropDownDiv.repositionForWindowResize=function(){if(Blockly.DropDownDiv.owner_){var a=Blockly.DropDownDiv.owner_,b=a.getSourceBlock();a=Blockly.DropDownDiv.positionToField_?Blockly.DropDownDiv.getScaledBboxOfField_(a):Blockly.DropDownDiv.getScaledBboxOfBlock_(b);b=a.left+(a.right-a.left)/2;Blockly.DropDownDiv.positionInternal_(b,a.bottom,b,a.top)}else Blockly.DropDownDiv.hide()};Blockly.Grid=function(a,b){this.gridPattern_=a;this.spacing_=b.spacing;this.length_=b.length;this.line2_=(this.line1_=a.firstChild)&&this.line1_.nextSibling;this.snapToGrid_=b.snap};Blockly.Grid.prototype.scale_=1;Blockly.Grid.prototype.dispose=function(){this.gridPattern_=null};Blockly.Grid.prototype.shouldSnap=function(){return this.snapToGrid_};Blockly.Grid.prototype.getSpacing=function(){return this.spacing_};Blockly.Grid.prototype.getPatternId=function(){return this.gridPattern_.id}; -Blockly.Grid.prototype.update=function(a){this.scale_=a;var b=this.spacing_*a||100;this.gridPattern_.setAttribute("width",b);this.gridPattern_.setAttribute("height",b);b=Math.floor(this.spacing_/2)+.5;var c=b-this.length_/2,d=b+this.length_/2;b*=a;c*=a;d*=a;this.setLineAttributes_(this.line1_,a,c,d,b,b);this.setLineAttributes_(this.line2_,a,b,b,c,d)}; -Blockly.Grid.prototype.setLineAttributes_=function(a,b,c,d,e,f){a&&(a.setAttribute("stroke-width",b),a.setAttribute("x1",c),a.setAttribute("y1",e),a.setAttribute("x2",d),a.setAttribute("y2",f))};Blockly.Grid.prototype.moveTo=function(a,b){this.gridPattern_.setAttribute("x",a);this.gridPattern_.setAttribute("y",b);(Blockly.utils.userAgent.IE||Blockly.utils.userAgent.EDGE)&&this.update(this.scale_)}; -Blockly.Grid.createDom=function(a,b,c){a=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.PATTERN,{id:"blocklyGridPattern"+a,patternUnits:"userSpaceOnUse"},c);0b.indexOf(d))throw Error(d+" is not a valid modifier key.");};Blockly.user.keyMap.createSerializedKey=function(a,b){var c="",d=Blockly.utils.object.values(Blockly.user.keyMap.modifierKeys);Blockly.user.keyMap.checkModifiers_(b,d);for(var e=0,f;f=d[e];e++)-1 document.");}else a=null;return a};Blockly.utils.xml={};Blockly.utils.xml.NAME_SPACE="https://developers.google.com/blockly/xml";Blockly.utils.xml.document=function(){return document};Blockly.utils.xml.createElement=function(a){return Blockly.utils.xml.document().createElementNS(Blockly.utils.xml.NAME_SPACE,a)};Blockly.utils.xml.createTextNode=function(a){return Blockly.utils.xml.document().createTextNode(a)};Blockly.utils.xml.textToDomDocument=function(a){return(new DOMParser).parseFromString(a,"text/xml")}; -Blockly.utils.xml.domToText=function(a){return(new XMLSerializer).serializeToString(a)};Blockly.Events.BlockBase=function(a){Blockly.Events.BlockBase.superClass_.constructor.call(this);this.blockId=(this.isBlank="undefined"==typeof a)?"":a.id;this.workspaceId=this.isBlank?"":a.workspace.id};Blockly.utils.object.inherits(Blockly.Events.BlockBase,Blockly.Events.Abstract);Blockly.Events.BlockBase.prototype.toJson=function(){var a=Blockly.Events.BlockBase.superClass_.toJson.call(this);a.blockId=this.blockId;return a}; -Blockly.Events.BlockBase.prototype.fromJson=function(a){Blockly.Events.BlockBase.superClass_.fromJson.call(this,a);this.blockId=a.blockId};Blockly.Events.Change=function(a,b,c,d,e){Blockly.Events.Change.superClass_.constructor.call(this,a);a&&(this.element="undefined"==typeof b?"":b,this.name="undefined"==typeof c?"":c,this.oldValue="undefined"==typeof d?"":d,this.newValue="undefined"==typeof e?"":e)};Blockly.utils.object.inherits(Blockly.Events.Change,Blockly.Events.BlockBase); -Blockly.Events.BlockChange=Blockly.Events.Change;Blockly.Events.Change.prototype.type=Blockly.Events.CHANGE;Blockly.Events.Change.prototype.toJson=function(){var a=Blockly.Events.Change.superClass_.toJson.call(this);a.element=this.element;this.name&&(a.name=this.name);a.newValue=this.newValue;return a};Blockly.Events.Change.prototype.fromJson=function(a){Blockly.Events.Change.superClass_.fromJson.call(this,a);this.element=a.element;this.name=a.name;this.newValue=a.newValue}; -Blockly.Events.Change.prototype.isNull=function(){return this.oldValue==this.newValue}; -Blockly.Events.Change.prototype.run=function(a){var b=this.getEventWorkspace_().getBlockById(this.blockId);if(b)switch(b.mutator&&b.mutator.setVisible(!1),a=a?this.newValue:this.oldValue,this.element){case "field":(b=b.getField(this.name))?b.setValue(a):console.warn("Can't set non-existent field: "+this.name);break;case "comment":b.setCommentText(a||null);break;case "collapsed":b.setCollapsed(!!a);break;case "disabled":b.setEnabled(!a);break;case "inline":b.setInputsInline(!!a);break;case "mutation":var c= -"";b.mutationToDom&&(c=(c=b.mutationToDom())&&Blockly.Xml.domToText(c));if(b.domToMutation){var d=Blockly.Xml.textToDom(a||"");b.domToMutation(d)}Blockly.Events.fire(new Blockly.Events.Change(b,"mutation",null,c,a));break;default:console.warn("Unknown change type: "+this.element)}else console.warn("Can't change non-existent block: "+this.blockId)}; -Blockly.Events.Create=function(a){Blockly.Events.Create.superClass_.constructor.call(this,a);a&&(a.isShadow()&&(this.recordUndo=!1),this.xml=a.workspace.rendered?Blockly.Xml.blockToDomWithXY(a):Blockly.Xml.blockToDom(a),this.ids=Blockly.Events.getDescendantIds(a))};Blockly.utils.object.inherits(Blockly.Events.Create,Blockly.Events.BlockBase);Blockly.Events.BlockCreate=Blockly.Events.Create;Blockly.Events.Create.prototype.type=Blockly.Events.CREATE; -Blockly.Events.Create.prototype.toJson=function(){var a=Blockly.Events.Create.superClass_.toJson.call(this);a.xml=Blockly.Xml.domToText(this.xml);a.ids=this.ids;return a};Blockly.Events.Create.prototype.fromJson=function(a){Blockly.Events.Create.superClass_.fromJson.call(this,a);this.xml=Blockly.Xml.textToDom(a.xml);this.ids=a.ids}; -Blockly.Events.Create.prototype.run=function(a){var b=this.getEventWorkspace_();if(a)a=Blockly.utils.xml.createElement("xml"),a.appendChild(this.xml),Blockly.Xml.domToWorkspace(a,b);else{a=0;for(var c;c=this.ids[a];a++){var d=b.getBlockById(c);d?d.dispose(!1):c==this.blockId&&console.warn("Can't uncreate non-existent block: "+c)}}}; -Blockly.Events.Delete=function(a){Blockly.Events.Delete.superClass_.constructor.call(this,a);if(a){if(a.getParent())throw Error("Connected blocks cannot be deleted.");a.isShadow()&&(this.recordUndo=!1);this.oldXml=a.workspace.rendered?Blockly.Xml.blockToDomWithXY(a):Blockly.Xml.blockToDom(a);this.ids=Blockly.Events.getDescendantIds(a)}};Blockly.utils.object.inherits(Blockly.Events.Delete,Blockly.Events.BlockBase);Blockly.Events.BlockDelete=Blockly.Events.Delete; -Blockly.Events.Delete.prototype.type=Blockly.Events.DELETE;Blockly.Events.Delete.prototype.toJson=function(){var a=Blockly.Events.Delete.superClass_.toJson.call(this);a.ids=this.ids;return a};Blockly.Events.Delete.prototype.fromJson=function(a){Blockly.Events.Delete.superClass_.fromJson.call(this,a);this.ids=a.ids}; -Blockly.Events.Delete.prototype.run=function(a){var b=this.getEventWorkspace_();if(a){a=0;for(var c;c=this.ids[a];a++){var d=b.getBlockById(c);d?d.dispose(!1):c==this.blockId&&console.warn("Can't delete non-existent block: "+c)}}else a=Blockly.utils.xml.createElement("xml"),a.appendChild(this.oldXml),Blockly.Xml.domToWorkspace(a,b)}; -Blockly.Events.Move=function(a){Blockly.Events.Move.superClass_.constructor.call(this,a);a&&(a.isShadow()&&(this.recordUndo=!1),a=this.currentLocation_(),this.oldParentId=a.parentId,this.oldInputName=a.inputName,this.oldCoordinate=a.coordinate)};Blockly.utils.object.inherits(Blockly.Events.Move,Blockly.Events.BlockBase);Blockly.Events.BlockMove=Blockly.Events.Move;Blockly.Events.Move.prototype.type=Blockly.Events.MOVE; -Blockly.Events.Move.prototype.toJson=function(){var a=Blockly.Events.Move.superClass_.toJson.call(this);this.newParentId&&(a.newParentId=this.newParentId);this.newInputName&&(a.newInputName=this.newInputName);this.newCoordinate&&(a.newCoordinate=Math.round(this.newCoordinate.x)+","+Math.round(this.newCoordinate.y));return a}; -Blockly.Events.Move.prototype.fromJson=function(a){Blockly.Events.Move.superClass_.fromJson.call(this,a);this.newParentId=a.newParentId;this.newInputName=a.newInputName;a.newCoordinate&&(a=a.newCoordinate.split(","),this.newCoordinate=new Blockly.utils.Coordinate(Number(a[0]),Number(a[1])))};Blockly.Events.Move.prototype.recordNew=function(){var a=this.currentLocation_();this.newParentId=a.parentId;this.newInputName=a.inputName;this.newCoordinate=a.coordinate}; -Blockly.Events.Move.prototype.currentLocation_=function(){var a=this.getEventWorkspace_().getBlockById(this.blockId),b={},c=a.getParent();if(c){if(b.parentId=c.id,a=c.getInputWithBlock(a))b.inputName=a.name}else b.coordinate=a.getRelativeToSurfaceXY();return b};Blockly.Events.Move.prototype.isNull=function(){return this.oldParentId==this.newParentId&&this.oldInputName==this.newInputName&&Blockly.utils.Coordinate.equals(this.oldCoordinate,this.newCoordinate)}; -Blockly.Events.Move.prototype.run=function(a){var b=this.getEventWorkspace_(),c=b.getBlockById(this.blockId);if(c){var d=a?this.newParentId:this.oldParentId,e=a?this.newInputName:this.oldInputName;a=a?this.newCoordinate:this.oldCoordinate;var f=null;if(d&&(f=b.getBlockById(d),!f)){console.warn("Can't connect to non-existent block: "+d);return}c.getParent()&&c.unplug();if(a)e=c.getRelativeToSurfaceXY(),c.moveBy(a.x-e.x,a.y-e.y);else{c=c.outputConnection||c.previousConnection;if(e){if(b=f.getInput(e))var g= -b.connection}else c.type==Blockly.PREVIOUS_STATEMENT&&(g=f.nextConnection);g?c.connect(g):console.warn("Can't connect to non-existent input: "+e)}}else console.warn("Can't move non-existent block: "+this.blockId)};Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.CREATE,Blockly.Events.Create);Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.DELETE,Blockly.Events.Delete);Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.CHANGE,Blockly.Events.Change); -Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.MOVE,Blockly.Events.Move);Blockly.Events.FinishedLoading=function(a){this.isBlank="undefined"==typeof a;this.workspaceId=a?a.id:"";this.group=Blockly.Events.getGroup();this.recordUndo=!1};Blockly.utils.object.inherits(Blockly.Events.FinishedLoading,Blockly.Events.Ui);Blockly.Events.FinishedLoading.prototype.type=Blockly.Events.FINISHED_LOADING;Blockly.Events.FinishedLoading.prototype.toJson=function(){var a={type:this.type};this.group&&(a.group=this.group);this.workspaceId&&(a.workspaceId=this.workspaceId);return a}; -Blockly.Events.FinishedLoading.prototype.fromJson=function(a){this.isBlank=!1;this.workspaceId=a.workspaceId;this.group=a.group};Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.FINISHED_LOADING,Blockly.Events.FinishedLoading);Blockly.Events.VarBase=function(a){Blockly.Events.VarBase.superClass_.constructor.call(this);this.varId=(this.isBlank="undefined"==typeof a)?"":a.getId();this.workspaceId=this.isBlank?"":a.workspace.id};Blockly.utils.object.inherits(Blockly.Events.VarBase,Blockly.Events.Abstract);Blockly.Events.VarBase.prototype.toJson=function(){var a=Blockly.Events.VarBase.superClass_.toJson.call(this);a.varId=this.varId;return a}; -Blockly.Events.VarBase.prototype.fromJson=function(a){Blockly.Events.VarBase.superClass_.toJson.call(this);this.varId=a.varId};Blockly.Events.VarCreate=function(a){Blockly.Events.VarCreate.superClass_.constructor.call(this,a);a&&(this.varType=a.type,this.varName=a.name)};Blockly.utils.object.inherits(Blockly.Events.VarCreate,Blockly.Events.VarBase);Blockly.Events.VarCreate.prototype.type=Blockly.Events.VAR_CREATE; -Blockly.Events.VarCreate.prototype.toJson=function(){var a=Blockly.Events.VarCreate.superClass_.toJson.call(this);a.varType=this.varType;a.varName=this.varName;return a};Blockly.Events.VarCreate.prototype.fromJson=function(a){Blockly.Events.VarCreate.superClass_.fromJson.call(this,a);this.varType=a.varType;this.varName=a.varName};Blockly.Events.VarCreate.prototype.run=function(a){var b=this.getEventWorkspace_();a?b.createVariable(this.varName,this.varType,this.varId):b.deleteVariableById(this.varId)}; -Blockly.Events.VarDelete=function(a){Blockly.Events.VarDelete.superClass_.constructor.call(this,a);a&&(this.varType=a.type,this.varName=a.name)};Blockly.utils.object.inherits(Blockly.Events.VarDelete,Blockly.Events.VarBase);Blockly.Events.VarDelete.prototype.type=Blockly.Events.VAR_DELETE;Blockly.Events.VarDelete.prototype.toJson=function(){var a=Blockly.Events.VarDelete.superClass_.toJson.call(this);a.varType=this.varType;a.varName=this.varName;return a}; -Blockly.Events.VarDelete.prototype.fromJson=function(a){Blockly.Events.VarDelete.superClass_.fromJson.call(this,a);this.varType=a.varType;this.varName=a.varName};Blockly.Events.VarDelete.prototype.run=function(a){var b=this.getEventWorkspace_();a?b.deleteVariableById(this.varId):b.createVariable(this.varName,this.varType,this.varId)};Blockly.Events.VarRename=function(a,b){Blockly.Events.VarRename.superClass_.constructor.call(this,a);a&&(this.oldName=a.name,this.newName="undefined"==typeof b?"":b)}; -Blockly.utils.object.inherits(Blockly.Events.VarRename,Blockly.Events.VarBase);Blockly.Events.VarRename.prototype.type=Blockly.Events.VAR_RENAME;Blockly.Events.VarRename.prototype.toJson=function(){var a=Blockly.Events.VarRename.superClass_.toJson.call(this);a.oldName=this.oldName;a.newName=this.newName;return a};Blockly.Events.VarRename.prototype.fromJson=function(a){Blockly.Events.VarRename.superClass_.fromJson.call(this,a);this.oldName=a.oldName;this.newName=a.newName}; -Blockly.Events.VarRename.prototype.run=function(a){var b=this.getEventWorkspace_();a?b.renameVariableById(this.varId,this.newName):b.renameVariableById(this.varId,this.oldName)};Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.VAR_CREATE,Blockly.Events.VarCreate);Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.VAR_DELETE,Blockly.Events.VarDelete);Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.VAR_RENAME,Blockly.Events.VarRename);Blockly.Xml={};Blockly.Xml.workspaceToDom=function(a,b){var c=Blockly.utils.xml.createElement("xml"),d=Blockly.Xml.variablesToDom(Blockly.Variables.allUsedVarModels(a));d.hasChildNodes()&&c.appendChild(d);var e=a.getTopComments(!0);d=0;for(var f;f=e[d];d++)c.appendChild(f.toXmlWithXY(b));a=a.getTopBlocks(!0);for(d=0;e=a[d];d++)c.appendChild(Blockly.Xml.blockToDomWithXY(e,b));return c}; -Blockly.Xml.variablesToDom=function(a){for(var b=Blockly.utils.xml.createElement("variables"),c=0,d;d=a[c];c++){var e=Blockly.utils.xml.createElement("variable");e.appendChild(Blockly.utils.xml.createTextNode(d.name));d.type&&e.setAttribute("type",d.type);e.id=d.getId();b.appendChild(e)}return b}; -Blockly.Xml.blockToDomWithXY=function(a,b){if(a.isInsertionMarker()&&(a=a.getChildren(!1)[0],!a))return new DocumentFragment;var c;a.workspace.RTL&&(c=a.workspace.getWidth());b=Blockly.Xml.blockToDom(a,b);var d=a.getRelativeToSurfaceXY();b.setAttribute("x",Math.round(a.workspace.RTL?c-d.x:d.x));b.setAttribute("y",Math.round(d.y));return b};Blockly.Xml.fieldToDom_=function(a){if(a.isSerializable()){var b=Blockly.utils.xml.createElement("field");b.setAttribute("name",a.name||"");return a.toXml(b)}return null}; -Blockly.Xml.allFieldsToDom_=function(a,b){for(var c=0,d;d=a.inputList[c];c++)for(var e=0,f;f=d.fieldRow[e];e++)(f=Blockly.Xml.fieldToDom_(f))&&b.appendChild(f)}; -Blockly.Xml.blockToDom=function(a,b){if(a.isInsertionMarker())return(a=a.getChildren(!1)[0])?Blockly.Xml.blockToDom(a):new DocumentFragment;var c=Blockly.utils.xml.createElement(a.isShadow()?"shadow":"block");c.setAttribute("type",a.type);b||c.setAttribute("id",a.id);if(a.mutationToDom){var d=a.mutationToDom();d&&(d.hasChildNodes()||d.hasAttributes())&&c.appendChild(d)}Blockly.Xml.allFieldsToDom_(a,c);if(d=a.getCommentText()){var e=a.commentModel.size,f=a.commentModel.pinned,g=Blockly.utils.xml.createElement("comment"); -g.appendChild(Blockly.utils.xml.createTextNode(d));g.setAttribute("pinned",f);g.setAttribute("h",e.height);g.setAttribute("w",e.width);c.appendChild(g)}a.data&&(d=Blockly.utils.xml.createElement("data"),d.appendChild(Blockly.utils.xml.createTextNode(a.data)),c.appendChild(d));for(d=0;e=a.inputList[d];d++){var h;f=!0;if(e.type!=Blockly.DUMMY_INPUT){var k=e.connection.targetBlock();e.type==Blockly.INPUT_VALUE?h=Blockly.utils.xml.createElement("value"):e.type==Blockly.NEXT_STATEMENT&&(h=Blockly.utils.xml.createElement("statement")); -g=e.connection.getShadowDom();!g||k&&k.isShadow()||h.appendChild(Blockly.Xml.cloneShadow_(g,b));k&&(g=Blockly.Xml.blockToDom(k,b),g.nodeType==Blockly.utils.dom.NodeType.ELEMENT_NODE&&(h.appendChild(g),f=!1));h.setAttribute("name",e.name);f||c.appendChild(h)}}void 0!=a.inputsInline&&a.inputsInline!=a.inputsInlineDefault&&c.setAttribute("inline",a.inputsInline);a.isCollapsed()&&c.setAttribute("collapsed",!0);a.isEnabled()||c.setAttribute("disabled",!0);a.isDeletable()||a.isShadow()||c.setAttribute("deletable", -!1);a.isMovable()||a.isShadow()||c.setAttribute("movable",!1);a.isEditable()||c.setAttribute("editable",!1);if(d=a.getNextBlock())g=Blockly.Xml.blockToDom(d,b),g.nodeType==Blockly.utils.dom.NodeType.ELEMENT_NODE&&(h=Blockly.utils.xml.createElement("next"),h.appendChild(g),c.appendChild(h));g=a.nextConnection&&a.nextConnection.getShadowDom();!g||d&&d.isShadow()||h.appendChild(Blockly.Xml.cloneShadow_(g,b));return c}; -Blockly.Xml.cloneShadow_=function(a,b){for(var c=a=a.cloneNode(!0),d;c;)if(b&&"shadow"==c.nodeName&&c.removeAttribute("id"),c.firstChild)c=c.firstChild;else{for(;c&&!c.nextSibling;)d=c,c=c.parentNode,d.nodeType==Blockly.utils.dom.NodeType.TEXT_NODE&&""==d.data.trim()&&c.firstChild!=d&&Blockly.utils.dom.removeNode(d);c&&(d=c,c=c.nextSibling,d.nodeType==Blockly.utils.dom.NodeType.TEXT_NODE&&""==d.data.trim()&&Blockly.utils.dom.removeNode(d))}return a}; -Blockly.Xml.domToText=function(a){return Blockly.utils.xml.domToText(a).replace(/<(\w+)([^<]*)\/>/g,"<$1$2>")};Blockly.Xml.domToPrettyText=function(a){a=Blockly.Xml.domToText(a).split("<");for(var b="",c=1;c"!=d.slice(-2)&&(b+=" ")}a=a.join("\n");a=a.replace(/(<(\w+)\b[^>]*>[^\n]*)\n *<\/\2>/g,"$1");return a.replace(/^\n/,"")}; -Blockly.Xml.textToDom=function(a){var b=Blockly.utils.xml.textToDomDocument(a);if(!b||!b.documentElement||b.getElementsByTagName("parsererror").length)throw Error("textToDom was unable to parse: "+a);return b.documentElement};Blockly.Xml.clearWorkspaceAndLoadFromXml=function(a,b){b.setResizesEnabled(!1);b.clear();a=Blockly.Xml.domToWorkspace(a,b);b.setResizesEnabled(!0);return a}; -Blockly.Xml.domToWorkspace=function(a,b){if(a instanceof Blockly.Workspace){var c=a;a=b;b=c;console.warn("Deprecated call to Blockly.Xml.domToWorkspace, swap the arguments.")}var d;b.RTL&&(d=b.getWidth());c=[];Blockly.utils.dom.startTextWidthCache();var e=Blockly.Events.getGroup();e||Blockly.Events.setGroup(!0);b.setResizesEnabled&&b.setResizesEnabled(!1);var f=!0;try{for(var g=0,h;h=a.childNodes[g];g++){var k=h.nodeName.toLowerCase(),l=h;if("block"==k||"shadow"==k&&!Blockly.Events.recordUndo){var m= -Blockly.Xml.domToBlock(l,b);c.push(m.id);var n=l.hasAttribute("x")?parseInt(l.getAttribute("x"),10):10,p=l.hasAttribute("y")?parseInt(l.getAttribute("y"),10):10;isNaN(n)||isNaN(p)||m.moveBy(b.RTL?d-n:n,p);f=!1}else{if("shadow"==k)throw TypeError("Shadow block cannot be a top-level block.");if("comment"==k)b.rendered?Blockly.WorkspaceCommentSvg?Blockly.WorkspaceCommentSvg.fromXml(l,b,d):console.warn("Missing require for Blockly.WorkspaceCommentSvg, ignoring workspace comment."):Blockly.WorkspaceComment? -Blockly.WorkspaceComment.fromXml(l,b):console.warn("Missing require for Blockly.WorkspaceComment, ignoring workspace comment.");else if("variables"==k){if(f)Blockly.Xml.domToVariables(l,b);else throw Error("'variables' tag must exist once before block and shadow tag elements in the workspace XML, but it was found in another location.");f=!1}}}}finally{e||Blockly.Events.setGroup(!1),Blockly.utils.dom.stopTextWidthCache()}b.setResizesEnabled&&b.setResizesEnabled(!0);Blockly.Events.fire(new Blockly.Events.FinishedLoading(b)); -return c};Blockly.Xml.appendDomToWorkspace=function(a,b){var c;Object.prototype.hasOwnProperty.call(b,"scale")&&(c=b.getBlocksBoundingBox());a=Blockly.Xml.domToWorkspace(a,b);if(c&&c.top!=c.bottom){var d=c.bottom;var e=b.RTL?c.right:c.left;var f=Infinity,g=-Infinity,h=Infinity;for(c=0;cg&&(g=k.x)}d=d-h+10;e=b.RTL?e-g:e-f;for(c=0;c=c+this.handleLength_&&(d+= -e);this.setHandlePosition(this.constrainHandle_(d));this.onScroll_();a.stopPropagation();a.preventDefault()}}; -Blockly.Scrollbar.prototype.onMouseDownHandle_=function(a){this.workspace_.markFocused();this.cleanUp_();Blockly.utils.isRightButton(a)?a.stopPropagation():(this.startDragHandle=this.handlePosition_,this.workspace_.setupDragSurface(),this.startDragMouse_=this.horizontal_?a.clientX:a.clientY,Blockly.Scrollbar.onMouseUpWrapper_=Blockly.bindEventWithChecks_(document,"mouseup",this,this.onMouseUpHandle_),Blockly.Scrollbar.onMouseMoveWrapper_=Blockly.bindEventWithChecks_(document,"mousemove",this,this.onMouseMoveHandle_), -a.stopPropagation(),a.preventDefault())};Blockly.Scrollbar.prototype.onMouseMoveHandle_=function(a){this.setHandlePosition(this.constrainHandle_(this.startDragHandle+((this.horizontal_?a.clientX:a.clientY)-this.startDragMouse_)));this.onScroll_()};Blockly.Scrollbar.prototype.onMouseUpHandle_=function(){this.workspace_.resetDragSurface();Blockly.Touch.clearTouchIdentifier();this.cleanUp_()}; -Blockly.Scrollbar.prototype.cleanUp_=function(){Blockly.hideChaff(!0);Blockly.Scrollbar.onMouseUpWrapper_&&(Blockly.unbindEvent_(Blockly.Scrollbar.onMouseUpWrapper_),Blockly.Scrollbar.onMouseUpWrapper_=null);Blockly.Scrollbar.onMouseMoveWrapper_&&(Blockly.unbindEvent_(Blockly.Scrollbar.onMouseMoveWrapper_),Blockly.Scrollbar.onMouseMoveWrapper_=null)}; -Blockly.Scrollbar.prototype.constrainHandle_=function(a){return a=0>=a||isNaN(a)||this.scrollViewSize_Blockly.Tooltip.RADIUS_OK&&Blockly.Tooltip.hide()}else Blockly.Tooltip.poisonedElement_!=Blockly.Tooltip.element_&&(clearTimeout(Blockly.Tooltip.showPid_),Blockly.Tooltip.lastX_=a.pageX,Blockly.Tooltip.lastY_=a.pageY,Blockly.Tooltip.showPid_=setTimeout(Blockly.Tooltip.show_, -Blockly.Tooltip.HOVER_MS))};Blockly.Tooltip.dispose=function(){Blockly.Tooltip.element_=null;Blockly.Tooltip.poisonedElement_=null;Blockly.Tooltip.hide()};Blockly.Tooltip.hide=function(){Blockly.Tooltip.visible&&(Blockly.Tooltip.visible=!1,Blockly.Tooltip.DIV&&(Blockly.Tooltip.DIV.style.display="none"));Blockly.Tooltip.showPid_&&clearTimeout(Blockly.Tooltip.showPid_)};Blockly.Tooltip.block=function(){Blockly.Tooltip.hide();Blockly.Tooltip.blocked_=!0}; -Blockly.Tooltip.unblock=function(){Blockly.Tooltip.blocked_=!1}; -Blockly.Tooltip.show_=function(){if(!Blockly.Tooltip.blocked_&&(Blockly.Tooltip.poisonedElement_=Blockly.Tooltip.element_,Blockly.Tooltip.DIV)){Blockly.Tooltip.DIV.textContent="";var a=Blockly.Tooltip.getTooltipOfObject(Blockly.Tooltip.element_);a=Blockly.utils.string.wrap(a,Blockly.Tooltip.LIMIT);a=a.split("\n");for(var b=0;bc+window.scrollY&&(e-=Blockly.Tooltip.DIV.offsetHeight+2*Blockly.Tooltip.OFFSET_Y);a?d=Math.max(Blockly.Tooltip.MARGINS-window.scrollX, -d):d+Blockly.Tooltip.DIV.offsetWidth>b+window.scrollX-2*Blockly.Tooltip.MARGINS&&(d=b-Blockly.Tooltip.DIV.offsetWidth-2*Blockly.Tooltip.MARGINS);Blockly.Tooltip.DIV.style.top=e+"px";Blockly.Tooltip.DIV.style.left=d+"px"}};Blockly.WorkspaceDragSurfaceSvg=function(a){this.container_=a;this.createDom()};Blockly.WorkspaceDragSurfaceSvg.prototype.SVG_=null;Blockly.WorkspaceDragSurfaceSvg.prototype.container_=null; -Blockly.WorkspaceDragSurfaceSvg.prototype.createDom=function(){this.SVG_||(this.SVG_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.SVG,{xmlns:Blockly.utils.dom.SVG_NS,"xmlns:html":Blockly.utils.dom.HTML_NS,"xmlns:xlink":Blockly.utils.dom.XLINK_NS,version:"1.1","class":"blocklyWsDragSurface blocklyOverflowVisible"},null),this.container_.appendChild(this.SVG_))}; -Blockly.WorkspaceDragSurfaceSvg.prototype.translateSurface=function(a,b){a=a.toFixed(0);b=b.toFixed(0);this.SVG_.style.display="block";Blockly.utils.dom.setCssTransform(this.SVG_,"translate3d("+a+"px, "+b+"px, 0px)")};Blockly.WorkspaceDragSurfaceSvg.prototype.getSurfaceTranslation=function(){return Blockly.utils.getRelativeXY(this.SVG_)}; -Blockly.WorkspaceDragSurfaceSvg.prototype.clearAndHide=function(a){if(!a)throw Error("Couldn't clear and hide the drag surface: missing new surface.");var b=this.SVG_.childNodes[0],c=this.SVG_.childNodes[1];if(!(b&&c&&Blockly.utils.dom.hasClass(b,"blocklyBlockCanvas")&&Blockly.utils.dom.hasClass(c,"blocklyBubbleCanvas")))throw Error("Couldn't clear and hide the drag surface. A node was missing.");null!=this.previousSibling_?Blockly.utils.dom.insertAfter(b,this.previousSibling_):a.insertBefore(b,a.firstChild); -Blockly.utils.dom.insertAfter(c,b);this.SVG_.style.display="none";if(this.SVG_.childNodes.length)throw Error("Drag surface was not cleared.");Blockly.utils.dom.setCssTransform(this.SVG_,"");this.previousSibling_=null}; -Blockly.WorkspaceDragSurfaceSvg.prototype.setContentsAndShow=function(a,b,c,d,e,f){if(this.SVG_.childNodes.length)throw Error("Already dragging a block.");this.previousSibling_=c;a.setAttribute("transform","translate(0, 0) scale("+f+")");b.setAttribute("transform","translate(0, 0) scale("+f+")");this.SVG_.setAttribute("width",d);this.SVG_.setAttribute("height",e);this.SVG_.appendChild(a);this.SVG_.appendChild(b);this.SVG_.style.display="block"};Blockly.ASTNode=function(a,b,c){if(!b)throw Error("Cannot create a node without a location.");this.type_=a;this.isConnection_=Blockly.ASTNode.isConnectionType_(a);this.location_=b;this.wsCoordinate_=null;this.processParams_(c||null)};Blockly.ASTNode.types={FIELD:"field",BLOCK:"block",INPUT:"input",OUTPUT:"output",NEXT:"next",PREVIOUS:"previous",STACK:"stack",WORKSPACE:"workspace"};Blockly.ASTNode.NAVIGATE_ALL_FIELDS=!1;Blockly.ASTNode.DEFAULT_OFFSET_Y=-20;Blockly.ASTNode.isConnectionType_=function(a){switch(a){case Blockly.ASTNode.types.PREVIOUS:case Blockly.ASTNode.types.NEXT:case Blockly.ASTNode.types.INPUT:case Blockly.ASTNode.types.OUTPUT:return!0}return!1}; -Blockly.ASTNode.createFieldNode=function(a){return a?new Blockly.ASTNode(Blockly.ASTNode.types.FIELD,a):null}; -Blockly.ASTNode.createConnectionNode=function(a){return a?a.type==Blockly.INPUT_VALUE||a.type==Blockly.NEXT_STATEMENT&&a.getParentInput()?Blockly.ASTNode.createInputNode(a.getParentInput()):a.type==Blockly.NEXT_STATEMENT?new Blockly.ASTNode(Blockly.ASTNode.types.NEXT,a):a.type==Blockly.OUTPUT_VALUE?new Blockly.ASTNode(Blockly.ASTNode.types.OUTPUT,a):a.type==Blockly.PREVIOUS_STATEMENT?new Blockly.ASTNode(Blockly.ASTNode.types.PREVIOUS,a):null:null}; -Blockly.ASTNode.createInputNode=function(a){return a&&a.connection?new Blockly.ASTNode(Blockly.ASTNode.types.INPUT,a.connection):null};Blockly.ASTNode.createBlockNode=function(a){return a?new Blockly.ASTNode(Blockly.ASTNode.types.BLOCK,a):null};Blockly.ASTNode.createStackNode=function(a){return a?new Blockly.ASTNode(Blockly.ASTNode.types.STACK,a):null};Blockly.ASTNode.createWorkspaceNode=function(a,b){return b&&a?new Blockly.ASTNode(Blockly.ASTNode.types.WORKSPACE,a,{wsCoordinate:b}):null}; -Blockly.ASTNode.createTopNode=function(a){var b=a.previousConnection||a.outputConnection;return b?Blockly.ASTNode.createConnectionNode(b):Blockly.ASTNode.createBlockNode(a)};Blockly.ASTNode.prototype.processParams_=function(a){a&&a.wsCoordinate&&(this.wsCoordinate_=a.wsCoordinate)};Blockly.ASTNode.prototype.getLocation=function(){return this.location_};Blockly.ASTNode.prototype.getType=function(){return this.type_};Blockly.ASTNode.prototype.getWsCoordinate=function(){return this.wsCoordinate_}; -Blockly.ASTNode.prototype.isConnection=function(){return this.isConnection_};Blockly.ASTNode.prototype.findNextForInput_=function(){var a=this.location_.getParentInput(),b=a.getSourceBlock();a=b.inputList.indexOf(a)+1;for(var c;c=b.inputList[a];a++){for(var d=c.fieldRow,e=0,f;f=d[e];e++)if(f.isClickable()||Blockly.ASTNode.NAVIGATE_ALL_FIELDS)return Blockly.ASTNode.createFieldNode(f);if(c.connection)return Blockly.ASTNode.createInputNode(c)}return null}; -Blockly.ASTNode.prototype.findNextForField_=function(){var a=this.location_,b=a.getParentInput(),c=a.getSourceBlock(),d=c.inputList.indexOf(b);for(a=b.fieldRow.indexOf(a)+1;b=c.inputList[d];d++){for(var e=b.fieldRow;ac)){var d=b.getSvgXY(a.getSvgRoot());a.outputConnection?(d.x+=(a.RTL?3:-3)*c,d.y+=13*c):a.previousConnection&&(d.x+=(a.RTL?-23:23)*c,d.y+=3*c);a=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.CIRCLE,{cx:d.x,cy:d.y,r:0,fill:"none",stroke:"#888","stroke-width":10},b.getParentSvg());Blockly.blockAnimations.connectionUiStep_(a,new Date,c)}}; -Blockly.blockAnimations.connectionUiStep_=function(a,b,c){var d=(new Date-b)/150;1a.workspace.scale)){var b=a.getHeightWidth().height;b=Math.atan(10/b)/Math.PI*180;a.RTL||(b*=-1);Blockly.blockAnimations.disconnectUiStep_(a.getSvgRoot(),b,new Date)}}; -Blockly.blockAnimations.disconnectUiStep_=function(a,b,c){var d=(new Date-c)/200;1b-Blockly.CURRENT_CONNECTION_PREFERENCE)}if(this.localConnection_||this.closestConnection_)console.error("Only one of localConnection_ and closestConnection_ was set."); -else return!0}else return!(!this.localConnection_||!this.closestConnection_);console.error("Returning true from shouldUpdatePreviews, but it's not clear why.");return!0};Blockly.InsertionMarkerManager.prototype.getCandidate_=function(a){for(var b=this.getStartRadius_(),c=null,d=null,e=0;ec||b.getSourceBlock().isInsertionMarker())return!1;switch(b.type){case Blockly.PREVIOUS_STATEMENT:return this.canConnectToPrevious_(a,b);case Blockly.OUTPUT_VALUE:if(b.isConnected()&&!b.targetBlock().isInsertionMarker()||a.isConnected())return!1;break;case Blockly.INPUT_VALUE:if(b.isConnected()&&!b.targetBlock().isMovable()&&!b.targetBlock().isShadow())return!1;break;case Blockly.NEXT_STATEMENT:if(b.isConnected()&& -!a.getSourceBlock().nextConnection&&!b.targetBlock().isShadow()&&b.targetBlock().nextConnection)return!1;break;default:return!1}return-1!=Blockly.draggingConnections.indexOf(b)?!1:!0};Blockly.ConnectionChecker.prototype.canConnectToPrevious_=function(a,b){if(a.targetConnection||-1!=Blockly.draggingConnections.indexOf(b))return!1;if(!b.targetConnection)return!0;a=b.targetBlock();return a.isInsertionMarker()?!a.getPreviousBlock():!1}; -Blockly.registry.register(Blockly.registry.Type.CONNECTION_CHECKER,Blockly.registry.DEFAULT,Blockly.ConnectionChecker);Blockly.VariableMap=function(a){this.variableMap_=Object.create(null);this.workspace=a};Blockly.VariableMap.prototype.clear=function(){this.variableMap_=Object.create(null)};Blockly.VariableMap.prototype.renameVariable=function(a,b){var c=this.getVariable(b,a.type),d=this.workspace.getAllBlocks(!1);Blockly.Events.setGroup(!0);try{c&&c.getId()!=a.getId()?this.renameVariableWithConflict_(a,b,c,d):this.renameVariableAndUses_(a,b,d)}finally{Blockly.Events.setGroup(!1)}}; -Blockly.VariableMap.prototype.renameVariableById=function(a,b){var c=this.getVariableById(a);if(!c)throw Error("Tried to rename a variable that didn't exist. ID: "+a);this.renameVariable(c,b)};Blockly.VariableMap.prototype.renameVariableAndUses_=function(a,b,c){Blockly.Events.fire(new Blockly.Events.VarRename(a,b));a.name=b;for(b=0;bthis.remainingCapacityOfType(c))return!1;b+=a[c]}return b>this.remainingCapacity()?!1:!0}; -Blockly.Workspace.prototype.hasBlockLimits=function(){return Infinity!=this.options.maxBlocks||!!this.options.maxInstances};Blockly.Workspace.prototype.getUndoStack=function(){return this.undoStack_};Blockly.Workspace.prototype.getRedoStack=function(){return this.redoStack_}; -Blockly.Workspace.prototype.undo=function(a){var b=a?this.redoStack_:this.undoStack_,c=a?this.undoStack_:this.redoStack_,d=b.pop();if(d){for(var e=[d];b.length&&d.group&&d.group==b[b.length-1].group;)e.push(b.pop());for(b=0;d=e[b];b++)c.push(d);e=Blockly.Events.filter(e,a);Blockly.Events.recordUndo=!1;try{for(b=0;d=e[b];b++)d.run(a)}finally{Blockly.Events.recordUndo=!0}}};Blockly.Workspace.prototype.clearUndo=function(){this.undoStack_.length=0;this.redoStack_.length=0;Blockly.Events.clearPendingUndo()}; -Blockly.Workspace.prototype.addChangeListener=function(a){this.listeners_.push(a);return a};Blockly.Workspace.prototype.removeChangeListener=function(a){Blockly.utils.arrayRemove(this.listeners_,a)};Blockly.Workspace.prototype.fireChangeListener=function(a){if(a.recordUndo)for(this.undoStack_.push(a),this.redoStack_.length=0;this.undoStack_.length>this.MAX_UNDO&&0<=this.MAX_UNDO;)this.undoStack_.shift();for(var b=0,c;c=this.listeners_[b];b++)c(a)}; -Blockly.Workspace.prototype.getBlockById=function(a){return this.blockDB_[a]||null};Blockly.Workspace.prototype.setBlockById=function(a,b){this.blockDB_[a]=b};Blockly.Workspace.prototype.removeBlockById=function(a){delete this.blockDB_[a]};Blockly.Workspace.prototype.getCommentById=function(a){return this.commentDB_[a]||null};Blockly.Workspace.prototype.allInputsFilled=function(a){for(var b=this.getTopBlocks(!1),c=0,d;d=b[c];c++)if(!d.allInputsFilled(a))return!1;return!0}; -Blockly.Workspace.prototype.getPotentialVariableMap=function(){return this.potentialVariableMap_};Blockly.Workspace.prototype.createPotentialVariableMap=function(){this.potentialVariableMap_=new Blockly.VariableMap(this)};Blockly.Workspace.prototype.getVariableMap=function(){return this.variableMap_};Blockly.Workspace.prototype.setVariableMap=function(a){this.variableMap_=a};Blockly.Workspace.WorkspaceDB_=Object.create(null); -Blockly.Workspace.getById=function(a){return Blockly.Workspace.WorkspaceDB_[a]||null};Blockly.Workspace.getAll=function(){var a=[],b;for(b in Blockly.Workspace.WorkspaceDB_)a.push(Blockly.Workspace.WorkspaceDB_[b]);return a};Blockly.Bubble=function(a,b,c,d,e,f){this.workspace_=a;this.content_=b;this.shape_=c;this.onMouseDownResizeWrapper_=this.onMouseDownBubbleWrapper_=this.moveCallback_=this.resizeCallback_=null;this.disposed=!1;c=Blockly.Bubble.ARROW_ANGLE;this.workspace_.RTL&&(c=-c);this.arrow_radians_=Blockly.utils.math.toRadians(c);a.getBubbleCanvas().appendChild(this.createDom_(b,!(!e||!f)));this.setAnchorLocation(d);e&&f||(a=this.content_.getBBox(),e=a.width+2*Blockly.Bubble.BORDER_WIDTH,f=a.height+2*Blockly.Bubble.BORDER_WIDTH); -this.setBubbleSize(e,f);this.positionBubble_();this.renderArrow_();this.rendered_=!0};Blockly.Bubble.BORDER_WIDTH=6;Blockly.Bubble.ARROW_THICKNESS=5;Blockly.Bubble.ARROW_ANGLE=20;Blockly.Bubble.ARROW_BEND=4;Blockly.Bubble.ANCHOR_RADIUS=8;Blockly.Bubble.onMouseUpWrapper_=null;Blockly.Bubble.onMouseMoveWrapper_=null; -Blockly.Bubble.unbindDragEvents_=function(){Blockly.Bubble.onMouseUpWrapper_&&(Blockly.unbindEvent_(Blockly.Bubble.onMouseUpWrapper_),Blockly.Bubble.onMouseUpWrapper_=null);Blockly.Bubble.onMouseMoveWrapper_&&(Blockly.unbindEvent_(Blockly.Bubble.onMouseMoveWrapper_),Blockly.Bubble.onMouseMoveWrapper_=null)};Blockly.Bubble.bubbleMouseUp_=function(a){Blockly.Touch.clearTouchIdentifier();Blockly.Bubble.unbindDragEvents_()};Blockly.Bubble.prototype.rendered_=!1;Blockly.Bubble.prototype.anchorXY_=null; -Blockly.Bubble.prototype.relativeLeft_=0;Blockly.Bubble.prototype.relativeTop_=0;Blockly.Bubble.prototype.width_=0;Blockly.Bubble.prototype.height_=0;Blockly.Bubble.prototype.autoLayout_=!0; -Blockly.Bubble.prototype.createDom_=function(a,b){this.bubbleGroup_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G,{},null);var c={filter:"url(#"+this.workspace_.getRenderer().getConstants().embossFilterId+")"};Blockly.utils.userAgent.JAVA_FX&&(c={});c=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G,c,this.bubbleGroup_);this.bubbleArrow_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.PATH,{},c);this.bubbleBack_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.RECT,{"class":"blocklyDraggable", -x:0,y:0,rx:Blockly.Bubble.BORDER_WIDTH,ry:Blockly.Bubble.BORDER_WIDTH},c);b?(this.resizeGroup_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G,{"class":this.workspace_.RTL?"blocklyResizeSW":"blocklyResizeSE"},this.bubbleGroup_),b=2*Blockly.Bubble.BORDER_WIDTH,Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.POLYGON,{points:"0,x x,x x,0".replace(/x/g,b.toString())},this.resizeGroup_),Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.LINE,{"class":"blocklyResizeLine",x1:b/3,y1:b-1,x2:b- -1,y2:b/3},this.resizeGroup_),Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.LINE,{"class":"blocklyResizeLine",x1:2*b/3,y1:b-1,x2:b-1,y2:2*b/3},this.resizeGroup_)):this.resizeGroup_=null;this.workspace_.options.readOnly||(this.onMouseDownBubbleWrapper_=Blockly.bindEventWithChecks_(this.bubbleBack_,"mousedown",this,this.bubbleMouseDown_),this.resizeGroup_&&(this.onMouseDownResizeWrapper_=Blockly.bindEventWithChecks_(this.resizeGroup_,"mousedown",this,this.resizeMouseDown_)));this.bubbleGroup_.appendChild(a); -return this.bubbleGroup_};Blockly.Bubble.prototype.getSvgRoot=function(){return this.bubbleGroup_};Blockly.Bubble.prototype.setSvgId=function(a){this.bubbleGroup_.dataset&&(this.bubbleGroup_.dataset.blockId=a)};Blockly.Bubble.prototype.bubbleMouseDown_=function(a){var b=this.workspace_.getGesture(a);b&&b.handleBubbleStart(a,this)};Blockly.Bubble.prototype.showContextMenu=function(a){};Blockly.Bubble.prototype.isDeletable=function(){return!1};Blockly.Bubble.prototype.setDeleteStyle=function(a){}; -Blockly.Bubble.prototype.resizeMouseDown_=function(a){this.promote();Blockly.Bubble.unbindDragEvents_();Blockly.utils.isRightButton(a)||(this.workspace_.startDrag(a,new Blockly.utils.Coordinate(this.workspace_.RTL?-this.width_:this.width_,this.height_)),Blockly.Bubble.onMouseUpWrapper_=Blockly.bindEventWithChecks_(document,"mouseup",this,Blockly.Bubble.bubbleMouseUp_),Blockly.Bubble.onMouseMoveWrapper_=Blockly.bindEventWithChecks_(document,"mousemove",this,this.resizeMouseMove_),Blockly.hideChaff()); -a.stopPropagation()};Blockly.Bubble.prototype.resizeMouseMove_=function(a){this.autoLayout_=!1;a=this.workspace_.moveDrag(a);this.setBubbleSize(this.workspace_.RTL?-a.x:a.x,a.y);this.workspace_.RTL&&this.positionBubble_()};Blockly.Bubble.prototype.registerResizeEvent=function(a){this.resizeCallback_=a};Blockly.Bubble.prototype.registerMoveEvent=function(a){this.moveCallback_=a}; -Blockly.Bubble.prototype.promote=function(){var a=this.bubbleGroup_.parentNode;return a.lastChild!==this.bubbleGroup_?(a.appendChild(this.bubbleGroup_),!0):!1};Blockly.Bubble.prototype.setAnchorLocation=function(a){this.anchorXY_=a;this.rendered_&&this.positionBubble_()}; -Blockly.Bubble.prototype.layoutBubble_=function(){var a=this.workspace_.getMetrics();a.viewLeft/=this.workspace_.scale;a.viewWidth/=this.workspace_.scale;a.viewTop/=this.workspace_.scale;a.viewHeight/=this.workspace_.scale;var b=this.getOptimalRelativeLeft_(a),c=this.getOptimalRelativeTop_(a),d=this.shape_.getBBox(),e={x:b,y:-this.height_-this.workspace_.getRenderer().getConstants().MIN_BLOCK_HEIGHT},f={x:-this.width_-30,y:c};c={x:d.width,y:c};var g={x:b,y:d.height};b=d.widtha.viewWidth)return b;if(this.workspace_.RTL)var c=this.anchorXY_.x-b,d=c-this.width_,e=a.viewLeft+a.viewWidth,f=a.viewLeft+Blockly.Scrollbar.scrollbarThickness/this.workspace_.scale;else d=b+this.anchorXY_.x,c=d+this.width_,f=a.viewLeft,e=a.viewLeft+a.viewWidth-Blockly.Scrollbar.scrollbarThickness/this.workspace_.scale;this.workspace_.RTL?de&&(b=-(e-this.anchorXY_.x)): -de&&(b=e-this.anchorXY_.x-this.width_);return b};Blockly.Bubble.prototype.getOptimalRelativeTop_=function(a){var b=-this.height_/4;if(this.height_>a.viewHeight)return b;var c=this.anchorXY_.y+b,d=c+this.height_,e=a.viewTop;a=a.viewTop+a.viewHeight-Blockly.Scrollbar.scrollbarThickness/this.workspace_.scale;var f=this.anchorXY_.y;ca&&(b=a-f-this.height_);return b}; -Blockly.Bubble.prototype.positionBubble_=function(){var a=this.anchorXY_.x;a=this.workspace_.RTL?a-(this.relativeLeft_+this.width_):a+this.relativeLeft_;this.moveTo(a,this.relativeTop_+this.anchorXY_.y)};Blockly.Bubble.prototype.moveTo=function(a,b){this.bubbleGroup_.setAttribute("transform","translate("+a+","+b+")")};Blockly.Bubble.prototype.setDragging=function(a){!a&&this.moveCallback_&&this.moveCallback_()}; -Blockly.Bubble.prototype.getBubbleSize=function(){return new Blockly.utils.Size(this.width_,this.height_)}; -Blockly.Bubble.prototype.setBubbleSize=function(a,b){var c=2*Blockly.Bubble.BORDER_WIDTH;a=Math.max(a,c+45);b=Math.max(b,c+20);this.width_=a;this.height_=b;this.bubbleBack_.setAttribute("width",a);this.bubbleBack_.setAttribute("height",b);this.resizeGroup_&&(this.workspace_.RTL?this.resizeGroup_.setAttribute("transform","translate("+2*Blockly.Bubble.BORDER_WIDTH+","+(b-c)+") scale(-1 1)"):this.resizeGroup_.setAttribute("transform","translate("+(a-c)+","+(b-c)+")"));this.autoLayout_&&this.layoutBubble_(); -this.positionBubble_();this.renderArrow_();this.resizeCallback_&&this.resizeCallback_()}; -Blockly.Bubble.prototype.renderArrow_=function(){var a=[],b=this.width_/2,c=this.height_/2,d=-this.relativeLeft_,e=-this.relativeTop_;if(b==d&&c==e)a.push("M "+b+","+c);else{e-=c;d-=b;this.workspace_.RTL&&(d*=-1);var f=Math.sqrt(e*e+d*d),g=Math.acos(d/f);0>e&&(g=2*Math.PI-g);var h=g+Math.PI/2;h>2*Math.PI&&(h-=2*Math.PI);var k=Math.sin(h),l=Math.cos(h),m=this.getBubbleSize();h=(m.width+m.height)/Blockly.Bubble.ARROW_THICKNESS;h=Math.min(h,m.width,m.height)/4;m=1-Blockly.Bubble.ANCHOR_RADIUS/f;d=b+ -m*d;e=c+m*e;m=b+h*l;var n=c+h*k;b-=h*l;c-=h*k;k=g+this.arrow_radians_;k>2*Math.PI&&(k-=2*Math.PI);g=Math.sin(k)*f/Blockly.Bubble.ARROW_BEND;f=Math.cos(k)*f/Blockly.Bubble.ARROW_BEND;a.push("M"+m+","+n);a.push("C"+(m+f)+","+(n+g)+" "+d+","+e+" "+d+","+e);a.push("C"+d+","+e+" "+(b+f)+","+(c+g)+" "+b+","+c)}a.push("z");this.bubbleArrow_.setAttribute("d",a.join(" "))};Blockly.Bubble.prototype.setColour=function(a){this.bubbleBack_.setAttribute("fill",a);this.bubbleArrow_.setAttribute("fill",a)}; -Blockly.Bubble.prototype.dispose=function(){this.onMouseDownBubbleWrapper_&&Blockly.unbindEvent_(this.onMouseDownBubbleWrapper_);this.onMouseDownResizeWrapper_&&Blockly.unbindEvent_(this.onMouseDownResizeWrapper_);Blockly.Bubble.unbindDragEvents_();Blockly.utils.dom.removeNode(this.bubbleGroup_);this.disposed=!0}; -Blockly.Bubble.prototype.moveDuringDrag=function(a,b){a?a.translateSurface(b.x,b.y):this.moveTo(b.x,b.y);this.relativeLeft_=this.workspace_.RTL?this.anchorXY_.x-b.x-this.width_:b.x-this.anchorXY_.x;this.relativeTop_=b.y-this.anchorXY_.y;this.renderArrow_()};Blockly.Bubble.prototype.getRelativeToSurfaceXY=function(){return new Blockly.utils.Coordinate(this.workspace_.RTL?-this.relativeLeft_+this.anchorXY_.x-this.width_:this.anchorXY_.x+this.relativeLeft_,this.anchorXY_.y+this.relativeTop_)}; -Blockly.Bubble.prototype.setAutoLayout=function(a){this.autoLayout_=a};Blockly.Events.CommentBase=function(a){this.commentId=(this.isBlank="undefined"==typeof a)?"":a.id;this.workspaceId=this.isBlank?"":a.workspace.id;this.group=Blockly.Events.getGroup();this.recordUndo=Blockly.Events.recordUndo};Blockly.utils.object.inherits(Blockly.Events.CommentBase,Blockly.Events.Abstract);Blockly.Events.CommentBase.prototype.toJson=function(){var a=Blockly.Events.CommentBase.superClass_.toJson.call(this);this.commentId&&(a.commentId=this.commentId);return a}; -Blockly.Events.CommentBase.prototype.fromJson=function(a){Blockly.Events.CommentBase.superClass_.fromJson.call(this,a);this.commentId=a.commentId};Blockly.Events.CommentChange=function(a,b,c){Blockly.Events.CommentChange.superClass_.constructor.call(this,a);a&&(this.oldContents_="undefined"==typeof b?"":b,this.newContents_="undefined"==typeof c?"":c)};Blockly.utils.object.inherits(Blockly.Events.CommentChange,Blockly.Events.CommentBase);Blockly.Events.CommentChange.prototype.type=Blockly.Events.COMMENT_CHANGE; -Blockly.Events.CommentChange.prototype.toJson=function(){var a=Blockly.Events.CommentChange.superClass_.toJson.call(this);a.newContents=this.newContents_;return a};Blockly.Events.CommentChange.prototype.fromJson=function(a){Blockly.Events.CommentChange.superClass_.fromJson.call(this,a);this.newContents_=a.newValue};Blockly.Events.CommentChange.prototype.isNull=function(){return this.oldContents_==this.newContents_}; -Blockly.Events.CommentChange.prototype.run=function(a){var b=this.getEventWorkspace_().getCommentById(this.commentId);b?b.setContent(a?this.newContents_:this.oldContents_):console.warn("Can't change non-existent comment: "+this.commentId)};Blockly.Events.CommentCreate=function(a){Blockly.Events.CommentCreate.superClass_.constructor.call(this,a);a&&(this.xml=a.toXmlWithXY())};Blockly.utils.object.inherits(Blockly.Events.CommentCreate,Blockly.Events.CommentBase); -Blockly.Events.CommentCreate.prototype.type=Blockly.Events.COMMENT_CREATE;Blockly.Events.CommentCreate.prototype.toJson=function(){var a=Blockly.Events.CommentCreate.superClass_.toJson.call(this);a.xml=Blockly.Xml.domToText(this.xml);return a};Blockly.Events.CommentCreate.prototype.fromJson=function(a){Blockly.Events.CommentCreate.superClass_.fromJson.call(this,a);this.xml=Blockly.Xml.textToDom(a.xml)}; -Blockly.Events.CommentCreate.prototype.run=function(a){Blockly.Events.CommentCreateDeleteHelper(this,a)};Blockly.Events.CommentCreateDeleteHelper=function(a,b){var c=a.getEventWorkspace_();b?(b=Blockly.utils.xml.createElement("xml"),b.appendChild(a.xml),Blockly.Xml.domToWorkspace(b,c)):(c=c.getCommentById(a.commentId))?c.dispose(!1,!1):console.warn("Can't uncreate non-existent comment: "+a.commentId)}; -Blockly.Events.CommentDelete=function(a){Blockly.Events.CommentDelete.superClass_.constructor.call(this,a);a&&(this.xml=a.toXmlWithXY())};Blockly.utils.object.inherits(Blockly.Events.CommentDelete,Blockly.Events.CommentBase);Blockly.Events.CommentDelete.prototype.type=Blockly.Events.COMMENT_DELETE;Blockly.Events.CommentDelete.prototype.toJson=function(){return Blockly.Events.CommentDelete.superClass_.toJson.call(this)}; -Blockly.Events.CommentDelete.prototype.fromJson=function(a){Blockly.Events.CommentDelete.superClass_.fromJson.call(this,a)};Blockly.Events.CommentDelete.prototype.run=function(a){Blockly.Events.CommentCreateDeleteHelper(this,!a)};Blockly.Events.CommentMove=function(a){Blockly.Events.CommentMove.superClass_.constructor.call(this,a);a&&(this.comment_=a,this.oldCoordinate_=a.getXY(),this.newCoordinate_=null)};Blockly.utils.object.inherits(Blockly.Events.CommentMove,Blockly.Events.CommentBase); -Blockly.Events.CommentMove.prototype.recordNew=function(){if(!this.comment_)throw Error("Tried to record the new position of a comment on the same event twice.");this.newCoordinate_=this.comment_.getXY();this.comment_=null};Blockly.Events.CommentMove.prototype.type=Blockly.Events.COMMENT_MOVE;Blockly.Events.CommentMove.prototype.setOldCoordinate=function(a){this.oldCoordinate_=a}; -Blockly.Events.CommentMove.prototype.toJson=function(){var a=Blockly.Events.CommentMove.superClass_.toJson.call(this);this.newCoordinate_&&(a.newCoordinate=Math.round(this.newCoordinate_.x)+","+Math.round(this.newCoordinate_.y));return a};Blockly.Events.CommentMove.prototype.fromJson=function(a){Blockly.Events.CommentMove.superClass_.fromJson.call(this,a);a.newCoordinate&&(a=a.newCoordinate.split(","),this.newCoordinate_=new Blockly.utils.Coordinate(Number(a[0]),Number(a[1])))}; -Blockly.Events.CommentMove.prototype.isNull=function(){return Blockly.utils.Coordinate.equals(this.oldCoordinate_,this.newCoordinate_)};Blockly.Events.CommentMove.prototype.run=function(a){var b=this.getEventWorkspace_().getCommentById(this.commentId);if(b){a=a?this.newCoordinate_:this.oldCoordinate_;var c=b.getXY();b.moveBy(a.x-c.x,a.y-c.y)}else console.warn("Can't move non-existent comment: "+this.commentId)};Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.COMMENT_CREATE,Blockly.Events.CommentCreate); -Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.COMMENT_CHANGE,Blockly.Events.CommentChange);Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.COMMENT_MOVE,Blockly.Events.CommentMove);Blockly.registry.register(Blockly.registry.Type.EVENT,Blockly.Events.COMMENT_DELETE,Blockly.Events.CommentDelete);Blockly.BubbleDragger=function(a,b){this.draggingBubble_=a;this.workspace_=b;this.deleteArea_=null;this.wouldDeleteBubble_=!1;this.startXY_=this.draggingBubble_.getRelativeToSurfaceXY();this.dragSurface_=Blockly.utils.is3dSupported()&&b.getBlockDragSurface()?b.getBlockDragSurface():null};Blockly.BubbleDragger.prototype.dispose=function(){this.dragSurface_=this.workspace_=this.draggingBubble_=null}; -Blockly.BubbleDragger.prototype.startBubbleDrag=function(){Blockly.Events.getGroup()||Blockly.Events.setGroup(!0);this.workspace_.setResizesEnabled(!1);this.draggingBubble_.setAutoLayout(!1);this.dragSurface_&&this.moveToDragSurface_();this.draggingBubble_.setDragging&&this.draggingBubble_.setDragging(!0);var a=this.workspace_.getToolbox();if(a&&"function"==typeof a.addStyle){var b=this.draggingBubble_.isDeletable()?"blocklyToolboxDelete":"blocklyToolboxGrab";a.addStyle(b)}}; -Blockly.BubbleDragger.prototype.dragBubble=function(a,b){b=this.pixelsToWorkspaceUnits_(b);b=Blockly.utils.Coordinate.sum(this.startXY_,b);this.draggingBubble_.moveDuringDrag(this.dragSurface_,b);this.draggingBubble_.isDeletable()&&(this.deleteArea_=this.workspace_.isDeleteArea(a),this.updateCursorDuringBubbleDrag_())}; -Blockly.BubbleDragger.prototype.maybeDeleteBubble_=function(){var a=this.workspace_.trashcan;this.wouldDeleteBubble_?(a&&setTimeout(a.closeLid.bind(a),100),this.fireMoveEvent_(),this.draggingBubble_.dispose(!1,!0)):a&&a.closeLid();return this.wouldDeleteBubble_}; -Blockly.BubbleDragger.prototype.updateCursorDuringBubbleDrag_=function(){this.wouldDeleteBubble_=this.deleteArea_!=Blockly.DELETE_AREA_NONE;var a=this.workspace_.trashcan;this.wouldDeleteBubble_?(this.draggingBubble_.setDeleteStyle(!0),this.deleteArea_==Blockly.DELETE_AREA_TRASH&&a&&a.setLidOpen(!0)):(this.draggingBubble_.setDeleteStyle(!1),a&&a.setLidOpen(!1))}; -Blockly.BubbleDragger.prototype.endBubbleDrag=function(a,b){this.dragBubble(a,b);a=this.pixelsToWorkspaceUnits_(b);a=Blockly.utils.Coordinate.sum(this.startXY_,a);this.draggingBubble_.moveTo(a.x,a.y);this.maybeDeleteBubble_()||(this.dragSurface_&&this.dragSurface_.clearAndHide(this.workspace_.getBubbleCanvas()),this.draggingBubble_.setDragging&&this.draggingBubble_.setDragging(!1),this.fireMoveEvent_());this.workspace_.setResizesEnabled(!0);(a=this.workspace_.getToolbox())&&"function"==typeof a.removeStyle&& -(b=this.draggingBubble_.isDeletable()?"blocklyToolboxDelete":"blocklyToolboxGrab",a.removeStyle(b));Blockly.Events.setGroup(!1)};Blockly.BubbleDragger.prototype.fireMoveEvent_=function(){if(this.draggingBubble_.isComment){var a=new Blockly.Events.CommentMove(this.draggingBubble_);a.setOldCoordinate(this.startXY_);a.recordNew();Blockly.Events.fire(a)}}; -Blockly.BubbleDragger.prototype.pixelsToWorkspaceUnits_=function(a){a=new Blockly.utils.Coordinate(a.x/this.workspace_.scale,a.y/this.workspace_.scale);this.workspace_.isMutator&&a.scale(1/this.workspace_.options.parentWorkspace.scale);return a};Blockly.BubbleDragger.prototype.moveToDragSurface_=function(){this.draggingBubble_.moveTo(0,0);this.dragSurface_.translateSurface(this.startXY_.x,this.startXY_.y);this.dragSurface_.setBlocksAndShow(this.draggingBubble_.getSvgRoot())};Blockly.WorkspaceDragger=function(a){this.workspace_=a;this.startScrollXY_=new Blockly.utils.Coordinate(a.scrollX,a.scrollY)};Blockly.WorkspaceDragger.prototype.dispose=function(){this.workspace_=null};Blockly.WorkspaceDragger.prototype.startDrag=function(){Blockly.selected&&Blockly.selected.unselect();this.workspace_.setupDragSurface()};Blockly.WorkspaceDragger.prototype.endDrag=function(a){this.drag(a);this.workspace_.resetDragSurface()}; -Blockly.WorkspaceDragger.prototype.drag=function(a){a=Blockly.utils.Coordinate.sum(this.startScrollXY_,a);this.workspace_.scroll(a.x,a.y)};Blockly.FlyoutDragger=function(a){Blockly.FlyoutDragger.superClass_.constructor.call(this,a.getWorkspace());this.scrollbar_=a.scrollbar;this.horizontalLayout_=a.horizontalLayout};Blockly.utils.object.inherits(Blockly.FlyoutDragger,Blockly.WorkspaceDragger);Blockly.FlyoutDragger.prototype.drag=function(a){a=Blockly.utils.Coordinate.sum(this.startScrollXY_,a);this.horizontalLayout_?this.scrollbar_.set(-a.x):this.scrollbar_.set(-a.y)};Blockly.Action=function(a,b){this.name=a;this.desc=b};Blockly.navigation={};Blockly.navigation.loggingCallback=null;Blockly.navigation.STATE_FLYOUT=1;Blockly.navigation.STATE_WS=2;Blockly.navigation.STATE_TOOLBOX=3;Blockly.navigation.WS_MOVE_DISTANCE=40;Blockly.navigation.currentState_=Blockly.navigation.STATE_WS; -Blockly.navigation.actionNames={PREVIOUS:"previous",NEXT:"next",IN:"in",OUT:"out",INSERT:"insert",MARK:"mark",DISCONNECT:"disconnect",TOOLBOX:"toolbox",EXIT:"exit",TOGGLE_KEYBOARD_NAV:"toggle_keyboard_nav",MOVE_WS_CURSOR_UP:"move workspace cursor up",MOVE_WS_CURSOR_DOWN:"move workspace cursor down",MOVE_WS_CURSOR_LEFT:"move workspace cursor left",MOVE_WS_CURSOR_RIGHT:"move workspace cursor right"};Blockly.navigation.MARKER_NAME="local_marker_1";Blockly.navigation.getMarker=function(){return Blockly.navigation.getNavigationWorkspace().getMarker(Blockly.navigation.MARKER_NAME)}; -Blockly.navigation.getNavigationWorkspace=function(){return Blockly.getMainWorkspace()};Blockly.navigation.focusToolbox_=function(){var a=Blockly.navigation.getNavigationWorkspace().getToolbox();a&&(Blockly.navigation.currentState_=Blockly.navigation.STATE_TOOLBOX,Blockly.navigation.resetFlyout_(!1),Blockly.navigation.getMarker().getCurNode()||Blockly.navigation.markAtCursor_(),a.getSelectedItem()||a.selectItemByPosition(0))}; -Blockly.navigation.focusFlyout_=function(){Blockly.navigation.currentState_=Blockly.navigation.STATE_FLYOUT;var a=Blockly.navigation.getNavigationWorkspace();var b=a.getToolbox();a=b?b.getFlyout():a.getFlyout();Blockly.navigation.getMarker().getCurNode()||Blockly.navigation.markAtCursor_();a&&a.getWorkspace()&&(a=a.getWorkspace().getTopBlocks(!0),0(this.flyout_?Blockly.FLYOUT_DRAG_RADIUS:Blockly.DRAG_RADIUS)}; -Blockly.Gesture.prototype.updateIsDraggingFromFlyout_=function(){return this.targetBlock_&&this.flyout_.isBlockCreatable_(this.targetBlock_)?!this.flyout_.isScrollable()||this.flyout_.isDragTowardWorkspace(this.currentDragDeltaXY_)?(this.startWorkspace_=this.flyout_.targetWorkspace,this.startWorkspace_.updateScreenCalculationsIfScrolled(),Blockly.Events.getGroup()||Blockly.Events.setGroup(!0),this.startBlock_=null,this.targetBlock_=this.flyout_.createBlock(this.targetBlock_),this.targetBlock_.select(), -!0):!1:!1};Blockly.Gesture.prototype.updateIsDraggingBubble_=function(){if(!this.startBubble_)return!1;this.isDraggingBubble_=!0;this.startDraggingBubble_();return!0};Blockly.Gesture.prototype.updateIsDraggingBlock_=function(){if(!this.targetBlock_)return!1;this.flyout_?this.isDraggingBlock_=this.updateIsDraggingFromFlyout_():this.targetBlock_.isMovable()&&(this.isDraggingBlock_=!0);return this.isDraggingBlock_?(this.startDraggingBlock_(),!0):!1}; -Blockly.Gesture.prototype.updateIsDraggingWorkspace_=function(){if(this.flyout_?this.flyout_.isScrollable():this.startWorkspace_&&this.startWorkspace_.isDraggable())this.workspaceDragger_=this.flyout_?new Blockly.FlyoutDragger(this.flyout_):new Blockly.WorkspaceDragger(this.startWorkspace_),this.isDraggingWorkspace_=!0,this.workspaceDragger_.startDrag()}; -Blockly.Gesture.prototype.updateIsDragging_=function(){if(this.calledUpdateIsDragging_)throw Error("updateIsDragging_ should only be called once per gesture.");this.calledUpdateIsDragging_=!0;this.updateIsDraggingBubble_()||this.updateIsDraggingBlock_()||this.updateIsDraggingWorkspace_()}; -Blockly.Gesture.prototype.startDraggingBlock_=function(){this.blockDragger_=new Blockly.BlockDragger(this.targetBlock_,this.startWorkspace_);this.blockDragger_.startBlockDrag(this.currentDragDeltaXY_,this.healStack_);this.blockDragger_.dragBlock(this.mostRecentEvent_,this.currentDragDeltaXY_)}; -Blockly.Gesture.prototype.startDraggingBubble_=function(){this.bubbleDragger_=new Blockly.BubbleDragger(this.startBubble_,this.startWorkspace_);this.bubbleDragger_.startBubbleDrag();this.bubbleDragger_.dragBubble(this.mostRecentEvent_,this.currentDragDeltaXY_)}; -Blockly.Gesture.prototype.doStart=function(a){Blockly.utils.isTargetInput(a)?this.cancel():(this.hasStarted_=!0,Blockly.blockAnimations.disconnectUiStop(),this.startWorkspace_.updateScreenCalculationsIfScrolled(),this.startWorkspace_.isMutator&&this.startWorkspace_.resize(),Blockly.hideChaff(!!this.flyout_),this.startWorkspace_.markFocused(),this.mostRecentEvent_=a,Blockly.Tooltip.block(),this.targetBlock_&&(!this.targetBlock_.isInFlyout&&a.shiftKey&&this.targetBlock_.workspace.keyboardAccessibilityMode? -this.creatorWorkspace_.getCursor().setCurNode(Blockly.ASTNode.createTopNode(this.targetBlock_)):this.targetBlock_.select()),Blockly.utils.isRightButton(a)?this.handleRightClick(a):("touchstart"!=a.type.toLowerCase()&&"pointerdown"!=a.type.toLowerCase()||"mouse"==a.pointerType||Blockly.longStart(a,this),this.mouseDownXY_=new Blockly.utils.Coordinate(a.clientX,a.clientY),this.healStack_=a.altKey||a.ctrlKey||a.metaKey,this.bindMouseEvents(a)))}; -Blockly.Gesture.prototype.bindMouseEvents=function(a){this.onMoveWrapper_=Blockly.bindEventWithChecks_(document,"mousemove",null,this.handleMove.bind(this));this.onUpWrapper_=Blockly.bindEventWithChecks_(document,"mouseup",null,this.handleUp.bind(this));a.preventDefault();a.stopPropagation()}; -Blockly.Gesture.prototype.handleMove=function(a){this.updateFromEvent_(a);this.isDraggingWorkspace_?this.workspaceDragger_.drag(this.currentDragDeltaXY_):this.isDraggingBlock_?this.blockDragger_.dragBlock(this.mostRecentEvent_,this.currentDragDeltaXY_):this.isDraggingBubble_&&this.bubbleDragger_.dragBubble(this.mostRecentEvent_,this.currentDragDeltaXY_);a.preventDefault();a.stopPropagation()}; -Blockly.Gesture.prototype.handleUp=function(a){this.updateFromEvent_(a);Blockly.longStop_();this.isEnding_?console.log("Trying to end a gesture recursively."):(this.isEnding_=!0,this.isDraggingBubble_?this.bubbleDragger_.endBubbleDrag(a,this.currentDragDeltaXY_):this.isDraggingBlock_?this.blockDragger_.endBlockDrag(a,this.currentDragDeltaXY_):this.isDraggingWorkspace_?this.workspaceDragger_.endDrag(this.currentDragDeltaXY_):this.isBubbleClick_()?this.doBubbleClick_():this.isFieldClick_()?this.doFieldClick_(): -this.isBlockClick_()?this.doBlockClick_():this.isWorkspaceClick_()&&this.doWorkspaceClick_(a),a.preventDefault(),a.stopPropagation(),this.dispose())}; -Blockly.Gesture.prototype.cancel=function(){this.isEnding_||(Blockly.longStop_(),this.isDraggingBubble_?this.bubbleDragger_.endBubbleDrag(this.mostRecentEvent_,this.currentDragDeltaXY_):this.isDraggingBlock_?this.blockDragger_.endBlockDrag(this.mostRecentEvent_,this.currentDragDeltaXY_):this.isDraggingWorkspace_&&this.workspaceDragger_.endDrag(this.currentDragDeltaXY_),this.dispose())}; -Blockly.Gesture.prototype.handleRightClick=function(a){this.targetBlock_?(this.bringBlockToFront_(),Blockly.hideChaff(!!this.flyout_),this.targetBlock_.showContextMenu(a)):this.startBubble_?this.startBubble_.showContextMenu(a):this.startWorkspace_&&!this.flyout_&&(Blockly.hideChaff(),this.startWorkspace_.showContextMenu(a));a.preventDefault();a.stopPropagation();this.dispose()}; -Blockly.Gesture.prototype.handleWsStart=function(a,b){if(this.hasStarted_)throw Error("Tried to call gesture.handleWsStart, but the gesture had already been started.");this.setStartWorkspace_(b);this.mostRecentEvent_=a;this.doStart(a);this.startWorkspace_.keyboardAccessibilityMode&&Blockly.navigation.setState(Blockly.navigation.STATE_WS)};Blockly.Gesture.prototype.fireWorkspaceClick_=function(a){var b=new Blockly.Events.Ui(null,"click",null,"workspace");b.workspaceId=a.id;Blockly.Events.fire(b)}; -Blockly.Gesture.prototype.handleFlyoutStart=function(a,b){if(this.hasStarted_)throw Error("Tried to call gesture.handleFlyoutStart, but the gesture had already been started.");this.setStartFlyout_(b);this.handleWsStart(a,b.getWorkspace())};Blockly.Gesture.prototype.handleBlockStart=function(a,b){if(this.hasStarted_)throw Error("Tried to call gesture.handleBlockStart, but the gesture had already been started.");this.setStartBlock(b);this.mostRecentEvent_=a}; -Blockly.Gesture.prototype.handleBubbleStart=function(a,b){if(this.hasStarted_)throw Error("Tried to call gesture.handleBubbleStart, but the gesture had already been started.");this.setStartBubble(b);this.mostRecentEvent_=a};Blockly.Gesture.prototype.doBubbleClick_=function(){this.startBubble_.setFocus&&this.startBubble_.setFocus();this.startBubble_.select&&this.startBubble_.select()};Blockly.Gesture.prototype.doFieldClick_=function(){this.startField_.showEditor(this.mostRecentEvent_);this.bringBlockToFront_()}; -Blockly.Gesture.prototype.doBlockClick_=function(){this.flyout_&&this.flyout_.autoClose?this.targetBlock_.isEnabled()&&(Blockly.Events.getGroup()||Blockly.Events.setGroup(!0),this.flyout_.createBlock(this.targetBlock_).scheduleSnapAndBump()):Blockly.Events.fire(new Blockly.Events.Ui(this.startBlock_,"click",void 0,"block"));this.bringBlockToFront_();Blockly.Events.setGroup(!1)}; -Blockly.Gesture.prototype.doWorkspaceClick_=function(a){var b=this.creatorWorkspace_;a.shiftKey&&b.keyboardAccessibilityMode?(a=new Blockly.utils.Coordinate(a.clientX,a.clientY),a=Blockly.utils.screenToWsCoordinates(b,a),a=Blockly.ASTNode.createWorkspaceNode(b,a),b.getCursor().setCurNode(a)):Blockly.selected&&Blockly.selected.unselect();this.fireWorkspaceClick_(this.startWorkspace_||b)};Blockly.Gesture.prototype.bringBlockToFront_=function(){this.targetBlock_&&!this.flyout_&&this.targetBlock_.bringToFront()}; -Blockly.Gesture.prototype.setStartField=function(a){if(this.hasStarted_)throw Error("Tried to call gesture.setStartField, but the gesture had already been started.");this.startField_||(this.startField_=a)};Blockly.Gesture.prototype.setStartBubble=function(a){this.startBubble_||(this.startBubble_=a)};Blockly.Gesture.prototype.setStartBlock=function(a){this.startBlock_||this.startBubble_||(this.startBlock_=a,a.isInFlyout&&a!=a.getRootBlock()?this.setTargetBlock_(a.getRootBlock()):this.setTargetBlock_(a))}; -Blockly.Gesture.prototype.setTargetBlock_=function(a){a.isShadow()?this.setTargetBlock_(a.getParent()):this.targetBlock_=a};Blockly.Gesture.prototype.setStartWorkspace_=function(a){this.startWorkspace_||(this.startWorkspace_=a)};Blockly.Gesture.prototype.setStartFlyout_=function(a){this.flyout_||(this.flyout_=a)};Blockly.Gesture.prototype.isBubbleClick_=function(){return!!this.startBubble_&&!this.hasExceededDragRadius_}; -Blockly.Gesture.prototype.isBlockClick_=function(){return!!this.startBlock_&&!this.hasExceededDragRadius_&&!this.isFieldClick_()};Blockly.Gesture.prototype.isFieldClick_=function(){return(this.startField_?this.startField_.isClickable():!1)&&!this.hasExceededDragRadius_&&(!this.flyout_||!this.flyout_.autoClose)};Blockly.Gesture.prototype.isWorkspaceClick_=function(){return!this.startBlock_&&!this.startBubble_&&!this.startField_&&!this.hasExceededDragRadius_}; -Blockly.Gesture.prototype.isDragging=function(){return this.isDraggingWorkspace_||this.isDraggingBlock_||this.isDraggingBubble_};Blockly.Gesture.prototype.hasStarted=function(){return this.hasStarted_};Blockly.Gesture.prototype.getInsertionMarkers=function(){return this.blockDragger_?this.blockDragger_.getInsertionMarkers():[]};Blockly.Gesture.inProgress=function(){for(var a=Blockly.Workspace.getAll(),b=0,c;c=a[b];b++)if(c.currentGesture_)return!0;return!1};Blockly.Field=function(a,b,c){this.value_=this.DEFAULT_VALUE;this.tooltip_=this.validator_=null;this.size_=new Blockly.utils.Size(0,0);this.constants_=this.mouseDownWrapper_=this.textContent_=this.textElement_=this.borderRect_=this.fieldGroup_=this.markerSvg_=this.cursorSvg_=null;c&&this.configure_(c);this.setValue(a);b&&this.setValidator(b)};Blockly.Field.prototype.DEFAULT_VALUE=null;Blockly.Field.prototype.name=void 0;Blockly.Field.prototype.disposed=!1; -Blockly.Field.prototype.maxDisplayLength=50;Blockly.Field.prototype.sourceBlock_=null;Blockly.Field.prototype.isDirty_=!0;Blockly.Field.prototype.visible_=!0;Blockly.Field.prototype.clickTarget_=null;Blockly.Field.NBSP="\u00a0";Blockly.Field.prototype.EDITABLE=!0;Blockly.Field.prototype.SERIALIZABLE=!1;Blockly.Field.prototype.configure_=function(a){var b=a.tooltip;"string"==typeof b&&(b=Blockly.utils.replaceMessageReferences(a.tooltip));b&&this.setTooltip(b)}; -Blockly.Field.prototype.setSourceBlock=function(a){if(this.sourceBlock_)throw Error("Field already bound to a block.");this.sourceBlock_=a};Blockly.Field.prototype.getConstants=function(){!this.constants_&&this.sourceBlock_&&this.sourceBlock_.workspace&&this.sourceBlock_.workspace.rendered&&(this.constants_=this.sourceBlock_.workspace.getRenderer().getConstants());return this.constants_};Blockly.Field.prototype.getSourceBlock=function(){return this.sourceBlock_}; -Blockly.Field.prototype.init=function(){this.fieldGroup_||(this.fieldGroup_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G,{},null),this.isVisible()||(this.fieldGroup_.style.display="none"),this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_),this.initView(),this.updateEditable(),this.setTooltip(this.tooltip_),this.bindEvents_(),this.initModel())};Blockly.Field.prototype.initView=function(){this.createBorderRect_();this.createTextElement_()};Blockly.Field.prototype.initModel=function(){}; -Blockly.Field.prototype.createBorderRect_=function(){this.borderRect_=Blockly.utils.dom.createSvgElement(Blockly.utils.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_)}; -Blockly.Field.prototype.createTextElement_=function(){this.textElement_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.TEXT,{"class":"blocklyText"},this.fieldGroup_);this.getConstants().FIELD_TEXT_BASELINE_CENTER&&this.textElement_.setAttribute("dominant-baseline","central");this.textContent_=document.createTextNode("");this.textElement_.appendChild(this.textContent_)}; -Blockly.Field.prototype.bindEvents_=function(){Blockly.Tooltip.bindMouseEvents(this.getClickTarget_());this.mouseDownWrapper_=Blockly.bindEventWithChecks_(this.getClickTarget_(),"mousedown",this,this.onMouseDown_)};Blockly.Field.prototype.fromXml=function(a){this.setValue(a.textContent)};Blockly.Field.prototype.toXml=function(a){a.textContent=this.getValue();return a}; -Blockly.Field.prototype.dispose=function(){Blockly.DropDownDiv.hideIfOwner(this);Blockly.WidgetDiv.hideIfOwner(this);Blockly.Tooltip.unbindMouseEvents(this.getClickTarget_());this.mouseDownWrapper_&&Blockly.unbindEvent_(this.mouseDownWrapper_);Blockly.utils.dom.removeNode(this.fieldGroup_);this.disposed=!0}; -Blockly.Field.prototype.updateEditable=function(){var a=this.fieldGroup_;this.EDITABLE&&a&&(this.sourceBlock_.isEditable()?(Blockly.utils.dom.addClass(a,"blocklyEditableText"),Blockly.utils.dom.removeClass(a,"blocklyNonEditableText"),a.style.cursor=this.CURSOR):(Blockly.utils.dom.addClass(a,"blocklyNonEditableText"),Blockly.utils.dom.removeClass(a,"blocklyEditableText"),a.style.cursor=""))}; -Blockly.Field.prototype.isClickable=function(){return!!this.sourceBlock_&&this.sourceBlock_.isEditable()&&!!this.showEditor_&&"function"===typeof this.showEditor_};Blockly.Field.prototype.isCurrentlyEditable=function(){return this.EDITABLE&&!!this.sourceBlock_&&this.sourceBlock_.isEditable()}; -Blockly.Field.prototype.isSerializable=function(){var a=!1;this.name&&(this.SERIALIZABLE?a=!0:this.EDITABLE&&(console.warn("Detected an editable field that was not serializable. Please define SERIALIZABLE property as true on all editable custom fields. Proceeding with serialization."),a=!0));return a};Blockly.Field.prototype.isVisible=function(){return this.visible_}; -Blockly.Field.prototype.setVisible=function(a){if(this.visible_!=a){this.visible_=a;var b=this.getSvgRoot();b&&(b.style.display=a?"block":"none")}};Blockly.Field.prototype.setValidator=function(a){this.validator_=a};Blockly.Field.prototype.getValidator=function(){return this.validator_};Blockly.Field.prototype.classValidator=function(a){Blockly.utils.deprecation.warn("Field.prototype.classValidator","May 2019","December 2020","Blockly.Field.prototype.doClassValidation_");return a}; -Blockly.Field.prototype.callValidator=function(a){Blockly.utils.deprecation.warn("Field.prototype.callValidator","May 2019","December 2020");var b=this.classValidator(a);if(null===b)return null;void 0!==b&&(a=b);if(b=this.getValidator()){b=b.call(this,a);if(null===b)return null;void 0!==b&&(a=b)}return a};Blockly.Field.prototype.getSvgRoot=function(){return this.fieldGroup_};Blockly.Field.prototype.applyColour=function(){}; -Blockly.Field.prototype.render_=function(){this.textContent_&&(this.textContent_.nodeValue=this.getDisplayText_());this.updateSize_()};Blockly.Field.prototype.showEditor=function(a){this.isClickable()&&this.showEditor_(a)};Blockly.Field.prototype.updateWidth=function(){Blockly.utils.deprecation.warn("Field.prototype.updateWidth","May 2019","December 2020","Blockly.Field.prototype.updateSize_ or Blockly.utils.dom.getTextWidth");this.updateSize_()}; -Blockly.Field.prototype.updateSize_=function(a){var b=this.getConstants();a=void 0!=a?a:this.borderRect_?this.getConstants().FIELD_BORDER_RECT_X_PADDING:0;var c=2*a,d=b.FIELD_TEXT_HEIGHT,e=0;this.textElement_&&(e=Blockly.utils.dom.getFastTextWidth(this.textElement_,b.FIELD_TEXT_FONTSIZE,b.FIELD_TEXT_FONTWEIGHT,b.FIELD_TEXT_FONTFAMILY),c+=e);this.borderRect_&&(d=Math.max(d,b.FIELD_BORDER_RECT_HEIGHT));this.size_.height=d;this.size_.width=c;this.positionTextElement_(a,e);this.positionBorderRect_()}; -Blockly.Field.prototype.positionTextElement_=function(a,b){if(this.textElement_){var c=this.getConstants(),d=this.size_.height/2;this.textElement_.setAttribute("x",this.sourceBlock_.RTL?this.size_.width-b-a:a);this.textElement_.setAttribute("y",c.FIELD_TEXT_BASELINE_CENTER?d:d-c.FIELD_TEXT_HEIGHT/2+c.FIELD_TEXT_BASELINE)}}; -Blockly.Field.prototype.positionBorderRect_=function(){this.borderRect_&&(this.borderRect_.setAttribute("width",this.size_.width),this.borderRect_.setAttribute("height",this.size_.height),this.borderRect_.setAttribute("rx",this.getConstants().FIELD_BORDER_RECT_RADIUS),this.borderRect_.setAttribute("ry",this.getConstants().FIELD_BORDER_RECT_RADIUS))}; -Blockly.Field.prototype.getSize=function(){if(!this.isVisible())return new Blockly.utils.Size(0,0);this.isDirty_?(this.render_(),this.isDirty_=!1):this.visible_&&0==this.size_.width&&(console.warn("Deprecated use of setting size_.width to 0 to rerender a field. Set field.isDirty_ to true instead."),this.render_());return this.size_}; -Blockly.Field.prototype.getScaledBBox=function(){if(this.borderRect_)a=this.borderRect_.getBoundingClientRect(),c=Blockly.utils.style.getPageOffset(this.borderRect_),d=a.width,a=a.height;else{var a=this.sourceBlock_.getHeightWidth(),b=this.sourceBlock_.workspace.scale,c=this.getAbsoluteXY_(),d=a.width*b;a=a.height*b;Blockly.utils.userAgent.GECKO?(c.x+=1.5*b,c.y+=1.5*b):Blockly.utils.userAgent.EDGE||Blockly.utils.userAgent.IE||(c.x-=.5*b,c.y-=.5*b);d+=1*b;a+=1*b}return new Blockly.utils.Rect(c.y,c.y+ -a,c.x,c.x+d)};Blockly.Field.prototype.getDisplayText_=function(){var a=this.getText();if(!a)return Blockly.Field.NBSP;a.length>this.maxDisplayLength&&(a=a.substring(0,this.maxDisplayLength-2)+"\u2026");a=a.replace(/\s/g,Blockly.Field.NBSP);this.sourceBlock_&&this.sourceBlock_.RTL&&(a+="\u200f");return a};Blockly.Field.prototype.getText=function(){if(this.getText_){var a=this.getText_.call(this);if(null!==a)return String(a)}return String(this.getValue())}; -Blockly.Field.prototype.setText=function(a){Blockly.utils.deprecation.warn("Field.prototype.setText","May 2019","December 2020","Blockly.Field.prototype.setValue");throw Error("setText method is deprecated");};Blockly.Field.prototype.markDirty=function(){this.isDirty_=!0;this.constants_=null};Blockly.Field.prototype.forceRerender=function(){this.isDirty_=!0;this.sourceBlock_&&this.sourceBlock_.rendered&&(this.sourceBlock_.render(),this.sourceBlock_.bumpNeighbours(),this.updateMarkers_())}; -Blockly.Field.prototype.setValue=function(a){if(null!==a){var b=this.doClassValidation_(a);a=this.processValidation_(a,b);if(!(a instanceof Error)){if(b=this.getValidator())if(b=b.call(this,a),a=this.processValidation_(a,b),a instanceof Error)return;b=this.sourceBlock_;if(!b||!b.disposed){var c=this.getValue();c!==a&&(b&&Blockly.Events.isEnabled()&&Blockly.Events.fire(new Blockly.Events.BlockChange(b,"field",this.name||null,c,a)),this.doValueUpdate_(a),this.isDirty_&&this.forceRerender())}}}}; -Blockly.Field.prototype.processValidation_=function(a,b){if(null===b)return this.doValueInvalid_(a),this.isDirty_&&this.forceRerender(),Error();void 0!==b&&(a=b);return a};Blockly.Field.prototype.getValue=function(){return this.value_};Blockly.Field.prototype.doClassValidation_=function(a){return null===a||void 0===a?null:a=this.classValidator(a)};Blockly.Field.prototype.doValueUpdate_=function(a){this.value_=a;this.isDirty_=!0};Blockly.Field.prototype.doValueInvalid_=function(a){}; -Blockly.Field.prototype.onMouseDown_=function(a){this.sourceBlock_&&this.sourceBlock_.workspace&&(a=this.sourceBlock_.workspace.getGesture(a))&&a.setStartField(this)};Blockly.Field.prototype.setTooltip=function(a){a||""===a||(a=this.sourceBlock_);var b=this.getClickTarget_();b?b.tooltip=a:this.tooltip_=a};Blockly.Field.prototype.getTooltip=function(){var a=this.getClickTarget_();return a?Blockly.Tooltip.getTooltipOfObject(a):Blockly.Tooltip.getTooltipOfObject({tooltip:this.tooltip_})}; -Blockly.Field.prototype.getClickTarget_=function(){return this.clickTarget_||this.getSvgRoot()};Blockly.Field.prototype.getAbsoluteXY_=function(){return Blockly.utils.style.getPageOffset(this.getClickTarget_())};Blockly.Field.prototype.referencesVariables=function(){return!1};Blockly.Field.prototype.getParentInput=function(){for(var a=null,b=this.sourceBlock_,c=b.inputList,d=0;da||a>this.fieldRow.length)throw Error("index "+a+" out of bounds.");if(!(b||""==b&&c))return a;"string"==typeof b&&(b=new Blockly.FieldLabel(b));b.setSourceBlock(this.sourceBlock_);this.sourceBlock_.rendered&&(b.init(),b.applyColour());b.name=c;b.setVisible(this.isVisible());c=b;c.prefixField&&(a=this.insertFieldAt(a,c.prefixField));this.fieldRow.splice(a,0,b);++a;c.suffixField&&(a=this.insertFieldAt(a,c.suffixField));this.sourceBlock_.rendered&& -(this.sourceBlock_=this.sourceBlock_,this.sourceBlock_.render(),this.sourceBlock_.bumpNeighbours());return a};Blockly.Input.prototype.removeField=function(a,b){for(var c=0,d;d=this.fieldRow[c];c++)if(d.name===a)return d.dispose(),this.fieldRow.splice(c,1),this.sourceBlock_.rendered&&(this.sourceBlock_=this.sourceBlock_,this.sourceBlock_.render(),this.sourceBlock_.bumpNeighbours()),!0;if(b)return!1;throw Error('Field "'+a+'" not found.');};Blockly.Input.prototype.isVisible=function(){return this.visible_}; -Blockly.Input.prototype.setVisible=function(a){var b=[];if(this.visible_==a)return b;this.visible_=a;for(var c=0,d;d=this.fieldRow[c];c++)d.setVisible(a);this.connection&&(this.connection=this.connection,a?b=this.connection.startTrackingAll():this.connection.stopTrackingAll(),c=this.connection.targetBlock())&&(c.getSvgRoot().style.display=a?"block":"none");return b};Blockly.Input.prototype.markDirty=function(){for(var a=0,b;b=this.fieldRow[a];a++)b.markDirty()}; -Blockly.Input.prototype.setCheck=function(a){if(!this.connection)throw Error("This input does not have a connection.");this.connection.setCheck(a);return this};Blockly.Input.prototype.setAlign=function(a){this.align=a;this.sourceBlock_.rendered&&(this.sourceBlock_=this.sourceBlock_,this.sourceBlock_.render());return this};Blockly.Input.prototype.setShadowDom=function(a){if(!this.connection)throw Error("This input does not have a connection.");this.connection.setShadowDom(a);return this}; -Blockly.Input.prototype.getShadowDom=function(){if(!this.connection)throw Error("This input does not have a connection.");return this.connection.getShadowDom()};Blockly.Input.prototype.init=function(){if(this.sourceBlock_.workspace.rendered)for(var a=0;aa&&(e=e.substring(0,a-3)+"...");return e};Blockly.Block.prototype.appendValueInput=function(a){return this.appendInput_(Blockly.INPUT_VALUE,a)};Blockly.Block.prototype.appendStatementInput=function(a){return this.appendInput_(Blockly.NEXT_STATEMENT,a)};Blockly.Block.prototype.appendDummyInput=function(a){return this.appendInput_(Blockly.DUMMY_INPUT,a||"")}; -Blockly.Block.prototype.jsonInit=function(a){var b=a.type?'Block "'+a.type+'": ':"";if(a.output&&a.previousStatement)throw Error(b+"Must not have both an output and a previousStatement.");a.style&&a.style.hat&&(this.hat=a.style.hat,a.style=null);if(a.style&&a.colour)throw Error(b+"Must not have both a colour and a style.");a.style?this.jsonInitStyle_(a,b):this.jsonInitColour_(a,b);for(var c=0;void 0!==a["message"+c];)this.interpolate_(a["message"+c],a["args"+c]||[],a["lastDummyAlign"+c],b),c++;void 0!== -a.inputsInline&&this.setInputsInline(a.inputsInline);void 0!==a.output&&this.setOutput(!0,a.output);void 0!==a.outputShape&&this.setOutputShape(a.outputShape);void 0!==a.previousStatement&&this.setPreviousStatement(!0,a.previousStatement);void 0!==a.nextStatement&&this.setNextStatement(!0,a.nextStatement);void 0!==a.tooltip&&(c=a.tooltip,c=Blockly.utils.replaceMessageReferences(c),this.setTooltip(c));void 0!==a.enableContextMenu&&(c=a.enableContextMenu,this.contextMenu=!!c);void 0!==a.helpUrl&&(c= -a.helpUrl,c=Blockly.utils.replaceMessageReferences(c),this.setHelpUrl(c));"string"==typeof a.extensions&&(console.warn(b+"JSON attribute 'extensions' should be an array of strings. Found raw string in JSON for '"+a.type+"' block."),a.extensions=[a.extensions]);void 0!==a.mutator&&Blockly.Extensions.apply(a.mutator,this,!0);if(Array.isArray(a.extensions))for(a=a.extensions,b=0;b=k||k>b.length)throw Error('Block "'+this.type+'": Message index %'+k+" out of range.");if(f[k])throw Error('Block "'+this.type+'": Message index %'+k+" duplicated.");f[k]=!0;g++;a.push(b[k-1])}else(k=k.trim())&&a.push(k)}if(g!=b.length)throw Error('Block "'+this.type+'": Message does not reference all '+b.length+" arg(s)."); -a.length&&("string"==typeof a[a.length-1]||Blockly.utils.string.startsWith(a[a.length-1].type,"field_"))&&(h={type:"input_dummy"},c&&(h.align=c),a.push(h));c={LEFT:Blockly.ALIGN_LEFT,RIGHT:Blockly.ALIGN_RIGHT,CENTRE:Blockly.ALIGN_CENTRE,CENTER:Blockly.ALIGN_CENTRE};b=[];for(h=0;h=this.inputList.length)throw RangeError("Input index "+a+" out of bounds.");if(b>this.inputList.length)throw RangeError("Reference input "+b+" out of bounds.");var c=this.inputList[a];this.inputList.splice(a,1);aa?this.menuItems_.length:a,-1)};Blockly.Menu.prototype.highlightFirst_=function(){this.highlightHelper_(-1,1)};Blockly.Menu.prototype.highlightLast_=function(){this.highlightHelper_(this.menuItems_.length,-1)};Blockly.Menu.prototype.highlightHelper_=function(a,b){a+=b;for(var c;c=this.menuItems_[a];){if(c.isEnabled()){this.setHighlighted(c);break}a+=b}}; -Blockly.Menu.prototype.handleMouseOver_=function(a){(a=this.getMenuItem_(a.target))&&(a.isEnabled()?this.highlightedItem_!=a&&this.setHighlighted(a):this.setHighlighted(null))};Blockly.Menu.prototype.handleClick_=function(a){var b=this.openingCoords;this.openingCoords=null;if(b&&"number"==typeof a.clientX){var c=new Blockly.utils.Coordinate(a.clientX,a.clientY);if(1>Blockly.utils.Coordinate.distance(b,c))return}(a=this.getMenuItem_(a.target))&&a.performAction()}; -Blockly.Menu.prototype.handleMouseEnter_=function(a){this.focus()};Blockly.Menu.prototype.handleMouseLeave_=function(a){this.getElement()&&(this.blur_(),this.setHighlighted(null))}; -Blockly.Menu.prototype.handleKeyEvent_=function(a){if(this.menuItems_.length&&!(a.shiftKey||a.ctrlKey||a.metaKey||a.altKey)){var b=this.highlightedItem_;switch(a.keyCode){case Blockly.utils.KeyCodes.ENTER:case Blockly.utils.KeyCodes.SPACE:b&&b.performAction();break;case Blockly.utils.KeyCodes.UP:this.highlightPrevious();break;case Blockly.utils.KeyCodes.DOWN:this.highlightNext();break;case Blockly.utils.KeyCodes.PAGE_UP:case Blockly.utils.KeyCodes.HOME:this.highlightFirst_();break;case Blockly.utils.KeyCodes.PAGE_DOWN:case Blockly.utils.KeyCodes.END:this.highlightLast_(); -break;default:return}a.preventDefault();a.stopPropagation()}};Blockly.Menu.prototype.getSize=function(){var a=this.getElement(),b=Blockly.utils.style.getSize(a);b.height=a.scrollHeight;return b};Blockly.MenuItem=function(a,b){this.content_=a;this.value_=b;this.enabled_=!0;this.element_=null;this.rightToLeft_=!1;this.roleName_=null;this.highlight_=this.checked_=this.checkable_=!1;this.actionHandler_=null}; -Blockly.MenuItem.prototype.createDom=function(){var a=document.createElement("div");a.id=Blockly.utils.IdGenerator.getNextUniqueId();this.element_=a;a.className="blocklyMenuItem goog-menuitem "+(this.enabled_?"":"blocklyMenuItemDisabled goog-menuitem-disabled ")+(this.checked_?"blocklyMenuItemSelected goog-option-selected ":"")+(this.highlight_?"blocklyMenuItemHighlight goog-menuitem-highlight ":"")+(this.rightToLeft_?"blocklyMenuItemRtl goog-menuitem-rtl ":"");var b=document.createElement("div"); -b.className="blocklyMenuItemContent goog-menuitem-content";if(this.checkable_){var c=document.createElement("div");c.className="blocklyMenuItemCheckbox goog-menuitem-checkbox";b.appendChild(c)}c=this.content_;"string"==typeof this.content_&&(c=document.createTextNode(this.content_));b.appendChild(c);a.appendChild(b);this.roleName_&&Blockly.utils.aria.setRole(a,this.roleName_);Blockly.utils.aria.setState(a,Blockly.utils.aria.State.SELECTED,this.checkable_&&this.checked_||!1);Blockly.utils.aria.setState(a, -Blockly.utils.aria.State.DISABLED,!this.enabled_);return a};Blockly.MenuItem.prototype.dispose=function(){this.element_=null};Blockly.MenuItem.prototype.getElement=function(){return this.element_};Blockly.MenuItem.prototype.getId=function(){return this.element_.id};Blockly.MenuItem.prototype.getValue=function(){return this.value_};Blockly.MenuItem.prototype.setRightToLeft=function(a){this.rightToLeft_=a};Blockly.MenuItem.prototype.setRole=function(a){this.roleName_=a}; -Blockly.MenuItem.prototype.setCheckable=function(a){this.checkable_=a};Blockly.MenuItem.prototype.setChecked=function(a){this.checked_=a};Blockly.MenuItem.prototype.setHighlighted=function(a){this.highlight_=a;var b=this.getElement();b&&this.isEnabled()&&(a?(Blockly.utils.dom.addClass(b,"blocklyMenuItemHighlight"),Blockly.utils.dom.addClass(b,"goog-menuitem-highlight")):(Blockly.utils.dom.removeClass(b,"blocklyMenuItemHighlight"),Blockly.utils.dom.removeClass(b,"goog-menuitem-highlight")))}; -Blockly.MenuItem.prototype.isEnabled=function(){return this.enabled_};Blockly.MenuItem.prototype.setEnabled=function(a){this.enabled_=a};Blockly.MenuItem.prototype.performAction=function(){this.isEnabled()&&this.actionHandler_&&this.actionHandler_(this)};Blockly.MenuItem.prototype.onAction=function(a,b){this.actionHandler_=a.bind(b)};Blockly.ContextMenu={};Blockly.ContextMenu.currentBlock=null;Blockly.ContextMenu.menu_=null;Blockly.ContextMenu.show=function(a,b,c){Blockly.WidgetDiv.show(Blockly.ContextMenu,c,Blockly.ContextMenu.dispose);if(b.length){var d=Blockly.ContextMenu.populate_(b,c);Blockly.ContextMenu.menu_=d;Blockly.ContextMenu.position_(d,a,c);setTimeout(function(){d.focus()},1);Blockly.ContextMenu.currentBlock=null}else Blockly.ContextMenu.hide()}; -Blockly.ContextMenu.populate_=function(a,b){var c=new Blockly.Menu;c.setRole(Blockly.utils.aria.Role.MENU);for(var d=0,e;e=a[d];d++){var f=new Blockly.MenuItem(e.text);f.setRightToLeft(b);f.setRole(Blockly.utils.aria.Role.MENUITEM);c.addChild(f);f.setEnabled(e.enabled);if(e.enabled)f.onAction(function(g){Blockly.ContextMenu.hide();this.callback(this.scope)},e)}return c}; -Blockly.ContextMenu.position_=function(a,b,c){var d=Blockly.utils.getViewportBBox();b=new Blockly.utils.Rect(b.clientY+d.top,b.clientY+d.top,b.clientX+d.left,b.clientX+d.left);Blockly.ContextMenu.createWidget_(a);var e=a.getSize();c&&(b.left+=e.width,b.right+=e.width,d.left+=e.width,d.right+=e.width);Blockly.WidgetDiv.positionWithAnchor(d,b,e,c);a.focus()}; -Blockly.ContextMenu.createWidget_=function(a){a.render(Blockly.WidgetDiv.DIV);var b=a.getElement();Blockly.utils.dom.addClass(b,"blocklyContextMenu");Blockly.bindEventWithChecks_(b,"contextmenu",null,Blockly.utils.noEvent);a.focus()};Blockly.ContextMenu.hide=function(){Blockly.WidgetDiv.hideIfOwner(Blockly.ContextMenu);Blockly.ContextMenu.currentBlock=null};Blockly.ContextMenu.dispose=function(){Blockly.ContextMenu.menu_&&(Blockly.ContextMenu.menu_.dispose(),Blockly.ContextMenu.menu_=null)}; -Blockly.ContextMenu.callbackFactory=function(a,b){return function(){Blockly.Events.disable();try{var c=Blockly.Xml.domToBlock(b,a.workspace),d=a.getRelativeToSurfaceXY();d.x=a.RTL?d.x-Blockly.SNAP_RADIUS:d.x+Blockly.SNAP_RADIUS;d.y+=2*Blockly.SNAP_RADIUS;c.moveBy(d.x,d.y)}finally{Blockly.Events.enable()}Blockly.Events.isEnabled()&&!c.isShadow()&&Blockly.Events.fire(new Blockly.Events.BlockCreate(c));c.select()}}; -Blockly.ContextMenu.commentDeleteOption=function(a){return{text:Blockly.Msg.REMOVE_COMMENT,enabled:!0,callback:function(){Blockly.Events.setGroup(!0);a.dispose(!0,!0);Blockly.Events.setGroup(!1)}}};Blockly.ContextMenu.commentDuplicateOption=function(a){return{text:Blockly.Msg.DUPLICATE_COMMENT,enabled:!0,callback:function(){Blockly.duplicate(a)}}}; -Blockly.ContextMenu.workspaceCommentOption=function(a,b){if(!Blockly.WorkspaceCommentSvg)throw Error("Missing require for Blockly.WorkspaceCommentSvg");var c={enabled:!Blockly.utils.userAgent.IE};c.text=Blockly.Msg.ADD_COMMENT;c.callback=function(){var d=new Blockly.WorkspaceCommentSvg(a,Blockly.Msg.WORKSPACE_COMMENT_DEFAULT_TEXT,Blockly.WorkspaceCommentSvg.DEFAULT_SIZE,Blockly.WorkspaceCommentSvg.DEFAULT_SIZE),e=a.getInjectionDiv().getBoundingClientRect();e=new Blockly.utils.Coordinate(b.clientX- -e.left,b.clientY-e.top);var f=a.getOriginOffsetInPixels();e=Blockly.utils.Coordinate.difference(e,f);e.scale(1/a.scale);d.moveBy(e.x,e.y);a.rendered&&(d.initSvg(),d.render(),d.select())};return c};Blockly.ContextMenuItems={};Blockly.ContextMenuItems.registerUndo=function(){Blockly.ContextMenuRegistry.registry.register({displayText:function(){return Blockly.Msg.UNDO},preconditionFn:function(a){return 0b.length?Blockly.ContextMenuItems.deleteNext_(b,c):Blockly.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.replace("%1",b.length),function(d){d&&Blockly.ContextMenuItems.deleteNext_(b,c)})}},scopeType:Blockly.ContextMenuRegistry.ScopeType.WORKSPACE,id:"workspaceDelete",weight:0})}; -Blockly.ContextMenuItems.registerWorkspaceOptions_=function(){Blockly.ContextMenuItems.registerUndo();Blockly.ContextMenuItems.registerRedo();Blockly.ContextMenuItems.registerCleanup();Blockly.ContextMenuItems.registerCollapse();Blockly.ContextMenuItems.registerExpand();Blockly.ContextMenuItems.registerDeleteAll()}; -Blockly.ContextMenuItems.registerDuplicate=function(){Blockly.ContextMenuRegistry.registry.register({displayText:function(){return Blockly.Msg.DUPLICATE_BLOCK},preconditionFn:function(a){a=a.block;return!a.isInFlyout&&a.isDeletable()&&a.isMovable()?a.isDuplicatable()?"enabled":"disabled":"hidden"},callback:function(a){a.block&&Blockly.duplicate(a.block)},scopeType:Blockly.ContextMenuRegistry.ScopeType.BLOCK,id:"blockDuplicate",weight:0})}; -Blockly.ContextMenuItems.registerComment=function(){Blockly.ContextMenuRegistry.registry.register({displayText:function(a){return a.block.getCommentIcon()?Blockly.Msg.REMOVE_COMMENT:Blockly.Msg.ADD_COMMENT},preconditionFn:function(a){a=a.block;return Blockly.utils.userAgent.IE||a.isInFlyout||!a.workspace.options.comments||a.isCollapsed()||!a.isEditable()?"hidden":"enabled"},callback:function(a){a=a.block;a.getCommentIcon()?a.setCommentText(null):a.setCommentText("")},scopeType:Blockly.ContextMenuRegistry.ScopeType.BLOCK, -id:"blockComment",weight:0})}; -Blockly.ContextMenuItems.registerInline=function(){Blockly.ContextMenuRegistry.registry.register({displayText:function(a){return a.block.getInputsInline()?Blockly.Msg.EXTERNAL_INPUTS:Blockly.Msg.INLINE_INPUTS},preconditionFn:function(a){a=a.block;if(!a.isInFlyout&&a.isMovable()&&!a.isCollapsed())for(var b=1;bb?!1:Blockly.RenderedConnection.superClass_.isConnectionAllowed.call(this,a)};Blockly.RenderedConnection.prototype.onFailedConnect=function(a){this.bumpAwayFrom(a)}; -Blockly.RenderedConnection.prototype.disconnectInternal_=function(a,b){Blockly.RenderedConnection.superClass_.disconnectInternal_.call(this,a,b);a.rendered&&a.render();b.rendered&&(b.updateDisabled(),b.render(),b.getSvgRoot().style.display="block")};Blockly.RenderedConnection.prototype.respawnShadow_=function(){Blockly.RenderedConnection.superClass_.respawnShadow_.call(this);var a=this.targetBlock();a&&(a.initSvg(),a.render(!1),a=this.getSourceBlock(),a.rendered&&a.render())}; -Blockly.RenderedConnection.prototype.neighbours=function(a){return this.dbOpposite_.getNeighbours(this,a)}; -Blockly.RenderedConnection.prototype.connect_=function(a){Blockly.RenderedConnection.superClass_.connect_.call(this,a);var b=this.getSourceBlock();a=a.getSourceBlock();var c=b.rendered,d=a.rendered;c&&b.updateDisabled();d&&a.updateDisabled();c&&d&&(this.type==Blockly.NEXT_STATEMENT||this.type==Blockly.PREVIOUS_STATEMENT?a.render():b.render());if(b=b.getInputWithBlock(a))b=b.isVisible(),a.getSvgRoot().style.display=b?"block":"none"}; -Blockly.RenderedConnection.prototype.onCheckChanged_=function(){!this.isConnected()||this.targetConnection&&this.getConnectionChecker().canConnect(this,this.targetConnection,!1)||((this.isSuperior()?this.targetBlock():this.sourceBlock_).unplug(),this.sourceBlock_.bumpNeighbours())};Blockly.Marker=function(){this.drawer_=this.curNode_=this.colour=null;this.type="marker"};Blockly.Marker.prototype.setDrawer=function(a){this.drawer_=a};Blockly.Marker.prototype.getDrawer=function(){return this.drawer_};Blockly.Marker.prototype.getCurNode=function(){return this.curNode_};Blockly.Marker.prototype.setCurNode=function(a){var b=this.curNode_;this.curNode_=a;this.drawer_&&this.drawer_.draw(b,this.curNode_)}; -Blockly.Marker.prototype.draw=function(){this.drawer_&&this.drawer_.draw(this.curNode_,this.curNode_)};Blockly.Marker.prototype.hide=function(){this.drawer_&&this.drawer_.hide()};Blockly.Marker.prototype.dispose=function(){this.getDrawer()&&this.getDrawer().dispose()};Blockly.Cursor=function(){Blockly.Cursor.superClass_.constructor.call(this);this.type="cursor"};Blockly.utils.object.inherits(Blockly.Cursor,Blockly.Marker);Blockly.Cursor.prototype.next=function(){var a=this.getCurNode();if(!a)return null;for(a=a.next();a&&a.next()&&(a.getType()==Blockly.ASTNode.types.NEXT||a.getType()==Blockly.ASTNode.types.BLOCK);)a=a.next();a&&this.setCurNode(a);return a}; -Blockly.Cursor.prototype.in=function(){var a=this.getCurNode();if(!a)return null;if(a.getType()==Blockly.ASTNode.types.PREVIOUS||a.getType()==Blockly.ASTNode.types.OUTPUT)a=a.next();(a=a.in())&&this.setCurNode(a);return a};Blockly.Cursor.prototype.prev=function(){var a=this.getCurNode();if(!a)return null;for(a=a.prev();a&&a.prev()&&(a.getType()==Blockly.ASTNode.types.NEXT||a.getType()==Blockly.ASTNode.types.BLOCK);)a=a.prev();a&&this.setCurNode(a);return a}; -Blockly.Cursor.prototype.out=function(){var a=this.getCurNode();if(!a)return null;(a=a.out())&&a.getType()==Blockly.ASTNode.types.BLOCK&&(a=a.prev()||a);a&&this.setCurNode(a);return a}; -Blockly.Cursor.prototype.onBlocklyAction=function(a){if(this.getCurNode()&&this.getCurNode().getType()===Blockly.ASTNode.types.FIELD&&this.getCurNode().getLocation().onBlocklyAction(a))return!0;switch(a.name){case Blockly.navigation.actionNames.PREVIOUS:return this.prev(),!0;case Blockly.navigation.actionNames.OUT:return this.out(),!0;case Blockly.navigation.actionNames.NEXT:return this.next(),!0;case Blockly.navigation.actionNames.IN:return this.in(),!0;default:return!1}};Blockly.BasicCursor=function(){Blockly.BasicCursor.superClass_.constructor.call(this)};Blockly.utils.object.inherits(Blockly.BasicCursor,Blockly.Cursor);Blockly.BasicCursor.prototype.next=function(){var a=this.getCurNode();if(!a)return null;(a=this.getNextNode_(a,this.validNode_))&&this.setCurNode(a);return a};Blockly.BasicCursor.prototype.in=function(){return this.next()}; -Blockly.BasicCursor.prototype.prev=function(){var a=this.getCurNode();if(!a)return null;(a=this.getPreviousNode_(a,this.validNode_))&&this.setCurNode(a);return a};Blockly.BasicCursor.prototype.out=function(){return this.prev()};Blockly.BasicCursor.prototype.getNextNode_=function(a,b){if(!a)return null;var c=a.in()||a.next();if(b(c))return c;if(c)return this.getNextNode_(c,b);a=this.findSiblingOrParent_(a.out());return b(a)?a:a?this.getNextNode_(a,b):null}; -Blockly.BasicCursor.prototype.getPreviousNode_=function(a,b){if(!a)return null;var c=a.prev();c=c?this.getRightMostChild_(c):a.out();return b(c)?c:c?this.getPreviousNode_(c,b):null};Blockly.BasicCursor.prototype.validNode_=function(a){var b=!1;a=a&&a.getType();if(a==Blockly.ASTNode.types.OUTPUT||a==Blockly.ASTNode.types.INPUT||a==Blockly.ASTNode.types.FIELD||a==Blockly.ASTNode.types.NEXT||a==Blockly.ASTNode.types.PREVIOUS||a==Blockly.ASTNode.types.WORKSPACE)b=!0;return b}; -Blockly.BasicCursor.prototype.findSiblingOrParent_=function(a){if(!a)return null;var b=a.next();return b?b:this.findSiblingOrParent_(a.out())};Blockly.BasicCursor.prototype.getRightMostChild_=function(a){if(!a.in())return a;for(a=a.in();a.next();)a=a.next();return this.getRightMostChild_(a)};Blockly.TabNavigateCursor=function(){Blockly.TabNavigateCursor.superClass_.constructor.call(this)};Blockly.utils.object.inherits(Blockly.TabNavigateCursor,Blockly.BasicCursor);Blockly.TabNavigateCursor.prototype.validNode_=function(a){var b=!1,c=a&&a.getType();a&&(a=a.getLocation(),c==Blockly.ASTNode.types.FIELD&&a&&a.isTabNavigable()&&a.isClickable()&&(b=!0));return b};Blockly.BlockSvg=function(a,b,c){this.svgGroup_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G,{},null);this.svgGroup_.translate_="";this.style=a.getRenderer().getConstants().getBlockStyle(null);this.pathObject=a.getRenderer().makePathObject(this.svgGroup_,this.style);this.renderIsInProgress_=this.rendered=!1;this.workspace=a;this.previousConnection=this.nextConnection=this.outputConnection=null;this.useDragSurface_=Blockly.utils.is3dSupported()&&!!a.getBlockDragSurface();var d=this.pathObject.svgPath; -d.tooltip=this;Blockly.Tooltip.bindMouseEvents(d);Blockly.BlockSvg.superClass_.constructor.call(this,a,b,c);this.svgGroup_.dataset&&(this.svgGroup_.dataset.id=this.id)};Blockly.utils.object.inherits(Blockly.BlockSvg,Blockly.Block);Blockly.BlockSvg.prototype.height=0;Blockly.BlockSvg.prototype.width=0;Blockly.BlockSvg.prototype.warningTextDb_=null;Blockly.BlockSvg.INLINE=-1;Blockly.BlockSvg.COLLAPSED_WARNING_ID="TEMP_COLLAPSED_WARNING_"; -Blockly.BlockSvg.prototype.initSvg=function(){if(!this.workspace.rendered)throw TypeError("Workspace is headless.");for(var a=0,b;b=this.inputList[a];a++)b.init();b=this.getIcons();for(a=0;a=this.connections_.length)return-1;b=a.y;for(var d=c;0<=d&&this.connections_[d].y==b;){if(this.connections_[d]==a)return d;d--}for(;ca)c=d;else{b=d;break}}return b};Blockly.ConnectionDB.prototype.removeConnection=function(a,b){a=this.findIndexOfConnection_(a,b);if(-1==a)throw Error("Unable to find connection in connectionDB.");this.connections_.splice(a,1)}; -Blockly.ConnectionDB.prototype.getNeighbours=function(a,b){function c(l){var m=e-d[l].x,n=f-d[l].y;Math.sqrt(m*m+n*n)<=b&&k.push(d[l]);return na)throw Error("Cannot unsubscribe a workspace that hasn't been subscribed.");this.subscribedWorkspaces_.splice(a,1)}; -Blockly.ThemeManager.prototype.subscribe=function(a,b,c){this.componentDB_[b]||(this.componentDB_[b]=[]);this.componentDB_[b].push({element:a,propertyName:c});b=this.theme_&&this.theme_.getComponentStyle(b);a.style[c]=b||""};Blockly.ThemeManager.prototype.unsubscribe=function(a){if(a)for(var b=Object.keys(this.componentDB_),c=0,d;d=b[c];c++){for(var e=this.componentDB_[d],f=e.length-1;0<=f;f--)e[f].element===a&&e.splice(f,1);this.componentDB_[d].length||delete this.componentDB_[d]}}; -Blockly.ThemeManager.prototype.dispose=function(){this.componentDB_=this.subscribedWorkspaces_=this.theme_=this.owner_=null};Blockly.TouchGesture=function(a,b){Blockly.TouchGesture.superClass_.constructor.call(this,a,b);this.isMultiTouch_=!1;this.cachedPoints_=Object.create(null);this.startDistance_=this.previousScale_=0;this.isPinchZoomEnabled_=this.onStartWrapper_=null};Blockly.utils.object.inherits(Blockly.TouchGesture,Blockly.Gesture);Blockly.TouchGesture.ZOOM_IN_MULTIPLIER=5;Blockly.TouchGesture.ZOOM_OUT_MULTIPLIER=6; -Blockly.TouchGesture.prototype.doStart=function(a){this.isPinchZoomEnabled_=this.startWorkspace_.options.zoomOptions&&this.startWorkspace_.options.zoomOptions.pinch;Blockly.TouchGesture.superClass_.doStart.call(this,a);!this.isEnding_&&Blockly.Touch.isTouchEvent(a)&&this.handleTouchStart(a)}; -Blockly.TouchGesture.prototype.bindMouseEvents=function(a){this.onStartWrapper_=Blockly.bindEventWithChecks_(document,"mousedown",null,this.handleStart.bind(this),!0);this.onMoveWrapper_=Blockly.bindEventWithChecks_(document,"mousemove",null,this.handleMove.bind(this),!0);this.onUpWrapper_=Blockly.bindEventWithChecks_(document,"mouseup",null,this.handleUp.bind(this),!0);a.preventDefault();a.stopPropagation()}; -Blockly.TouchGesture.prototype.handleStart=function(a){!this.isDragging()&&Blockly.Touch.isTouchEvent(a)&&(this.handleTouchStart(a),this.isMultiTouch()&&Blockly.longStop_())};Blockly.TouchGesture.prototype.handleMove=function(a){this.isDragging()?Blockly.Touch.shouldHandleEvent(a)&&Blockly.TouchGesture.superClass_.handleMove.call(this,a):this.isMultiTouch()?(Blockly.Touch.isTouchEvent(a)&&this.handleTouchMove(a),Blockly.longStop_()):Blockly.TouchGesture.superClass_.handleMove.call(this,a)}; -Blockly.TouchGesture.prototype.handleUp=function(a){Blockly.Touch.isTouchEvent(a)&&!this.isDragging()&&this.handleTouchEnd(a);!this.isMultiTouch()||this.isDragging()?Blockly.Touch.shouldHandleEvent(a)&&Blockly.TouchGesture.superClass_.handleUp.call(this,a):(a.preventDefault(),a.stopPropagation(),this.dispose())};Blockly.TouchGesture.prototype.isMultiTouch=function(){return this.isMultiTouch_}; -Blockly.TouchGesture.prototype.dispose=function(){Blockly.TouchGesture.superClass_.dispose.call(this);this.onStartWrapper_&&Blockly.unbindEvent_(this.onStartWrapper_)};Blockly.TouchGesture.prototype.handleTouchStart=function(a){var b=Blockly.Touch.getTouchIdentifierFromEvent(a);this.cachedPoints_[b]=this.getTouchPoint(a);b=Object.keys(this.cachedPoints_);2==b.length&&(this.startDistance_=Blockly.utils.Coordinate.distance(this.cachedPoints_[b[0]],this.cachedPoints_[b[1]]),this.isMultiTouch_=!0,a.preventDefault())}; -Blockly.TouchGesture.prototype.handleTouchMove=function(a){var b=Blockly.Touch.getTouchIdentifierFromEvent(a);this.cachedPoints_[b]=this.getTouchPoint(a);b=Object.keys(this.cachedPoints_);this.isPinchZoomEnabled_&&2===b.length?this.handlePinch_(a):Blockly.TouchGesture.superClass_.handleMove.call(this,a)}; -Blockly.TouchGesture.prototype.handlePinch_=function(a){var b=Object.keys(this.cachedPoints_);b=Blockly.utils.Coordinate.distance(this.cachedPoints_[b[0]],this.cachedPoints_[b[1]])/this.startDistance_;if(0this.previousScale_){var c=b-this.previousScale_;c=0Object.keys(this.cachedPoints_).length&&(this.cachedPoints_=Object.create(null),this.previousScale_=0)};Blockly.TouchGesture.prototype.getTouchPoint=function(a){return this.startWorkspace_?new Blockly.utils.Coordinate(a.pageX?a.pageX:a.changedTouches[0].pageX,a.pageY?a.pageY:a.changedTouches[0].pageY):null};Blockly.WorkspaceAudio=function(a){this.parentWorkspace_=a;this.SOUNDS_=Object.create(null)};Blockly.WorkspaceAudio.prototype.lastSound_=null;Blockly.WorkspaceAudio.prototype.dispose=function(){this.SOUNDS_=this.parentWorkspace_=null}; -Blockly.WorkspaceAudio.prototype.load=function(a,b){if(a.length){try{var c=new Blockly.utils.global.Audio}catch(h){return}for(var d,e=0;e=this.remainingCapacity()||(this.currentGesture_&&this.currentGesture_.cancel(),"comment"==a.tagName.toLowerCase()?this.pasteWorkspaceComment_(a):this.pasteBlock_(a))}; -Blockly.WorkspaceSvg.prototype.pasteBlock_=function(a){Blockly.Events.disable();try{var b=Blockly.Xml.domToBlock(a,this),c=this.getMarker(Blockly.navigation.MARKER_NAME).getCurNode();if(this.keyboardAccessibilityMode&&c&&c.isConnection()){var d=c.getLocation();Blockly.navigation.insertBlock(b,d);return}var e=parseInt(a.getAttribute("x"),10),f=parseInt(a.getAttribute("y"),10);if(!isNaN(e)&&!isNaN(f)){this.RTL&&(e=-e);do{a=!1;var g=this.getAllBlocks(!1);c=0;for(var h;h=g[c];c++){var k=h.getRelativeToSurfaceXY(); -if(1>=Math.abs(e-k.x)&&1>=Math.abs(f-k.y)){a=!0;break}}if(!a){var l=b.getConnections_(!1);c=0;for(var m;m=l[c];c++)if(m.closest(Blockly.SNAP_RADIUS,new Blockly.utils.Coordinate(e,f)).connection){a=!0;break}}a&&(e=this.RTL?e-Blockly.SNAP_RADIUS:e+Blockly.SNAP_RADIUS,f+=2*Blockly.SNAP_RADIUS)}while(a);b.moveBy(e,f)}}finally{Blockly.Events.enable()}Blockly.Events.isEnabled()&&!b.isShadow()&&Blockly.Events.fire(new Blockly.Events.BlockCreate(b));b.select()}; -Blockly.WorkspaceSvg.prototype.pasteWorkspaceComment_=function(a){Blockly.Events.disable();try{var b=Blockly.WorkspaceCommentSvg.fromXml(a,this),c=parseInt(a.getAttribute("x"),10),d=parseInt(a.getAttribute("y"),10);isNaN(c)||isNaN(d)||(this.RTL&&(c=-c),b.moveBy(c+50,d+50))}finally{Blockly.Events.enable()}Blockly.Events.isEnabled();b.select()}; -Blockly.WorkspaceSvg.prototype.refreshToolboxSelection=function(){var a=this.isFlyout?this.targetWorkspace:this;a&&!a.currentGesture_&&a.toolbox_&&a.toolbox_.getFlyout()&&a.toolbox_.refreshSelection()};Blockly.WorkspaceSvg.prototype.renameVariableById=function(a,b){Blockly.WorkspaceSvg.superClass_.renameVariableById.call(this,a,b);this.refreshToolboxSelection()};Blockly.WorkspaceSvg.prototype.deleteVariableById=function(a){Blockly.WorkspaceSvg.superClass_.deleteVariableById.call(this,a);this.refreshToolboxSelection()}; -Blockly.WorkspaceSvg.prototype.createVariable=function(a,b,c){a=Blockly.WorkspaceSvg.superClass_.createVariable.call(this,a,b,c);this.refreshToolboxSelection();return a};Blockly.WorkspaceSvg.prototype.recordDeleteAreas=function(){this.deleteAreaTrash_=this.trashcan&&this.svgGroup_.parentNode?this.trashcan.getClientRect():null;this.deleteAreaToolbox_=this.flyout_?this.flyout_.getClientRect():this.toolbox_&&"function"==typeof this.toolbox_.getClientRect?this.toolbox_.getClientRect():null}; -Blockly.WorkspaceSvg.prototype.isDeleteArea=function(a){return this.deleteAreaTrash_&&this.deleteAreaTrash_.contains(a.clientX,a.clientY)?Blockly.DELETE_AREA_TRASH:this.deleteAreaToolbox_&&this.deleteAreaToolbox_.contains(a.clientX,a.clientY)?Blockly.DELETE_AREA_TOOLBOX:Blockly.DELETE_AREA_NONE};Blockly.WorkspaceSvg.prototype.onMouseDown_=function(a){var b=this.getGesture(a);b&&b.handleWsStart(a,this)}; -Blockly.WorkspaceSvg.prototype.startDrag=function(a,b){a=Blockly.utils.mouseToSvg(a,this.getParentSvg(),this.getInverseScreenCTM());a.x/=this.scale;a.y/=this.scale;this.dragDeltaXY_=Blockly.utils.Coordinate.difference(b,a)};Blockly.WorkspaceSvg.prototype.moveDrag=function(a){a=Blockly.utils.mouseToSvg(a,this.getParentSvg(),this.getInverseScreenCTM());a.x/=this.scale;a.y/=this.scale;return Blockly.utils.Coordinate.sum(this.dragDeltaXY_,a)}; -Blockly.WorkspaceSvg.prototype.isDragging=function(){return null!=this.currentGesture_&&this.currentGesture_.isDragging()};Blockly.WorkspaceSvg.prototype.isDraggable=function(){return this.options.moveOptions&&this.options.moveOptions.drag}; -Blockly.WorkspaceSvg.prototype.isContentBounded=function(){return this.options.moveOptions&&this.options.moveOptions.scrollbars||this.options.moveOptions&&this.options.moveOptions.wheel||this.options.moveOptions&&this.options.moveOptions.drag||this.options.zoomOptions&&this.options.zoomOptions.controls||this.options.zoomOptions&&this.options.zoomOptions.wheel||this.options.zoomOptions&&this.options.zoomOptions.pinch}; -Blockly.WorkspaceSvg.prototype.isMovable=function(){return this.options.moveOptions&&this.options.moveOptions.scrollbars||this.options.moveOptions&&this.options.moveOptions.wheel||this.options.moveOptions&&this.options.moveOptions.drag||this.options.zoomOptions&&this.options.zoomOptions.wheel||this.options.zoomOptions&&this.options.zoomOptions.pinch}; -Blockly.WorkspaceSvg.prototype.onMouseWheel_=function(a){if(Blockly.Gesture.inProgress())a.preventDefault(),a.stopPropagation();else{var b=this.options.zoomOptions&&this.options.zoomOptions.wheel,c=this.options.moveOptions&&this.options.moveOptions.wheel;if(b||c){var d=Blockly.utils.getScrollDeltaPixels(a);!b||!a.ctrlKey&&c?(b=this.scrollX-d.x,c=this.scrollY-d.y,a.shiftKey&&!d.x&&(b=this.scrollX-d.y,c=this.scrollY),this.scroll(b,c)):(d=-d.y/50,b=Blockly.utils.mouseToSvg(a,this.getParentSvg(),this.getInverseScreenCTM()), -this.zoom(b.x,b.y,d));a.preventDefault()}}};Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox=function(){var a=this.getTopBoundedElements();if(!a.length)return new Blockly.utils.Rect(0,0,0,0);for(var b=a[0].getBoundingRectangle(),c=1;cb.bottom&&(b.bottom=d.bottom);d.leftb.right&&(b.right=d.right)}return b}; -Blockly.WorkspaceSvg.prototype.cleanUp=function(){this.setResizesEnabled(!1);Blockly.Events.setGroup(!0);for(var a=this.getTopBlocks(!0),b=0,c=0,d;d=a[c];c++)if(d.isMovable()){var e=d.getRelativeToSurfaceXY();d.moveBy(-e.x,b-e.y);d.snapToGrid();b=d.getRelativeToSurfaceXY().y+d.getHeightWidth().height+this.renderer_.getConstants().MIN_BLOCK_HEIGHT}Blockly.Events.setGroup(!1);this.setResizesEnabled(!0)}; -Blockly.WorkspaceSvg.prototype.showContextMenu=function(a){if(!this.options.readOnly&&!this.isFlyout){var b=Blockly.ContextMenuRegistry.registry.getContextMenuOptions(Blockly.ContextMenuRegistry.ScopeType.WORKSPACE,{workspace:this});this.configureContextMenu&&this.configureContextMenu(b,a);Blockly.ContextMenu.show(a,b,this.RTL)}}; -Blockly.WorkspaceSvg.prototype.updateToolbox=function(a){if(a=Blockly.utils.toolbox.convertToolboxDefToJson(a)){if(!this.options.languageTree)throw Error("Existing toolbox is null. Can't create new toolbox.");if(Blockly.utils.toolbox.hasCategories(a)){if(!this.toolbox_)throw Error("Existing toolbox has no categories. Can't change mode.");this.options.languageTree=a;this.toolbox_.render(a)}else{if(!this.flyout_)throw Error("Existing toolbox has categories. Can't change mode.");this.options.languageTree= -a;this.flyout_.show(a)}}else if(this.options.languageTree)throw Error("Can't nullify an existing toolbox.");};Blockly.WorkspaceSvg.prototype.markFocused=function(){this.options.parentWorkspace?this.options.parentWorkspace.markFocused():(Blockly.mainWorkspace=this,this.setBrowserFocus())};Blockly.WorkspaceSvg.prototype.setBrowserFocus=function(){document.activeElement&&document.activeElement.blur();try{this.getParentSvg().focus({preventScroll:!0})}catch(a){try{this.getParentSvg().parentNode.setActive()}catch(b){this.getParentSvg().parentNode.focus({preventScroll:!0})}}}; -Blockly.WorkspaceSvg.prototype.zoom=function(a,b,c){c=Math.pow(this.options.zoomOptions.scaleSpeed,c);var d=this.scale*c;if(this.scale!=d){d>this.options.zoomOptions.maxScale?c=this.options.zoomOptions.maxScale/this.scale:dthis.options.zoomOptions.maxScale?a=this.options.zoomOptions.maxScale:this.options.zoomOptions.minScale&&ag.viewBottom||g.contentLeftg.viewRight){h=null;f&&(h=Blockly.Events.getGroup(),Blockly.Events.setGroup(f.group));switch(f.type){case Blockly.Events.BLOCK_CREATE:case Blockly.Events.BLOCK_MOVE:var l= -e.getBlockById(f.blockId);l&&(l=l.getRootBlock());break;case Blockly.Events.COMMENT_CREATE:case Blockly.Events.COMMENT_MOVE:l=e.getCommentById(f.commentId)}if(l){k=l.getBoundingRectangle();k.height=k.bottom-k.top;k.width=k.right-k.left;var m=g.viewTop,n=g.viewBottom-k.height;n=Math.max(m,n);m=Blockly.utils.math.clamp(m,k.top,n)-k.top;n=g.viewLeft;var p=g.viewRight-k.width;g.RTL?n=Math.min(p,n):p=Math.max(n,p);g=Blockly.utils.math.clamp(n,k.left,p)-k.left;l.moveBy(g,m)}f&&(!f.group&&l&&console.warn("Moved object in bounds but there was no event group. This may break undo."), -null!==h&&Blockly.Events.setGroup(h))}}});Blockly.svgResize(e);Blockly.WidgetDiv.createDom();Blockly.DropDownDiv.createDom();Blockly.Tooltip.createDom();return e}; -Blockly.init_=function(a){var b=a.options,c=a.getParentSvg();Blockly.bindEventWithChecks_(c.parentNode,"contextmenu",null,function(e){Blockly.utils.isTargetInput(e)||e.preventDefault()});c=Blockly.bindEventWithChecks_(window,"resize",null,function(){Blockly.hideChaff(!0);Blockly.svgResize(a)});a.setResizeHandlerWrapper(c);Blockly.inject.bindDocumentEvents_();if(b.languageTree){c=a.getToolbox();var d=a.getFlyout(!0);c?c.init():d&&(d.init(a),d.show(b.languageTree),"function"==typeof d.scrollToStart&& -d.scrollToStart())}c=Blockly.Scrollbar.scrollbarThickness;b.hasTrashcan&&(c=a.trashcan.init(c));b.zoomOptions&&b.zoomOptions.controls&&a.zoomControls_.init(c);b.moveOptions&&b.moveOptions.scrollbars?(a.scrollbar=new Blockly.ScrollbarPair(a),a.scrollbar.resize()):a.setMetrics({x:.5,y:.5});b.hasSounds&&Blockly.inject.loadSounds_(b.pathToMedia,a)}; -Blockly.inject.bindDocumentEvents_=function(){Blockly.documentEventsBound_||(Blockly.bindEventWithChecks_(document,"scroll",null,function(){for(var a=Blockly.Workspace.getAll(),b=0,c;c=a[b];b++)c.updateInverseScreenCTM&&c.updateInverseScreenCTM()}),Blockly.bindEventWithChecks_(document,"keydown",null,Blockly.onKeyDown),Blockly.bindEvent_(document,"touchend",null,Blockly.longStop_),Blockly.bindEvent_(document,"touchcancel",null,Blockly.longStop_),Blockly.utils.userAgent.IPAD&&Blockly.bindEventWithChecks_(window, -"orientationchange",document,function(){Blockly.svgResize(Blockly.getMainWorkspace())}));Blockly.documentEventsBound_=!0}; -Blockly.inject.loadSounds_=function(a,b){var c=b.getAudioManager();c.load([a+"click.mp3",a+"click.wav",a+"click.ogg"],"click");c.load([a+"disconnect.wav",a+"disconnect.mp3",a+"disconnect.ogg"],"disconnect");c.load([a+"delete.mp3",a+"delete.ogg",a+"delete.wav"],"delete");var d=[];a=function(){for(;d.length;)Blockly.unbindEvent_(d.pop());c.preload()};d.push(Blockly.bindEventWithChecks_(document,"mousemove",null,a,!0));d.push(Blockly.bindEventWithChecks_(document,"touchstart",null,a,!0))};Blockly.Names=function(a,b){this.variablePrefix_=b||"";this.reservedDict_=Object.create(null);if(a)for(a=a.split(","),b=0;b1'),d.appendChild(c),b.push(d));if(Blockly.Blocks.variables_get){a.sort(Blockly.VariableModel.compareByName);c=0;for(var e;e=a[c];c++)d=Blockly.utils.xml.createElement("block"),d.setAttribute("type","variables_get"),d.setAttribute("gap",8),d.appendChild(Blockly.Variables.generateVariableFieldDom(e)),b.push(d)}}return b}; -Blockly.Variables.VAR_LETTER_OPTIONS="ijkmnopqrstuvwxyzabcdefgh";Blockly.Variables.generateUniqueName=function(a){return Blockly.Variables.generateUniqueNameFromOptions(Blockly.Variables.VAR_LETTER_OPTIONS.charAt(0),a.getAllVariableNames())}; -Blockly.Variables.generateUniqueNameFromOptions=function(a,b){if(!b.length)return a;for(var c=Blockly.Variables.VAR_LETTER_OPTIONS,d="",e=c.indexOf(a);;){for(var f=!1,g=0;ge?Blockly.WidgetDiv.positionInternal_(a,0,c.height+e):Blockly.WidgetDiv.positionInternal_(a,e,c.height)};Blockly.WidgetDiv.calculateX_=function(a,b,c,d){if(d)return b=Math.max(b.right-c.width,a.left),Math.min(b,a.right-c.width);b=Math.min(b.left,a.right-c.width);return Math.max(b,a.left)}; -Blockly.WidgetDiv.calculateY_=function(a,b,c){return b.bottom+c.height>=a.bottom?b.top-c.height:b.bottom};Blockly.VERSION="3.20200924.0";Blockly.mainWorkspace=null;Blockly.selected=null;Blockly.draggingConnections=[];Blockly.clipboardXml_=null;Blockly.clipboardSource_=null;Blockly.clipboardTypeCounts_=null;Blockly.cache3dSupported_=null;Blockly.parentContainer=null;Blockly.svgSize=function(a){return new Blockly.utils.Size(a.cachedWidth_,a.cachedHeight_)};Blockly.resizeSvgContents=function(a){a.resizeContents()}; -Blockly.svgResize=function(a){for(;a.options.parentWorkspace;)a=a.options.parentWorkspace;var b=a.getParentSvg(),c=b.parentNode;if(c){var d=c.offsetWidth;c=c.offsetHeight;b.cachedWidth_!=d&&(b.setAttribute("width",d+"px"),b.cachedWidth_=d);b.cachedHeight_!=c&&(b.setAttribute("height",c+"px"),b.cachedHeight_=c);a.resize()}}; -Blockly.onKeyDown=function(a){var b=Blockly.mainWorkspace;if(b&&!(Blockly.utils.isTargetInput(a)||b.rendered&&!b.isVisible()))if(b.options.readOnly)Blockly.navigation.onKeyPress(a);else{var c=!1;if(a.keyCode==Blockly.utils.KeyCodes.ESC)Blockly.hideChaff(),Blockly.navigation.onBlocklyAction(Blockly.navigation.ACTION_EXIT);else{if(!Blockly.Gesture.inProgress()&&Blockly.navigation.onKeyPress(a))return;if(a.keyCode==Blockly.utils.KeyCodes.BACKSPACE||a.keyCode==Blockly.utils.KeyCodes.DELETE){a.preventDefault(); -if(Blockly.Gesture.inProgress())return;Blockly.selected&&Blockly.selected.isDeletable()&&(c=!0)}else if(a.altKey||a.ctrlKey||a.metaKey){if(Blockly.Gesture.inProgress())return;Blockly.selected&&Blockly.selected.isDeletable()&&Blockly.selected.isMovable()&&(a.keyCode==Blockly.utils.KeyCodes.C?(Blockly.hideChaff(),Blockly.copy_(Blockly.selected)):a.keyCode!=Blockly.utils.KeyCodes.X||Blockly.selected.workspace.isFlyout||(Blockly.copy_(Blockly.selected),c=!0));a.keyCode==Blockly.utils.KeyCodes.V?Blockly.clipboardXml_&& -(a=Blockly.clipboardSource_,a.isFlyout&&(a=a.targetWorkspace),Blockly.clipboardTypeCounts_&&a.isCapacityAvailable(Blockly.clipboardTypeCounts_)&&(Blockly.Events.setGroup(!0),a.paste(Blockly.clipboardXml_),Blockly.Events.setGroup(!1))):a.keyCode==Blockly.utils.KeyCodes.Z?(Blockly.hideChaff(),b.undo(a.shiftKey)):a.ctrlKey&&a.keyCode==Blockly.utils.KeyCodes.Y&&(Blockly.hideChaff(),b.undo(!0))}}c&&!Blockly.selected.workspace.isFlyout&&(Blockly.Events.setGroup(!0),Blockly.hideChaff(),Blockly.selected.dispose(!0, -!0),Blockly.Events.setGroup(!1))}};Blockly.copy_=function(a){if(a=a.toCopyData())Blockly.clipboardXml_=a.xml,Blockly.clipboardSource_=a.source,Blockly.clipboardTypeCounts_=a.typeCounts};Blockly.duplicate=function(a){var b=Blockly.clipboardXml_,c=Blockly.clipboardSource_;Blockly.copy_(a);a.workspace.paste(Blockly.clipboardXml_);Blockly.clipboardXml_=b;Blockly.clipboardSource_=c};Blockly.onContextMenu_=function(a){Blockly.utils.isTargetInput(a)||a.preventDefault()}; -Blockly.hideChaff=function(a){Blockly.Tooltip.hide();Blockly.WidgetDiv.hide();Blockly.DropDownDiv.hideWithoutAnimation();a||(a=Blockly.getMainWorkspace(),a.trashcan&&a.trashcan.flyout&&a.trashcan.closeFlyout(),(a=a.getToolbox())&&a.getFlyout()&&a.getFlyout().autoClose&&a.clearSelection())};Blockly.getMainWorkspace=function(){return Blockly.mainWorkspace};Blockly.alert=function(a,b){alert(a);b&&b()};Blockly.confirm=function(a,b){b(confirm(a))};Blockly.prompt=function(a,b,c){c(prompt(a,b))}; -Blockly.jsonInitFactory_=function(a){return function(){this.jsonInit(a)}}; -Blockly.defineBlocksWithJsonArray=function(a){for(var b=0;b90-b||a>-90-b&&a<-90+b?!0:!1}; -Blockly.HorizontalFlyout.prototype.getClientRect=function(){if(!this.svgGroup_)return null;var a=this.svgGroup_.getBoundingClientRect(),b=a.top;return this.toolboxPosition_==Blockly.TOOLBOX_AT_TOP?new Blockly.utils.Rect(-1E9,b+a.height,-1E9,1E9):new Blockly.utils.Rect(b,1E9,-1E9,1E9)}; -Blockly.HorizontalFlyout.prototype.reflowInternal_=function(){this.workspace_.scale=this.targetWorkspace.scale;for(var a=0,b=this.workspace_.getTopBlocks(!1),c=0,d;d=b[c];c++)a=Math.max(a,d.getHeightWidth().height);a+=1.5*this.MARGIN;a*=this.workspace_.scale;a+=Blockly.Scrollbar.scrollbarThickness;if(this.height_!=a){for(c=0;d=b[c];c++)d.flyoutRect_&&this.moveRectToBlock_(d.flyoutRect_,d);this.targetWorkspace.toolboxPosition!=this.toolboxPosition_||this.toolboxPosition_!=Blockly.TOOLBOX_AT_TOP||this.targetWorkspace.getToolbox()|| -this.targetWorkspace.translate(0,this.targetWorkspace.scrollY+a);this.height_=a;this.position()}};Blockly.registry.register(Blockly.registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX,Blockly.registry.DEFAULT,Blockly.HorizontalFlyout);Blockly.VerticalFlyout=function(a){Blockly.VerticalFlyout.superClass_.constructor.call(this,a)};Blockly.utils.object.inherits(Blockly.VerticalFlyout,Blockly.Flyout);Blockly.VerticalFlyout.registryName="verticalFlyout"; -Blockly.VerticalFlyout.prototype.getMetrics_=function(){if(!this.isVisible())return null;try{var a=this.workspace_.getCanvas().getBBox()}catch(e){a={height:0,y:0,width:0,x:0}}var b=this.SCROLLBAR_PADDING,c=this.height_-2*this.SCROLLBAR_PADDING,d=this.width_;this.RTL||(d-=this.SCROLLBAR_PADDING);return{contentHeight:a.height*this.workspace_.scale+2*this.MARGIN,contentWidth:a.width*this.workspace_.scale+2*this.MARGIN,contentTop:a.y,contentLeft:a.x,viewHeight:c,viewWidth:d,viewTop:-this.workspace_.scrollY+ -a.y,viewLeft:-this.workspace_.scrollX,absoluteTop:b,absoluteLeft:0}};Blockly.VerticalFlyout.prototype.setMetrics_=function(a){var b=this.getMetrics_();b&&("number"==typeof a.y&&(this.workspace_.scrollY=-b.contentHeight*a.y),this.workspace_.translate(this.workspace_.scrollX+b.absoluteLeft,this.workspace_.scrollY+b.absoluteTop))}; -Blockly.VerticalFlyout.prototype.position=function(){if(this.isVisible()){var a=this.targetWorkspace.getMetrics();a&&(this.height_=a.viewHeight,this.setBackgroundPath_(this.width_-this.CORNER_RADIUS,a.viewHeight-2*this.CORNER_RADIUS),this.positionAt_(this.width_,this.height_,this.targetWorkspace.toolboxPosition==this.toolboxPosition_?a.toolboxWidth?this.toolboxPosition_==Blockly.TOOLBOX_AT_LEFT?a.toolboxWidth:a.viewWidth-this.width_:this.toolboxPosition_==Blockly.TOOLBOX_AT_LEFT?0:a.viewWidth:this.toolboxPosition_== -Blockly.TOOLBOX_AT_LEFT?0:a.viewWidth+a.absoluteLeft-this.width_,0))}}; -Blockly.VerticalFlyout.prototype.setBackgroundPath_=function(a,b){var c=this.toolboxPosition_==Blockly.TOOLBOX_AT_RIGHT,d=a+this.CORNER_RADIUS;d=["M "+(c?d:0)+",0"];d.push("h",c?-a:a);d.push("a",this.CORNER_RADIUS,this.CORNER_RADIUS,0,0,c?0:1,c?-this.CORNER_RADIUS:this.CORNER_RADIUS,this.CORNER_RADIUS);d.push("v",Math.max(0,b));d.push("a",this.CORNER_RADIUS,this.CORNER_RADIUS,0,0,c?0:1,c?this.CORNER_RADIUS:-this.CORNER_RADIUS,this.CORNER_RADIUS);d.push("h",c?a:-a);d.push("z");this.svgBackground_.setAttribute("d", -d.join(" "))};Blockly.VerticalFlyout.prototype.scrollToStart=function(){this.scrollbar.set(0)};Blockly.VerticalFlyout.prototype.wheel_=function(a){var b=Blockly.utils.getScrollDeltaPixels(a);if(b.y){var c=this.getMetrics_();b=c.viewTop-c.contentTop+b.y;b=Math.min(b,c.contentHeight-c.viewHeight);b=Math.max(b,0);this.scrollbar.set(b);Blockly.WidgetDiv.hide();Blockly.DropDownDiv.hideWithoutAnimation()}a.preventDefault();a.stopPropagation()}; -Blockly.VerticalFlyout.prototype.layout_=function(a,b){this.workspace_.scale=this.targetWorkspace.scale;for(var c=this.MARGIN,d=this.RTL?c:c+this.tabWidth_,e=0,f;f=a[e];e++)if("block"==f.type){f=f.block;for(var g=f.getDescendants(!1),h=0,k;k=g[h];h++)k.isInFlyout=!0;f.render();g=f.getSvgRoot();h=f.getHeightWidth();k=f.outputConnection?d-this.tabWidth_:d;f.moveBy(k,c);k=this.createRect_(f,this.RTL?k-h.width:k,c,h,e);this.addBlockListeners_(g,f,k);c+=h.height+b[e]}else"button"==f.type&&(this.initFlyoutButton_(f.button, -d,c),c+=f.button.height+b[e])};Blockly.VerticalFlyout.prototype.isDragTowardWorkspace=function(a){a=Math.atan2(a.y,a.x)/Math.PI*180;var b=this.dragAngleRange_;return a-b||a<-180+b||a>180-b?!0:!1};Blockly.VerticalFlyout.prototype.getClientRect=function(){if(!this.svgGroup_)return null;var a=this.svgGroup_.getBoundingClientRect(),b=a.left;return this.toolboxPosition_==Blockly.TOOLBOX_AT_LEFT?new Blockly.utils.Rect(-1E9,1E9,-1E9,b+a.width):new Blockly.utils.Rect(-1E9,1E9,b,1E9)}; -Blockly.VerticalFlyout.prototype.reflowInternal_=function(){this.workspace_.scale=this.targetWorkspace.scale;for(var a=0,b=this.workspace_.getTopBlocks(!1),c=0,d;d=b[c];c++){var e=d.getHeightWidth().width;d.outputConnection&&(e-=this.tabWidth_);a=Math.max(a,e)}for(c=0;d=this.buttons_[c];c++)a=Math.max(a,d.width);a+=1.5*this.MARGIN+this.tabWidth_;a*=this.workspace_.scale;a+=Blockly.Scrollbar.scrollbarThickness;if(this.width_!=a){for(c=0;d=b[c];c++){if(this.RTL){e=d.getRelativeToSurfaceXY().x;var f= -a/this.workspace_.scale-this.MARGIN;d.outputConnection||(f-=this.tabWidth_);d.moveBy(f-e,0)}d.flyoutRect_&&this.moveRectToBlock_(d.flyoutRect_,d)}if(this.RTL)for(c=0;d=this.buttons_[c];c++)b=d.getPosition().y,d.moveTo(a/this.workspace_.scale-d.width-this.MARGIN-this.tabWidth_,b);this.targetWorkspace.toolboxPosition!=this.toolboxPosition_||this.toolboxPosition_!=Blockly.TOOLBOX_AT_LEFT||this.targetWorkspace.getToolbox()||this.targetWorkspace.translate(this.targetWorkspace.scrollX+a,0);this.width_= -a;this.position()}};Blockly.registry.register(Blockly.registry.Type.FLYOUTS_VERTICAL_TOOLBOX,Blockly.registry.DEFAULT,Blockly.VerticalFlyout);Blockly.FlyoutButton=function(a,b,c,d){this.workspace_=a;this.targetWorkspace_=b;this.text_=c.text;this.position_=new Blockly.utils.Coordinate(0,0);this.isLabel_=d;this.callbackKey_=c.callbackKey||c.callbackkey;this.cssClass_=c["web-class"]||null;this.onMouseUpWrapper_=null;this.info=c};Blockly.FlyoutButton.MARGIN_X=5;Blockly.FlyoutButton.MARGIN_Y=2;Blockly.FlyoutButton.prototype.width=0;Blockly.FlyoutButton.prototype.height=0; -Blockly.FlyoutButton.prototype.createDom=function(){var a=this.isLabel_?"blocklyFlyoutLabel":"blocklyFlyoutButton";this.cssClass_&&(a+=" "+this.cssClass_);this.svgGroup_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G,{"class":a},this.workspace_.getCanvas());if(!this.isLabel_)var b=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.RECT,{"class":"blocklyFlyoutButtonShadow",rx:4,ry:4,x:1,y:1},this.svgGroup_);a=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.RECT,{"class":this.isLabel_? -"blocklyFlyoutLabelBackground":"blocklyFlyoutButtonBackground",rx:4,ry:4},this.svgGroup_);var c=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.TEXT,{"class":this.isLabel_?"blocklyFlyoutLabelText":"blocklyText",x:0,y:0,"text-anchor":"middle"},this.svgGroup_),d=Blockly.utils.replaceMessageReferences(this.text_);this.workspace_.RTL&&(d+="\u200f");c.textContent=d;this.isLabel_&&(this.svgText_=c,this.workspace_.getThemeManager().subscribe(this.svgText_,"flyoutForegroundColour","fill"));var e=Blockly.utils.style.getComputedStyle(c, -"fontSize"),f=Blockly.utils.style.getComputedStyle(c,"fontWeight"),g=Blockly.utils.style.getComputedStyle(c,"fontFamily");this.width=Blockly.utils.dom.getFastTextWidthWithSizeString(c,e,f,g);d=Blockly.utils.dom.measureFontMetrics(d,e,f,g);this.height=d.height;this.isLabel_||(this.width+=2*Blockly.FlyoutButton.MARGIN_X,this.height+=2*Blockly.FlyoutButton.MARGIN_Y,b.setAttribute("width",this.width),b.setAttribute("height",this.height));a.setAttribute("width",this.width);a.setAttribute("height",this.height); -c.setAttribute("x",this.width/2);c.setAttribute("y",this.height/2-d.height/2+d.baseline);this.updateTransform_();this.onMouseUpWrapper_=Blockly.bindEventWithChecks_(this.svgGroup_,"mouseup",this,this.onMouseUp_);return this.svgGroup_};Blockly.FlyoutButton.prototype.show=function(){this.updateTransform_();this.svgGroup_.setAttribute("display","block")}; -Blockly.FlyoutButton.prototype.updateTransform_=function(){this.svgGroup_.setAttribute("transform","translate("+this.position_.x+","+this.position_.y+")")};Blockly.FlyoutButton.prototype.moveTo=function(a,b){this.position_.x=a;this.position_.y=b;this.updateTransform_()};Blockly.FlyoutButton.prototype.isLabel=function(){return this.isLabel_};Blockly.FlyoutButton.prototype.getPosition=function(){return this.position_};Blockly.FlyoutButton.prototype.getButtonText=function(){return this.text_}; -Blockly.FlyoutButton.prototype.getTargetWorkspace=function(){return this.targetWorkspace_};Blockly.FlyoutButton.prototype.dispose=function(){this.onMouseUpWrapper_&&Blockly.unbindEvent_(this.onMouseUpWrapper_);this.svgGroup_&&Blockly.utils.dom.removeNode(this.svgGroup_);this.svgText_&&this.workspace_.getThemeManager().unsubscribe(this.svgText_)}; -Blockly.FlyoutButton.prototype.onMouseUp_=function(a){(a=this.targetWorkspace_.getGesture(a))&&a.cancel();this.isLabel_&&this.callbackKey_?console.warn("Labels should not have callbacks. Label text: "+this.text_):this.isLabel_||this.callbackKey_&&this.targetWorkspace_.getButtonCallback(this.callbackKey_)?this.isLabel_||this.targetWorkspace_.getButtonCallback(this.callbackKey_)(this):console.warn("Buttons should have callbacks. Button text: "+this.text_)};Blockly.Css.register(".blocklyFlyoutButton {,fill: #888;,cursor: default;,},.blocklyFlyoutButtonShadow {,fill: #666;,},.blocklyFlyoutButton:hover {,fill: #aaa;,},.blocklyFlyoutLabel {,cursor: default;,},.blocklyFlyoutLabelBackground {,opacity: 0;,}".split(","));Blockly.Generator=function(a){this.name_=a;this.FUNCTION_NAME_PLACEHOLDER_REGEXP_=new RegExp(this.FUNCTION_NAME_PLACEHOLDER_,"g")};Blockly.Generator.NAME_TYPE="generated_function";Blockly.Generator.prototype.INFINITE_LOOP_TRAP=null;Blockly.Generator.prototype.STATEMENT_PREFIX=null;Blockly.Generator.prototype.STATEMENT_SUFFIX=null;Blockly.Generator.prototype.INDENT=" ";Blockly.Generator.prototype.COMMENT_WRAP=60;Blockly.Generator.prototype.ORDER_OVERRIDES=[]; -Blockly.Generator.prototype.workspaceToCode=function(a){a||(console.warn("No workspace specified in workspaceToCode call. Guessing."),a=Blockly.getMainWorkspace());var b=[];this.init(a);a=a.getTopBlocks(!0);for(var c=0,d;d=a[c];c++){var e=this.blockToCode(d);Array.isArray(e)&&(e=e[0]);e&&(d.outputConnection&&(e=this.scrubNakedValue(e),this.STATEMENT_PREFIX&&!d.suppressPrefixSuffix&&(e=this.injectId(this.STATEMENT_PREFIX,d)+e),this.STATEMENT_SUFFIX&&!d.suppressPrefixSuffix&&(e+=this.injectId(this.STATEMENT_SUFFIX, -d))),b.push(e))}b=b.join("\n");b=this.finish(b);b=b.replace(/^\s+\n/,"");b=b.replace(/\n\s+$/,"\n");return b=b.replace(/[ \t]+\n/g,"\n")};Blockly.Generator.prototype.prefixLines=function(a,b){return b+a.replace(/(?!\n$)\n/g,"\n"+b)};Blockly.Generator.prototype.allNestedComments=function(a){var b=[];a=a.getDescendants(!0);for(var c=0;c>>/sprites.png);","height: 16px;","vertical-align: middle;","visibility: hidden;","width: 16px;","}",".blocklyTreeIconClosed {","background-position: -32px -1px;","}",'.blocklyToolboxDiv[dir="RTL"] .blocklyTreeIconClosed {',"background-position: 0 -1px;","}",".blocklyTreeSelected>.blocklyTreeIconClosed {","background-position: -32px -17px;","}",'.blocklyToolboxDiv[dir="RTL"] .blocklyTreeSelected>.blocklyTreeIconClosed {', -"background-position: 0 -17px;","}",".blocklyTreeIconOpen {","background-position: -16px -1px;","}",".blocklyTreeSelected>.blocklyTreeIconOpen {","background-position: -16px -17px;","}",".blocklyTreeLabel {","cursor: default;","font: 16px sans-serif;","padding: 0 3px;","vertical-align: middle;","}",".blocklyToolboxDelete .blocklyTreeLabel {",'cursor: url("<<>>/handdelete.cur"), auto;',"}",".blocklyTreeSelected .blocklyTreeLabel {","color: #fff;","}"]); -Blockly.registry.register(Blockly.registry.Type.TOOLBOX_ITEM,Blockly.ToolboxCategory.registrationName,Blockly.ToolboxCategory);Blockly.ToolboxSeparator=function(a,b){Blockly.ToolboxSeparator.superClass_.constructor.call(this,a,b);this.cssConfig_={container:"blocklyTreeSeparator"};Blockly.utils.object.mixin(this.cssConfig_,a.cssconfig||a.cssConfig)};Blockly.utils.object.inherits(Blockly.ToolboxSeparator,Blockly.ToolboxItem);Blockly.ToolboxSeparator.registrationName="sep";Blockly.ToolboxSeparator.prototype.init=function(){this.createDom_()}; -Blockly.ToolboxSeparator.prototype.createDom_=function(){var a=document.createElement("div");Blockly.utils.dom.addClass(a,this.cssConfig_.container);return this.htmlDiv_=a};Blockly.ToolboxSeparator.prototype.getDiv=function(){return this.htmlDiv_};Blockly.ToolboxSeparator.prototype.dispose=function(){Blockly.utils.dom.removeNode(this.htmlDiv_)};Blockly.Css.register('.blocklyTreeSeparator {,border-bottom: solid #e5e5e5 1px;,height: 0;,margin: 5px 0;,},.blocklyToolboxDiv[layout="h"] .blocklyTreeSeparator {,border-right: solid #e5e5e5 1px;,border-bottom: none;,height: auto;,margin: 0 5px 0 5px;,padding: 5px 0;,width: 0;,}'.split(",")); -Blockly.registry.register(Blockly.registry.Type.TOOLBOX_ITEM,Blockly.ToolboxSeparator.registrationName,Blockly.ToolboxSeparator);Blockly.CollapsibleToolboxCategory=function(a,b,c){this.subcategoriesDiv_=null;this.expanded_=!1;this.toolboxItems_=[];Blockly.CollapsibleToolboxCategory.superClass_.constructor.call(this,a,b,c)};Blockly.utils.object.inherits(Blockly.CollapsibleToolboxCategory,Blockly.ToolboxCategory);Blockly.CollapsibleToolboxCategory.registrationName="collapsibleCategory"; -Blockly.CollapsibleToolboxCategory.prototype.makeDefaultCssConfig_=function(){var a=Blockly.CollapsibleToolboxCategory.superClass_.makeDefaultCssConfig_.call(this);a.contents="blocklyToolboxContents";return a}; -Blockly.CollapsibleToolboxCategory.prototype.parseContents_=function(a){var b=a.contents,c=!0;if(a.custom)this.flyoutItems_=a.custom;else if(b){a=0;for(var d;d=b[a];a++)!Blockly.registry.hasItem(Blockly.registry.Type.TOOLBOX_ITEM,d.kind)||d.kind.toLowerCase()==Blockly.ToolboxSeparator.registrationName&&c?(this.flyoutItems_.push(d),c=!0):(this.createToolboxItem_(d),c=!1)}}; -Blockly.CollapsibleToolboxCategory.prototype.createToolboxItem_=function(a){var b=a.kind;"CATEGORY"==b.toUpperCase()&&Blockly.utils.toolbox.isCategoryCollapsible(a)&&(b=Blockly.CollapsibleToolboxCategory.registrationName);a=new (Blockly.registry.getClass(Blockly.registry.Type.TOOLBOX_ITEM,b))(a,this.parentToolbox_,this);this.toolboxItems_.push(a)}; -Blockly.CollapsibleToolboxCategory.prototype.init=function(){Blockly.CollapsibleToolboxCategory.superClass_.init.call(this);this.setExpanded("true"==this.toolboxItemDef_.expanded||this.toolboxItemDef_.expanded)}; -Blockly.CollapsibleToolboxCategory.prototype.createDom_=function(){Blockly.CollapsibleToolboxCategory.superClass_.createDom_.call(this);var a=this.getChildToolboxItems();this.subcategoriesDiv_=this.createSubCategoriesDom_(a);Blockly.utils.aria.setRole(this.subcategoriesDiv_,Blockly.utils.aria.Role.GROUP);this.htmlDiv_.appendChild(this.subcategoriesDiv_);return this.htmlDiv_}; -Blockly.CollapsibleToolboxCategory.prototype.createIconDom_=function(){var a=document.createElement("span");this.parentToolbox_.isHorizontal()||(Blockly.utils.dom.addClass(a,this.cssConfig_.icon),a.style.visibility="visible");a.style.display="inline-block";return a};Blockly.CollapsibleToolboxCategory.prototype.createSubCategoriesDom_=function(a){var b=document.createElement("div");Blockly.utils.dom.addClass(b,this.cssConfig_.contents);for(var c=0;c>>/handdelete.cur"), auto;',"}",".blocklyToolboxGrab {",'cursor: url("<<>>/handclosed.cur"), auto;',"cursor: grabbing;","cursor: -webkit-grabbing;","}",".blocklyToolboxDiv {","background-color: #ddd;","overflow-x: visible;","overflow-y: auto;","padding: 4px 0 4px 0;","position: absolute;","z-index: 70;","-webkit-tap-highlight-color: transparent;","}",".blocklyToolboxContents {","display: flex;","flex-wrap: wrap;","flex-direction: column;", -"}",".blocklyToolboxContents:focus {","outline: none;","}"]);Blockly.registry.register(Blockly.registry.Type.TOOLBOX,Blockly.registry.DEFAULT,Blockly.Toolbox);Blockly.Trashcan=function(a){this.workspace_=a;this.contents_=[];this.flyout=null;if(!(0>=this.workspace_.options.maxTrashcanContents)){a=new Blockly.Options({scrollbars:!0,parentWorkspace:this.workspace_,rtl:this.workspace_.RTL,oneBasedIndex:this.workspace_.options.oneBasedIndex,renderer:this.workspace_.options.renderer,rendererOverrides:this.workspace_.options.rendererOverrides});if(this.workspace_.horizontalLayout){a.toolboxPosition=this.workspace_.toolboxPosition==Blockly.utils.toolbox.Position.TOP? -Blockly.utils.toolbox.Position.BOTTOM:Blockly.utils.toolbox.Position.TOP;if(!Blockly.HorizontalFlyout)throw Error("Missing require for Blockly.HorizontalFlyout");this.flyout=new Blockly.HorizontalFlyout(a)}else{a.toolboxPosition=this.workspace_.toolboxPosition==Blockly.utils.toolbox.Position.RIGHT?Blockly.utils.toolbox.Position.LEFT:Blockly.utils.toolbox.Position.RIGHT;if(!Blockly.VerticalFlyout)throw Error("Missing require for Blockly.VerticalFlyout");this.flyout=new Blockly.VerticalFlyout(a)}this.workspace_.addChangeListener(this.onDelete_.bind(this))}}; -Blockly.Trashcan.prototype.WIDTH_=47;Blockly.Trashcan.prototype.BODY_HEIGHT_=44;Blockly.Trashcan.prototype.LID_HEIGHT_=16;Blockly.Trashcan.prototype.MARGIN_BOTTOM_=20;Blockly.Trashcan.prototype.MARGIN_SIDE_=20;Blockly.Trashcan.prototype.MARGIN_HOTSPOT_=10;Blockly.Trashcan.prototype.SPRITE_LEFT_=0;Blockly.Trashcan.prototype.SPRITE_TOP_=32;Blockly.Trashcan.prototype.HAS_BLOCKS_LID_ANGLE_=.1;Blockly.Trashcan.ANIMATION_LENGTH_=80;Blockly.Trashcan.ANIMATION_FRAMES_=4;Blockly.Trashcan.OPACITY_MIN_=.4; -Blockly.Trashcan.OPACITY_MAX_=.8;Blockly.Trashcan.MAX_LID_ANGLE_=45;Blockly.Trashcan.prototype.isLidOpen=!1;Blockly.Trashcan.prototype.minOpenness_=0;Blockly.Trashcan.prototype.svgGroup_=null;Blockly.Trashcan.prototype.svgLid_=null;Blockly.Trashcan.prototype.lidTask_=0;Blockly.Trashcan.prototype.lidOpen_=0;Blockly.Trashcan.prototype.left_=0;Blockly.Trashcan.prototype.top_=0; -Blockly.Trashcan.prototype.createDom=function(){this.svgGroup_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G,{"class":"blocklyTrash"},null);var a=String(Math.random()).substring(2);var b=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.CLIPPATH,{id:"blocklyTrashBodyClipPath"+a},this.svgGroup_);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.RECT,{width:this.WIDTH_,height:this.BODY_HEIGHT_,y:this.LID_HEIGHT_},b);var c=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.IMAGE,{width:Blockly.SPRITE.width, -x:-this.SPRITE_LEFT_,height:Blockly.SPRITE.height,y:-this.SPRITE_TOP_,"clip-path":"url(#blocklyTrashBodyClipPath"+a+")"},this.svgGroup_);c.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",this.workspace_.options.pathToMedia+Blockly.SPRITE.url);b=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.CLIPPATH,{id:"blocklyTrashLidClipPath"+a},this.svgGroup_);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.RECT,{width:this.WIDTH_,height:this.LID_HEIGHT_},b);this.svgLid_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.IMAGE, -{width:Blockly.SPRITE.width,x:-this.SPRITE_LEFT_,height:Blockly.SPRITE.height,y:-this.SPRITE_TOP_,"clip-path":"url(#blocklyTrashLidClipPath"+a+")"},this.svgGroup_);this.svgLid_.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",this.workspace_.options.pathToMedia+Blockly.SPRITE.url);Blockly.bindEventWithChecks_(this.svgGroup_,"mouseup",this,this.click);Blockly.bindEvent_(c,"mouseover",this,this.mouseOver_);Blockly.bindEvent_(c,"mouseout",this,this.mouseOut_);this.animateLid_();return this.svgGroup_}; -Blockly.Trashcan.prototype.init=function(a){0this.minOpenness_&&1>this.lidOpen_&&(this.lidTask_=setTimeout(this.animateLid_.bind(this),Blockly.Trashcan.ANIMATION_LENGTH_/ -a))};Blockly.Trashcan.prototype.setLidAngle_=function(a){var b=this.workspace_.toolboxPosition==Blockly.TOOLBOX_AT_RIGHT||this.workspace_.horizontalLayout&&this.workspace_.RTL;this.svgLid_.setAttribute("transform","rotate("+(b?-a:a)+","+(b?4:this.WIDTH_-4)+","+(this.LID_HEIGHT_-2)+")")};Blockly.Trashcan.prototype.setMinOpenness_=function(a){this.minOpenness_=a;this.isLidOpen||this.setLidAngle_(a*Blockly.Trashcan.MAX_LID_ANGLE_)};Blockly.Trashcan.prototype.closeLid=function(){this.setLidOpen(!1)}; -Blockly.Trashcan.prototype.click=function(){this.hasContents_()&&this.openFlyout()};Blockly.Trashcan.prototype.fireUiEvent_=function(a){a=new Blockly.Events.Ui(null,"trashcanOpen",null,a);a.workspaceId=this.workspace_.id;Blockly.Events.fire(a)};Blockly.Trashcan.prototype.mouseOver_=function(){this.hasContents_()&&this.setLidOpen(!0)};Blockly.Trashcan.prototype.mouseOut_=function(){this.setLidOpen(!1)}; -Blockly.Trashcan.prototype.onDelete_=function(a){if(!(0>=this.workspace_.options.maxTrashcanContents)&&a.type==Blockly.Events.BLOCK_DELETE&&a.oldXml.tagName&&"shadow"!=a.oldXml.tagName.toLowerCase()&&(a=this.cleanBlockXML_(a.oldXml),-1==this.contents_.indexOf(a))){for(this.contents_.unshift(a);this.contents_.length>this.workspace_.options.maxTrashcanContents;)this.contents_.pop();this.setMinOpenness_(this.HAS_BLOCKS_LID_ANGLE_)}}; -Blockly.Trashcan.prototype.cleanBlockXML_=function(a){for(var b=a=a.cloneNode(!0);b;){b.removeAttribute&&(b.removeAttribute("x"),b.removeAttribute("y"),b.removeAttribute("id"),b.removeAttribute("disabled"),"comment"==b.nodeName&&(b.removeAttribute("h"),b.removeAttribute("w"),b.removeAttribute("pinned")));var c=b.firstChild||b.nextSibling;if(!c)for(c=b.parentNode;c;){if(c.nextSibling){c=c.nextSibling;break}c=c.parentNode}b=c}return Blockly.Xml.domToText(a)};Blockly.VariablesDynamic={};Blockly.VariablesDynamic.onCreateVariableButtonClick_String=function(a){Blockly.Variables.createVariableButtonHandler(a.getTargetWorkspace(),void 0,"String")};Blockly.VariablesDynamic.onCreateVariableButtonClick_Number=function(a){Blockly.Variables.createVariableButtonHandler(a.getTargetWorkspace(),void 0,"Number")};Blockly.VariablesDynamic.onCreateVariableButtonClick_Colour=function(a){Blockly.Variables.createVariableButtonHandler(a.getTargetWorkspace(),void 0,"Colour")}; -Blockly.VariablesDynamic.flyoutCategory=function(a){var b=[],c=document.createElement("button");c.setAttribute("text",Blockly.Msg.NEW_STRING_VARIABLE);c.setAttribute("callbackKey","CREATE_VARIABLE_STRING");b.push(c);c=document.createElement("button");c.setAttribute("text",Blockly.Msg.NEW_NUMBER_VARIABLE);c.setAttribute("callbackKey","CREATE_VARIABLE_NUMBER");b.push(c);c=document.createElement("button");c.setAttribute("text",Blockly.Msg.NEW_COLOUR_VARIABLE);c.setAttribute("callbackKey","CREATE_VARIABLE_COLOUR"); -b.push(c);a.registerButtonCallback("CREATE_VARIABLE_STRING",Blockly.VariablesDynamic.onCreateVariableButtonClick_String);a.registerButtonCallback("CREATE_VARIABLE_NUMBER",Blockly.VariablesDynamic.onCreateVariableButtonClick_Number);a.registerButtonCallback("CREATE_VARIABLE_COLOUR",Blockly.VariablesDynamic.onCreateVariableButtonClick_Colour);a=Blockly.VariablesDynamic.flyoutCategoryBlocks(a);return b=b.concat(a)}; -Blockly.VariablesDynamic.flyoutCategoryBlocks=function(a){a=a.getAllVariables();var b=[];if(0image, .blocklyZoom>svg>image {","opacity: .4;","}",".blocklyZoom>image:hover, .blocklyZoom>svg>image:hover {","opacity: .6;","}",".blocklyZoom>image:active, .blocklyZoom>svg>image:active {","opacity: .8;","}"]);Blockly.Mutator=function(a){Blockly.Mutator.superClass_.constructor.call(this,null);this.quarkNames_=a};Blockly.utils.object.inherits(Blockly.Mutator,Blockly.Icon);Blockly.Mutator.prototype.workspaceWidth_=0;Blockly.Mutator.prototype.workspaceHeight_=0;Blockly.Mutator.prototype.setBlock=function(a){this.block_=a};Blockly.Mutator.prototype.getWorkspace=function(){return this.workspace_}; -Blockly.Mutator.prototype.drawIcon_=function(a){Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.RECT,{"class":"blocklyIconShape",rx:"4",ry:"4",height:"16",width:"16"},a);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.PATH,{"class":"blocklyIconSymbol",d:"m4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 0.41,0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 1.8,0 0.3,-0.299 0.127,-1.138 1.185,-0.682 1.046,0.458 0.409,-0.11 0.9,-1.559 -0.11,-0.41 -0.92,-0.677 0,-1.366 0.92,-0.677 0.11,-0.41 -0.9,-1.559 -0.409,-0.109 -1.046,0.458 -1.185,-0.682 -0.127,-1.138 -0.3,-0.299 -1.8,0 -0.3,0.3 -0.126,1.135 -1.187,0.682 -1.043,-0.457 -0.41,0.11 -0.899,1.559 0.108,0.409z"}, -a);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.CIRCLE,{"class":"blocklyIconShape",r:"2.7",cx:"8",cy:"8"},a)};Blockly.Mutator.prototype.iconClick_=function(a){this.block_.isEditable()&&Blockly.Icon.prototype.iconClick_.call(this,a)}; -Blockly.Mutator.prototype.createEditor_=function(){this.svgDialog_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.SVG,{x:Blockly.Bubble.BORDER_WIDTH,y:Blockly.Bubble.BORDER_WIDTH},null);if(this.quarkNames_.length)for(var a=Blockly.utils.xml.createElement("xml"),b=0,c;c=this.quarkNames_[b];b++){var d=Blockly.utils.xml.createElement("block");d.setAttribute("type",c);a.appendChild(d)}else a=null;b=new Blockly.Options({disable:!1,parentWorkspace:this.block_.workspace,media:this.block_.workspace.options.pathToMedia, -rtl:this.block_.RTL,horizontalLayout:!1,renderer:this.block_.workspace.options.renderer,rendererOverrides:this.block_.workspace.options.rendererOverrides});b.toolboxPosition=this.block_.RTL?Blockly.utils.toolbox.Position.RIGHT:Blockly.utils.toolbox.Position.LEFT;if(c=!!a)b.languageTree=Blockly.utils.toolbox.convertToolboxDefToJson(a),b.getMetrics=this.getFlyoutMetrics_.bind(this);this.workspace_=new Blockly.WorkspaceSvg(b);this.workspace_.isMutator=!0;this.workspace_.addChangeListener(Blockly.Events.disableOrphans); -a=c?this.workspace_.addFlyout(Blockly.utils.Svg.G):null;b=this.workspace_.createDom("blocklyMutatorBackground");a&&b.insertBefore(a,this.workspace_.svgBlockCanvas_);this.svgDialog_.appendChild(b);return this.svgDialog_}; -Blockly.Mutator.prototype.updateEditable=function(){Blockly.Mutator.superClass_.updateEditable.call(this);this.block_.isInFlyout||(this.block_.isEditable()?this.iconGroup_&&Blockly.utils.dom.removeClass(this.iconGroup_,"blocklyIconGroupReadonly"):(this.setVisible(!1),this.iconGroup_&&Blockly.utils.dom.addClass(this.iconGroup_,"blocklyIconGroupReadonly")))}; -Blockly.Mutator.prototype.resizeBubble_=function(){var a=2*Blockly.Bubble.BORDER_WIDTH,b=this.workspace_.getCanvas().getBBox(),c=b.width+b.x,d=b.height+3*a,e=this.workspace_.getFlyout();if(e){var f=e.getMetrics_();d=Math.max(d,f.contentHeight+20);c+=e.getWidth()}this.block_.RTL&&(c=-b.x);c+=3*a;if(Math.abs(this.workspaceWidth_-c)>a||Math.abs(this.workspaceHeight_-d)>a)this.workspaceWidth_=c,this.workspaceHeight_=d,this.bubble_.setBubbleSize(c+a,d+a),this.svgDialog_.setAttribute("width",this.workspaceWidth_), -this.svgDialog_.setAttribute("height",this.workspaceHeight_);this.block_.RTL&&(a="translate("+this.workspaceWidth_+",0)",this.workspace_.getCanvas().setAttribute("transform",a));this.workspace_.resize()};Blockly.Mutator.prototype.onBubbleMove_=function(){this.workspace_&&this.workspace_.recordDeleteAreas()}; -Blockly.Mutator.prototype.setVisible=function(a){if(a!=this.isVisible())if(Blockly.Events.fire(new Blockly.Events.Ui(this.block_,"mutatorOpen",!a,a)),a){this.bubble_=new Blockly.Bubble(this.block_.workspace,this.createEditor_(),this.block_.pathObject.svgPath,this.iconXY_,null,null);this.bubble_.setSvgId(this.block_.id);this.bubble_.registerMoveEvent(this.onBubbleMove_.bind(this));var b=this.workspace_.options.languageTree;a=this.workspace_.getFlyout();b&&(a.init(this.workspace_),a.show(b));this.rootBlock_= -this.block_.decompose(this.workspace_);b=this.rootBlock_.getDescendants(!1);for(var c=0,d;d=b[c];c++)d.render();this.rootBlock_.setMovable(!1);this.rootBlock_.setDeletable(!1);a?(b=2*a.CORNER_RADIUS,a=this.rootBlock_.RTL?a.getWidth()+b:b):a=b=16;this.block_.RTL&&(a=-a);this.rootBlock_.moveBy(a,b);if(this.block_.saveConnections){var e=this,f=this.block_;f.saveConnections(this.rootBlock_);this.sourceListener_=function(){f.saveConnections(e.rootBlock_)};this.block_.workspace.addChangeListener(this.sourceListener_)}this.resizeBubble_(); -this.workspace_.addChangeListener(this.workspaceChanged_.bind(this));this.applyColour()}else this.svgDialog_=null,this.workspace_.dispose(),this.rootBlock_=this.workspace_=null,this.bubble_.dispose(),this.bubble_=null,this.workspaceHeight_=this.workspaceWidth_=0,this.sourceListener_&&(this.block_.workspace.removeChangeListener(this.sourceListener_),this.sourceListener_=null)}; -Blockly.Mutator.prototype.workspaceChanged_=function(a){if(a.type!=Blockly.Events.UI&&(a.type!=Blockly.Events.CHANGE||"disabled"!=a.element)){if(!this.workspace_.isDragging())for(var b=this.workspace_.getTopBlocks(!1),c=0;a=b[c];c++){var d=a.getRelativeToSurfaceXY();20>d.y&&a.moveBy(0,20-d.y);if(a.RTL){var e=-20,f=this.workspace_.getFlyout();f&&(e-=f.getWidth());d.x>e&&a.moveBy(e-d.x,0)}else 20>d.x&&a.moveBy(20-d.x,0)}this.rootBlock_.workspace==this.workspace_&&(Blockly.Events.setGroup(!0),a=this.block_, -b=(b=a.mutationToDom())&&Blockly.Xml.domToText(b),c=a.rendered,a.rendered=!1,a.compose(this.rootBlock_),a.rendered=c,a.initSvg(),Blockly.getMainWorkspace().keyboardAccessibilityMode&&Blockly.navigation.moveCursorOnBlockMutation(a),a.rendered&&a.render(),c=(c=a.mutationToDom())&&Blockly.Xml.domToText(c),b!=c&&Blockly.Events.fire(new Blockly.Events.BlockChange(a,"mutation",null,b,c)),this.workspace_.isDragging()||this.resizeBubble_(),Blockly.Events.setGroup(!1))}}; -Blockly.Mutator.prototype.getFlyoutMetrics_=function(){var a=this.workspace_.getFlyout();a=a?a.getWidth():0;return{contentHeight:0,contentWidth:0,contentTop:0,contentLeft:0,viewHeight:this.workspaceHeight_,viewWidth:this.workspaceWidth_-a,viewTop:0,viewLeft:0,absoluteTop:0,absoluteLeft:this.workspace_.RTL?0:a}};Blockly.Mutator.prototype.dispose=function(){this.block_.mutator=null;Blockly.Icon.prototype.dispose.call(this)}; -Blockly.Mutator.prototype.updateBlockStyle=function(){var a=this.workspace_;if(a&&a.getAllBlocks(!1)){for(var b=a.getAllBlocks(!1),c=0;c=a&&this.sourceBlock_.outputConnection&&!b}else this.fullBlockClickTarget_=!1;this.fullBlockClickTarget_?this.clickTarget_=this.sourceBlock_.getSvgRoot():this.createBorderRect_();this.createTextElement_()}; -Blockly.FieldTextInput.prototype.doClassValidation_=function(a){return null===a||void 0===a?null:String(a)};Blockly.FieldTextInput.prototype.doValueInvalid_=function(a){this.isBeingEdited_&&(this.isTextValid_=!1,a=this.value_,this.value_=this.htmlInput_.untypedDefaultValue_,this.sourceBlock_&&Blockly.Events.isEnabled()&&Blockly.Events.fire(new Blockly.Events.BlockChange(this.sourceBlock_,"field",this.name||null,a,this.value_)))}; -Blockly.FieldTextInput.prototype.doValueUpdate_=function(a){this.isTextValid_=!0;this.value_=a;this.isBeingEdited_||(this.isDirty_=!0)};Blockly.FieldTextInput.prototype.applyColour=function(){this.sourceBlock_&&this.getConstants().FULL_BLOCK_FIELDS&&(this.borderRect_?this.borderRect_.setAttribute("stroke",this.sourceBlock_.style.colourTertiary):this.sourceBlock_.pathObject.svgPath.setAttribute("fill",this.getConstants().FIELD_BORDER_RECT_COLOUR))}; -Blockly.FieldTextInput.prototype.render_=function(){Blockly.FieldTextInput.superClass_.render_.call(this);if(this.isBeingEdited_){this.resizeEditor_();var a=this.htmlInput_;this.isTextValid_?(Blockly.utils.dom.removeClass(a,"blocklyInvalidInput"),Blockly.utils.aria.setState(a,Blockly.utils.aria.State.INVALID,!1)):(Blockly.utils.dom.addClass(a,"blocklyInvalidInput"),Blockly.utils.aria.setState(a,Blockly.utils.aria.State.INVALID,!0))}}; -Blockly.FieldTextInput.prototype.setSpellcheck=function(a){a!=this.spellcheck_&&(this.spellcheck_=a,this.htmlInput_&&this.htmlInput_.setAttribute("spellcheck",this.spellcheck_))};Blockly.FieldTextInput.prototype.showEditor_=function(a,b){this.workspace_=this.sourceBlock_.workspace;a=b||!1;!a&&(Blockly.utils.userAgent.MOBILE||Blockly.utils.userAgent.ANDROID||Blockly.utils.userAgent.IPAD)?this.showPromptEditor_():this.showInlineEditor_(a)}; -Blockly.FieldTextInput.prototype.showPromptEditor_=function(){var a=this;Blockly.prompt(Blockly.Msg.CHANGE_VALUE_TITLE,this.getText(),function(b){a.setValue(b)})};Blockly.FieldTextInput.prototype.showInlineEditor_=function(a){Blockly.WidgetDiv.show(this,this.sourceBlock_.RTL,this.widgetDispose_.bind(this));this.htmlInput_=this.widgetCreate_();this.isBeingEdited_=!0;a||(this.htmlInput_.focus({preventScroll:!0}),this.htmlInput_.select())}; -Blockly.FieldTextInput.prototype.widgetCreate_=function(){var a=Blockly.WidgetDiv.DIV;Blockly.utils.dom.addClass(this.getClickTarget_(),"editing");var b=document.createElement("input");b.className="blocklyHtmlInput";b.setAttribute("spellcheck",this.spellcheck_);var c=this.workspace_.getScale(),d=this.getConstants().FIELD_TEXT_FONTSIZE*c+"pt";a.style.fontSize=d;b.style.fontSize=d;d=Blockly.FieldTextInput.BORDERRADIUS*c+"px";if(this.fullBlockClickTarget_){d=this.getScaledBBox();d=(d.bottom-d.top)/2+ -"px";var e=this.sourceBlock_.getParent()?this.sourceBlock_.getParent().style.colourTertiary:this.sourceBlock_.style.colourTertiary;b.style.border=1*c+"px solid "+e;a.style.borderRadius=d;a.style.transition="box-shadow 0.25s ease 0s";this.getConstants().FIELD_TEXTINPUT_BOX_SHADOW&&(a.style.boxShadow="rgba(255, 255, 255, 0.3) 0px 0px 0px "+4*c+"px")}b.style.borderRadius=d;a.appendChild(b);b.value=b.defaultValue=this.getEditorText_(this.value_);b.untypedDefaultValue_=this.value_;b.oldValue_=null;this.resizeEditor_(); -this.bindInputEvents_(b);return b};Blockly.FieldTextInput.prototype.widgetDispose_=function(){this.isBeingEdited_=!1;this.isTextValid_=!0;this.forceRerender();if(this.onFinishEditing_)this.onFinishEditing_(this.value_);this.unbindInputEvents_();var a=Blockly.WidgetDiv.DIV.style;a.width="auto";a.height="auto";a.fontSize="";a.transition="";a.boxShadow="";this.htmlInput_=null;Blockly.utils.dom.removeClass(this.getClickTarget_(),"editing")}; -Blockly.FieldTextInput.prototype.bindInputEvents_=function(a){this.onKeyDownWrapper_=Blockly.bindEventWithChecks_(a,"keydown",this,this.onHtmlInputKeyDown_);this.onKeyInputWrapper_=Blockly.bindEventWithChecks_(a,"input",this,this.onHtmlInputChange_)}; -Blockly.FieldTextInput.prototype.unbindInputEvents_=function(){this.onKeyDownWrapper_&&(Blockly.unbindEvent_(this.onKeyDownWrapper_),this.onKeyDownWrapper_=null);this.onKeyInputWrapper_&&(Blockly.unbindEvent_(this.onKeyInputWrapper_),this.onKeyInputWrapper_=null)}; -Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_=function(a){a.keyCode==Blockly.utils.KeyCodes.ENTER?(Blockly.WidgetDiv.hide(),Blockly.DropDownDiv.hideWithoutAnimation()):a.keyCode==Blockly.utils.KeyCodes.ESC?(this.htmlInput_.value=this.htmlInput_.defaultValue,Blockly.WidgetDiv.hide(),Blockly.DropDownDiv.hideWithoutAnimation()):a.keyCode==Blockly.utils.KeyCodes.TAB&&(Blockly.WidgetDiv.hide(),Blockly.DropDownDiv.hideWithoutAnimation(),this.sourceBlock_.tab(this,!a.shiftKey),a.preventDefault())}; -Blockly.FieldTextInput.prototype.onHtmlInputChange_=function(a){a=this.htmlInput_.value;a!==this.htmlInput_.oldValue_&&(this.htmlInput_.oldValue_=a,Blockly.Events.setGroup(!0),a=this.getValueFromEditorText_(a),this.setValue(a),this.forceRerender(),this.resizeEditor_(),Blockly.Events.setGroup(!1))};Blockly.FieldTextInput.prototype.setEditorValue_=function(a){this.isDirty_=!0;this.isBeingEdited_&&(this.htmlInput_.value=this.getEditorText_(a));this.setValue(a)}; -Blockly.FieldTextInput.prototype.resizeEditor_=function(){var a=Blockly.WidgetDiv.DIV,b=this.getScaledBBox();a.style.width=b.right-b.left+"px";a.style.height=b.bottom-b.top+"px";b=new Blockly.utils.Coordinate(this.sourceBlock_.RTL?b.right-a.offsetWidth:b.left,b.top);a.style.left=b.x+"px";a.style.top=b.y+"px"}; -Blockly.FieldTextInput.numberValidator=function(a){console.warn("Blockly.FieldTextInput.numberValidator is deprecated. Use Blockly.FieldNumber instead.");if(null===a)return null;a=String(a);a=a.replace(/O/ig,"0");a=a.replace(/,/g,"");a=Number(a||0);return isNaN(a)?null:String(a)};Blockly.FieldTextInput.nonnegativeIntegerValidator=function(a){(a=Blockly.FieldTextInput.numberValidator(a))&&(a=String(Math.max(0,Math.floor(a))));return a};Blockly.FieldTextInput.prototype.isTabNavigable=function(){return!0}; -Blockly.FieldTextInput.prototype.getText_=function(){return this.isBeingEdited_&&this.htmlInput_?this.htmlInput_.value:null};Blockly.FieldTextInput.prototype.getEditorText_=function(a){return String(a)};Blockly.FieldTextInput.prototype.getValueFromEditorText_=function(a){return a};Blockly.fieldRegistry.register("field_input",Blockly.FieldTextInput);Blockly.FieldAngle=function(a,b,c){this.clockwise_=Blockly.FieldAngle.CLOCKWISE;this.offset_=Blockly.FieldAngle.OFFSET;this.wrap_=Blockly.FieldAngle.WRAP;this.round_=Blockly.FieldAngle.ROUND;Blockly.FieldAngle.superClass_.constructor.call(this,a,b,c);this.moveSurfaceWrapper_=this.clickSurfaceWrapper_=this.clickWrapper_=this.line_=this.gauge_=null};Blockly.utils.object.inherits(Blockly.FieldAngle,Blockly.FieldTextInput);Blockly.FieldAngle.prototype.DEFAULT_VALUE=0; -Blockly.FieldAngle.fromJson=function(a){return new Blockly.FieldAngle(a.angle,void 0,a)};Blockly.FieldAngle.prototype.SERIALIZABLE=!0;Blockly.FieldAngle.ROUND=15;Blockly.FieldAngle.HALF=50;Blockly.FieldAngle.CLOCKWISE=!1;Blockly.FieldAngle.OFFSET=0;Blockly.FieldAngle.WRAP=360;Blockly.FieldAngle.RADIUS=Blockly.FieldAngle.HALF-1; -Blockly.FieldAngle.prototype.configure_=function(a){Blockly.FieldAngle.superClass_.configure_.call(this,a);switch(a.mode){case "compass":this.clockwise_=!0;this.offset_=90;break;case "protractor":this.clockwise_=!1,this.offset_=0}var b=a.clockwise;"boolean"==typeof b&&(this.clockwise_=b);b=a.offset;null!=b&&(b=Number(b),isNaN(b)||(this.offset_=b));b=a.wrap;null!=b&&(b=Number(b),isNaN(b)||(this.wrap_=b));a=a.round;null!=a&&(a=Number(a),isNaN(a)||(this.round_=a))}; -Blockly.FieldAngle.prototype.initView=function(){Blockly.FieldAngle.superClass_.initView.call(this);this.symbol_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.TSPAN,{},null);this.symbol_.appendChild(document.createTextNode("\u00b0"));this.textElement_.appendChild(this.symbol_)};Blockly.FieldAngle.prototype.render_=function(){Blockly.FieldAngle.superClass_.render_.call(this);this.updateGraph_()}; -Blockly.FieldAngle.prototype.showEditor_=function(a){Blockly.FieldAngle.superClass_.showEditor_.call(this,a,Blockly.utils.userAgent.MOBILE||Blockly.utils.userAgent.ANDROID||Blockly.utils.userAgent.IPAD);a=this.dropdownCreate_();Blockly.DropDownDiv.getContentDiv().appendChild(a);Blockly.DropDownDiv.setColour(this.sourceBlock_.style.colourPrimary,this.sourceBlock_.style.colourTertiary);Blockly.DropDownDiv.showPositionedByField(this,this.dropdownDispose_.bind(this));this.updateGraph_()}; -Blockly.FieldAngle.prototype.dropdownCreate_=function(){var a=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.SVG,{xmlns:Blockly.utils.dom.SVG_NS,"xmlns:html":Blockly.utils.dom.HTML_NS,"xmlns:xlink":Blockly.utils.dom.XLINK_NS,version:"1.1",height:2*Blockly.FieldAngle.HALF+"px",width:2*Blockly.FieldAngle.HALF+"px",style:"touch-action: none"},null),b=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.CIRCLE,{cx:Blockly.FieldAngle.HALF,cy:Blockly.FieldAngle.HALF,r:Blockly.FieldAngle.RADIUS,"class":"blocklyAngleCircle"}, -a);this.gauge_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.PATH,{"class":"blocklyAngleGauge"},a);this.line_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.LINE,{x1:Blockly.FieldAngle.HALF,y1:Blockly.FieldAngle.HALF,"class":"blocklyAngleLine"},a);for(var c=0;360>c;c+=15)Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.LINE,{x1:Blockly.FieldAngle.HALF+Blockly.FieldAngle.RADIUS,y1:Blockly.FieldAngle.HALF,x2:Blockly.FieldAngle.HALF+Blockly.FieldAngle.RADIUS-(0==c%45?10:5),y2:Blockly.FieldAngle.HALF, -"class":"blocklyAngleMarks",transform:"rotate("+c+","+Blockly.FieldAngle.HALF+","+Blockly.FieldAngle.HALF+")"},a);this.clickWrapper_=Blockly.bindEventWithChecks_(a,"click",this,this.hide_);this.clickSurfaceWrapper_=Blockly.bindEventWithChecks_(b,"click",this,this.onMouseMove_,!0,!0);this.moveSurfaceWrapper_=Blockly.bindEventWithChecks_(b,"mousemove",this,this.onMouseMove_,!0,!0);return a}; -Blockly.FieldAngle.prototype.dropdownDispose_=function(){this.clickWrapper_&&(Blockly.unbindEvent_(this.clickWrapper_),this.clickWrapper_=null);this.clickSurfaceWrapper_&&(Blockly.unbindEvent_(this.clickSurfaceWrapper_),this.clickSurfaceWrapper_=null);this.moveSurfaceWrapper_&&(Blockly.unbindEvent_(this.moveSurfaceWrapper_),this.moveSurfaceWrapper_=null);this.line_=this.gauge_=null};Blockly.FieldAngle.prototype.hide_=function(){Blockly.DropDownDiv.hideIfOwner(this);Blockly.WidgetDiv.hide()}; -Blockly.FieldAngle.prototype.onMouseMove_=function(a){var b=this.gauge_.ownerSVGElement.getBoundingClientRect(),c=a.clientX-b.left-Blockly.FieldAngle.HALF;a=a.clientY-b.top-Blockly.FieldAngle.HALF;b=Math.atan(-a/c);isNaN(b)||(b=Blockly.utils.math.toDegrees(b),0>c?b+=180:0a&&(a+=360);a>this.wrap_&&(a-=360);return a};Blockly.Css.register(".blocklyAngleCircle {,stroke: #444;,stroke-width: 1;,fill: #ddd;,fill-opacity: .8;,},.blocklyAngleMarks {,stroke: #444;,stroke-width: 1;,},.blocklyAngleGauge {,fill: #f88;,fill-opacity: .8;,pointer-events: none;,},.blocklyAngleLine {,stroke: #f00;,stroke-width: 2;,stroke-linecap: round;,pointer-events: none;,}".split(",")); -Blockly.fieldRegistry.register("field_angle",Blockly.FieldAngle);Blockly.FieldCheckbox=function(a,b,c){this.checkChar_=null;Blockly.FieldCheckbox.superClass_.constructor.call(this,a,b,c)};Blockly.utils.object.inherits(Blockly.FieldCheckbox,Blockly.Field);Blockly.FieldCheckbox.prototype.DEFAULT_VALUE=!1;Blockly.FieldCheckbox.fromJson=function(a){return new Blockly.FieldCheckbox(a.checked,void 0,a)};Blockly.FieldCheckbox.CHECK_CHAR="\u2713";Blockly.FieldCheckbox.prototype.SERIALIZABLE=!0;Blockly.FieldCheckbox.prototype.CURSOR="default"; -Blockly.FieldCheckbox.prototype.configure_=function(a){Blockly.FieldCheckbox.superClass_.configure_.call(this,a);a.checkCharacter&&(this.checkChar_=a.checkCharacter)};Blockly.FieldCheckbox.prototype.initView=function(){Blockly.FieldCheckbox.superClass_.initView.call(this);Blockly.utils.dom.addClass(this.textElement_,"blocklyCheckbox");this.textElement_.style.display=this.value_?"block":"none"}; -Blockly.FieldCheckbox.prototype.render_=function(){this.textContent_&&(this.textContent_.nodeValue=this.getDisplayText_());this.updateSize_(this.getConstants().FIELD_CHECKBOX_X_OFFSET)};Blockly.FieldCheckbox.prototype.getDisplayText_=function(){return this.checkChar_||Blockly.FieldCheckbox.CHECK_CHAR};Blockly.FieldCheckbox.prototype.setCheckCharacter=function(a){this.checkChar_=a;this.forceRerender()};Blockly.FieldCheckbox.prototype.showEditor_=function(){this.setValue(!this.value_)}; -Blockly.FieldCheckbox.prototype.doClassValidation_=function(a){return!0===a||"TRUE"===a?"TRUE":!1===a||"FALSE"===a?"FALSE":null};Blockly.FieldCheckbox.prototype.doValueUpdate_=function(a){this.value_=this.convertValueToBool_(a);this.textElement_&&(this.textElement_.style.display=this.value_?"block":"none")};Blockly.FieldCheckbox.prototype.getValue=function(){return this.value_?"TRUE":"FALSE"};Blockly.FieldCheckbox.prototype.getValueBoolean=function(){return this.value_}; -Blockly.FieldCheckbox.prototype.getText=function(){return String(this.convertValueToBool_(this.value_))};Blockly.FieldCheckbox.prototype.convertValueToBool_=function(a){return"string"==typeof a?"TRUE"==a:!!a};Blockly.fieldRegistry.register("field_checkbox",Blockly.FieldCheckbox);Blockly.FieldColour=function(a,b,c){Blockly.FieldColour.superClass_.constructor.call(this,a,b,c);this.onKeyDownWrapper_=this.onMouseLeaveWrapper_=this.onMouseEnterWrapper_=this.onMouseMoveWrapper_=this.onClickWrapper_=this.highlightedIndex_=this.picker_=null};Blockly.utils.object.inherits(Blockly.FieldColour,Blockly.Field);Blockly.FieldColour.fromJson=function(a){return new Blockly.FieldColour(a.colour,void 0,a)};Blockly.FieldColour.prototype.SERIALIZABLE=!0;Blockly.FieldColour.prototype.CURSOR="default"; -Blockly.FieldColour.prototype.isDirty_=!1;Blockly.FieldColour.prototype.colours_=null;Blockly.FieldColour.prototype.titles_=null;Blockly.FieldColour.prototype.columns_=0;Blockly.FieldColour.prototype.configure_=function(a){Blockly.FieldColour.superClass_.configure_.call(this,a);a.colourOptions&&(this.colours_=a.colourOptions,this.titles_=a.colourTitles);a.columns&&(this.columns_=a.columns)}; -Blockly.FieldColour.prototype.initView=function(){this.size_=new Blockly.utils.Size(this.getConstants().FIELD_COLOUR_DEFAULT_WIDTH,this.getConstants().FIELD_COLOUR_DEFAULT_HEIGHT);this.getConstants().FIELD_COLOUR_FULL_BLOCK?this.clickTarget_=this.sourceBlock_.getSvgRoot():(this.createBorderRect_(),this.borderRect_.style.fillOpacity="1")}; -Blockly.FieldColour.prototype.applyColour=function(){this.getConstants().FIELD_COLOUR_FULL_BLOCK?(this.sourceBlock_.pathObject.svgPath.setAttribute("fill",this.getValue()),this.sourceBlock_.pathObject.svgPath.setAttribute("stroke","#fff")):this.borderRect_&&(this.borderRect_.style.fill=this.getValue())};Blockly.FieldColour.prototype.doClassValidation_=function(a){return"string"!=typeof a?null:Blockly.utils.colour.parse(a)}; -Blockly.FieldColour.prototype.doValueUpdate_=function(a){this.value_=a;this.borderRect_?this.borderRect_.style.fill=a:this.sourceBlock_&&this.sourceBlock_.rendered&&(this.sourceBlock_.pathObject.svgPath.setAttribute("fill",a),this.sourceBlock_.pathObject.svgPath.setAttribute("stroke","#fff"))};Blockly.FieldColour.prototype.getText=function(){var a=this.value_;/^#(.)\1(.)\2(.)\3$/.test(a)&&(a="#"+a[1]+a[3]+a[5]);return a};Blockly.FieldColour.COLOURS="#ffffff #cccccc #c0c0c0 #999999 #666666 #333333 #000000 #ffcccc #ff6666 #ff0000 #cc0000 #990000 #660000 #330000 #ffcc99 #ff9966 #ff9900 #ff6600 #cc6600 #993300 #663300 #ffff99 #ffff66 #ffcc66 #ffcc33 #cc9933 #996633 #663333 #ffffcc #ffff33 #ffff00 #ffcc00 #999900 #666600 #333300 #99ff99 #66ff99 #33ff33 #33cc00 #009900 #006600 #003300 #99ffff #33ffff #66cccc #00cccc #339999 #336666 #003333 #ccffff #66ffff #33ccff #3366ff #3333ff #000099 #000066 #ccccff #9999ff #6666cc #6633ff #6600cc #333399 #330099 #ffccff #ff99ff #cc66cc #cc33cc #993399 #663366 #330033".split(" "); -Blockly.FieldColour.prototype.DEFAULT_VALUE=Blockly.FieldColour.COLOURS[0];Blockly.FieldColour.TITLES=[];Blockly.FieldColour.COLUMNS=7;Blockly.FieldColour.prototype.setColours=function(a,b){this.colours_=a;b&&(this.titles_=b);return this};Blockly.FieldColour.prototype.setColumns=function(a){this.columns_=a;return this}; -Blockly.FieldColour.prototype.showEditor_=function(){this.picker_=this.dropdownCreate_();Blockly.DropDownDiv.getContentDiv().appendChild(this.picker_);Blockly.DropDownDiv.showPositionedByField(this,this.dropdownDispose_.bind(this));this.picker_.focus({preventScroll:!0})};Blockly.FieldColour.prototype.onClick_=function(a){a=(a=a.target)&&a.label;null!==a&&(this.setValue(a),Blockly.DropDownDiv.hideIfOwner(this))}; -Blockly.FieldColour.prototype.onKeyDown_=function(a){var b=!1;if(a.keyCode===Blockly.utils.KeyCodes.UP)this.moveHighlightBy_(0,-1),b=!0;else if(a.keyCode===Blockly.utils.KeyCodes.DOWN)this.moveHighlightBy_(0,1),b=!0;else if(a.keyCode===Blockly.utils.KeyCodes.LEFT)this.moveHighlightBy_(-1,0),b=!0;else if(a.keyCode===Blockly.utils.KeyCodes.RIGHT)this.moveHighlightBy_(1,0),b=!0;else if(a.keyCode===Blockly.utils.KeyCodes.ENTER){if(b=this.getHighlighted_())b=b&&b.label,null!==b&&this.setValue(b);Blockly.DropDownDiv.hideWithoutAnimation(); -b=!0}b&&a.stopPropagation()};Blockly.FieldColour.prototype.onBlocklyAction=function(a){if(this.picker_){if(a===Blockly.navigation.ACTION_PREVIOUS)return this.moveHighlightBy_(0,-1),!0;if(a===Blockly.navigation.ACTION_NEXT)return this.moveHighlightBy_(0,1),!0;if(a===Blockly.navigation.ACTION_OUT)return this.moveHighlightBy_(-1,0),!0;if(a===Blockly.navigation.ACTION_IN)return this.moveHighlightBy_(1,0),!0}return Blockly.FieldColour.superClass_.onBlocklyAction.call(this,a)}; -Blockly.FieldColour.prototype.moveHighlightBy_=function(a,b){var c=this.colours_||Blockly.FieldColour.COLOURS,d=this.columns_||Blockly.FieldColour.COLUMNS,e=this.highlightedIndex_%d,f=Math.floor(this.highlightedIndex_/d);e+=a;f+=b;0>a?0>e&&0e&&(e=0):0d-1&&fd-1&&e--:0>b?0>f&&(f=0):0Math.floor(c.length/d)-1&&(f=Math.floor(c.length/d)-1);this.setHighlightedCell_(this.picker_.childNodes[f].childNodes[e],f*d+e)}; -Blockly.FieldColour.prototype.onMouseMove_=function(a){var b=(a=a.target)&&Number(a.getAttribute("data-index"));null!==b&&b!==this.highlightedIndex_&&this.setHighlightedCell_(a,b)};Blockly.FieldColour.prototype.onMouseEnter_=function(){this.picker_.focus({preventScroll:!0})};Blockly.FieldColour.prototype.onMouseLeave_=function(){this.picker_.blur();var a=this.getHighlighted_();a&&Blockly.utils.dom.removeClass(a,"blocklyColourHighlighted")}; -Blockly.FieldColour.prototype.getHighlighted_=function(){var a=this.columns_||Blockly.FieldColour.COLUMNS,b=this.picker_.childNodes[Math.floor(this.highlightedIndex_/a)];return b?b.childNodes[this.highlightedIndex_%a]:null}; -Blockly.FieldColour.prototype.setHighlightedCell_=function(a,b){var c=this.getHighlighted_();c&&Blockly.utils.dom.removeClass(c,"blocklyColourHighlighted");Blockly.utils.dom.addClass(a,"blocklyColourHighlighted");this.highlightedIndex_=b;Blockly.utils.aria.setState(this.picker_,Blockly.utils.aria.State.ACTIVEDESCENDANT,a.getAttribute("id"))}; -Blockly.FieldColour.prototype.dropdownCreate_=function(){var a=this.columns_||Blockly.FieldColour.COLUMNS,b=this.colours_||Blockly.FieldColour.COLOURS,c=this.titles_||Blockly.FieldColour.TITLES,d=this.getValue(),e=document.createElement("table");e.className="blocklyColourTable";e.tabIndex=0;e.dir="ltr";Blockly.utils.aria.setRole(e,Blockly.utils.aria.Role.GRID);Blockly.utils.aria.setState(e,Blockly.utils.aria.State.EXPANDED,!0);Blockly.utils.aria.setState(e,Blockly.utils.aria.State.ROWCOUNT,Math.floor(b.length/ -a));Blockly.utils.aria.setState(e,Blockly.utils.aria.State.COLCOUNT,a);for(var f,g=0;gtr>td {","border: .5px solid #888;","box-sizing: border-box;","cursor: pointer;","display: inline-block;","height: 20px;","padding: 0;","width: 20px;","}",".blocklyColourTable>tr>td.blocklyColourHighlighted {","border-color: #eee;","box-shadow: 2px 2px 7px 2px rgba(0,0,0,.3);","position: relative;","}",".blocklyColourSelected, .blocklyColourSelected:hover {", -"border-color: #eee !important;","outline: 1px solid #333;","position: relative;","}"]);Blockly.fieldRegistry.register("field_colour",Blockly.FieldColour);Blockly.FieldDropdown=function(a,b,c){"function"!=typeof a&&Blockly.FieldDropdown.validateOptions_(a);this.menuGenerator_=a;this.suffixField=this.prefixField=this.generatedOptions_=null;this.trimOptions_();this.selectedOption_=this.getOptions(!1)[0];Blockly.FieldDropdown.superClass_.constructor.call(this,this.selectedOption_[1],b,c);this.svgArrow_=this.arrow_=this.imageElement_=this.menu_=this.selectedMenuItem_=null};Blockly.utils.object.inherits(Blockly.FieldDropdown,Blockly.Field); -Blockly.FieldDropdown.fromJson=function(a){return new Blockly.FieldDropdown(a.options,void 0,a)};Blockly.FieldDropdown.prototype.SERIALIZABLE=!0;Blockly.FieldDropdown.CHECKMARK_OVERHANG=25;Blockly.FieldDropdown.MAX_MENU_HEIGHT_VH=.45;Blockly.FieldDropdown.IMAGE_Y_OFFSET=5;Blockly.FieldDropdown.IMAGE_Y_PADDING=2*Blockly.FieldDropdown.IMAGE_Y_OFFSET;Blockly.FieldDropdown.ARROW_CHAR=Blockly.utils.userAgent.ANDROID?"\u25bc":"\u25be";Blockly.FieldDropdown.prototype.CURSOR="default"; -Blockly.FieldDropdown.prototype.initView=function(){this.shouldAddBorderRect_()?this.createBorderRect_():this.clickTarget_=this.sourceBlock_.getSvgRoot();this.createTextElement_();this.imageElement_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.IMAGE,{},this.fieldGroup_);this.getConstants().FIELD_DROPDOWN_SVG_ARROW?this.createSVGArrow_():this.createTextArrow_();this.borderRect_&&Blockly.utils.dom.addClass(this.borderRect_,"blocklyDropdownRect")}; -Blockly.FieldDropdown.prototype.shouldAddBorderRect_=function(){return!this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW||this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW&&!this.sourceBlock_.isShadow()}; -Blockly.FieldDropdown.prototype.createTextArrow_=function(){this.arrow_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.TSPAN,{},this.textElement_);this.arrow_.appendChild(document.createTextNode(this.sourceBlock_.RTL?Blockly.FieldDropdown.ARROW_CHAR+" ":" "+Blockly.FieldDropdown.ARROW_CHAR));this.sourceBlock_.RTL?this.textElement_.insertBefore(this.arrow_,this.textContent_):this.textElement_.appendChild(this.arrow_)}; -Blockly.FieldDropdown.prototype.createSVGArrow_=function(){this.svgArrow_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.IMAGE,{height:this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE+"px",width:this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE+"px"},this.fieldGroup_);this.svgArrow_.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",this.getConstants().FIELD_DROPDOWN_SVG_ARROW_DATAURI)}; -Blockly.FieldDropdown.prototype.showEditor_=function(a){this.menu_=this.dropdownCreate_();this.menu_.openingCoords=a&&"number"===typeof a.clientX?new Blockly.utils.Coordinate(a.clientX,a.clientY):null;this.menu_.render(Blockly.DropDownDiv.getContentDiv());a=this.menu_.getElement();Blockly.utils.dom.addClass(a,"blocklyDropdownMenu");if(this.getConstants().FIELD_DROPDOWN_COLOURED_DIV){a=this.sourceBlock_.isShadow()?this.sourceBlock_.getParent().getColour():this.sourceBlock_.getColour();var b=this.sourceBlock_.isShadow()? -this.sourceBlock_.getParent().style.colourTertiary:this.sourceBlock_.style.colourTertiary;Blockly.DropDownDiv.setColour(a,b)}Blockly.DropDownDiv.showPositionedByField(this,this.dropdownDispose_.bind(this));this.menu_.focus();this.selectedMenuItem_&&this.menu_.setHighlighted(this.selectedMenuItem_);this.applyColour()}; -Blockly.FieldDropdown.prototype.dropdownCreate_=function(){var a=new Blockly.Menu;a.setRole(Blockly.utils.aria.Role.LISTBOX);var b=this.getOptions(!1);this.selectedMenuItem_=null;for(var c=0;ca.length)){b=[];for(c=0;c=c||0>=b)throw Error("Height and width values of an image field must be greater than 0.");this.flipRtl_=!1;this.altText_="";Blockly.FieldImage.superClass_.constructor.call(this, -a,null,g);g||(this.flipRtl_=!!f,this.altText_=Blockly.utils.replaceMessageReferences(d)||"");this.size_=new Blockly.utils.Size(b,c+Blockly.FieldImage.Y_PADDING);this.imageHeight_=c;this.clickHandler_=null;"function"==typeof e&&(this.clickHandler_=e);this.imageElement_=null};Blockly.utils.object.inherits(Blockly.FieldImage,Blockly.Field);Blockly.FieldImage.prototype.DEFAULT_VALUE="";Blockly.FieldImage.fromJson=function(a){return new Blockly.FieldImage(a.src,a.width,a.height,void 0,void 0,void 0,a)}; -Blockly.FieldImage.Y_PADDING=1;Blockly.FieldImage.prototype.EDITABLE=!1;Blockly.FieldImage.prototype.isDirty_=!1;Blockly.FieldImage.prototype.configure_=function(a){Blockly.FieldImage.superClass_.configure_.call(this,a);this.flipRtl_=!!a.flipRtl;this.altText_=Blockly.utils.replaceMessageReferences(a.alt)||""}; -Blockly.FieldImage.prototype.initView=function(){this.imageElement_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.IMAGE,{height:this.imageHeight_+"px",width:this.size_.width+"px",alt:this.altText_},this.fieldGroup_);this.imageElement_.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",this.value_);this.clickHandler_&&(this.imageElement_.style.cursor="pointer")};Blockly.FieldImage.prototype.updateSize_=function(){}; -Blockly.FieldImage.prototype.doClassValidation_=function(a){return"string"!=typeof a?null:a};Blockly.FieldImage.prototype.doValueUpdate_=function(a){this.value_=a;this.imageElement_&&this.imageElement_.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",String(this.value_))};Blockly.FieldImage.prototype.getFlipRtl=function(){return this.flipRtl_};Blockly.FieldImage.prototype.setAlt=function(a){a!=this.altText_&&(this.altText_=a||"",this.imageElement_&&this.imageElement_.setAttribute("alt",this.altText_))}; -Blockly.FieldImage.prototype.showEditor_=function(){this.clickHandler_&&this.clickHandler_(this)};Blockly.FieldImage.prototype.setOnClickHandler=function(a){this.clickHandler_=a};Blockly.FieldImage.prototype.getText_=function(){return this.altText_};Blockly.fieldRegistry.register("field_image",Blockly.FieldImage);Blockly.FieldMultilineInput=function(a,b,c){Blockly.FieldMultilineInput.superClass_.constructor.call(this,a,b,c);this.textGroup_=null};Blockly.utils.object.inherits(Blockly.FieldMultilineInput,Blockly.FieldTextInput);Blockly.FieldMultilineInput.fromJson=function(a){var b=Blockly.utils.replaceMessageReferences(a.text);return new Blockly.FieldMultilineInput(b,void 0,a)};Blockly.FieldMultilineInput.prototype.toXml=function(a){a.textContent=this.getValue().replace(/\n/g," ");return a}; -Blockly.FieldMultilineInput.prototype.fromXml=function(a){this.setValue(a.textContent.replace(/ /g,"\n"))};Blockly.FieldMultilineInput.prototype.initView=function(){this.createBorderRect_();this.textGroup_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G,{"class":"blocklyEditableText"},this.fieldGroup_)}; -Blockly.FieldMultilineInput.prototype.getDisplayText_=function(){var a=this.value_;if(!a)return Blockly.Field.NBSP;var b=a.split("\n");a="";for(var c=0;cthis.maxDisplayLength&&(d=d.substring(0,this.maxDisplayLength-4)+"...");d=d.replace(/\s/g,Blockly.Field.NBSP);a+=d;c!==b.length-1&&(a+="\n")}this.sourceBlock_.RTL&&(a+="\u200f");return a}; -Blockly.FieldMultilineInput.prototype.render_=function(){for(var a;a=this.textGroup_.firstChild;)this.textGroup_.removeChild(a);a=this.getDisplayText_().split("\n");for(var b=0,c=0;cb&&(b=e);c+=this.getConstants().FIELD_TEXT_HEIGHT+(0this.max_&&Blockly.utils.aria.setState(a,Blockly.utils.aria.State.VALUEMAX,this.max_);return a};Blockly.fieldRegistry.register("field_number",Blockly.FieldNumber);Blockly.FieldVariable=function(a,b,c,d,e){this.menuGenerator_=Blockly.FieldVariable.dropdownCreate;this.defaultVariableName=a||"";this.size_=new Blockly.utils.Size(0,0);e&&this.configure_(e);b&&this.setValidator(b);e||this.setTypes_(c,d)};Blockly.utils.object.inherits(Blockly.FieldVariable,Blockly.FieldDropdown);Blockly.FieldVariable.fromJson=function(a){var b=Blockly.utils.replaceMessageReferences(a.variable);return new Blockly.FieldVariable(b,void 0,void 0,void 0,a)}; -Blockly.FieldVariable.prototype.SERIALIZABLE=!0;Blockly.FieldVariable.prototype.configure_=function(a){Blockly.FieldVariable.superClass_.configure_.call(this,a);this.setTypes_(a.variableTypes,a.defaultType)};Blockly.FieldVariable.prototype.initModel=function(){if(!this.variable_){var a=Blockly.Variables.getOrCreateVariablePackage(this.sourceBlock_.workspace,null,this.defaultVariableName,this.defaultType_);this.doValueUpdate_(a.getId())}}; -Blockly.FieldVariable.prototype.shouldAddBorderRect_=function(){return Blockly.FieldVariable.superClass_.shouldAddBorderRect_.call(this)&&(!this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW||"variables_get"!=this.sourceBlock_.type)}; -Blockly.FieldVariable.prototype.fromXml=function(a){var b=a.getAttribute("id"),c=a.textContent,d=a.getAttribute("variabletype")||a.getAttribute("variableType")||"";b=Blockly.Variables.getOrCreateVariablePackage(this.sourceBlock_.workspace,b,c,d);if(null!=d&&d!==b.type)throw Error("Serialized variable type with id '"+b.getId()+"' had type "+b.type+", and does not match variable field that references it: "+Blockly.Xml.domToText(a)+".");this.setValue(b.getId())}; -Blockly.FieldVariable.prototype.toXml=function(a){this.initModel();a.id=this.variable_.getId();a.textContent=this.variable_.name;this.variable_.type&&a.setAttribute("variabletype",this.variable_.type);return a};Blockly.FieldVariable.prototype.setSourceBlock=function(a){if(a.isShadow())throw Error("Variable fields are not allowed to exist on shadow blocks.");Blockly.FieldVariable.superClass_.setSourceBlock.call(this,a)}; -Blockly.FieldVariable.prototype.getValue=function(){return this.variable_?this.variable_.getId():null};Blockly.FieldVariable.prototype.getText=function(){return this.variable_?this.variable_.name:""};Blockly.FieldVariable.prototype.getVariable=function(){return this.variable_};Blockly.FieldVariable.prototype.getValidator=function(){return this.variable_?this.validator_:null}; -Blockly.FieldVariable.prototype.doClassValidation_=function(a){if(null===a)return null;var b=Blockly.Variables.getVariable(this.sourceBlock_.workspace,a);if(!b)return console.warn("Variable id doesn't point to a real variable! ID was "+a),null;b=b.type;return this.typeIsAllowed_(b)?a:(console.warn("Variable type doesn't match this field! Type was "+b),null)}; -Blockly.FieldVariable.prototype.doValueUpdate_=function(a){this.variable_=Blockly.Variables.getVariable(this.sourceBlock_.workspace,a);Blockly.FieldVariable.superClass_.doValueUpdate_.call(this,a)};Blockly.FieldVariable.prototype.typeIsAllowed_=function(a){var b=this.getVariableTypes_();if(!b)return!0;for(var c=0;crect,",a+" .blocklyEditableText>rect {","fill: "+this.FIELD_BORDER_RECT_COLOUR+";","fill-opacity: .6;","stroke: none;","}",a+" .blocklyNonEditableText>text,",a+" .blocklyEditableText>text {","fill: #000;", -"}",a+" .blocklyFlyoutLabelText {","fill: #000;","}",a+" .blocklyText.blocklyBubbleText {","fill: #000;","}",a+" .blocklyEditableText:not(.editing):hover>rect {","stroke: #fff;","stroke-width: 2;","}",a+" .blocklyHtmlInput {","font-family: "+this.FIELD_TEXT_FONTFAMILY+";","font-weight: "+this.FIELD_TEXT_FONTWEIGHT+";","}",a+" .blocklySelected>.blocklyPath {","stroke: #fc3;","stroke-width: 3px;","}",a+" .blocklyHighlightedConnectionPath {","stroke: #fc3;","}",a+" .blocklyReplaceable .blocklyPath {", -"fill-opacity: .5;","}",a+" .blocklyReplaceable .blocklyPathLight,",a+" .blocklyReplaceable .blocklyPathDark {","display: none;","}",a+" .blocklyInsertionMarker>.blocklyPath {","fill-opacity: "+this.INSERTION_MARKER_OPACITY+";","stroke: none;","}"]};Blockly.blockRendering.MarkerSvg=function(a,b,c){this.workspace_=a;this.marker_=c;this.parent_=null;this.constants_=b;this.currentMarkerSvg=null;a=this.isCursor()?this.constants_.CURSOR_COLOUR:this.constants_.MARKER_COLOUR;this.colour_=c.colour||a};Blockly.blockRendering.MarkerSvg.CURSOR_CLASS="blocklyCursor";Blockly.blockRendering.MarkerSvg.MARKER_CLASS="blocklyMarker";Blockly.blockRendering.MarkerSvg.HEIGHT_MULTIPLIER=.75;Blockly.blockRendering.MarkerSvg.prototype.getSvgRoot=function(){return this.svgGroup_}; -Blockly.blockRendering.MarkerSvg.prototype.getMarker=function(){return this.marker_};Blockly.blockRendering.MarkerSvg.prototype.isCursor=function(){return"cursor"==this.marker_.type};Blockly.blockRendering.MarkerSvg.prototype.createDom=function(){var a=this.isCursor()?Blockly.blockRendering.MarkerSvg.CURSOR_CLASS:Blockly.blockRendering.MarkerSvg.MARKER_CLASS;this.svgGroup_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G,{"class":a},null);this.createDomInternal_();return this.svgGroup_}; -Blockly.blockRendering.MarkerSvg.prototype.setParent_=function(a){this.isCursor()?(this.parent_&&this.parent_.setCursorSvg(null),a.setCursorSvg(this.getSvgRoot())):(this.parent_&&this.parent_.setMarkerSvg(null),a.setMarkerSvg(this.getSvgRoot()));this.parent_=a}; -Blockly.blockRendering.MarkerSvg.prototype.draw=function(a,b){if(b){this.constants_=this.workspace_.getRenderer().getConstants();var c=this.isCursor()?this.constants_.CURSOR_COLOUR:this.constants_.MARKER_COLOUR;this.colour_=this.marker_.colour||c;this.applyColour_(b);this.showAtLocation_(b);this.fireMarkerEvent_(a,b);a=this.currentMarkerSvg.childNodes[0];void 0!==a&&a.beginElement&&a.beginElement()}else this.hide()}; -Blockly.blockRendering.MarkerSvg.prototype.showAtLocation_=function(a){var b=a.getLocation();a.getType()==Blockly.ASTNode.types.BLOCK?this.showWithBlock_(a):a.getType()==Blockly.ASTNode.types.OUTPUT?this.showWithOutput_(a):b.type==Blockly.INPUT_VALUE?this.showWithInput_(a):b.type==Blockly.NEXT_STATEMENT?this.showWithNext_(a):a.getType()==Blockly.ASTNode.types.PREVIOUS?this.showWithPrevious_(a):a.getType()==Blockly.ASTNode.types.FIELD?this.showWithField_(a):a.getType()==Blockly.ASTNode.types.WORKSPACE? -this.showWithCoordinates_(a):a.getType()==Blockly.ASTNode.types.STACK&&this.showWithStack_(a)}; -Blockly.blockRendering.MarkerSvg.prototype.showWithBlockPrevOutput_=function(a){a=a.getSourceBlock();var b=a.width,c=a.height,d=c*Blockly.blockRendering.MarkerSvg.HEIGHT_MULTIPLIER,e=this.constants_.CURSOR_BLOCK_PADDING;if(a.previousConnection){var f=this.constants_.shapeFor(a.previousConnection);this.positionPrevious_(b,e,d,f)}else a.outputConnection?(f=this.constants_.shapeFor(a.outputConnection),this.positionOutput_(b,c,f)):this.positionBlock_(b,e,d);this.setParent_(a);this.showCurrent_()}; -Blockly.blockRendering.MarkerSvg.prototype.showWithBlock_=function(a){this.showWithBlockPrevOutput_(a)};Blockly.blockRendering.MarkerSvg.prototype.showWithPrevious_=function(a){this.showWithBlockPrevOutput_(a)};Blockly.blockRendering.MarkerSvg.prototype.showWithOutput_=function(a){this.showWithBlockPrevOutput_(a)}; -Blockly.blockRendering.MarkerSvg.prototype.showWithCoordinates_=function(a){var b=a.getWsCoordinate();a=b.x;b=b.y;this.workspace_.RTL&&(a-=this.constants_.CURSOR_WS_WIDTH);this.positionLine_(a,b,this.constants_.CURSOR_WS_WIDTH);this.setParent_(this.workspace_);this.showCurrent_()};Blockly.blockRendering.MarkerSvg.prototype.showWithField_=function(a){a=a.getLocation();var b=a.getSize().width,c=a.getSize().height;this.positionRect_(0,0,b,c);this.setParent_(a);this.showCurrent_()}; -Blockly.blockRendering.MarkerSvg.prototype.showWithInput_=function(a){a=a.getLocation();var b=a.getSourceBlock();this.positionInput_(a);this.setParent_(b);this.showCurrent_()};Blockly.blockRendering.MarkerSvg.prototype.showWithNext_=function(a){var b=a.getLocation();a=b.getSourceBlock();var c=0;b=b.getOffsetInBlock().y;var d=a.getHeightWidth().width;this.workspace_.RTL&&(c=-d);this.positionLine_(c,b,d);this.setParent_(a);this.showCurrent_()}; -Blockly.blockRendering.MarkerSvg.prototype.showWithStack_=function(a){a=a.getLocation();var b=a.getHeightWidth(),c=b.width+this.constants_.CURSOR_STACK_PADDING;b=b.height+this.constants_.CURSOR_STACK_PADDING;var d=-this.constants_.CURSOR_STACK_PADDING/2,e=-this.constants_.CURSOR_STACK_PADDING/2,f=d;this.workspace_.RTL&&(f=-(c+d));this.positionRect_(f,e,c,b);this.setParent_(a);this.showCurrent_()}; -Blockly.blockRendering.MarkerSvg.prototype.showCurrent_=function(){this.hide();this.currentMarkerSvg.style.display=""};Blockly.blockRendering.MarkerSvg.prototype.positionBlock_=function(a,b,c){a=Blockly.utils.svgPaths.moveBy(-b,c)+Blockly.utils.svgPaths.lineOnAxis("V",-b)+Blockly.utils.svgPaths.lineOnAxis("H",a+2*b)+Blockly.utils.svgPaths.lineOnAxis("V",c);this.markerBlock_.setAttribute("d",a);this.workspace_.RTL&&this.flipRtl_(this.markerBlock_);this.currentMarkerSvg=this.markerBlock_}; -Blockly.blockRendering.MarkerSvg.prototype.positionInput_=function(a){var b=a.getOffsetInBlock().x,c=a.getOffsetInBlock().y;a=Blockly.utils.svgPaths.moveTo(0,0)+this.constants_.shapeFor(a).pathDown;this.markerInput_.setAttribute("d",a);this.markerInput_.setAttribute("transform","translate("+b+","+c+")"+(this.workspace_.RTL?" scale(-1 1)":""));this.currentMarkerSvg=this.markerInput_}; -Blockly.blockRendering.MarkerSvg.prototype.positionLine_=function(a,b,c){this.markerSvgLine_.setAttribute("x",a);this.markerSvgLine_.setAttribute("y",b);this.markerSvgLine_.setAttribute("width",c);this.currentMarkerSvg=this.markerSvgLine_}; -Blockly.blockRendering.MarkerSvg.prototype.positionOutput_=function(a,b,c){a=Blockly.utils.svgPaths.moveBy(a,0)+Blockly.utils.svgPaths.lineOnAxis("h",-(a-c.width))+Blockly.utils.svgPaths.lineOnAxis("v",this.constants_.TAB_OFFSET_FROM_TOP)+c.pathDown+Blockly.utils.svgPaths.lineOnAxis("V",b)+Blockly.utils.svgPaths.lineOnAxis("H",a);this.markerBlock_.setAttribute("d",a);this.workspace_.RTL&&this.flipRtl_(this.markerBlock_);this.currentMarkerSvg=this.markerBlock_}; -Blockly.blockRendering.MarkerSvg.prototype.positionPrevious_=function(a,b,c,d){a=Blockly.utils.svgPaths.moveBy(-b,c)+Blockly.utils.svgPaths.lineOnAxis("V",-b)+Blockly.utils.svgPaths.lineOnAxis("H",this.constants_.NOTCH_OFFSET_LEFT)+d.pathLeft+Blockly.utils.svgPaths.lineOnAxis("H",a+2*b)+Blockly.utils.svgPaths.lineOnAxis("V",c);this.markerBlock_.setAttribute("d",a);this.workspace_.RTL&&this.flipRtl_(this.markerBlock_);this.currentMarkerSvg=this.markerBlock_}; -Blockly.blockRendering.MarkerSvg.prototype.positionRect_=function(a,b,c,d){this.markerSvgRect_.setAttribute("x",a);this.markerSvgRect_.setAttribute("y",b);this.markerSvgRect_.setAttribute("width",c);this.markerSvgRect_.setAttribute("height",d);this.currentMarkerSvg=this.markerSvgRect_};Blockly.blockRendering.MarkerSvg.prototype.flipRtl_=function(a){a.setAttribute("transform","scale(-1 1)")}; -Blockly.blockRendering.MarkerSvg.prototype.hide=function(){this.markerSvgLine_.style.display="none";this.markerSvgRect_.style.display="none";this.markerInput_.style.display="none";this.markerBlock_.style.display="none"};Blockly.blockRendering.MarkerSvg.prototype.fireMarkerEvent_=function(a,b){var c=b.getSourceBlock(),d=this.isCursor()?"cursorMove":"markerMove";a=new Blockly.Events.Ui(c,d,a,b);b.getType()==Blockly.ASTNode.types.WORKSPACE&&(a.workspaceId=b.getLocation().id);Blockly.Events.fire(a)}; -Blockly.blockRendering.MarkerSvg.prototype.getBlinkProperties_=function(){return{attributeType:"XML",attributeName:"fill",dur:"1s",values:this.colour_+";transparent;transparent;",repeatCount:"indefinite"}}; -Blockly.blockRendering.MarkerSvg.prototype.createDomInternal_=function(){this.markerSvg_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G,{width:this.constants_.CURSOR_WS_WIDTH,height:this.constants_.WS_CURSOR_HEIGHT},this.svgGroup_);this.markerSvgLine_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.RECT,{width:this.constants_.CURSOR_WS_WIDTH,height:this.constants_.WS_CURSOR_HEIGHT,style:"display: none"},this.markerSvg_);this.markerSvgRect_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.RECT, -{"class":"blocklyVerticalMarker",rx:10,ry:10,style:"display: none"},this.markerSvg_);this.markerInput_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.PATH,{transform:"",style:"display: none"},this.markerSvg_);this.markerBlock_=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.PATH,{transform:"",style:"display: none",fill:"none","stroke-width":this.constants_.CURSOR_STROKE_WIDTH},this.markerSvg_);if(this.isCursor()){var a=this.getBlinkProperties_();Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.ANIMATE, -a,this.markerSvgLine_);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.ANIMATE,a,this.markerInput_);a.attributeName="stroke";Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.ANIMATE,a,this.markerBlock_)}return this.markerSvg_}; -Blockly.blockRendering.MarkerSvg.prototype.applyColour_=function(a){this.markerSvgLine_.setAttribute("fill",this.colour_);this.markerSvgRect_.setAttribute("stroke",this.colour_);this.markerInput_.setAttribute("fill",this.colour_);this.markerBlock_.setAttribute("stroke",this.colour_);this.isCursor()&&(a=this.colour_+";transparent;transparent;",this.markerSvgLine_.firstChild.setAttribute("values",a),this.markerInput_.firstChild.setAttribute("values",a),this.markerBlock_.firstChild.setAttribute("values", -a))};Blockly.blockRendering.MarkerSvg.prototype.dispose=function(){this.svgGroup_&&Blockly.utils.dom.removeNode(this.svgGroup_)};Blockly.blockRendering.Types={NONE:0,FIELD:1,HAT:2,ICON:4,SPACER:8,BETWEEN_ROW_SPACER:16,IN_ROW_SPACER:32,EXTERNAL_VALUE_INPUT:64,INPUT:128,INLINE_INPUT:256,STATEMENT_INPUT:512,CONNECTION:1024,PREVIOUS_CONNECTION:2048,NEXT_CONNECTION:4096,OUTPUT_CONNECTION:8192,CORNER:16384,LEFT_SQUARE_CORNER:32768,LEFT_ROUND_CORNER:65536,RIGHT_SQUARE_CORNER:131072,RIGHT_ROUND_CORNER:262144,JAGGED_EDGE:524288,ROW:1048576,TOP_ROW:2097152,BOTTOM_ROW:4194304,INPUT_ROW:8388608}; -Blockly.blockRendering.Types.LEFT_CORNER=Blockly.blockRendering.Types.LEFT_SQUARE_CORNER|Blockly.blockRendering.Types.LEFT_ROUND_CORNER;Blockly.blockRendering.Types.RIGHT_CORNER=Blockly.blockRendering.Types.RIGHT_SQUARE_CORNER|Blockly.blockRendering.Types.RIGHT_ROUND_CORNER;Blockly.blockRendering.Types.nextTypeValue_=16777216; -Blockly.blockRendering.Types.getType=function(a){Object.prototype.hasOwnProperty.call(Blockly.blockRendering.Types,a)||(Blockly.blockRendering.Types[a]=Blockly.blockRendering.Types.nextTypeValue_,Blockly.blockRendering.Types.nextTypeValue_<<=1);return Blockly.blockRendering.Types[a]};Blockly.blockRendering.Types.isField=function(a){return a.type&Blockly.blockRendering.Types.FIELD};Blockly.blockRendering.Types.isHat=function(a){return a.type&Blockly.blockRendering.Types.HAT}; -Blockly.blockRendering.Types.isIcon=function(a){return a.type&Blockly.blockRendering.Types.ICON};Blockly.blockRendering.Types.isSpacer=function(a){return a.type&Blockly.blockRendering.Types.SPACER};Blockly.blockRendering.Types.isInRowSpacer=function(a){return a.type&Blockly.blockRendering.Types.IN_ROW_SPACER};Blockly.blockRendering.Types.isInput=function(a){return a.type&Blockly.blockRendering.Types.INPUT};Blockly.blockRendering.Types.isExternalInput=function(a){return a.type&Blockly.blockRendering.Types.EXTERNAL_VALUE_INPUT}; -Blockly.blockRendering.Types.isInlineInput=function(a){return a.type&Blockly.blockRendering.Types.INLINE_INPUT};Blockly.blockRendering.Types.isStatementInput=function(a){return a.type&Blockly.blockRendering.Types.STATEMENT_INPUT};Blockly.blockRendering.Types.isPreviousConnection=function(a){return a.type&Blockly.blockRendering.Types.PREVIOUS_CONNECTION};Blockly.blockRendering.Types.isNextConnection=function(a){return a.type&Blockly.blockRendering.Types.NEXT_CONNECTION}; -Blockly.blockRendering.Types.isPreviousOrNextConnection=function(a){return a.type&(Blockly.blockRendering.Types.PREVIOUS_CONNECTION|Blockly.blockRendering.Types.NEXT_CONNECTION)};Blockly.blockRendering.Types.isLeftRoundedCorner=function(a){return a.type&Blockly.blockRendering.Types.LEFT_ROUND_CORNER};Blockly.blockRendering.Types.isRightRoundedCorner=function(a){return a.type&Blockly.blockRendering.Types.RIGHT_ROUND_CORNER}; -Blockly.blockRendering.Types.isLeftSquareCorner=function(a){return a.type&Blockly.blockRendering.Types.LEFT_SQUARE_CORNER};Blockly.blockRendering.Types.isRightSquareCorner=function(a){return a.type&Blockly.blockRendering.Types.RIGHT_SQUARE_CORNER};Blockly.blockRendering.Types.isCorner=function(a){return a.type&Blockly.blockRendering.Types.CORNER};Blockly.blockRendering.Types.isJaggedEdge=function(a){return a.type&Blockly.blockRendering.Types.JAGGED_EDGE}; -Blockly.blockRendering.Types.isRow=function(a){return a.type&Blockly.blockRendering.Types.ROW};Blockly.blockRendering.Types.isBetweenRowSpacer=function(a){return a.type&Blockly.blockRendering.Types.BETWEEN_ROW_SPACER};Blockly.blockRendering.Types.isTopRow=function(a){return a.type&Blockly.blockRendering.Types.TOP_ROW};Blockly.blockRendering.Types.isBottomRow=function(a){return a.type&Blockly.blockRendering.Types.BOTTOM_ROW}; -Blockly.blockRendering.Types.isTopOrBottomRow=function(a){return a.type&(Blockly.blockRendering.Types.TOP_ROW|Blockly.blockRendering.Types.BOTTOM_ROW)};Blockly.blockRendering.Types.isInputRow=function(a){return a.type&Blockly.blockRendering.Types.INPUT_ROW};Blockly.blockRendering.Measurable=function(a){this.height=this.width=0;this.type=Blockly.blockRendering.Types.NONE;this.centerline=this.xPos=0;this.constants_=a;this.notchOffset=this.constants_.NOTCH_OFFSET_LEFT};Blockly.blockRendering.Connection=function(a,b){Blockly.blockRendering.Connection.superClass_.constructor.call(this,a);this.connectionModel=b;this.shape=this.constants_.shapeFor(b);this.isDynamicShape=!!this.shape.isDynamic;this.type|=Blockly.blockRendering.Types.CONNECTION};Blockly.utils.object.inherits(Blockly.blockRendering.Connection,Blockly.blockRendering.Measurable); -Blockly.blockRendering.OutputConnection=function(a,b){Blockly.blockRendering.OutputConnection.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.OUTPUT_CONNECTION;this.height=this.isDynamicShape?0:this.shape.height;this.startX=this.width=this.isDynamicShape?0:this.shape.width;this.connectionOffsetY=this.constants_.TAB_OFFSET_FROM_TOP;this.connectionOffsetX=0};Blockly.utils.object.inherits(Blockly.blockRendering.OutputConnection,Blockly.blockRendering.Connection); -Blockly.blockRendering.PreviousConnection=function(a,b){Blockly.blockRendering.PreviousConnection.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.PREVIOUS_CONNECTION;this.height=this.shape.height;this.width=this.shape.width};Blockly.utils.object.inherits(Blockly.blockRendering.PreviousConnection,Blockly.blockRendering.Connection); -Blockly.blockRendering.NextConnection=function(a,b){Blockly.blockRendering.NextConnection.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.NEXT_CONNECTION;this.height=this.shape.height;this.width=this.shape.width};Blockly.utils.object.inherits(Blockly.blockRendering.NextConnection,Blockly.blockRendering.Connection);Blockly.blockRendering.InputConnection=function(a,b){Blockly.blockRendering.InputConnection.superClass_.constructor.call(this,a,b.connection);this.type|=Blockly.blockRendering.Types.INPUT;this.input=b;this.align=b.align;(this.connectedBlock=b.connection&&b.connection.targetBlock()?b.connection.targetBlock():null)?(a=this.connectedBlock.getHeightWidth(),this.connectedBlockWidth=a.width,this.connectedBlockHeight=a.height):this.connectedBlockHeight=this.connectedBlockWidth=0;this.connectionOffsetY=this.connectionOffsetX= -0};Blockly.utils.object.inherits(Blockly.blockRendering.InputConnection,Blockly.blockRendering.Connection); -Blockly.blockRendering.InlineInput=function(a,b){Blockly.blockRendering.InlineInput.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.INLINE_INPUT;this.connectedBlock?(this.width=this.connectedBlockWidth,this.height=this.connectedBlockHeight):(this.height=this.constants_.EMPTY_INLINE_INPUT_HEIGHT,this.width=this.constants_.EMPTY_INLINE_INPUT_PADDING);this.connectionHeight=this.isDynamicShape?this.shape.height(this.height):this.shape.height;this.connectionWidth=this.isDynamicShape? -this.shape.width(this.height):this.shape.width;this.connectedBlock||(this.width+=this.connectionWidth*(this.isDynamicShape?2:1));this.connectionOffsetY=this.isDynamicShape?this.shape.connectionOffsetY(this.connectionHeight):this.constants_.TAB_OFFSET_FROM_TOP;this.connectionOffsetX=this.isDynamicShape?this.shape.connectionOffsetX(this.connectionWidth):0};Blockly.utils.object.inherits(Blockly.blockRendering.InlineInput,Blockly.blockRendering.InputConnection); -Blockly.blockRendering.StatementInput=function(a,b){Blockly.blockRendering.StatementInput.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.STATEMENT_INPUT;this.height=this.connectedBlock?this.connectedBlockHeight+this.constants_.STATEMENT_BOTTOM_SPACER:this.constants_.EMPTY_STATEMENT_INPUT_HEIGHT;this.width=this.constants_.STATEMENT_INPUT_NOTCH_OFFSET+this.shape.width};Blockly.utils.object.inherits(Blockly.blockRendering.StatementInput,Blockly.blockRendering.InputConnection); -Blockly.blockRendering.ExternalValueInput=function(a,b){Blockly.blockRendering.ExternalValueInput.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.EXTERNAL_VALUE_INPUT;this.height=this.connectedBlock?this.connectedBlockHeight-this.constants_.TAB_OFFSET_FROM_TOP-this.constants_.MEDIUM_PADDING:this.shape.height;this.width=this.shape.width+this.constants_.EXTERNAL_VALUE_INPUT_PADDING;this.connectionOffsetY=this.constants_.TAB_OFFSET_FROM_TOP;this.connectionHeight=this.shape.height; -this.connectionWidth=this.shape.width};Blockly.utils.object.inherits(Blockly.blockRendering.ExternalValueInput,Blockly.blockRendering.InputConnection);Blockly.blockRendering.Icon=function(a,b){Blockly.blockRendering.Icon.superClass_.constructor.call(this,a);this.icon=b;this.isVisible=b.isVisible();this.type|=Blockly.blockRendering.Types.ICON;a=b.getCorrectedSize();this.height=a.height;this.width=a.width};Blockly.utils.object.inherits(Blockly.blockRendering.Icon,Blockly.blockRendering.Measurable); -Blockly.blockRendering.JaggedEdge=function(a){Blockly.blockRendering.JaggedEdge.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.JAGGED_EDGE;this.height=this.constants_.JAGGED_TEETH.height;this.width=this.constants_.JAGGED_TEETH.width};Blockly.utils.object.inherits(Blockly.blockRendering.JaggedEdge,Blockly.blockRendering.Measurable); -Blockly.blockRendering.Field=function(a,b,c){Blockly.blockRendering.Field.superClass_.constructor.call(this,a);this.field=b;this.isEditable=b.EDITABLE;this.flipRtl=b.getFlipRtl();this.type|=Blockly.blockRendering.Types.FIELD;a=this.field.getSize();this.height=a.height;this.width=a.width;this.parentInput=c};Blockly.utils.object.inherits(Blockly.blockRendering.Field,Blockly.blockRendering.Measurable); -Blockly.blockRendering.Hat=function(a){Blockly.blockRendering.Hat.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.HAT;this.height=this.constants_.START_HAT.height;this.width=this.constants_.START_HAT.width;this.ascenderHeight=this.height};Blockly.utils.object.inherits(Blockly.blockRendering.Hat,Blockly.blockRendering.Measurable); -Blockly.blockRendering.SquareCorner=function(a,b){Blockly.blockRendering.SquareCorner.superClass_.constructor.call(this,a);this.type=(b&&"left"!=b?Blockly.blockRendering.Types.RIGHT_SQUARE_CORNER:Blockly.blockRendering.Types.LEFT_SQUARE_CORNER)|Blockly.blockRendering.Types.CORNER;this.width=this.height=this.constants_.NO_PADDING};Blockly.utils.object.inherits(Blockly.blockRendering.SquareCorner,Blockly.blockRendering.Measurable); -Blockly.blockRendering.RoundCorner=function(a,b){Blockly.blockRendering.RoundCorner.superClass_.constructor.call(this,a);this.type=(b&&"left"!=b?Blockly.blockRendering.Types.RIGHT_ROUND_CORNER:Blockly.blockRendering.Types.LEFT_ROUND_CORNER)|Blockly.blockRendering.Types.CORNER;this.width=this.constants_.CORNER_RADIUS;this.height=this.constants_.CORNER_RADIUS/2};Blockly.utils.object.inherits(Blockly.blockRendering.RoundCorner,Blockly.blockRendering.Measurable); -Blockly.blockRendering.InRowSpacer=function(a,b){Blockly.blockRendering.InRowSpacer.superClass_.constructor.call(this,a);this.type=this.type|Blockly.blockRendering.Types.SPACER|Blockly.blockRendering.Types.IN_ROW_SPACER;this.width=b;this.height=this.constants_.SPACER_DEFAULT_HEIGHT};Blockly.utils.object.inherits(Blockly.blockRendering.InRowSpacer,Blockly.blockRendering.Measurable);Blockly.blockRendering.Row=function(a){this.type=Blockly.blockRendering.Types.ROW;this.elements=[];this.xPos=this.yPos=this.widthWithConnectedBlocks=this.minWidth=this.minHeight=this.width=this.height=0;this.hasJaggedEdge=this.hasDummyInput=this.hasInlineInput=this.hasStatement=this.hasExternalInput=!1;this.constants_=a;this.notchOffset=this.constants_.NOTCH_OFFSET_LEFT;this.align=null}; -Blockly.blockRendering.Row.prototype.measure=function(){throw Error("Unexpected attempt to measure a base Row.");};Blockly.blockRendering.Row.prototype.getLastInput=function(){for(var a=this.elements.length-1,b;b=this.elements[a];a--)if(Blockly.blockRendering.Types.isInput(b))return b;return null};Blockly.blockRendering.Row.prototype.startsWithElemSpacer=function(){return!0};Blockly.blockRendering.Row.prototype.endsWithElemSpacer=function(){return!0}; -Blockly.blockRendering.Row.prototype.getFirstSpacer=function(){for(var a=0,b;b=this.elements[a];a++)if(Blockly.blockRendering.Types.isSpacer(b))return b;return null};Blockly.blockRendering.Row.prototype.getLastSpacer=function(){for(var a=this.elements.length-1,b;b=this.elements[a];a--)if(Blockly.blockRendering.Types.isSpacer(b))return b;return null}; -Blockly.blockRendering.TopRow=function(a){Blockly.blockRendering.TopRow.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.TOP_ROW;this.ascenderHeight=this.capline=0;this.hasPreviousConnection=!1;this.connection=null};Blockly.utils.object.inherits(Blockly.blockRendering.TopRow,Blockly.blockRendering.Row); -Blockly.blockRendering.TopRow.prototype.hasLeftSquareCorner=function(a){var b=(a.hat?"cap"===a.hat:this.constants_.ADD_START_HATS)&&!a.outputConnection&&!a.previousConnection,c=a.getPreviousBlock();return!!a.outputConnection||b||(c?c.getNextBlock()==a:!1)};Blockly.blockRendering.TopRow.prototype.hasRightSquareCorner=function(a){return!0}; -Blockly.blockRendering.TopRow.prototype.measure=function(){for(var a=0,b=0,c=0,d=0,e;e=this.elements[d];d++)b+=e.width,Blockly.blockRendering.Types.isSpacer(e)||(Blockly.blockRendering.Types.isHat(e)?c=Math.max(c,e.ascenderHeight):a=Math.max(a,e.height));this.width=Math.max(this.minWidth,b);this.height=Math.max(this.minHeight,a)+c;this.capline=this.ascenderHeight=c;this.widthWithConnectedBlocks=this.width};Blockly.blockRendering.TopRow.prototype.startsWithElemSpacer=function(){return!1}; -Blockly.blockRendering.TopRow.prototype.endsWithElemSpacer=function(){return!1};Blockly.blockRendering.BottomRow=function(a){Blockly.blockRendering.BottomRow.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.BOTTOM_ROW;this.hasNextConnection=!1;this.connection=null;this.baseline=this.descenderHeight=0};Blockly.utils.object.inherits(Blockly.blockRendering.BottomRow,Blockly.blockRendering.Row); -Blockly.blockRendering.BottomRow.prototype.hasLeftSquareCorner=function(a){return!!a.outputConnection||!!a.getNextBlock()};Blockly.blockRendering.BottomRow.prototype.hasRightSquareCorner=function(a){return!0}; -Blockly.blockRendering.BottomRow.prototype.measure=function(){for(var a=0,b=0,c=0,d=0,e;e=this.elements[d];d++)b+=e.width,Blockly.blockRendering.Types.isSpacer(e)||(Blockly.blockRendering.Types.isNextConnection(e)?c=Math.max(c,e.height):a=Math.max(a,e.height));this.width=Math.max(this.minWidth,b);this.height=Math.max(this.minHeight,a)+c;this.descenderHeight=c;this.widthWithConnectedBlocks=this.width};Blockly.blockRendering.BottomRow.prototype.startsWithElemSpacer=function(){return!1}; -Blockly.blockRendering.BottomRow.prototype.endsWithElemSpacer=function(){return!1};Blockly.blockRendering.SpacerRow=function(a,b,c){Blockly.blockRendering.SpacerRow.superClass_.constructor.call(this,a);this.type=this.type|Blockly.blockRendering.Types.SPACER|Blockly.blockRendering.Types.BETWEEN_ROW_SPACER;this.width=c;this.height=b;this.followsStatement=!1;this.widthWithConnectedBlocks=0;this.elements=[new Blockly.blockRendering.InRowSpacer(this.constants_,c)]}; -Blockly.utils.object.inherits(Blockly.blockRendering.SpacerRow,Blockly.blockRendering.Row);Blockly.blockRendering.SpacerRow.prototype.measure=function(){};Blockly.blockRendering.InputRow=function(a){Blockly.blockRendering.InputRow.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.INPUT_ROW;this.connectedBlockWidths=0};Blockly.utils.object.inherits(Blockly.blockRendering.InputRow,Blockly.blockRendering.Row); -Blockly.blockRendering.InputRow.prototype.measure=function(){this.width=this.minWidth;this.height=this.minHeight;for(var a=0,b=0,c;c=this.elements[b];b++)this.width+=c.width,Blockly.blockRendering.Types.isInput(c)&&(Blockly.blockRendering.Types.isStatementInput(c)?a+=c.connectedBlockWidth:Blockly.blockRendering.Types.isExternalInput(c)&&0!=c.connectedBlockWidth&&(a+=c.connectedBlockWidth-c.connectionWidth)),Blockly.blockRendering.Types.isSpacer(c)||(this.height=Math.max(this.height,c.height));this.connectedBlockWidths= -a;this.widthWithConnectedBlocks=this.width+a};Blockly.blockRendering.InputRow.prototype.endsWithElemSpacer=function(){return!this.hasExternalInput&&!this.hasStatement};Blockly.blockRendering.RenderInfo=function(a,b){this.block_=b;this.renderer_=a;this.constants_=this.renderer_.getConstants();this.outputConnection=b.outputConnection?new Blockly.blockRendering.OutputConnection(this.constants_,b.outputConnection):null;this.isInline=b.getInputsInline()&&!b.isCollapsed();this.isCollapsed=b.isCollapsed();this.isInsertionMarker=b.isInsertionMarker();this.RTL=b.RTL;this.statementEdge=this.width=this.widthWithChildren=this.height=0;this.rows=[];this.inputRows=[];this.hiddenIcons= -[];this.topRow=new Blockly.blockRendering.TopRow(this.constants_);this.bottomRow=new Blockly.blockRendering.BottomRow(this.constants_);this.startY=this.startX=0};Blockly.blockRendering.RenderInfo.prototype.getRenderer=function(){return this.renderer_};Blockly.blockRendering.RenderInfo.prototype.measure=function(){this.createRows_();this.addElemSpacing_();this.addRowSpacing_();this.computeBounds_();this.alignRowElements_();this.finalize_()}; -Blockly.blockRendering.RenderInfo.prototype.createRows_=function(){this.populateTopRow_();this.rows.push(this.topRow);var a=new Blockly.blockRendering.InputRow(this.constants_);this.inputRows.push(a);var b=this.block_.getIcons();if(b.length)for(var c=0,d;d=b[c];c++){var e=new Blockly.blockRendering.Icon(this.constants_,d);this.isCollapsed&&d.collapseHidden?this.hiddenIcons.push(e):a.elements.push(e)}d=null;for(c=0;b=this.block_.inputList[c];c++)if(b.isVisible()){this.shouldStartNewRow_(b,d)&&(this.rows.push(a), -a=new Blockly.blockRendering.InputRow(this.constants_),this.inputRows.push(a));for(d=0;e=b.fieldRow[d];d++)a.elements.push(new Blockly.blockRendering.Field(this.constants_,e,b));this.addInput_(b,a);d=b}this.isCollapsed&&(a.hasJaggedEdge=!0,a.elements.push(new Blockly.blockRendering.JaggedEdge(this.constants_)));(a.elements.length||a.hasDummyInput)&&this.rows.push(a);this.populateBottomRow_();this.rows.push(this.bottomRow)}; -Blockly.blockRendering.RenderInfo.prototype.populateTopRow_=function(){var a=!!this.block_.previousConnection,b=(this.block_.hat?"cap"===this.block_.hat:this.constants_.ADD_START_HATS)&&!this.outputConnection&&!a;this.topRow.hasLeftSquareCorner(this.block_)?this.topRow.elements.push(new Blockly.blockRendering.SquareCorner(this.constants_)):this.topRow.elements.push(new Blockly.blockRendering.RoundCorner(this.constants_));b?(a=new Blockly.blockRendering.Hat(this.constants_),this.topRow.elements.push(a), -this.topRow.capline=a.ascenderHeight):a&&(this.topRow.hasPreviousConnection=!0,this.topRow.connection=new Blockly.blockRendering.PreviousConnection(this.constants_,this.block_.previousConnection),this.topRow.elements.push(this.topRow.connection));this.block_.inputList.length&&this.block_.inputList[0].type==Blockly.NEXT_STATEMENT&&!this.block_.isCollapsed()?this.topRow.minHeight=this.constants_.TOP_ROW_PRECEDES_STATEMENT_MIN_HEIGHT:this.topRow.minHeight=this.constants_.TOP_ROW_MIN_HEIGHT;this.topRow.hasRightSquareCorner(this.block_)? -this.topRow.elements.push(new Blockly.blockRendering.SquareCorner(this.constants_,"right")):this.topRow.elements.push(new Blockly.blockRendering.RoundCorner(this.constants_,"right"))}; -Blockly.blockRendering.RenderInfo.prototype.populateBottomRow_=function(){this.bottomRow.hasNextConnection=!!this.block_.nextConnection;this.bottomRow.minHeight=this.block_.inputList.length&&this.block_.inputList[this.block_.inputList.length-1].type==Blockly.NEXT_STATEMENT?this.constants_.BOTTOM_ROW_AFTER_STATEMENT_MIN_HEIGHT:this.constants_.BOTTOM_ROW_MIN_HEIGHT;this.bottomRow.hasLeftSquareCorner(this.block_)?this.bottomRow.elements.push(new Blockly.blockRendering.SquareCorner(this.constants_)): -this.bottomRow.elements.push(new Blockly.blockRendering.RoundCorner(this.constants_));this.bottomRow.hasNextConnection&&(this.bottomRow.connection=new Blockly.blockRendering.NextConnection(this.constants_,this.block_.nextConnection),this.bottomRow.elements.push(this.bottomRow.connection));this.bottomRow.hasRightSquareCorner(this.block_)?this.bottomRow.elements.push(new Blockly.blockRendering.SquareCorner(this.constants_,"right")):this.bottomRow.elements.push(new Blockly.blockRendering.RoundCorner(this.constants_, -"right"))}; -Blockly.blockRendering.RenderInfo.prototype.addInput_=function(a,b){this.isInline&&a.type==Blockly.INPUT_VALUE?(b.elements.push(new Blockly.blockRendering.InlineInput(this.constants_,a)),b.hasInlineInput=!0):a.type==Blockly.NEXT_STATEMENT?(b.elements.push(new Blockly.blockRendering.StatementInput(this.constants_,a)),b.hasStatement=!0):a.type==Blockly.INPUT_VALUE?(b.elements.push(new Blockly.blockRendering.ExternalValueInput(this.constants_,a)),b.hasExternalInput=!0):a.type==Blockly.DUMMY_INPUT&&(b.minHeight= -Math.max(b.minHeight,a.getSourceBlock()&&a.getSourceBlock().isShadow()?this.constants_.DUMMY_INPUT_SHADOW_MIN_HEIGHT:this.constants_.DUMMY_INPUT_MIN_HEIGHT),b.hasDummyInput=!0);null==b.align&&(b.align=a.align)};Blockly.blockRendering.RenderInfo.prototype.shouldStartNewRow_=function(a,b){return b?a.type==Blockly.NEXT_STATEMENT||b.type==Blockly.NEXT_STATEMENT?!0:a.type==Blockly.INPUT_VALUE||a.type==Blockly.DUMMY_INPUT?!this.isInline:!1:!1}; -Blockly.blockRendering.RenderInfo.prototype.addElemSpacing_=function(){for(var a=0,b;b=this.rows[a];a++){var c=b.elements;b.elements=[];b.startsWithElemSpacer()&&b.elements.push(new Blockly.blockRendering.InRowSpacer(this.constants_,this.getInRowSpacing_(null,c[0])));if(c.length){for(var d=0;d.blocklyPathLight,",a+" .blocklyInsertionMarker>.blocklyPathDark {","fill-opacity: "+this.INSERTION_MARKER_OPACITY+";","stroke: none;","}"])};Blockly.geras.Highlighter=function(a){this.info_=a;this.inlineSteps_=this.steps_="";this.RTL_=this.info_.RTL;a=a.getRenderer();this.constants_=a.getConstants();this.highlightConstants_=a.getHighlightConstants();this.highlightOffset_=this.highlightConstants_.OFFSET;this.outsideCornerPaths_=this.highlightConstants_.OUTSIDE_CORNER;this.insideCornerPaths_=this.highlightConstants_.INSIDE_CORNER;this.puzzleTabPaths_=this.highlightConstants_.PUZZLE_TAB;this.notchPaths_=this.highlightConstants_.NOTCH;this.startPaths_= -this.highlightConstants_.START_HAT;this.jaggedTeethPaths_=this.highlightConstants_.JAGGED_TEETH};Blockly.geras.Highlighter.prototype.getPath=function(){return this.steps_+"\n"+this.inlineSteps_}; -Blockly.geras.Highlighter.prototype.drawTopCorner=function(a){this.steps_+=Blockly.utils.svgPaths.moveBy(a.xPos,this.info_.startY);for(var b=0,c;c=a.elements[b];b++)Blockly.blockRendering.Types.isLeftSquareCorner(c)?this.steps_+=this.highlightConstants_.START_POINT:Blockly.blockRendering.Types.isLeftRoundedCorner(c)?this.steps_+=this.outsideCornerPaths_.topLeft(this.RTL_):Blockly.blockRendering.Types.isPreviousConnection(c)?this.steps_+=this.notchPaths_.pathLeft:Blockly.blockRendering.Types.isHat(c)? -this.steps_+=this.startPaths_.path(this.RTL_):Blockly.blockRendering.Types.isSpacer(c)&&0!=c.width&&(this.steps_+=Blockly.utils.svgPaths.lineOnAxis("H",c.xPos+c.width-this.highlightOffset_));this.steps_+=Blockly.utils.svgPaths.lineOnAxis("H",a.xPos+a.width-this.highlightOffset_)};Blockly.geras.Highlighter.prototype.drawJaggedEdge_=function(a){this.info_.RTL&&(this.steps_+=this.jaggedTeethPaths_.pathLeft+Blockly.utils.svgPaths.lineOnAxis("v",a.height-this.jaggedTeethPaths_.height-this.highlightOffset_))}; -Blockly.geras.Highlighter.prototype.drawValueInput=function(a){var b=a.getLastInput();if(this.RTL_){var c=a.height-b.connectionHeight;this.steps_+=Blockly.utils.svgPaths.moveTo(b.xPos+b.width-this.highlightOffset_,a.yPos)+this.puzzleTabPaths_.pathDown(this.RTL_)+Blockly.utils.svgPaths.lineOnAxis("v",c)}else this.steps_+=Blockly.utils.svgPaths.moveTo(b.xPos+b.width,a.yPos)+this.puzzleTabPaths_.pathDown(this.RTL_)}; -Blockly.geras.Highlighter.prototype.drawStatementInput=function(a){var b=a.getLastInput();if(this.RTL_){var c=a.height-2*this.insideCornerPaths_.height;this.steps_+=Blockly.utils.svgPaths.moveTo(b.xPos,a.yPos)+this.insideCornerPaths_.pathTop(this.RTL_)+Blockly.utils.svgPaths.lineOnAxis("v",c)+this.insideCornerPaths_.pathBottom(this.RTL_)+Blockly.utils.svgPaths.lineTo(a.width-b.xPos-this.insideCornerPaths_.width,0)}else this.steps_+=Blockly.utils.svgPaths.moveTo(b.xPos,a.yPos+a.height)+this.insideCornerPaths_.pathBottom(this.RTL_)+ -Blockly.utils.svgPaths.lineTo(a.width-b.xPos-this.insideCornerPaths_.width,0)};Blockly.geras.Highlighter.prototype.drawRightSideRow=function(a){var b=a.xPos+a.width-this.highlightOffset_;a.followsStatement&&(this.steps_+=Blockly.utils.svgPaths.lineOnAxis("H",b));this.RTL_&&(this.steps_+=Blockly.utils.svgPaths.lineOnAxis("H",b),a.height>this.highlightOffset_&&(this.steps_+=Blockly.utils.svgPaths.lineOnAxis("V",a.yPos+a.height-this.highlightOffset_)))}; -Blockly.geras.Highlighter.prototype.drawBottomRow=function(a){if(this.RTL_)this.steps_+=Blockly.utils.svgPaths.lineOnAxis("V",a.baseline-this.highlightOffset_);else{var b=this.info_.bottomRow.elements[0];Blockly.blockRendering.Types.isLeftSquareCorner(b)?this.steps_+=Blockly.utils.svgPaths.moveTo(a.xPos+this.highlightOffset_,a.baseline-this.highlightOffset_):Blockly.blockRendering.Types.isLeftRoundedCorner(b)&&(this.steps_+=Blockly.utils.svgPaths.moveTo(a.xPos,a.baseline),this.steps_+=this.outsideCornerPaths_.bottomLeft())}}; -Blockly.geras.Highlighter.prototype.drawLeft=function(){var a=this.info_.outputConnection;a&&(a=a.connectionOffsetY+a.height,this.RTL_?this.steps_+=Blockly.utils.svgPaths.moveTo(this.info_.startX,a):(this.steps_+=Blockly.utils.svgPaths.moveTo(this.info_.startX+this.highlightOffset_,this.info_.bottomRow.baseline-this.highlightOffset_),this.steps_+=Blockly.utils.svgPaths.lineOnAxis("V",a)),this.steps_+=this.puzzleTabPaths_.pathUp(this.RTL_));this.RTL_||(a=this.info_.topRow,Blockly.blockRendering.Types.isLeftRoundedCorner(a.elements[0])? -this.steps_+=Blockly.utils.svgPaths.lineOnAxis("V",this.outsideCornerPaths_.height):this.steps_+=Blockly.utils.svgPaths.lineOnAxis("V",a.capline+this.highlightOffset_))}; -Blockly.geras.Highlighter.prototype.drawInlineInput=function(a){var b=this.highlightOffset_,c=a.xPos+a.connectionWidth,d=a.centerline-a.height/2,e=a.width-a.connectionWidth,f=d+b;this.RTL_?(d=a.connectionOffsetY-b,a=a.height-(a.connectionOffsetY+a.connectionHeight)+b,this.inlineSteps_+=Blockly.utils.svgPaths.moveTo(c-b,f)+Blockly.utils.svgPaths.lineOnAxis("v",d)+this.puzzleTabPaths_.pathDown(this.RTL_)+Blockly.utils.svgPaths.lineOnAxis("v",a)+Blockly.utils.svgPaths.lineOnAxis("h",e)):this.inlineSteps_+= -Blockly.utils.svgPaths.moveTo(a.xPos+a.width+b,f)+Blockly.utils.svgPaths.lineOnAxis("v",a.height)+Blockly.utils.svgPaths.lineOnAxis("h",-e)+Blockly.utils.svgPaths.moveTo(c,d+a.connectionOffsetY)+this.puzzleTabPaths_.pathDown(this.RTL_)};Blockly.geras.InlineInput=function(a,b){Blockly.geras.InlineInput.superClass_.constructor.call(this,a,b);this.connectedBlock&&(this.width+=this.constants_.DARK_PATH_OFFSET,this.height+=this.constants_.DARK_PATH_OFFSET)};Blockly.utils.object.inherits(Blockly.geras.InlineInput,Blockly.blockRendering.InlineInput);Blockly.geras.StatementInput=function(a,b){Blockly.geras.StatementInput.superClass_.constructor.call(this,a,b);this.connectedBlock&&(this.height+=this.constants_.DARK_PATH_OFFSET)}; -Blockly.utils.object.inherits(Blockly.geras.StatementInput,Blockly.blockRendering.StatementInput);Blockly.geras.RenderInfo=function(a,b){Blockly.geras.RenderInfo.superClass_.constructor.call(this,a,b)};Blockly.utils.object.inherits(Blockly.geras.RenderInfo,Blockly.blockRendering.RenderInfo);Blockly.geras.RenderInfo.prototype.getRenderer=function(){return this.renderer_}; -Blockly.geras.RenderInfo.prototype.populateBottomRow_=function(){Blockly.geras.RenderInfo.superClass_.populateBottomRow_.call(this);this.block_.inputList.length&&this.block_.inputList[this.block_.inputList.length-1].type==Blockly.NEXT_STATEMENT||(this.bottomRow.minHeight=this.constants_.MEDIUM_PADDING-this.constants_.DARK_PATH_OFFSET)}; -Blockly.geras.RenderInfo.prototype.addInput_=function(a,b){this.isInline&&a.type==Blockly.INPUT_VALUE?(b.elements.push(new Blockly.geras.InlineInput(this.constants_,a)),b.hasInlineInput=!0):a.type==Blockly.NEXT_STATEMENT?(b.elements.push(new Blockly.geras.StatementInput(this.constants_,a)),b.hasStatement=!0):a.type==Blockly.INPUT_VALUE?(b.elements.push(new Blockly.blockRendering.ExternalValueInput(this.constants_,a)),b.hasExternalInput=!0):a.type==Blockly.DUMMY_INPUT&&(b.minHeight=Math.max(b.minHeight, -this.constants_.DUMMY_INPUT_MIN_HEIGHT),b.hasDummyInput=!0);this.isInline||null!=b.align||(b.align=a.align)}; -Blockly.geras.RenderInfo.prototype.addElemSpacing_=function(){for(var a=!1,b=0,c;c=this.rows[b];b++)c.hasExternalInput&&(a=!0);for(b=0;c=this.rows[b];b++){var d=c.elements;c.elements=[];c.startsWithElemSpacer()&&c.elements.push(new Blockly.blockRendering.InRowSpacer(this.constants_,this.getInRowSpacing_(null,d[0])));if(d.length){for(var e=0;eb?b:f;e=e?-1:1;c=(d?-1:1)*c/2;return Blockly.utils.svgPaths.lineTo(-e*f,c)+Blockly.utils.svgPaths.lineTo(e*f,c)}var b=this.MAX_DYNAMIC_CONNECTION_SHAPE_WIDTH;return{type:this.SHAPES.HEXAGONAL,isDynamic:!0,width:function(c){c/=2;return c>b?b:c},height:function(c){return c},connectionOffsetY:function(c){return c/2},connectionOffsetX:function(c){return-c},pathDown:function(c){return a(c,!1,!1)},pathUp:function(c){return a(c, -!0,!1)},pathRightDown:function(c){return a(c,!1,!0)},pathRightUp:function(c){return a(c,!1,!0)}}}; -Blockly.zelos.ConstantProvider.prototype.makeRounded=function(){function a(d,e,f){var g=d>c?d-c:0;d=(d>c?c:d)/2;return Blockly.utils.svgPaths.arc("a","0 0,1",d,Blockly.utils.svgPaths.point((e?-1:1)*d,(e?-1:1)*d))+Blockly.utils.svgPaths.lineOnAxis("v",(f?1:-1)*g)+Blockly.utils.svgPaths.arc("a","0 0,1",d,Blockly.utils.svgPaths.point((e?1:-1)*d,(e?-1:1)*d))}var b=this.MAX_DYNAMIC_CONNECTION_SHAPE_WIDTH,c=2*b;return{type:this.SHAPES.ROUND,isDynamic:!0,width:function(d){d/=2;return d>b?b:d},height:function(d){return d}, -connectionOffsetY:function(d){return d/2},connectionOffsetX:function(d){return-d},pathDown:function(d){return a(d,!1,!1)},pathUp:function(d){return a(d,!0,!1)},pathRightDown:function(d){return a(d,!1,!0)},pathRightUp:function(d){return a(d,!1,!0)}}}; -Blockly.zelos.ConstantProvider.prototype.makeSquared=function(){function a(c,d,e){c-=2*b;return Blockly.utils.svgPaths.arc("a","0 0,1",b,Blockly.utils.svgPaths.point((d?-1:1)*b,(d?-1:1)*b))+Blockly.utils.svgPaths.lineOnAxis("v",(e?1:-1)*c)+Blockly.utils.svgPaths.arc("a","0 0,1",b,Blockly.utils.svgPaths.point((d?1:-1)*b,(d?-1:1)*b))}var b=this.CORNER_RADIUS;return{type:this.SHAPES.SQUARE,isDynamic:!0,width:function(c){return b},height:function(c){return c},connectionOffsetY:function(c){return c/2}, -connectionOffsetX:function(c){return-c},pathDown:function(c){return a(c,!1,!1)},pathUp:function(c){return a(c,!0,!1)},pathRightDown:function(c){return a(c,!1,!0)},pathRightUp:function(c){return a(c,!1,!0)}}}; -Blockly.zelos.ConstantProvider.prototype.shapeFor=function(a){var b=a.getCheck();!b&&a.targetConnection&&(b=a.targetConnection.getCheck());switch(a.type){case Blockly.INPUT_VALUE:case Blockly.OUTPUT_VALUE:a=a.getSourceBlock().getOutputShape();if(null!=a)switch(a){case this.SHAPES.HEXAGONAL:return this.HEXAGONAL;case this.SHAPES.ROUND:return this.ROUNDED;case this.SHAPES.SQUARE:return this.SQUARED}if(b&&-1!=b.indexOf("Boolean"))return this.HEXAGONAL;if(b&&-1!=b.indexOf("Number"))return this.ROUNDED; -b&&b.indexOf("String");return this.ROUNDED;case Blockly.PREVIOUS_STATEMENT:case Blockly.NEXT_STATEMENT:return this.NOTCH;default:throw Error("Unknown type");}}; -Blockly.zelos.ConstantProvider.prototype.makeNotch=function(){function a(l){return Blockly.utils.svgPaths.curve("c",[Blockly.utils.svgPaths.point(l*e/2,0),Blockly.utils.svgPaths.point(l*e*3/4,g/2),Blockly.utils.svgPaths.point(l*e,g)])+Blockly.utils.svgPaths.line([Blockly.utils.svgPaths.point(l*e,f)])+Blockly.utils.svgPaths.curve("c",[Blockly.utils.svgPaths.point(l*e/4,g/2),Blockly.utils.svgPaths.point(l*e/2,g),Blockly.utils.svgPaths.point(l*e,g)])+Blockly.utils.svgPaths.lineOnAxis("h",l*d)+Blockly.utils.svgPaths.curve("c", -[Blockly.utils.svgPaths.point(l*e/2,0),Blockly.utils.svgPaths.point(l*e*3/4,-(g/2)),Blockly.utils.svgPaths.point(l*e,-g)])+Blockly.utils.svgPaths.line([Blockly.utils.svgPaths.point(l*e,-f)])+Blockly.utils.svgPaths.curve("c",[Blockly.utils.svgPaths.point(l*e/4,-(g/2)),Blockly.utils.svgPaths.point(l*e/2,-g),Blockly.utils.svgPaths.point(l*e,-g)])}var b=this.NOTCH_WIDTH,c=this.NOTCH_HEIGHT,d=b/3,e=d/3,f=c/2,g=f/2,h=a(1),k=a(-1);return{type:this.SHAPES.NOTCH,width:b,height:c,pathLeft:h,pathRight:k}}; -Blockly.zelos.ConstantProvider.prototype.makeInsideCorners=function(){var a=this.CORNER_RADIUS,b=Blockly.utils.svgPaths.arc("a","0 0,0",a,Blockly.utils.svgPaths.point(-a,a)),c=Blockly.utils.svgPaths.arc("a","0 0,1",a,Blockly.utils.svgPaths.point(-a,a)),d=Blockly.utils.svgPaths.arc("a","0 0,0",a,Blockly.utils.svgPaths.point(a,a)),e=Blockly.utils.svgPaths.arc("a","0 0,1",a,Blockly.utils.svgPaths.point(a,a));return{width:a,height:a,pathTop:b,pathBottom:d,rightWidth:a,rightHeight:a,pathTopRight:c,pathBottomRight:e}}; -Blockly.zelos.ConstantProvider.prototype.generateSecondaryColour_=function(a){return Blockly.utils.colour.blend("#000",a,.15)||a};Blockly.zelos.ConstantProvider.prototype.generateTertiaryColour_=function(a){return Blockly.utils.colour.blend("#000",a,.25)||a}; -Blockly.zelos.ConstantProvider.prototype.createDom=function(a,b,c){Blockly.zelos.ConstantProvider.superClass_.createDom.call(this,a,b,c);a=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.DEFS,{},a);b=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FILTER,{id:"blocklySelectedGlowFilter"+this.randomIdentifier,height:"160%",width:"180%",y:"-30%",x:"-40%"},a);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FEGAUSSIANBLUR,{"in":"SourceGraphic",stdDeviation:this.SELECTED_GLOW_SIZE},b);c= -Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FECOMPONENTTRANSFER,{result:"outBlur"},b);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FEFUNCA,{type:"table",tableValues:"0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"},c);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FEFLOOD,{"flood-color":this.SELECTED_GLOW_COLOUR,"flood-opacity":1,result:"outColor"},b);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FECOMPOSITE,{"in":"outColor",in2:"outBlur",operator:"in",result:"outGlow"},b);this.selectedGlowFilterId= -b.id;this.selectedGlowFilter_=b;a=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FILTER,{id:"blocklyReplacementGlowFilter"+this.randomIdentifier,height:"160%",width:"180%",y:"-30%",x:"-40%"},a);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FEGAUSSIANBLUR,{"in":"SourceGraphic",stdDeviation:this.REPLACEMENT_GLOW_SIZE},a);b=Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FECOMPONENTTRANSFER,{result:"outBlur"},a);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FEFUNCA,{type:"table", -tableValues:"0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"},b);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FEFLOOD,{"flood-color":this.REPLACEMENT_GLOW_COLOUR,"flood-opacity":1,result:"outColor"},a);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FECOMPOSITE,{"in":"outColor",in2:"outBlur",operator:"in",result:"outGlow"},a);Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.FECOMPOSITE,{"in":"SourceGraphic",in2:"outGlow",operator:"over"},a);this.replacementGlowFilterId=a.id;this.replacementGlowFilter_= -a}; -Blockly.zelos.ConstantProvider.prototype.getCSS_=function(a){return[a+" .blocklyText,",a+" .blocklyFlyoutLabelText {","font: "+this.FIELD_TEXT_FONTWEIGHT+" "+this.FIELD_TEXT_FONTSIZE+"pt "+this.FIELD_TEXT_FONTFAMILY+";","}",a+" .blocklyText {","fill: #fff;","}",a+" .blocklyNonEditableText>rect:not(.blocklyDropdownRect),",a+" .blocklyEditableText>rect:not(.blocklyDropdownRect) {","fill: "+this.FIELD_BORDER_RECT_COLOUR+";","}",a+" .blocklyNonEditableText>text,",a+" .blocklyEditableText>text,",a+" .blocklyNonEditableText>g>text,", -a+" .blocklyEditableText>g>text {","fill: #575E75;","}",a+" .blocklyFlyoutLabelText {","fill: #575E75;","}",a+" .blocklyText.blocklyBubbleText {","fill: #575E75;","}",a+" .blocklyDraggable:not(.blocklyDisabled)"," .blocklyEditableText:not(.editing):hover>rect,",a+" .blocklyDraggable:not(.blocklyDisabled)"," .blocklyEditableText:not(.editing):hover>.blocklyPath {","stroke: #fff;","stroke-width: 2;","}",a+" .blocklyHtmlInput {","font-family: "+this.FIELD_TEXT_FONTFAMILY+";","font-weight: "+this.FIELD_TEXT_FONTWEIGHT+ -";","color: #575E75;","}",a+" .blocklyDropdownText {","fill: #fff !important;","}",a+".blocklyWidgetDiv .goog-menuitem,",a+".blocklyDropDownDiv .goog-menuitem {","font-family: "+this.FIELD_TEXT_FONTFAMILY+";","}",a+".blocklyDropDownDiv .goog-menuitem-content {","color: #fff;","}",a+" .blocklyHighlightedConnectionPath {","stroke: "+this.SELECTED_GLOW_COLOUR+";","}",a+" .blocklyDisabled > .blocklyOutlinePath {","fill: url(#blocklyDisabledPattern"+this.randomIdentifier+")","}",a+" .blocklyInsertionMarker>.blocklyPath {", -"fill-opacity: "+this.INSERTION_MARKER_OPACITY+";","stroke: none;","}"]};Blockly.zelos.TopRow=function(a){Blockly.zelos.TopRow.superClass_.constructor.call(this,a)};Blockly.utils.object.inherits(Blockly.zelos.TopRow,Blockly.blockRendering.TopRow);Blockly.zelos.TopRow.prototype.endsWithElemSpacer=function(){return!1};Blockly.zelos.TopRow.prototype.hasLeftSquareCorner=function(a){var b=(a.hat?"cap"===a.hat:this.constants_.ADD_START_HATS)&&!a.outputConnection&&!a.previousConnection;return!!a.outputConnection||b}; -Blockly.zelos.TopRow.prototype.hasRightSquareCorner=function(a){return!!a.outputConnection&&!a.statementInputCount&&!a.nextConnection};Blockly.zelos.BottomRow=function(a){Blockly.zelos.BottomRow.superClass_.constructor.call(this,a)};Blockly.utils.object.inherits(Blockly.zelos.BottomRow,Blockly.blockRendering.BottomRow);Blockly.zelos.BottomRow.prototype.endsWithElemSpacer=function(){return!1};Blockly.zelos.BottomRow.prototype.hasLeftSquareCorner=function(a){return!!a.outputConnection}; -Blockly.zelos.BottomRow.prototype.hasRightSquareCorner=function(a){return!!a.outputConnection&&!a.statementInputCount&&!a.nextConnection};Blockly.zelos.RightConnectionShape=function(a){Blockly.zelos.RightConnectionShape.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.getType("RIGHT_CONNECTION");this.width=this.height=0};Blockly.utils.object.inherits(Blockly.zelos.RightConnectionShape,Blockly.blockRendering.Measurable);Blockly.zelos.StatementInput=function(a,b){Blockly.zelos.StatementInput.superClass_.constructor.call(this,a,b);if(this.connectedBlock){for(a=this.connectedBlock;a.getNextBlock();)a=a.getNextBlock();a.nextConnection||(this.height=this.connectedBlockHeight,this.connectedBottomNextConnection=!0)}};Blockly.utils.object.inherits(Blockly.zelos.StatementInput,Blockly.blockRendering.StatementInput);Blockly.zelos.RenderInfo=function(a,b){Blockly.zelos.RenderInfo.superClass_.constructor.call(this,a,b);this.topRow=new Blockly.zelos.TopRow(this.constants_);this.bottomRow=new Blockly.zelos.BottomRow(this.constants_);this.isInline=!0;this.isMultiRow=!b.getInputsInline()||b.isCollapsed();this.hasStatementInput=0=this.rows.length-1?!!this.bottomRow.hasNextConnection:!!f.precedesStatement;if(Blockly.blockRendering.Types.isInputRow(e)&&e.hasStatement)e.measure(),b=e.width-e.getLastInput().width+a;else if(d&&(2==c||f)&& -Blockly.blockRendering.Types.isInputRow(e)&&!e.hasStatement){f=e.xPos;d=null;for(var g=0,h;h=e.elements[g];g++)Blockly.blockRendering.Types.isSpacer(h)&&(d=h),!(d&&(Blockly.blockRendering.Types.isField(h)||Blockly.blockRendering.Types.isInput(h))&&fc?c:this.height/2,b-c*(1-Math.sin(Math.acos((c-this.constants_.SMALL_PADDING)/c)));default:return 0}if(Blockly.blockRendering.Types.isInlineInput(a)){var e=a.connectedBlock;a=e?e.pathObject.outputShapeType: -a.shape.type;return e&&e.outputConnection&&(e.statementInputCount||e.nextConnection)||c==d.SHAPES.HEXAGONAL&&c!=a?0:b-this.constants_.SHAPE_IN_SHAPE_PADDING[c][a]}return Blockly.blockRendering.Types.isField(a)?c==d.SHAPES.ROUND&&a.field instanceof Blockly.FieldTextInput?b-2.75*d.GRID_UNIT:b-this.constants_.SHAPE_IN_SHAPE_PADDING[c][0]:Blockly.blockRendering.Types.isIcon(a)?this.constants_.SMALL_PADDING:0}; -Blockly.zelos.RenderInfo.prototype.finalizeVerticalAlignment_=function(){if(!this.outputConnection)for(var a=2;a=this.rows.length-1?!!this.bottomRow.hasNextConnection:!!d.precedesStatement;if(e?this.topRow.hasPreviousConnection:b.followsStatement){var g=3==c.elements.length&&(c.elements[1].field instanceof Blockly.FieldLabel||c.elements[1].field instanceof Blockly.FieldImage);if(!e&&g)b.height-=this.constants_.SMALL_PADDING, -d.height-=this.constants_.SMALL_PADDING,c.height-=this.constants_.MEDIUM_PADDING;else if(!e&&!f)b.height+=this.constants_.SMALL_PADDING;else if(f){e=!1;for(f=0;g=c.elements[f];f++)if(Blockly.blockRendering.Types.isInlineInput(g)&&g.connectedBlock&&!g.connectedBlock.isShadow()&&40<=g.connectedBlock.getHeightWidth().height){e=!0;break}e&&(b.height-=this.constants_.SMALL_PADDING,d.height-=this.constants_.SMALL_PADDING)}}}}; -Blockly.zelos.RenderInfo.prototype.finalize_=function(){this.finalizeOutputConnection_();this.finalizeHorizontalAlignment_();this.finalizeVerticalAlignment_();Blockly.zelos.RenderInfo.superClass_.finalize_.call(this);this.rightSide&&(this.widthWithChildren+=this.rightSide.width)};Blockly.zelos.Drawer=function(a,b){Blockly.zelos.Drawer.superClass_.constructor.call(this,a,b)};Blockly.utils.object.inherits(Blockly.zelos.Drawer,Blockly.blockRendering.Drawer); -Blockly.zelos.Drawer.prototype.draw=function(){var a=this.block_.pathObject;a.beginDrawing();this.hideHiddenIcons_();this.drawOutline_();this.drawInternals_();a.setPath(this.outlinePath_+"\n"+this.inlinePath_);this.info_.RTL&&a.flipRTL();Blockly.blockRendering.useDebugger&&this.block_.renderingDebugger.drawDebug(this.block_,this.info_);this.recordSizeOnBlock_();this.info_.outputConnection&&(a.outputShapeType=this.info_.outputConnection.shape.type);a.endDrawing()}; -Blockly.zelos.Drawer.prototype.drawOutline_=function(){this.info_.outputConnection&&this.info_.outputConnection.isDynamicShape&&!this.info_.hasStatementInput&&!this.info_.bottomRow.hasNextConnection?(this.drawFlatTop_(),this.drawRightDynamicConnection_(),this.drawFlatBottom_(),this.drawLeftDynamicConnection_()):Blockly.zelos.Drawer.superClass_.drawOutline_.call(this)}; -Blockly.zelos.Drawer.prototype.drawLeft_=function(){this.info_.outputConnection&&this.info_.outputConnection.isDynamicShape?this.drawLeftDynamicConnection_():Blockly.zelos.Drawer.superClass_.drawLeft_.call(this)}; -Blockly.zelos.Drawer.prototype.drawRightSideRow_=function(a){if(!(0>=a.height))if(a.precedesStatement||a.followsStatement){var b=this.constants_.INSIDE_CORNERS.rightHeight;b=a.height-(a.precedesStatement?b:0);this.outlinePath_+=(a.followsStatement?this.constants_.INSIDE_CORNERS.pathBottomRight:"")+(0'); - document.write(''); -} diff --git a/blocks/blocks.ts b/blocks/blocks.ts new file mode 100644 index 00000000000..a9874e54df1 --- /dev/null +++ b/blocks/blocks.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.libraryBlocks + +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 { + lists, + logic, + 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( + {}, + lists.blocks, + logic.blocks, + loops.blocks, + math.blocks, + procedures.blocks, + texts.blocks, + variables.blocks, + variablesDynamic.blocks, +); diff --git a/blocks/colour.js b/blocks/colour.js deleted file mode 100644 index 9ab3acb9517..00000000000 --- a/blocks/colour.js +++ /dev/null @@ -1,122 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Colour blocks for Blockly. - * - * This file is scraped to extract a .json file of block definitions. The array - * passed to defineBlocksWithJsonArray(..) must be strict JSON: double quotes - * only, no outside references, no functions, no trailing commas, etc. The one - * exception is end-of-line comments, which the scraper will remove. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Blocks.colour'); // Deprecated -goog.provide('Blockly.Constants.Colour'); - -goog.require('Blockly'); -goog.require('Blockly.Blocks'); -goog.require('Blockly.FieldColour'); -goog.require('Blockly.FieldLabel'); - - -/** - * Unused constant for the common HSV hue for all blocks in this category. - * @deprecated Use Blockly.Msg['COLOUR_HUE']. (2018 April 5) - */ -Blockly.Constants.Colour.HUE = 20; - -Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT - // Block for colour picker. - { - "type": "colour_picker", - "message0": "%1", - "args0": [ - { - "type": "field_colour", - "name": "COLOUR", - "colour": "#ff0000" - } - ], - "output": "Colour", - "helpUrl": "%{BKY_COLOUR_PICKER_HELPURL}", - "style": "colour_blocks", - "tooltip": "%{BKY_COLOUR_PICKER_TOOLTIP}", - "extensions": ["parent_tooltip_when_inline"] - }, - - // Block for random colour. - { - "type": "colour_random", - "message0": "%{BKY_COLOUR_RANDOM_TITLE}", - "output": "Colour", - "helpUrl": "%{BKY_COLOUR_RANDOM_HELPURL}", - "style": "colour_blocks", - "tooltip": "%{BKY_COLOUR_RANDOM_TOOLTIP}" - }, - - // Block for composing a colour from RGB components. - { - "type": "colour_rgb", - "message0": "%{BKY_COLOUR_RGB_TITLE} %{BKY_COLOUR_RGB_RED} %1 %{BKY_COLOUR_RGB_GREEN} %2 %{BKY_COLOUR_RGB_BLUE} %3", - "args0": [ - { - "type": "input_value", - "name": "RED", - "check": "Number", - "align": "RIGHT" - }, - { - "type": "input_value", - "name": "GREEN", - "check": "Number", - "align": "RIGHT" - }, - { - "type": "input_value", - "name": "BLUE", - "check": "Number", - "align": "RIGHT" - } - ], - "output": "Colour", - "helpUrl": "%{BKY_COLOUR_RGB_HELPURL}", - "style": "colour_blocks", - "tooltip": "%{BKY_COLOUR_RGB_TOOLTIP}" - }, - - // 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", - "args0": [ - { - "type": "input_value", - "name": "COLOUR1", - "check": "Colour", - "align": "RIGHT" - }, - { - "type": "input_value", - "name": "COLOUR2", - "check": "Colour", - "align": "RIGHT" - }, - { - "type": "input_value", - "name": "RATIO", - "check": "Number", - "align": "RIGHT" - } - ], - "output": "Colour", - "helpUrl": "%{BKY_COLOUR_BLEND_HELPURL}", - "style": "colour_blocks", - "tooltip": "%{BKY_COLOUR_BLEND_TOOLTIP}" - } -]); // END JSON EXTRACT (Do not delete this comment.) diff --git a/blocks/lists.js b/blocks/lists.js deleted file mode 100644 index 44167685eae..00000000000 --- a/blocks/lists.js +++ /dev/null @@ -1,862 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview List blocks for Blockly. - * - * This file is scraped to extract a .json file of block definitions. The array - * passed to defineBlocksWithJsonArray(..) must be strict JSON: double quotes - * only, no outside references, no functions, no trailing commas, etc. The one - * exception is end-of-line comments, which the scraper will remove. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Blocks.lists'); // Deprecated -goog.provide('Blockly.Constants.Lists'); - -goog.require('Blockly'); -goog.require('Blockly.Blocks'); -goog.require('Blockly.FieldDropdown'); -goog.require('Blockly.FieldLabel'); -goog.require('Blockly.Mutator'); - - -/** - * Unused constant for the common HSV hue for all blocks in this category. - * @deprecated Use Blockly.Msg['LISTS_HUE']. (2018 April 5) - */ -Blockly.Constants.Lists.HUE = 260; - -Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT - // Block for creating an empty list - // The 'list_create_with' block is preferred as it is more flexible. - // - // - // - { - "type": "lists_create_empty", - "message0": "%{BKY_LISTS_CREATE_EMPTY_TITLE}", - "output": "Array", - "style": "list_blocks", - "tooltip": "%{BKY_LISTS_CREATE_EMPTY_TOOLTIP}", - "helpUrl": "%{BKY_LISTS_CREATE_EMPTY_HELPURL}" - }, - // Block for creating a list with one element repeated. - { - "type": "lists_repeat", - "message0": "%{BKY_LISTS_REPEAT_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "ITEM" - }, - { - "type": "input_value", - "name": "NUM", - "check": "Number" - } - ], - "output": "Array", - "style": "list_blocks", - "tooltip": "%{BKY_LISTS_REPEAT_TOOLTIP}", - "helpUrl": "%{BKY_LISTS_REPEAT_HELPURL}" - }, - // Block for reversing a list. - { - "type": "lists_reverse", - "message0": "%{BKY_LISTS_REVERSE_MESSAGE0}", - "args0": [ - { - "type": "input_value", - "name": "LIST", - "check": "Array" - } - ], - "output": "Array", - "inputsInline": true, - "style": "list_blocks", - "tooltip": "%{BKY_LISTS_REVERSE_TOOLTIP}", - "helpUrl": "%{BKY_LISTS_REVERSE_HELPURL}" - }, - // Block for checking if a list is empty - { - "type": "lists_isEmpty", - "message0": "%{BKY_LISTS_ISEMPTY_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "VALUE", - "check": ["String", "Array"] - } - ], - "output": "Boolean", - "style": "list_blocks", - "tooltip": "%{BKY_LISTS_ISEMPTY_TOOLTIP}", - "helpUrl": "%{BKY_LISTS_ISEMPTY_HELPURL}" - }, - // Block for getting the list length - { - "type": "lists_length", - "message0": "%{BKY_LISTS_LENGTH_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "VALUE", - "check": ["String", "Array"] - } - ], - "output": "Number", - "style": "list_blocks", - "tooltip": "%{BKY_LISTS_LENGTH_TOOLTIP}", - "helpUrl": "%{BKY_LISTS_LENGTH_HELPURL}" - } -]); // END JSON EXTRACT (Do not delete this comment.) - -Blockly.Blocks['lists_create_with'] = { - /** - * Block for creating a list with any number of elements of any type. - * @this {Blockly.Block} - */ - init: function() { - this.setHelpUrl(Blockly.Msg['LISTS_CREATE_WITH_HELPURL']); - this.setStyle('list_blocks'); - this.itemCount_ = 3; - this.updateShape_(); - this.setOutput(true, 'Array'); - this.setMutator(new Blockly.Mutator(['lists_create_with_item'])); - this.setTooltip(Blockly.Msg['LISTS_CREATE_WITH_TOOLTIP']); - }, - /** - * Create XML to represent list inputs. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('items', this.itemCount_); - return container; - }, - /** - * Parse XML to restore the list inputs. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10); - this.updateShape_(); - }, - /** - * Populate the mutator's dialog with this block's components. - * @param {!Blockly.Workspace} workspace Mutator's workspace. - * @return {!Blockly.Block} Root block in mutator. - * @this {Blockly.Block} - */ - decompose: function(workspace) { - var containerBlock = workspace.newBlock('lists_create_with_container'); - containerBlock.initSvg(); - var connection = containerBlock.getInput('STACK').connection; - for (var i = 0; i < this.itemCount_; i++) { - var itemBlock = workspace.newBlock('lists_create_with_item'); - itemBlock.initSvg(); - connection.connect(itemBlock.previousConnection); - connection = itemBlock.nextConnection; - } - return containerBlock; - }, - /** - * Reconfigure this block based on the mutator dialog's components. - * @param {!Blockly.Block} containerBlock Root block in mutator. - * @this {Blockly.Block} - */ - compose: function(containerBlock) { - var itemBlock = containerBlock.getInputTargetBlock('STACK'); - // Count number of inputs. - var connections = []; - while (itemBlock) { - connections.push(itemBlock.valueConnection_); - itemBlock = itemBlock.nextConnection && - itemBlock.nextConnection.targetBlock(); - } - // Disconnect any children that don't belong. - for (var i = 0; i < this.itemCount_; i++) { - var connection = this.getInput('ADD' + i).connection.targetConnection; - if (connection && connections.indexOf(connection) == -1) { - connection.disconnect(); - } - } - this.itemCount_ = connections.length; - this.updateShape_(); - // Reconnect any child blocks. - for (var i = 0; i < this.itemCount_; i++) { - Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i); - } - }, - /** - * Store pointers to any connected child blocks. - * @param {!Blockly.Block} containerBlock Root block in mutator. - * @this {Blockly.Block} - */ - saveConnections: function(containerBlock) { - var itemBlock = containerBlock.getInputTargetBlock('STACK'); - var i = 0; - while (itemBlock) { - var input = this.getInput('ADD' + i); - itemBlock.valueConnection_ = input && input.connection.targetConnection; - i++; - itemBlock = itemBlock.nextConnection && - itemBlock.nextConnection.targetBlock(); - } - }, - /** - * Modify this block to have the correct number of inputs. - * @private - * @this {Blockly.Block} - */ - updateShape_: function() { - if (this.itemCount_ && this.getInput('EMPTY')) { - this.removeInput('EMPTY'); - } else if (!this.itemCount_ && !this.getInput('EMPTY')) { - this.appendDummyInput('EMPTY') - .appendField(Blockly.Msg['LISTS_CREATE_EMPTY_TITLE']); - } - // Add new inputs. - for (var i = 0; i < this.itemCount_; i++) { - if (!this.getInput('ADD' + i)) { - var input = this.appendValueInput('ADD' + i) - .setAlign(Blockly.ALIGN_RIGHT); - if (i == 0) { - input.appendField(Blockly.Msg['LISTS_CREATE_WITH_INPUT_WITH']); - } - } - } - // Remove deleted inputs. - while (this.getInput('ADD' + i)) { - this.removeInput('ADD' + i); - i++; - } - } -}; - -Blockly.Blocks['lists_create_with_container'] = { - /** - * Mutator block for list container. - * @this {Blockly.Block} - */ - init: function() { - this.setStyle('list_blocks'); - this.appendDummyInput() - .appendField(Blockly.Msg['LISTS_CREATE_WITH_CONTAINER_TITLE_ADD']); - this.appendStatementInput('STACK'); - this.setTooltip(Blockly.Msg['LISTS_CREATE_WITH_CONTAINER_TOOLTIP']); - this.contextMenu = false; - } -}; - -Blockly.Blocks['lists_create_with_item'] = { - /** - * Mutator block for adding items. - * @this {Blockly.Block} - */ - init: function() { - this.setStyle('list_blocks'); - this.appendDummyInput() - .appendField(Blockly.Msg['LISTS_CREATE_WITH_ITEM_TITLE']); - this.setPreviousStatement(true); - this.setNextStatement(true); - this.setTooltip(Blockly.Msg['LISTS_CREATE_WITH_ITEM_TOOLTIP']); - this.contextMenu = false; - } -}; - -Blockly.Blocks['lists_indexOf'] = { - /** - * Block for finding an item in the list. - * @this {Blockly.Block} - */ - init: function() { - var OPERATORS = - [ - [Blockly.Msg['LISTS_INDEX_OF_FIRST'], 'FIRST'], - [Blockly.Msg['LISTS_INDEX_OF_LAST'], 'LAST'] - ]; - this.setHelpUrl(Blockly.Msg['LISTS_INDEX_OF_HELPURL']); - this.setStyle('list_blocks'); - this.setOutput(true, 'Number'); - this.appendValueInput('VALUE') - .setCheck('Array') - .appendField(Blockly.Msg['LISTS_INDEX_OF_INPUT_IN_LIST']); - this.appendValueInput('FIND') - .appendField(new Blockly.FieldDropdown(OPERATORS), 'END'); - this.setInputsInline(true); - // Assign 'this' to a variable for use in the tooltip closure below. - var thisBlock = this; - this.setTooltip(function() { - return Blockly.Msg['LISTS_INDEX_OF_TOOLTIP'].replace('%1', - thisBlock.workspace.options.oneBasedIndex ? '0' : '-1'); - }); - } -}; - -Blockly.Blocks['lists_getIndex'] = { - /** - * Block for getting element at index. - * @this {Blockly.Block} - */ - init: function() { - var MODE = - [ - [Blockly.Msg['LISTS_GET_INDEX_GET'], 'GET'], - [Blockly.Msg['LISTS_GET_INDEX_GET_REMOVE'], 'GET_REMOVE'], - [Blockly.Msg['LISTS_GET_INDEX_REMOVE'], 'REMOVE'] - ]; - this.WHERE_OPTIONS = - [ - [Blockly.Msg['LISTS_GET_INDEX_FROM_START'], 'FROM_START'], - [Blockly.Msg['LISTS_GET_INDEX_FROM_END'], 'FROM_END'], - [Blockly.Msg['LISTS_GET_INDEX_FIRST'], 'FIRST'], - [Blockly.Msg['LISTS_GET_INDEX_LAST'], 'LAST'], - [Blockly.Msg['LISTS_GET_INDEX_RANDOM'], 'RANDOM'] - ]; - this.setHelpUrl(Blockly.Msg['LISTS_GET_INDEX_HELPURL']); - this.setStyle('list_blocks'); - var modeMenu = new Blockly.FieldDropdown(MODE, function(value) { - var isStatement = (value == 'REMOVE'); - this.getSourceBlock().updateStatement_(isStatement); - }); - this.appendValueInput('VALUE') - .setCheck('Array') - .appendField(Blockly.Msg['LISTS_GET_INDEX_INPUT_IN_LIST']); - this.appendDummyInput() - .appendField(modeMenu, 'MODE') - .appendField('', 'SPACE'); - this.appendDummyInput('AT'); - if (Blockly.Msg['LISTS_GET_INDEX_TAIL']) { - this.appendDummyInput('TAIL') - .appendField(Blockly.Msg['LISTS_GET_INDEX_TAIL']); - } - this.setInputsInline(true); - this.setOutput(true); - this.updateAt_(true); - // Assign 'this' to a variable for use in the tooltip closure below. - var thisBlock = this; - this.setTooltip(function() { - var mode = thisBlock.getFieldValue('MODE'); - var where = thisBlock.getFieldValue('WHERE'); - var tooltip = ''; - switch (mode + ' ' + where) { - case 'GET FROM_START': - case 'GET FROM_END': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_GET_FROM']; - break; - case 'GET FIRST': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_GET_FIRST']; - break; - case 'GET LAST': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_GET_LAST']; - break; - case 'GET RANDOM': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_GET_RANDOM']; - break; - case 'GET_REMOVE FROM_START': - case 'GET_REMOVE FROM_END': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM']; - break; - case 'GET_REMOVE FIRST': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST']; - break; - case 'GET_REMOVE LAST': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_LAST']; - break; - case 'GET_REMOVE RANDOM': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_RANDOM']; - break; - case 'REMOVE FROM_START': - case 'REMOVE FROM_END': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM']; - break; - case 'REMOVE FIRST': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST']; - break; - case 'REMOVE LAST': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST']; - break; - case 'REMOVE RANDOM': - tooltip = Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM']; - break; - } - if (where == 'FROM_START' || where == 'FROM_END') { - var msg = (where == 'FROM_START') ? - Blockly.Msg['LISTS_INDEX_FROM_START_TOOLTIP'] : - Blockly.Msg['LISTS_INDEX_FROM_END_TOOLTIP']; - tooltip += ' ' + msg.replace('%1', - thisBlock.workspace.options.oneBasedIndex ? '#1' : '#0'); - } - return tooltip; - }); - }, - /** - * Create XML to represent whether the block is a statement or a value. - * Also represent whether there is an 'AT' input. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - var isStatement = !this.outputConnection; - container.setAttribute('statement', isStatement); - var isAt = this.getInput('AT').type == Blockly.INPUT_VALUE; - container.setAttribute('at', isAt); - return container; - }, - /** - * Parse XML to restore the 'AT' input. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - // Note: Until January 2013 this block did not have mutations, - // so 'statement' defaults to false and 'at' defaults to true. - var isStatement = (xmlElement.getAttribute('statement') == 'true'); - this.updateStatement_(isStatement); - var isAt = (xmlElement.getAttribute('at') != 'false'); - this.updateAt_(isAt); - }, - /** - * Switch between a value block and a statement block. - * @param {boolean} newStatement True if the block should be a statement. - * False if the block should be a value. - * @private - * @this {Blockly.Block} - */ - updateStatement_: function(newStatement) { - var oldStatement = !this.outputConnection; - if (newStatement != oldStatement) { - this.unplug(true, true); - if (newStatement) { - this.setOutput(false); - this.setPreviousStatement(true); - this.setNextStatement(true); - } else { - this.setPreviousStatement(false); - this.setNextStatement(false); - this.setOutput(true); - } - } - }, - /** - * Create or delete an input for the numeric index. - * @param {boolean} isAt True if the input should exist. - * @private - * @this {Blockly.Block} - */ - updateAt_: function(isAt) { - // Destroy old 'AT' and 'ORDINAL' inputs. - this.removeInput('AT'); - this.removeInput('ORDINAL', true); - // Create either a value 'AT' input or a dummy input. - if (isAt) { - this.appendValueInput('AT').setCheck('Number'); - if (Blockly.Msg['ORDINAL_NUMBER_SUFFIX']) { - this.appendDummyInput('ORDINAL') - .appendField(Blockly.Msg['ORDINAL_NUMBER_SUFFIX']); - } - } else { - this.appendDummyInput('AT'); - } - var menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, function(value) { - var newAt = (value == 'FROM_START') || (value == 'FROM_END'); - // The 'isAt' variable is available due to this function being a closure. - if (newAt != isAt) { - var block = this.getSourceBlock(); - block.updateAt_(newAt); - // This menu has been destroyed and replaced. Update the replacement. - block.setFieldValue(value, 'WHERE'); - return null; - } - return undefined; - }); - this.getInput('AT').appendField(menu, 'WHERE'); - if (Blockly.Msg['LISTS_GET_INDEX_TAIL']) { - this.moveInputBefore('TAIL', null); - } - } -}; - -Blockly.Blocks['lists_setIndex'] = { - /** - * Block for setting the element at index. - * @this {Blockly.Block} - */ - init: function() { - var MODE = - [ - [Blockly.Msg['LISTS_SET_INDEX_SET'], 'SET'], - [Blockly.Msg['LISTS_SET_INDEX_INSERT'], 'INSERT'] - ]; - this.WHERE_OPTIONS = - [ - [Blockly.Msg['LISTS_GET_INDEX_FROM_START'], 'FROM_START'], - [Blockly.Msg['LISTS_GET_INDEX_FROM_END'], 'FROM_END'], - [Blockly.Msg['LISTS_GET_INDEX_FIRST'], 'FIRST'], - [Blockly.Msg['LISTS_GET_INDEX_LAST'], 'LAST'], - [Blockly.Msg['LISTS_GET_INDEX_RANDOM'], 'RANDOM'] - ]; - this.setHelpUrl(Blockly.Msg['LISTS_SET_INDEX_HELPURL']); - this.setStyle('list_blocks'); - this.appendValueInput('LIST') - .setCheck('Array') - .appendField(Blockly.Msg['LISTS_SET_INDEX_INPUT_IN_LIST']); - this.appendDummyInput() - .appendField(new Blockly.FieldDropdown(MODE), 'MODE') - .appendField('', 'SPACE'); - this.appendDummyInput('AT'); - this.appendValueInput('TO') - .appendField(Blockly.Msg['LISTS_SET_INDEX_INPUT_TO']); - this.setInputsInline(true); - this.setPreviousStatement(true); - this.setNextStatement(true); - this.setTooltip(Blockly.Msg['LISTS_SET_INDEX_TOOLTIP']); - this.updateAt_(true); - // Assign 'this' to a variable for use in the tooltip closure below. - var thisBlock = this; - this.setTooltip(function() { - var mode = thisBlock.getFieldValue('MODE'); - var where = thisBlock.getFieldValue('WHERE'); - var tooltip = ''; - switch (mode + ' ' + where) { - case 'SET FROM_START': - case 'SET FROM_END': - tooltip = Blockly.Msg['LISTS_SET_INDEX_TOOLTIP_SET_FROM']; - break; - case 'SET FIRST': - tooltip = Blockly.Msg['LISTS_SET_INDEX_TOOLTIP_SET_FIRST']; - break; - case 'SET LAST': - tooltip = Blockly.Msg['LISTS_SET_INDEX_TOOLTIP_SET_LAST']; - break; - case 'SET RANDOM': - tooltip = Blockly.Msg['LISTS_SET_INDEX_TOOLTIP_SET_RANDOM']; - break; - case 'INSERT FROM_START': - case 'INSERT FROM_END': - tooltip = Blockly.Msg['LISTS_SET_INDEX_TOOLTIP_INSERT_FROM']; - break; - case 'INSERT FIRST': - tooltip = Blockly.Msg['LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST']; - break; - case 'INSERT LAST': - tooltip = Blockly.Msg['LISTS_SET_INDEX_TOOLTIP_INSERT_LAST']; - break; - case 'INSERT RANDOM': - tooltip = Blockly.Msg['LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM']; - break; - } - if (where == 'FROM_START' || where == 'FROM_END') { - tooltip += ' ' + Blockly.Msg['LISTS_INDEX_FROM_START_TOOLTIP'] - .replace('%1', - thisBlock.workspace.options.oneBasedIndex ? '#1' : '#0'); - } - return tooltip; - }); - }, - /** - * Create XML to represent whether there is an 'AT' input. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - var isAt = this.getInput('AT').type == Blockly.INPUT_VALUE; - container.setAttribute('at', isAt); - return container; - }, - /** - * Parse XML to restore the 'AT' input. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - // Note: Until January 2013 this block did not have mutations, - // so 'at' defaults to true. - var isAt = (xmlElement.getAttribute('at') != 'false'); - this.updateAt_(isAt); - }, - /** - * Create or delete an input for the numeric index. - * @param {boolean} isAt True if the input should exist. - * @private - * @this {Blockly.Block} - */ - updateAt_: function(isAt) { - // Destroy old 'AT' and 'ORDINAL' input. - this.removeInput('AT'); - this.removeInput('ORDINAL', true); - // Create either a value 'AT' input or a dummy input. - if (isAt) { - this.appendValueInput('AT').setCheck('Number'); - if (Blockly.Msg['ORDINAL_NUMBER_SUFFIX']) { - this.appendDummyInput('ORDINAL') - .appendField(Blockly.Msg['ORDINAL_NUMBER_SUFFIX']); - } - } else { - this.appendDummyInput('AT'); - } - var menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, function(value) { - var newAt = (value == 'FROM_START') || (value == 'FROM_END'); - // The 'isAt' variable is available due to this function being a closure. - if (newAt != isAt) { - var block = this.getSourceBlock(); - block.updateAt_(newAt); - // This menu has been destroyed and replaced. Update the replacement. - block.setFieldValue(value, 'WHERE'); - return null; - } - return undefined; - }); - this.moveInputBefore('AT', 'TO'); - if (this.getInput('ORDINAL')) { - this.moveInputBefore('ORDINAL', 'TO'); - } - - this.getInput('AT').appendField(menu, 'WHERE'); - } -}; - -Blockly.Blocks['lists_getSublist'] = { - /** - * Block for getting sublist. - * @this {Blockly.Block} - */ - init: function() { - this['WHERE_OPTIONS_1'] = - [ - [Blockly.Msg['LISTS_GET_SUBLIST_START_FROM_START'], 'FROM_START'], - [Blockly.Msg['LISTS_GET_SUBLIST_START_FROM_END'], 'FROM_END'], - [Blockly.Msg['LISTS_GET_SUBLIST_START_FIRST'], 'FIRST'] - ]; - this['WHERE_OPTIONS_2'] = - [ - [Blockly.Msg['LISTS_GET_SUBLIST_END_FROM_START'], 'FROM_START'], - [Blockly.Msg['LISTS_GET_SUBLIST_END_FROM_END'], 'FROM_END'], - [Blockly.Msg['LISTS_GET_SUBLIST_END_LAST'], 'LAST'] - ]; - this.setHelpUrl(Blockly.Msg['LISTS_GET_SUBLIST_HELPURL']); - this.setStyle('list_blocks'); - this.appendValueInput('LIST') - .setCheck('Array') - .appendField(Blockly.Msg['LISTS_GET_SUBLIST_INPUT_IN_LIST']); - this.appendDummyInput('AT1'); - this.appendDummyInput('AT2'); - if (Blockly.Msg['LISTS_GET_SUBLIST_TAIL']) { - this.appendDummyInput('TAIL') - .appendField(Blockly.Msg['LISTS_GET_SUBLIST_TAIL']); - } - this.setInputsInline(true); - this.setOutput(true, 'Array'); - this.updateAt_(1, true); - this.updateAt_(2, true); - this.setTooltip(Blockly.Msg['LISTS_GET_SUBLIST_TOOLTIP']); - }, - /** - * Create XML to represent whether there are 'AT' inputs. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - var isAt1 = this.getInput('AT1').type == Blockly.INPUT_VALUE; - container.setAttribute('at1', isAt1); - var isAt2 = this.getInput('AT2').type == Blockly.INPUT_VALUE; - container.setAttribute('at2', isAt2); - return container; - }, - /** - * Parse XML to restore the 'AT' inputs. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - var isAt1 = (xmlElement.getAttribute('at1') == 'true'); - var isAt2 = (xmlElement.getAttribute('at2') == 'true'); - this.updateAt_(1, isAt1); - this.updateAt_(2, isAt2); - }, - /** - * Create or delete an input for a numeric index. - * This block has two such inputs, independent of each other. - * @param {number} n Specify first or second input (1 or 2). - * @param {boolean} isAt True if the input should exist. - * @private - * @this {Blockly.Block} - */ - updateAt_: function(n, isAt) { - // Create or delete an input for the numeric index. - // Destroy old 'AT' and 'ORDINAL' inputs. - this.removeInput('AT' + n); - this.removeInput('ORDINAL' + n, true); - // Create either a value 'AT' input or a dummy input. - if (isAt) { - this.appendValueInput('AT' + n).setCheck('Number'); - if (Blockly.Msg['ORDINAL_NUMBER_SUFFIX']) { - this.appendDummyInput('ORDINAL' + n) - .appendField(Blockly.Msg['ORDINAL_NUMBER_SUFFIX']); - } - } else { - this.appendDummyInput('AT' + n); - } - var menu = new Blockly.FieldDropdown(this['WHERE_OPTIONS_' + n], - function(value) { - var newAt = (value == 'FROM_START') || (value == 'FROM_END'); - // The 'isAt' variable is available due to this function being a - // closure. - if (newAt != isAt) { - var block = this.getSourceBlock(); - block.updateAt_(n, newAt); - // This menu has been destroyed and replaced. - // Update the replacement. - block.setFieldValue(value, 'WHERE' + n); - return null; - } - }); - this.getInput('AT' + n) - .appendField(menu, 'WHERE' + n); - if (n == 1) { - this.moveInputBefore('AT1', 'AT2'); - if (this.getInput('ORDINAL1')) { - this.moveInputBefore('ORDINAL1', 'AT2'); - } - } - if (Blockly.Msg['LISTS_GET_SUBLIST_TAIL']) { - this.moveInputBefore('TAIL', null); - } - } -}; - -Blockly.Blocks['lists_sort'] = { - /** - * Block for sorting a list. - * @this {Blockly.Block} - */ - init: function() { - this.jsonInit({ - "message0": Blockly.Msg['LISTS_SORT_TITLE'], - "args0": [ - { - "type": "field_dropdown", - "name": "TYPE", - "options": [ - [Blockly.Msg['LISTS_SORT_TYPE_NUMERIC'], "NUMERIC"], - [Blockly.Msg['LISTS_SORT_TYPE_TEXT'], "TEXT"], - [Blockly.Msg['LISTS_SORT_TYPE_IGNORECASE'], "IGNORE_CASE"] - ] - }, - { - "type": "field_dropdown", - "name": "DIRECTION", - "options": [ - [Blockly.Msg['LISTS_SORT_ORDER_ASCENDING'], "1"], - [Blockly.Msg['LISTS_SORT_ORDER_DESCENDING'], "-1"] - ] - }, - { - "type": "input_value", - "name": "LIST", - "check": "Array" - } - ], - "output": "Array", - "style": "list_blocks", - "tooltip": Blockly.Msg['LISTS_SORT_TOOLTIP'], - "helpUrl": Blockly.Msg['LISTS_SORT_HELPURL'] - }); - } -}; - -Blockly.Blocks['lists_split'] = { - /** - * Block for splitting text into a list, or joining a list into text. - * @this {Blockly.Block} - */ - init: function() { - // Assign 'this' to a variable for use in the closures below. - var thisBlock = this; - var dropdown = new Blockly.FieldDropdown( - [ - [Blockly.Msg['LISTS_SPLIT_LIST_FROM_TEXT'], 'SPLIT'], - [Blockly.Msg['LISTS_SPLIT_TEXT_FROM_LIST'], 'JOIN'] - ], - function(newMode) { - thisBlock.updateType_(newMode); - }); - this.setHelpUrl(Blockly.Msg['LISTS_SPLIT_HELPURL']); - this.setStyle('list_blocks'); - this.appendValueInput('INPUT') - .setCheck('String') - .appendField(dropdown, 'MODE'); - this.appendValueInput('DELIM') - .setCheck('String') - .appendField(Blockly.Msg['LISTS_SPLIT_WITH_DELIMITER']); - this.setInputsInline(true); - this.setOutput(true, 'Array'); - this.setTooltip(function() { - var mode = thisBlock.getFieldValue('MODE'); - if (mode == 'SPLIT') { - return Blockly.Msg['LISTS_SPLIT_TOOLTIP_SPLIT']; - } else if (mode == 'JOIN') { - return Blockly.Msg['LISTS_SPLIT_TOOLTIP_JOIN']; - } - throw Error('Unknown mode: ' + mode); - }); - }, - /** - * Modify this block to have the correct input and output types. - * @param {string} newMode Either 'SPLIT' or 'JOIN'. - * @private - * @this {Blockly.Block} - */ - updateType_: function(newMode) { - var mode = this.getFieldValue('MODE'); - if (mode != newMode) { - var inputConnection = this.getInput('INPUT').connection; - inputConnection.setShadowDom(null); - var inputBlock = inputConnection.targetBlock(); - if (inputBlock) { - inputConnection.disconnect(); - if (inputBlock.isShadow()) { - inputBlock.dispose(); - } else { - this.bumpNeighbours(); - } - } - } - if (newMode == 'SPLIT') { - this.outputConnection.setCheck('Array'); - this.getInput('INPUT').setCheck('String'); - } else { - this.outputConnection.setCheck('String'); - this.getInput('INPUT').setCheck('Array'); - } - }, - /** - * Create XML to represent the input and output types. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('mode', this.getFieldValue('MODE')); - return container; - }, - /** - * Parse XML to restore the input and output types. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - this.updateType_(xmlElement.getAttribute('mode')); - } -}; diff --git a/blocks/lists.ts b/blocks/lists.ts new file mode 100644 index 00000000000..3b468dc9d21 --- /dev/null +++ b/blocks/lists.ts @@ -0,0 +1,1085 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.libraryBlocks.lists + +import * as fieldRegistry from '../core/field_registry.js'; +import * as xmlUtils from '../core/utils/xml.js'; +import {Align} from '../core/inputs/align.js'; +import type {Block} from '../core/block.js'; +import type {Connection} from '../core/connection.js'; +import type {BlockSvg} from '../core/block_svg.js'; +import type {FieldDropdown} from '../core/field_dropdown.js'; +import {Msg} from '../core/msg.js'; +import {MutatorIcon} from '../core/icons/mutator_icon.js'; +import type {Workspace} from '../core/workspace.js'; +import { + createBlockDefinitionsFromJsonArray, + defineBlocks, +} from '../core/common.js'; +import '../core/field_dropdown.js'; +import {ValueInput} from '../core/inputs/value_input.js'; + +/** + * A dictionary of the block definitions provided by this module. + */ +export const blocks = createBlockDefinitionsFromJsonArray([ + // Block for creating an empty list + // The 'list_create_with' block is preferred as it is more flexible. + // + // + // + { + 'type': 'lists_create_empty', + 'message0': '%{BKY_LISTS_CREATE_EMPTY_TITLE}', + 'output': 'Array', + 'style': 'list_blocks', + 'tooltip': '%{BKY_LISTS_CREATE_EMPTY_TOOLTIP}', + 'helpUrl': '%{BKY_LISTS_CREATE_EMPTY_HELPURL}', + }, + // Block for creating a list with one element repeated. + { + 'type': 'lists_repeat', + 'message0': '%{BKY_LISTS_REPEAT_TITLE}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'ITEM', + }, + { + 'type': 'input_value', + 'name': 'NUM', + 'check': 'Number', + }, + ], + 'output': 'Array', + 'style': 'list_blocks', + 'tooltip': '%{BKY_LISTS_REPEAT_TOOLTIP}', + 'helpUrl': '%{BKY_LISTS_REPEAT_HELPURL}', + }, + // Block for reversing a list. + { + 'type': 'lists_reverse', + 'message0': '%{BKY_LISTS_REVERSE_MESSAGE0}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'LIST', + 'check': 'Array', + }, + ], + 'output': 'Array', + 'inputsInline': true, + 'style': 'list_blocks', + 'tooltip': '%{BKY_LISTS_REVERSE_TOOLTIP}', + 'helpUrl': '%{BKY_LISTS_REVERSE_HELPURL}', + }, + // Block for checking if a list is empty + { + 'type': 'lists_isEmpty', + 'message0': '%{BKY_LISTS_ISEMPTY_TITLE}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'VALUE', + 'check': ['String', 'Array'], + }, + ], + 'output': 'Boolean', + 'style': 'list_blocks', + 'tooltip': '%{BKY_LISTS_ISEMPTY_TOOLTIP}', + 'helpUrl': '%{BKY_LISTS_ISEMPTY_HELPURL}', + }, + // Block for getting the list length + { + 'type': 'lists_length', + 'message0': '%{BKY_LISTS_LENGTH_TITLE}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'VALUE', + 'check': ['String', 'Array'], + }, + ], + 'output': 'Number', + 'style': 'list_blocks', + 'tooltip': '%{BKY_LISTS_LENGTH_TOOLTIP}', + 'helpUrl': '%{BKY_LISTS_LENGTH_HELPURL}', + }, +]); + +/** + * Type of a 'lists_create_with' block. + * + * @internal + */ +export type CreateWithBlock = Block & ListCreateWithMixin; +interface ListCreateWithMixin extends ListCreateWithMixinType { + itemCount_: number; +} +type ListCreateWithMixinType = typeof LISTS_CREATE_WITH; + +const LISTS_CREATE_WITH = { + /** + * Block for creating a list with any number of elements of any type. + */ + init: function (this: CreateWithBlock) { + this.setHelpUrl(Msg['LISTS_CREATE_WITH_HELPURL']); + this.setStyle('list_blocks'); + this.itemCount_ = 3; + this.updateShape_(); + this.setOutput(true, 'Array'); + this.setMutator( + new MutatorIcon(['lists_create_with_item'], this as unknown as BlockSvg), + ); // BUG(#6905) + this.setTooltip(Msg['LISTS_CREATE_WITH_TOOLTIP']); + }, + /** + * Create XML to represent list inputs. + * Backwards compatible serialization implementation. + */ + mutationToDom: function (this: CreateWithBlock): Element { + const container = xmlUtils.createElement('mutation'); + container.setAttribute('items', String(this.itemCount_)); + return container; + }, + /** + * Parse XML to restore the list inputs. + * Backwards compatible serialization implementation. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: CreateWithBlock, xmlElement: Element) { + const items = xmlElement.getAttribute('items'); + if (!items) throw new TypeError('element did not have items'); + this.itemCount_ = parseInt(items, 10); + this.updateShape_(); + }, + /** + * Returns the state of this block as a JSON serializable object. + * + * @returns The state of this block, ie the item count. + */ + saveExtraState: function (this: CreateWithBlock): {itemCount: number} { + return { + 'itemCount': this.itemCount_, + }; + }, + /** + * Applies the given state to this block. + * + * @param state The state to apply to this block, ie the item count. + */ + loadExtraState: function (this: CreateWithBlock, state: AnyDuringMigration) { + this.itemCount_ = state['itemCount']; + this.updateShape_(); + }, + /** + * Populate the mutator's dialog with this block's components. + * + * @param workspace Mutator's workspace. + * @returns Root block in mutator. + */ + decompose: function ( + this: CreateWithBlock, + workspace: Workspace, + ): ContainerBlock { + const containerBlock = workspace.newBlock( + 'lists_create_with_container', + ) as ContainerBlock; + (containerBlock as BlockSvg).initSvg(); + let connection = containerBlock.getInput('STACK')!.connection; + for (let i = 0; i < this.itemCount_; i++) { + const itemBlock = workspace.newBlock( + 'lists_create_with_item', + ) as ItemBlock; + (itemBlock as BlockSvg).initSvg(); + if (!itemBlock.previousConnection) { + throw new Error('itemBlock has no previousConnection'); + } + connection!.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * + * @param containerBlock Root block in mutator. + */ + compose: function (this: CreateWithBlock, containerBlock: Block) { + let itemBlock: ItemBlock | null = containerBlock.getInputTargetBlock( + 'STACK', + ) as ItemBlock; + // Count number of inputs. + const connections: Connection[] = []; + while (itemBlock) { + if (itemBlock.isInsertionMarker()) { + itemBlock = itemBlock.getNextBlock() as ItemBlock | null; + continue; + } + connections.push(itemBlock.valueConnection_ as Connection); + itemBlock = itemBlock.getNextBlock() as ItemBlock | null; + } + // Disconnect any children that don't belong. + for (let i = 0; i < this.itemCount_; i++) { + const connection = this.getInput('ADD' + i)!.connection!.targetConnection; + if (connection && !connections.includes(connection)) { + connection.disconnect(); + } + } + this.itemCount_ = connections.length; + this.updateShape_(); + // Reconnect any child blocks. + for (let i = 0; i < this.itemCount_; i++) { + connections[i]?.reconnect(this, 'ADD' + i); + } + }, + /** + * Store pointers to any connected child blocks. + * + * @param containerBlock Root block in mutator. + */ + saveConnections: function (this: CreateWithBlock, containerBlock: Block) { + let itemBlock: ItemBlock | null = containerBlock.getInputTargetBlock( + 'STACK', + ) as ItemBlock; + let i = 0; + while (itemBlock) { + if (itemBlock.isInsertionMarker()) { + itemBlock = itemBlock.getNextBlock() as ItemBlock | null; + continue; + } + const input = this.getInput('ADD' + i); + itemBlock.valueConnection_ = input?.connection! + .targetConnection as Connection; + itemBlock = itemBlock.getNextBlock() as ItemBlock | null; + i++; + } + }, + /** + * Modify this block to have the correct number of inputs. + */ + updateShape_: function (this: CreateWithBlock) { + if (this.itemCount_ && this.getInput('EMPTY')) { + this.removeInput('EMPTY'); + } else if (!this.itemCount_ && !this.getInput('EMPTY')) { + this.appendDummyInput('EMPTY').appendField( + Msg['LISTS_CREATE_EMPTY_TITLE'], + ); + } + // Add new inputs. + for (let i = 0; i < this.itemCount_; i++) { + if (!this.getInput('ADD' + i)) { + const input = this.appendValueInput('ADD' + i).setAlign(Align.RIGHT); + if (i === 0) { + input.appendField(Msg['LISTS_CREATE_WITH_INPUT_WITH']); + } + } + } + // Remove deleted inputs. + for (let i = this.itemCount_; this.getInput('ADD' + i); i++) { + this.removeInput('ADD' + i); + } + }, +}; +blocks['lists_create_with'] = LISTS_CREATE_WITH; + +/** Type for a 'lists_create_with_container' block. */ +type ContainerBlock = Block & ContainerMutator; +interface ContainerMutator extends ContainerMutatorType {} +type ContainerMutatorType = typeof LISTS_CREATE_WITH_CONTAINER; + +const LISTS_CREATE_WITH_CONTAINER = { + /** + * Mutator block for list container. + */ + init: function (this: ContainerBlock) { + this.setStyle('list_blocks'); + this.appendDummyInput().appendField( + Msg['LISTS_CREATE_WITH_CONTAINER_TITLE_ADD'], + ); + this.appendStatementInput('STACK'); + this.setTooltip(Msg['LISTS_CREATE_WITH_CONTAINER_TOOLTIP']); + this.contextMenu = false; + }, +}; +blocks['lists_create_with_container'] = LISTS_CREATE_WITH_CONTAINER; + +/** Type for a 'lists_create_with_item' block. */ +type ItemBlock = Block & ItemMutator; +interface ItemMutator extends ItemMutatorType { + valueConnection_?: Connection; +} +type ItemMutatorType = typeof LISTS_CREATE_WITH_ITEM; + +const LISTS_CREATE_WITH_ITEM = { + /** + * Mutator block for adding items. + */ + init: function (this: ItemBlock) { + this.setStyle('list_blocks'); + this.appendDummyInput().appendField(Msg['LISTS_CREATE_WITH_ITEM_TITLE']); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Msg['LISTS_CREATE_WITH_ITEM_TOOLTIP']); + this.contextMenu = false; + }, +}; +blocks['lists_create_with_item'] = LISTS_CREATE_WITH_ITEM; + +/** Type for a 'lists_indexOf' block. */ +type IndexOfBlock = Block & IndexOfMutator; +interface IndexOfMutator extends IndexOfMutatorType {} +type IndexOfMutatorType = typeof LISTS_INDEXOF; + +const LISTS_INDEXOF = { + /** + * Block for finding an item in the list. + */ + init: function (this: IndexOfBlock) { + const OPERATORS = [ + [Msg['LISTS_INDEX_OF_FIRST'], 'FIRST'], + [Msg['LISTS_INDEX_OF_LAST'], 'LAST'], + ]; + this.setHelpUrl(Msg['LISTS_INDEX_OF_HELPURL']); + this.setStyle('list_blocks'); + this.setOutput(true, 'Number'); + this.appendValueInput('VALUE') + .setCheck('Array') + .appendField(Msg['LISTS_INDEX_OF_INPUT_IN_LIST']); + const operatorsDropdown = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: OPERATORS, + }); + if (!operatorsDropdown) throw new Error('field_dropdown not found'); + this.appendValueInput('FIND').appendField(operatorsDropdown, 'END'); + this.setInputsInline(true); + this.setTooltip(() => { + return Msg['LISTS_INDEX_OF_TOOLTIP'].replace( + '%1', + this.workspace.options.oneBasedIndex ? '0' : '-1', + ); + }); + }, +}; +blocks['lists_indexOf'] = LISTS_INDEXOF; + +/** Type for a 'lists_getIndex' block. */ +type GetIndexBlock = Block & GetIndexMutator; +interface GetIndexMutator extends GetIndexMutatorType { + WHERE_OPTIONS: Array<[string, string]>; +} +type GetIndexMutatorType = typeof LISTS_GETINDEX; + +const LISTS_GETINDEX = { + /** + * Block for getting element at index. + */ + init: function (this: GetIndexBlock) { + const MODE = [ + [Msg['LISTS_GET_INDEX_GET'], 'GET'], + [Msg['LISTS_GET_INDEX_GET_REMOVE'], 'GET_REMOVE'], + [Msg['LISTS_GET_INDEX_REMOVE'], 'REMOVE'], + ]; + this.WHERE_OPTIONS = [ + [Msg['LISTS_GET_INDEX_FROM_START'], 'FROM_START'], + [Msg['LISTS_GET_INDEX_FROM_END'], 'FROM_END'], + [Msg['LISTS_GET_INDEX_FIRST'], 'FIRST'], + [Msg['LISTS_GET_INDEX_LAST'], 'LAST'], + [Msg['LISTS_GET_INDEX_RANDOM'], 'RANDOM'], + ]; + this.setHelpUrl(Msg['LISTS_GET_INDEX_HELPURL']); + this.setStyle('list_blocks'); + const modeMenu = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: MODE, + }) as FieldDropdown; + modeMenu.setValidator( + /** @param value The input value. */ + function (this: FieldDropdown, value: string) { + const isStatement = value === 'REMOVE'; + (this.getSourceBlock() as GetIndexBlock).updateStatement_(isStatement); + return undefined; + }, + ); + this.appendValueInput('VALUE') + .setCheck('Array') + .appendField(Msg['LISTS_GET_INDEX_INPUT_IN_LIST']); + this.appendDummyInput() + .appendField(modeMenu, 'MODE') + .appendField('', 'SPACE'); + this.appendDummyInput('AT'); + if (Msg['LISTS_GET_INDEX_TAIL']) { + this.appendDummyInput('TAIL').appendField(Msg['LISTS_GET_INDEX_TAIL']); + } + this.setInputsInline(true); + this.setOutput(true); + this.updateAt_(true); + this.setTooltip(() => { + const mode = this.getFieldValue('MODE'); + const where = this.getFieldValue('WHERE'); + let tooltip = ''; + switch (mode + ' ' + where) { + case 'GET FROM_START': + case 'GET FROM_END': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_GET_FROM']; + break; + case 'GET FIRST': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_GET_FIRST']; + break; + case 'GET LAST': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_GET_LAST']; + break; + case 'GET RANDOM': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_GET_RANDOM']; + break; + case 'GET_REMOVE FROM_START': + case 'GET_REMOVE FROM_END': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM']; + break; + case 'GET_REMOVE FIRST': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST']; + break; + case 'GET_REMOVE LAST': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_LAST']; + break; + case 'GET_REMOVE RANDOM': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_RANDOM']; + break; + case 'REMOVE FROM_START': + case 'REMOVE FROM_END': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM']; + break; + case 'REMOVE FIRST': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST']; + break; + case 'REMOVE LAST': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST']; + break; + case 'REMOVE RANDOM': + tooltip = Msg['LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM']; + break; + } + if (where === 'FROM_START' || where === 'FROM_END') { + const msg = + where === 'FROM_START' + ? Msg['LISTS_INDEX_FROM_START_TOOLTIP'] + : Msg['LISTS_INDEX_FROM_END_TOOLTIP']; + tooltip += + ' ' + + msg.replace('%1', this.workspace.options.oneBasedIndex ? '#1' : '#0'); + } + return tooltip; + }); + }, + /** + * Create XML to represent whether the block is a statement or a value. + * Also represent whether there is an 'AT' input. + * + * @returns XML storage element. + */ + mutationToDom: function (this: GetIndexBlock): Element { + const container = xmlUtils.createElement('mutation'); + const isStatement = !this.outputConnection; + container.setAttribute('statement', String(isStatement)); + const isAt = this.getInput('AT') instanceof ValueInput; + container.setAttribute('at', String(isAt)); + return container; + }, + /** + * Parse XML to restore the 'AT' input. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: GetIndexBlock, xmlElement: Element) { + // Note: Until January 2013 this block did not have mutations, + // so 'statement' defaults to false and 'at' defaults to true. + const isStatement = xmlElement.getAttribute('statement') === 'true'; + this.updateStatement_(isStatement); + const isAt = xmlElement.getAttribute('at') !== 'false'; + this.updateAt_(isAt); + }, + /** + * Returns the state of this block as a JSON serializable object. + * Returns null for efficiency if no state is needed (not a statement) + * + * @returns The state of this block, ie whether it's a statement. + */ + saveExtraState: function (this: GetIndexBlock): { + isStatement: boolean; + } | null { + if (!this.outputConnection) { + return { + isStatement: true, + }; + } + return null; + }, + + /** + * Applies the given state to this block. + * + * @param state The state to apply to this block, ie whether it's a + * statement. + */ + loadExtraState: function (this: GetIndexBlock, state: AnyDuringMigration) { + if (state['isStatement']) { + this.updateStatement_(true); + } else if (typeof state === 'string') { + // backward compatible for json serialised mutations + this.domToMutation(xmlUtils.textToDom(state)); + } + }, + + /** + * Switch between a value block and a statement block. + * + * @param newStatement True if the block should be a statement. + * False if the block should be a value. + */ + updateStatement_: function (this: GetIndexBlock, newStatement: boolean) { + const oldStatement = !this.outputConnection; + if (newStatement !== oldStatement) { + // TODO(#6920): The .unplug only has one parameter. + (this.unplug as (arg0?: boolean, arg1?: boolean) => void)(true, true); + if (newStatement) { + this.setOutput(false); + this.setPreviousStatement(true); + this.setNextStatement(true); + } else { + this.setPreviousStatement(false); + this.setNextStatement(false); + this.setOutput(true); + } + } + }, + /** + * Create or delete an input for the numeric index. + * + * @param isAt True if the input should exist. + */ + updateAt_: function (this: GetIndexBlock, isAt: boolean) { + // Destroy old 'AT' and 'ORDINAL' inputs. + this.removeInput('AT'); + this.removeInput('ORDINAL', true); + // Create either a value 'AT' input or a dummy input. + if (isAt) { + this.appendValueInput('AT').setCheck('Number'); + if (Msg['ORDINAL_NUMBER_SUFFIX']) { + this.appendDummyInput('ORDINAL').appendField( + Msg['ORDINAL_NUMBER_SUFFIX'], + ); + } + } else { + this.appendDummyInput('AT'); + } + const menu = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: this.WHERE_OPTIONS, + }) as FieldDropdown; + menu.setValidator( + /** + * @param value The input value. + * @returns Null if the field has been replaced; otherwise undefined. + */ + function (this: FieldDropdown, value: string) { + const newAt = value === 'FROM_START' || value === 'FROM_END'; + // The 'isAt' variable is available due to this function being a + // closure. + if (newAt !== isAt) { + const block = this.getSourceBlock() as GetIndexBlock; + block.updateAt_(newAt); + // This menu has been destroyed and replaced. Update the + // replacement. + block.setFieldValue(value, 'WHERE'); + return null; + } + return undefined; + }, + ); + this.getInput('AT')!.appendField(menu, 'WHERE'); + if (Msg['LISTS_GET_INDEX_TAIL']) { + this.moveInputBefore('TAIL', null); + } + }, +}; +blocks['lists_getIndex'] = LISTS_GETINDEX; + +/** Type for a 'lists_setIndex' block. */ +type SetIndexBlock = Block & SetIndexMutator; +interface SetIndexMutator extends SetIndexMutatorType { + WHERE_OPTIONS: Array<[string, string]>; +} +type SetIndexMutatorType = typeof LISTS_SETINDEX; + +const LISTS_SETINDEX = { + /** + * Block for setting the element at index. + */ + init: function (this: SetIndexBlock) { + const MODE = [ + [Msg['LISTS_SET_INDEX_SET'], 'SET'], + [Msg['LISTS_SET_INDEX_INSERT'], 'INSERT'], + ]; + this.WHERE_OPTIONS = [ + [Msg['LISTS_GET_INDEX_FROM_START'], 'FROM_START'], + [Msg['LISTS_GET_INDEX_FROM_END'], 'FROM_END'], + [Msg['LISTS_GET_INDEX_FIRST'], 'FIRST'], + [Msg['LISTS_GET_INDEX_LAST'], 'LAST'], + [Msg['LISTS_GET_INDEX_RANDOM'], 'RANDOM'], + ]; + this.setHelpUrl(Msg['LISTS_SET_INDEX_HELPURL']); + this.setStyle('list_blocks'); + this.appendValueInput('LIST') + .setCheck('Array') + .appendField(Msg['LISTS_SET_INDEX_INPUT_IN_LIST']); + const operationDropdown = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: MODE, + }) as FieldDropdown; + this.appendDummyInput() + .appendField(operationDropdown, 'MODE') + .appendField('', 'SPACE'); + this.appendDummyInput('AT'); + this.appendValueInput('TO').appendField(Msg['LISTS_SET_INDEX_INPUT_TO']); + this.setInputsInline(true); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Msg['LISTS_SET_INDEX_TOOLTIP']); + this.updateAt_(true); + this.setTooltip(() => { + const mode = this.getFieldValue('MODE'); + const where = this.getFieldValue('WHERE'); + let tooltip = ''; + switch (mode + ' ' + where) { + case 'SET FROM_START': + case 'SET FROM_END': + tooltip = Msg['LISTS_SET_INDEX_TOOLTIP_SET_FROM']; + break; + case 'SET FIRST': + tooltip = Msg['LISTS_SET_INDEX_TOOLTIP_SET_FIRST']; + break; + case 'SET LAST': + tooltip = Msg['LISTS_SET_INDEX_TOOLTIP_SET_LAST']; + break; + case 'SET RANDOM': + tooltip = Msg['LISTS_SET_INDEX_TOOLTIP_SET_RANDOM']; + break; + case 'INSERT FROM_START': + case 'INSERT FROM_END': + tooltip = Msg['LISTS_SET_INDEX_TOOLTIP_INSERT_FROM']; + break; + case 'INSERT FIRST': + tooltip = Msg['LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST']; + break; + case 'INSERT LAST': + tooltip = Msg['LISTS_SET_INDEX_TOOLTIP_INSERT_LAST']; + break; + case 'INSERT RANDOM': + tooltip = Msg['LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM']; + break; + } + if (where === 'FROM_START' || where === 'FROM_END') { + tooltip += + ' ' + + Msg['LISTS_INDEX_FROM_START_TOOLTIP'].replace( + '%1', + this.workspace.options.oneBasedIndex ? '#1' : '#0', + ); + } + return tooltip; + }); + }, + /** + * Create XML to represent whether there is an 'AT' input. + * + * @returns XML storage element. + */ + mutationToDom: function (this: SetIndexBlock): Element { + const container = xmlUtils.createElement('mutation'); + const isAt = this.getInput('AT') instanceof ValueInput; + container.setAttribute('at', String(isAt)); + return container; + }, + /** + * Parse XML to restore the 'AT' input. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: SetIndexBlock, xmlElement: Element) { + // Note: Until January 2013 this block did not have mutations, + // so 'at' defaults to true. + const isAt = xmlElement.getAttribute('at') !== 'false'; + this.updateAt_(isAt); + }, + + /** + * Returns the state of this block as a JSON serializable object. + * This block does not need to serialize any specific state as it is already + * encoded in the dropdown values, but must have an implementation to avoid + * the backward compatible XML mutations being serialized. + * + * @returns The state of this block. + */ + saveExtraState: function (this: SetIndexBlock): null { + return null; + }, + + /** + * Applies the given state to this block. + * No extra state is needed or expected as it is already encoded in the + * dropdown values. + */ + loadExtraState: function (this: SetIndexBlock) {}, + + /** + * Create or delete an input for the numeric index. + * + * @param isAt True if the input should exist. + */ + updateAt_: function (this: SetIndexBlock, isAt: boolean) { + // Destroy old 'AT' and 'ORDINAL' input. + this.removeInput('AT'); + this.removeInput('ORDINAL', true); + // Create either a value 'AT' input or a dummy input. + if (isAt) { + this.appendValueInput('AT').setCheck('Number'); + if (Msg['ORDINAL_NUMBER_SUFFIX']) { + this.appendDummyInput('ORDINAL').appendField( + Msg['ORDINAL_NUMBER_SUFFIX'], + ); + } + } else { + this.appendDummyInput('AT'); + } + const menu = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: this.WHERE_OPTIONS, + }) as FieldDropdown; + menu.setValidator( + /** + * @param value The input value. + * @returns Null if the field has been replaced; otherwise undefined. + */ + function (this: FieldDropdown, value: string) { + const newAt = value === 'FROM_START' || value === 'FROM_END'; + // The 'isAt' variable is available due to this function being a + // closure. + if (newAt !== isAt) { + const block = this.getSourceBlock() as SetIndexBlock; + block.updateAt_(newAt); + // This menu has been destroyed and replaced. Update the + // replacement. + block.setFieldValue(value, 'WHERE'); + return null; + } + return undefined; + }, + ); + this.moveInputBefore('AT', 'TO'); + if (this.getInput('ORDINAL')) { + this.moveInputBefore('ORDINAL', 'TO'); + } + + this.getInput('AT')!.appendField(menu, 'WHERE'); + }, +}; +blocks['lists_setIndex'] = LISTS_SETINDEX; + +/** Type for a 'lists_getSublist' block. */ +type GetSublistBlock = Block & GetSublistMutator; +interface GetSublistMutator extends GetSublistMutatorType { + WHERE_OPTIONS_1: Array<[string, string]>; + WHERE_OPTIONS_2: Array<[string, string]>; +} +type GetSublistMutatorType = typeof LISTS_GETSUBLIST; + +const LISTS_GETSUBLIST = { + /** + * Block for getting sublist. + */ + init: function (this: GetSublistBlock) { + this['WHERE_OPTIONS_1'] = [ + [Msg['LISTS_GET_SUBLIST_START_FROM_START'], 'FROM_START'], + [Msg['LISTS_GET_SUBLIST_START_FROM_END'], 'FROM_END'], + [Msg['LISTS_GET_SUBLIST_START_FIRST'], 'FIRST'], + ]; + this['WHERE_OPTIONS_2'] = [ + [Msg['LISTS_GET_SUBLIST_END_FROM_START'], 'FROM_START'], + [Msg['LISTS_GET_SUBLIST_END_FROM_END'], 'FROM_END'], + [Msg['LISTS_GET_SUBLIST_END_LAST'], 'LAST'], + ]; + this.setHelpUrl(Msg['LISTS_GET_SUBLIST_HELPURL']); + this.setStyle('list_blocks'); + this.appendValueInput('LIST') + .setCheck('Array') + .appendField(Msg['LISTS_GET_SUBLIST_INPUT_IN_LIST']); + this.appendDummyInput('AT1'); + this.appendDummyInput('AT2'); + if (Msg['LISTS_GET_SUBLIST_TAIL']) { + this.appendDummyInput('TAIL').appendField(Msg['LISTS_GET_SUBLIST_TAIL']); + } + this.setInputsInline(true); + this.setOutput(true, 'Array'); + this.updateAt_(1, true); + this.updateAt_(2, true); + this.setTooltip(Msg['LISTS_GET_SUBLIST_TOOLTIP']); + }, + /** + * Create XML to represent whether there are 'AT' inputs. + * + * @returns XML storage element. + */ + mutationToDom: function (this: GetSublistBlock): Element { + const container = xmlUtils.createElement('mutation'); + const isAt1 = this.getInput('AT1') instanceof ValueInput; + container.setAttribute('at1', String(isAt1)); + const isAt2 = this.getInput('AT2') instanceof ValueInput; + container.setAttribute('at2', String(isAt2)); + return container; + }, + /** + * Parse XML to restore the 'AT' inputs. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: GetSublistBlock, xmlElement: Element) { + const isAt1 = xmlElement.getAttribute('at1') === 'true'; + const isAt2 = xmlElement.getAttribute('at2') === 'true'; + this.updateAt_(1, isAt1); + this.updateAt_(2, isAt2); + }, + + /** + * Returns the state of this block as a JSON serializable object. + * This block does not need to serialize any specific state as it is already + * encoded in the dropdown values, but must have an implementation to avoid + * the backward compatible XML mutations being serialized. + * + * @returns The state of this block. + */ + saveExtraState: function (this: GetSublistBlock): null { + return null; + }, + + /** + * Applies the given state to this block. + * No extra state is needed or expected as it is already encoded in the + * dropdown values. + */ + loadExtraState: function (this: GetSublistBlock) {}, + + /** + * Create or delete an input for a numeric index. + * This block has two such inputs, independent of each other. + * + * @param n Specify first or second input (1 or 2). + * @param isAt True if the input should exist. + */ + updateAt_: function (this: GetSublistBlock, n: 1 | 2, isAt: boolean) { + // Create or delete an input for the numeric index. + // Destroy old 'AT' and 'ORDINAL' inputs. + this.removeInput('AT' + n); + this.removeInput('ORDINAL' + n, true); + // Create either a value 'AT' input or a dummy input. + if (isAt) { + this.appendValueInput('AT' + n).setCheck('Number'); + if (Msg['ORDINAL_NUMBER_SUFFIX']) { + this.appendDummyInput('ORDINAL' + n).appendField( + Msg['ORDINAL_NUMBER_SUFFIX'], + ); + } + } else { + this.appendDummyInput('AT' + n); + } + const menu = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: + this[('WHERE_OPTIONS_' + n) as 'WHERE_OPTIONS_1' | 'WHERE_OPTIONS_2'], + }) as FieldDropdown; + menu.setValidator( + /** + * @param value The input value. + * @returns Null if the field has been replaced; otherwise undefined. + */ + function (this: FieldDropdown, value: string) { + const newAt = value === 'FROM_START' || value === 'FROM_END'; + // The 'isAt' variable is available due to this function being a + // closure. + if (newAt !== isAt) { + const block = this.getSourceBlock() as GetSublistBlock; + block.updateAt_(n, newAt); + // This menu has been destroyed and replaced. + // Update the replacement. + block.setFieldValue(value, 'WHERE' + n); + return null; + } + }, + ); + this.getInput('AT' + n)!.appendField(menu, 'WHERE' + n); + if (n === 1) { + this.moveInputBefore('AT1', 'AT2'); + if (this.getInput('ORDINAL1')) { + this.moveInputBefore('ORDINAL1', 'AT2'); + } + } + if (Msg['LISTS_GET_SUBLIST_TAIL']) { + this.moveInputBefore('TAIL', null); + } + }, +}; +blocks['lists_getSublist'] = LISTS_GETSUBLIST; + +type SortBlock = Block | (typeof blocks)['lists_sort']; + +blocks['lists_sort'] = { + /** + * Block for sorting a list. + */ + init: function (this: SortBlock) { + this.jsonInit({ + 'message0': '%{BKY_LISTS_SORT_TITLE}', + 'args0': [ + { + 'type': 'field_dropdown', + 'name': 'TYPE', + 'options': [ + ['%{BKY_LISTS_SORT_TYPE_NUMERIC}', 'NUMERIC'], + ['%{BKY_LISTS_SORT_TYPE_TEXT}', 'TEXT'], + ['%{BKY_LISTS_SORT_TYPE_IGNORECASE}', 'IGNORE_CASE'], + ], + }, + { + 'type': 'field_dropdown', + 'name': 'DIRECTION', + 'options': [ + ['%{BKY_LISTS_SORT_ORDER_ASCENDING}', '1'], + ['%{BKY_LISTS_SORT_ORDER_DESCENDING}', '-1'], + ], + }, + { + 'type': 'input_value', + 'name': 'LIST', + 'check': 'Array', + }, + ], + 'output': 'Array', + 'style': 'list_blocks', + 'tooltip': '%{BKY_LISTS_SORT_TOOLTIP}', + 'helpUrl': '%{BKY_LISTS_SORT_HELPURL}', + }); + }, +}; + +type SplitBlock = Block | (typeof blocks)['lists_split']; + +blocks['lists_split'] = { + /** + * Block for splitting text into a list, or joining a list into text. + */ + init: function (this: SplitBlock) { + const dropdown = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: [ + [Msg['LISTS_SPLIT_LIST_FROM_TEXT'], 'SPLIT'], + [Msg['LISTS_SPLIT_TEXT_FROM_LIST'], 'JOIN'], + ], + }); + if (!dropdown) throw new Error('field_dropdown not found'); + dropdown.setValidator((newMode) => { + this.updateType_(newMode); + }); + this.setHelpUrl(Msg['LISTS_SPLIT_HELPURL']); + this.setStyle('list_blocks'); + this.appendValueInput('INPUT') + .setCheck('String') + .appendField(dropdown, 'MODE'); + this.appendValueInput('DELIM') + .setCheck('String') + .appendField(Msg['LISTS_SPLIT_WITH_DELIMITER']); + this.setInputsInline(true); + this.setOutput(true, 'Array'); + this.setTooltip(() => { + const mode = this.getFieldValue('MODE'); + if (mode === 'SPLIT') { + return Msg['LISTS_SPLIT_TOOLTIP_SPLIT']; + } else if (mode === 'JOIN') { + return Msg['LISTS_SPLIT_TOOLTIP_JOIN']; + } + throw Error('Unknown mode: ' + mode); + }); + }, + /** + * Modify this block to have the correct input and output types. + * + * @param newMode Either 'SPLIT' or 'JOIN'. + */ + updateType_: function (this: SplitBlock, newMode: string) { + const mode = this.getFieldValue('MODE'); + if (mode !== newMode) { + const inputConnection = this.getInput('INPUT')!.connection; + inputConnection!.setShadowDom(null); + const inputBlock = inputConnection!.targetBlock(); + // TODO(#6920): This is probably not needed; see details in bug. + if (inputBlock) { + inputConnection!.disconnect(); + if (inputBlock.isShadow()) { + inputBlock.dispose(false); + } else { + this.bumpNeighbours(); + } + } + } + if (newMode === 'SPLIT') { + this.outputConnection!.setCheck('Array'); + this.getInput('INPUT')!.setCheck('String'); + } else { + this.outputConnection!.setCheck('String'); + this.getInput('INPUT')!.setCheck('Array'); + } + }, + /** + * Create XML to represent the input and output types. + * + * @returns XML storage element. + */ + mutationToDom: function (this: SplitBlock): Element { + const container = xmlUtils.createElement('mutation'); + container.setAttribute('mode', this.getFieldValue('MODE')); + return container; + }, + /** + * Parse XML to restore the input and output types. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: SplitBlock, xmlElement: Element) { + this.updateType_(xmlElement.getAttribute('mode')); + }, + + /** + * Returns the state of this block as a JSON serializable object. + * This block does not need to serialize any specific state as it is already + * encoded in the dropdown values, but must have an implementation to avoid + * the backward compatible XML mutations being serialized. + * + * @returns The state of this block. + */ + saveExtraState: function (this: SplitBlock): null { + return null; + }, + + /** + * Applies the given state to this block. + * No extra state is needed or expected as it is already encoded in the + * dropdown values. + */ + loadExtraState: function (this: SplitBlock) {}, +}; + +// Register provided blocks. +defineBlocks(blocks); diff --git a/blocks/logic.js b/blocks/logic.js deleted file mode 100644 index afec94d3fa6..00000000000 --- a/blocks/logic.js +++ /dev/null @@ -1,635 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Logic blocks for Blockly. - * - * This file is scraped to extract a .json file of block definitions. The array - * passed to defineBlocksWithJsonArray(..) must be strict JSON: double quotes - * only, no outside references, no functions, no trailing commas, etc. The one - * exception is end-of-line comments, which the scraper will remove. - * @author q.neutron@gmail.com (Quynh Neutron) - */ -'use strict'; - -goog.provide('Blockly.Blocks.logic'); // Deprecated -goog.provide('Blockly.Constants.Logic'); - -goog.require('Blockly'); -goog.require('Blockly.Blocks'); -goog.require('Blockly.FieldDropdown'); -goog.require('Blockly.FieldLabel'); -goog.require('Blockly.Mutator'); - - -/** - * Unused constant for the common HSV hue for all blocks in this category. - * @deprecated Use Blockly.Msg['LOGIC_HUE']. (2018 April 5) - */ -Blockly.Constants.Logic.HUE = 210; - -Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT - // Block for boolean data type: true and false. - { - "type": "logic_boolean", - "message0": "%1", - "args0": [ - { - "type": "field_dropdown", - "name": "BOOL", - "options": [ - ["%{BKY_LOGIC_BOOLEAN_TRUE}", "TRUE"], - ["%{BKY_LOGIC_BOOLEAN_FALSE}", "FALSE"] - ] - } - ], - "output": "Boolean", - "style": "logic_blocks", - "tooltip": "%{BKY_LOGIC_BOOLEAN_TOOLTIP}", - "helpUrl": "%{BKY_LOGIC_BOOLEAN_HELPURL}" - }, - // Block for if/elseif/else condition. - { - "type": "controls_if", - "message0": "%{BKY_CONTROLS_IF_MSG_IF} %1", - "args0": [ - { - "type": "input_value", - "name": "IF0", - "check": "Boolean" - } - ], - "message1": "%{BKY_CONTROLS_IF_MSG_THEN} %1", - "args1": [ - { - "type": "input_statement", - "name": "DO0" - } - ], - "previousStatement": null, - "nextStatement": null, - "style": "logic_blocks", - "helpUrl": "%{BKY_CONTROLS_IF_HELPURL}", - "mutator": "controls_if_mutator", - "extensions": ["controls_if_tooltip"] - }, - // If/else block that does not use a mutator. - { - "type": "controls_ifelse", - "message0": "%{BKY_CONTROLS_IF_MSG_IF} %1", - "args0": [ - { - "type": "input_value", - "name": "IF0", - "check": "Boolean" - } - ], - "message1": "%{BKY_CONTROLS_IF_MSG_THEN} %1", - "args1": [ - { - "type": "input_statement", - "name": "DO0" - } - ], - "message2": "%{BKY_CONTROLS_IF_MSG_ELSE} %1", - "args2": [ - { - "type": "input_statement", - "name": "ELSE" - } - ], - "previousStatement": null, - "nextStatement": null, - "style": "logic_blocks", - "tooltip": "%{BKYCONTROLS_IF_TOOLTIP_2}", - "helpUrl": "%{BKY_CONTROLS_IF_HELPURL}", - "extensions": ["controls_if_tooltip"] - }, - // Block for comparison operator. - { - "type": "logic_compare", - "message0": "%1 %2 %3", - "args0": [ - { - "type": "input_value", - "name": "A" - }, - { - "type": "field_dropdown", - "name": "OP", - "options": [ - ["=", "EQ"], - ["\u2260", "NEQ"], - ["\u200F<", "LT"], - ["\u200F\u2264", "LTE"], - ["\u200F>", "GT"], - ["\u200F\u2265", "GTE"] - ] - }, - { - "type": "input_value", - "name": "B" - } - ], - "inputsInline": true, - "output": "Boolean", - "style": "logic_blocks", - "helpUrl": "%{BKY_LOGIC_COMPARE_HELPURL}", - "extensions": ["logic_compare", "logic_op_tooltip"] - }, - // Block for logical operations: 'and', 'or'. - { - "type": "logic_operation", - "message0": "%1 %2 %3", - "args0": [ - { - "type": "input_value", - "name": "A", - "check": "Boolean" - }, - { - "type": "field_dropdown", - "name": "OP", - "options": [ - ["%{BKY_LOGIC_OPERATION_AND}", "AND"], - ["%{BKY_LOGIC_OPERATION_OR}", "OR"] - ] - }, - { - "type": "input_value", - "name": "B", - "check": "Boolean" - } - ], - "inputsInline": true, - "output": "Boolean", - "style": "logic_blocks", - "helpUrl": "%{BKY_LOGIC_OPERATION_HELPURL}", - "extensions": ["logic_op_tooltip"] - }, - // Block for negation. - { - "type": "logic_negate", - "message0": "%{BKY_LOGIC_NEGATE_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "BOOL", - "check": "Boolean" - } - ], - "output": "Boolean", - "style": "logic_blocks", - "tooltip": "%{BKY_LOGIC_NEGATE_TOOLTIP}", - "helpUrl": "%{BKY_LOGIC_NEGATE_HELPURL}" - }, - // Block for null data type. - { - "type": "logic_null", - "message0": "%{BKY_LOGIC_NULL}", - "output": null, - "style": "logic_blocks", - "tooltip": "%{BKY_LOGIC_NULL_TOOLTIP}", - "helpUrl": "%{BKY_LOGIC_NULL_HELPURL}" - }, - // Block for ternary operator. - { - "type": "logic_ternary", - "message0": "%{BKY_LOGIC_TERNARY_CONDITION} %1", - "args0": [ - { - "type": "input_value", - "name": "IF", - "check": "Boolean" - } - ], - "message1": "%{BKY_LOGIC_TERNARY_IF_TRUE} %1", - "args1": [ - { - "type": "input_value", - "name": "THEN" - } - ], - "message2": "%{BKY_LOGIC_TERNARY_IF_FALSE} %1", - "args2": [ - { - "type": "input_value", - "name": "ELSE" - } - ], - "output": null, - "style": "logic_blocks", - "tooltip": "%{BKY_LOGIC_TERNARY_TOOLTIP}", - "helpUrl": "%{BKY_LOGIC_TERNARY_HELPURL}", - "extensions": ["logic_ternary"] - } -]); // END JSON EXTRACT (Do not delete this comment.) - -Blockly.defineBlocksWithJsonArray([ // Mutator blocks. Do not extract. - // Block representing the if statement in the controls_if mutator. - { - "type": "controls_if_if", - "message0": "%{BKY_CONTROLS_IF_IF_TITLE_IF}", - "nextStatement": null, - "enableContextMenu": false, - "style": "logic_blocks", - "tooltip": "%{BKY_CONTROLS_IF_IF_TOOLTIP}" - }, - // Block representing the else-if statement in the controls_if mutator. - { - "type": "controls_if_elseif", - "message0": "%{BKY_CONTROLS_IF_ELSEIF_TITLE_ELSEIF}", - "previousStatement": null, - "nextStatement": null, - "enableContextMenu": false, - "style": "logic_blocks", - "tooltip": "%{BKY_CONTROLS_IF_ELSEIF_TOOLTIP}" - }, - // Block representing the else statement in the controls_if mutator. - { - "type": "controls_if_else", - "message0": "%{BKY_CONTROLS_IF_ELSE_TITLE_ELSE}", - "previousStatement": null, - "enableContextMenu": false, - "style": "logic_blocks", - "tooltip": "%{BKY_CONTROLS_IF_ELSE_TOOLTIP}" - } -]); - -/** - * Tooltip text, keyed by block OP value. Used by logic_compare and - * logic_operation blocks. - * @see {Blockly.Extensions#buildTooltipForDropdown} - * @package - * @readonly - */ -Blockly.Constants.Logic.TOOLTIPS_BY_OP = { - // logic_compare - 'EQ': '%{BKY_LOGIC_COMPARE_TOOLTIP_EQ}', - 'NEQ': '%{BKY_LOGIC_COMPARE_TOOLTIP_NEQ}', - 'LT': '%{BKY_LOGIC_COMPARE_TOOLTIP_LT}', - 'LTE': '%{BKY_LOGIC_COMPARE_TOOLTIP_LTE}', - 'GT': '%{BKY_LOGIC_COMPARE_TOOLTIP_GT}', - 'GTE': '%{BKY_LOGIC_COMPARE_TOOLTIP_GTE}', - - // logic_operation - 'AND': '%{BKY_LOGIC_OPERATION_TOOLTIP_AND}', - 'OR': '%{BKY_LOGIC_OPERATION_TOOLTIP_OR}' -}; - -Blockly.Extensions.register('logic_op_tooltip', - Blockly.Extensions.buildTooltipForDropdown( - 'OP', Blockly.Constants.Logic.TOOLTIPS_BY_OP)); - -/** - * Mutator methods added to controls_if blocks. - * @mixin - * @augments Blockly.Block - * @package - * @readonly - */ -Blockly.Constants.Logic.CONTROLS_IF_MUTATOR_MIXIN = { - elseifCount_: 0, - elseCount_: 0, - - /** - * Don't automatically add STATEMENT_PREFIX and STATEMENT_SUFFIX to generated - * code. These will be handled manually in this block's generators. - */ - suppressPrefixSuffix: true, - - /** - * Create XML to represent the number of else-if and else inputs. - * @return {Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - if (!this.elseifCount_ && !this.elseCount_) { - return null; - } - var container = Blockly.utils.xml.createElement('mutation'); - if (this.elseifCount_) { - container.setAttribute('elseif', this.elseifCount_); - } - if (this.elseCount_) { - container.setAttribute('else', 1); - } - return container; - }, - /** - * Parse XML to restore the else-if and else inputs. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - this.elseifCount_ = parseInt(xmlElement.getAttribute('elseif'), 10) || 0; - this.elseCount_ = parseInt(xmlElement.getAttribute('else'), 10) || 0; - this.rebuildShape_(); - }, - /** - * Populate the mutator's dialog with this block's components. - * @param {!Blockly.Workspace} workspace Mutator's workspace. - * @return {!Blockly.Block} Root block in mutator. - * @this {Blockly.Block} - */ - decompose: function(workspace) { - var containerBlock = workspace.newBlock('controls_if_if'); - containerBlock.initSvg(); - var connection = containerBlock.nextConnection; - for (var i = 1; i <= this.elseifCount_; i++) { - var elseifBlock = workspace.newBlock('controls_if_elseif'); - elseifBlock.initSvg(); - connection.connect(elseifBlock.previousConnection); - connection = elseifBlock.nextConnection; - } - if (this.elseCount_) { - var elseBlock = workspace.newBlock('controls_if_else'); - elseBlock.initSvg(); - connection.connect(elseBlock.previousConnection); - } - return containerBlock; - }, - /** - * Reconfigure this block based on the mutator dialog's components. - * @param {!Blockly.Block} containerBlock Root block in mutator. - * @this {Blockly.Block} - */ - compose: function(containerBlock) { - var clauseBlock = containerBlock.nextConnection.targetBlock(); - // Count number of inputs. - this.elseifCount_ = 0; - this.elseCount_ = 0; - var valueConnections = [null]; - var statementConnections = [null]; - var elseStatementConnection = null; - while (clauseBlock) { - switch (clauseBlock.type) { - case 'controls_if_elseif': - this.elseifCount_++; - valueConnections.push(clauseBlock.valueConnection_); - statementConnections.push(clauseBlock.statementConnection_); - break; - case 'controls_if_else': - this.elseCount_++; - elseStatementConnection = clauseBlock.statementConnection_; - break; - default: - throw TypeError('Unknown block type: ' + clauseBlock.type); - } - clauseBlock = clauseBlock.nextConnection && - clauseBlock.nextConnection.targetBlock(); - } - this.updateShape_(); - // Reconnect any child blocks. - this.reconnectChildBlocks_(valueConnections, statementConnections, - elseStatementConnection); - }, - /** - * Store pointers to any connected child blocks. - * @param {!Blockly.Block} containerBlock Root block in mutator. - * @this {Blockly.Block} - */ - saveConnections: function(containerBlock) { - var clauseBlock = containerBlock.nextConnection.targetBlock(); - var i = 1; - while (clauseBlock) { - switch (clauseBlock.type) { - case 'controls_if_elseif': - var inputIf = this.getInput('IF' + i); - var inputDo = this.getInput('DO' + i); - clauseBlock.valueConnection_ = - inputIf && inputIf.connection.targetConnection; - clauseBlock.statementConnection_ = - inputDo && inputDo.connection.targetConnection; - i++; - break; - case 'controls_if_else': - var inputDo = this.getInput('ELSE'); - clauseBlock.statementConnection_ = - inputDo && inputDo.connection.targetConnection; - break; - default: - throw TypeError('Unknown block type: ' + clauseBlock.type); - } - clauseBlock = clauseBlock.nextConnection && - clauseBlock.nextConnection.targetBlock(); - } - }, - /** - * Reconstructs the block with all child blocks attached. - * @this {Blockly.Block} - */ - rebuildShape_: function() { - var valueConnections = [null]; - var statementConnections = [null]; - var elseStatementConnection = null; - - if (this.getInput('ELSE')) { - elseStatementConnection = this.getInput('ELSE').connection.targetConnection; - } - var i = 1; - while (this.getInput('IF' + i)) { - var inputIf = this.getInput('IF' + i); - var inputDo = this.getInput('DO' + i); - valueConnections.push(inputIf.connection.targetConnection); - statementConnections.push(inputDo.connection.targetConnection); - i++; - } - this.updateShape_(); - this.reconnectChildBlocks_(valueConnections, statementConnections, - elseStatementConnection); - }, - /** - * Modify this block to have the correct number of inputs. - * @this {Blockly.Block} - * @private - */ - updateShape_: function() { - // Delete everything. - if (this.getInput('ELSE')) { - this.removeInput('ELSE'); - } - var i = 1; - while (this.getInput('IF' + i)) { - this.removeInput('IF' + i); - this.removeInput('DO' + i); - i++; - } - // Rebuild block. - for (i = 1; i <= this.elseifCount_; i++) { - this.appendValueInput('IF' + i) - .setCheck('Boolean') - .appendField(Blockly.Msg['CONTROLS_IF_MSG_ELSEIF']); - this.appendStatementInput('DO' + i) - .appendField(Blockly.Msg['CONTROLS_IF_MSG_THEN']); - } - if (this.elseCount_) { - this.appendStatementInput('ELSE') - .appendField(Blockly.Msg['CONTROLS_IF_MSG_ELSE']); - } - }, - /** - * Reconnects child blocks. - * @param {!Array.} valueConnections List of - * value connections for 'if' input. - * @param {!Array.} statementConnections List of - * statement connections for 'do' input. - * @param {?Blockly.RenderedConnection} elseStatementConnection Statement - * connection for else input. - * @this {Blockly.Block} - */ - reconnectChildBlocks_: function(valueConnections, statementConnections, - elseStatementConnection) { - for (var i = 1; i <= this.elseifCount_; i++) { - Blockly.Mutator.reconnect(valueConnections[i], this, 'IF' + i); - Blockly.Mutator.reconnect(statementConnections[i], this, 'DO' + i); - } - Blockly.Mutator.reconnect(elseStatementConnection, this, 'ELSE'); - } -}; - -Blockly.Extensions.registerMutator('controls_if_mutator', - Blockly.Constants.Logic.CONTROLS_IF_MUTATOR_MIXIN, null, - ['controls_if_elseif', 'controls_if_else']); -/** - * "controls_if" extension function. Adds mutator, shape updating methods, and - * dynamic tooltip to "controls_if" blocks. - * @this {Blockly.Block} - * @package - */ -Blockly.Constants.Logic.CONTROLS_IF_TOOLTIP_EXTENSION = function() { - - this.setTooltip(function() { - if (!this.elseifCount_ && !this.elseCount_) { - return Blockly.Msg['CONTROLS_IF_TOOLTIP_1']; - } else if (!this.elseifCount_ && this.elseCount_) { - return Blockly.Msg['CONTROLS_IF_TOOLTIP_2']; - } else if (this.elseifCount_ && !this.elseCount_) { - return Blockly.Msg['CONTROLS_IF_TOOLTIP_3']; - } else if (this.elseifCount_ && this.elseCount_) { - return Blockly.Msg['CONTROLS_IF_TOOLTIP_4']; - } - return ''; - }.bind(this)); -}; - -Blockly.Extensions.register('controls_if_tooltip', - Blockly.Constants.Logic.CONTROLS_IF_TOOLTIP_EXTENSION); - -/** - * Adds dynamic type validation for the left and right sides of a logic_compare - * block. - * @mixin - * @augments Blockly.Block - * @package - * @readonly - */ -Blockly.Constants.Logic.LOGIC_COMPARE_ONCHANGE_MIXIN = { - /** - * Called whenever anything on the workspace changes. - * Prevent mismatched types from being compared. - * @param {!Blockly.Events.Abstract} e Change event. - * @this {Blockly.Block} - */ - onchange: function(e) { - if (!this.prevBlocks_) { - this.prevBlocks_ = [null, null]; - } - - var blockA = this.getInputTargetBlock('A'); - var 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)) { - // Mismatch between two inputs. Revert the block connections, - // bumping away the newly connected block(s). - Blockly.Events.setGroup(e.group); - var prevA = this.prevBlocks_[0]; - if (prevA !== blockA) { - blockA.unplug(); - if (prevA && !prevA.isDisposed() && !prevA.isShadow()) { - // The shadow block is automatically replaced during unplug(). - this.getInput('A').connection.connect(prevA.outputConnection); - } - } - var prevB = this.prevBlocks_[1]; - if (prevB !== blockB) { - blockB.unplug(); - if (prevB && !prevB.isDisposed() && !prevB.isShadow()) { - // The shadow block is automatically replaced during unplug(). - this.getInput('B').connection.connect(prevB.outputConnection); - } - } - this.bumpNeighbours(); - Blockly.Events.setGroup(false); - } - this.prevBlocks_[0] = this.getInputTargetBlock('A'); - this.prevBlocks_[1] = this.getInputTargetBlock('B'); - } -}; - -/** - * "logic_compare" extension function. Adds type left and right side type - * checking to "logic_compare" blocks. - * @this {Blockly.Block} - * @package - * @readonly - */ -Blockly.Constants.Logic.LOGIC_COMPARE_EXTENSION = function() { - // Add onchange handler to ensure types are compatible. - this.mixin(Blockly.Constants.Logic.LOGIC_COMPARE_ONCHANGE_MIXIN); -}; - -Blockly.Extensions.register('logic_compare', - Blockly.Constants.Logic.LOGIC_COMPARE_EXTENSION); - -/** - * Adds type coordination between inputs and output. - * @mixin - * @augments Blockly.Block - * @package - * @readonly - */ -Blockly.Constants.Logic.LOGIC_TERNARY_ONCHANGE_MIXIN = { - prevParentConnection_: null, - - /** - * Called whenever anything on the workspace changes. - * Prevent mismatched types. - * @param {!Blockly.Events.Abstract} e Change event. - * @this {Blockly.Block} - */ - onchange: function(e) { - var blockA = this.getInputTargetBlock('THEN'); - var blockB = this.getInputTargetBlock('ELSE'); - var parentConnection = this.outputConnection.targetConnection; - // Disconnect blocks that existed prior to this change if they don't match. - if ((blockA || blockB) && parentConnection) { - for (var i = 0; i < 2; i++) { - var block = (i == 1) ? blockA : blockB; - if (block && - !block.workspace.connectionChecker.doTypeChecks( - block.outputConnection, parentConnection)) { - // Ensure that any disconnections are grouped with the causing event. - Blockly.Events.setGroup(e.group); - if (parentConnection === this.prevParentConnection_) { - this.unplug(); - parentConnection.getSourceBlock().bumpNeighbours(); - } else { - block.unplug(); - block.bumpNeighbours(); - } - Blockly.Events.setGroup(false); - } - } - } - this.prevParentConnection_ = parentConnection; - } -}; - -Blockly.Extensions.registerMixin('logic_ternary', - Blockly.Constants.Logic.LOGIC_TERNARY_ONCHANGE_MIXIN); diff --git a/blocks/logic.ts b/blocks/logic.ts new file mode 100644 index 00000000000..3e5b807ebcf --- /dev/null +++ b/blocks/logic.ts @@ -0,0 +1,712 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: 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. + */ +export const blocks = createBlockDefinitionsFromJsonArray([ + // Block for boolean data type: true and false. + { + 'type': 'logic_boolean', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_dropdown', + 'name': 'BOOL', + 'options': [ + ['%{BKY_LOGIC_BOOLEAN_TRUE}', 'TRUE'], + ['%{BKY_LOGIC_BOOLEAN_FALSE}', 'FALSE'], + ], + }, + ], + 'output': 'Boolean', + 'style': 'logic_blocks', + 'tooltip': '%{BKY_LOGIC_BOOLEAN_TOOLTIP}', + 'helpUrl': '%{BKY_LOGIC_BOOLEAN_HELPURL}', + }, + // Block for if/elseif/else condition. + { + 'type': 'controls_if', + 'message0': '%{BKY_CONTROLS_IF_MSG_IF} %1', + 'args0': [ + { + 'type': 'input_value', + 'name': 'IF0', + 'check': 'Boolean', + }, + ], + 'message1': '%{BKY_CONTROLS_IF_MSG_THEN} %1', + 'args1': [ + { + 'type': 'input_statement', + 'name': 'DO0', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'style': 'logic_blocks', + 'helpUrl': '%{BKY_CONTROLS_IF_HELPURL}', + 'suppressPrefixSuffix': true, + 'mutator': 'controls_if_mutator', + 'extensions': ['controls_if_tooltip'], + }, + // If/else block that does not use a mutator. + { + 'type': 'controls_ifelse', + 'message0': '%{BKY_CONTROLS_IF_MSG_IF} %1', + 'args0': [ + { + 'type': 'input_value', + 'name': 'IF0', + 'check': 'Boolean', + }, + ], + 'message1': '%{BKY_CONTROLS_IF_MSG_THEN} %1', + 'args1': [ + { + 'type': 'input_statement', + 'name': 'DO0', + }, + ], + 'message2': '%{BKY_CONTROLS_IF_MSG_ELSE} %1', + 'args2': [ + { + 'type': 'input_statement', + 'name': 'ELSE', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'style': 'logic_blocks', + 'tooltip': '%{BKYCONTROLS_IF_TOOLTIP_2}', + 'helpUrl': '%{BKY_CONTROLS_IF_HELPURL}', + 'suppressPrefixSuffix': true, + 'extensions': ['controls_if_tooltip'], + }, + // Block for comparison operator. + { + 'type': 'logic_compare', + 'message0': '%1 %2 %3', + 'args0': [ + { + 'type': 'input_value', + 'name': 'A', + }, + { + 'type': 'field_dropdown', + 'name': 'OP', + 'options': [ + ['=', 'EQ'], + ['\u2260', 'NEQ'], + ['\u200F<', 'LT'], + ['\u200F\u2264', 'LTE'], + ['\u200F>', 'GT'], + ['\u200F\u2265', 'GTE'], + ], + }, + { + 'type': 'input_value', + 'name': 'B', + }, + ], + 'inputsInline': true, + 'output': 'Boolean', + 'style': 'logic_blocks', + 'helpUrl': '%{BKY_LOGIC_COMPARE_HELPURL}', + 'extensions': ['logic_compare', 'logic_op_tooltip'], + }, + // Block for logical operations: 'and', 'or'. + { + 'type': 'logic_operation', + 'message0': '%1 %2 %3', + 'args0': [ + { + 'type': 'input_value', + 'name': 'A', + 'check': 'Boolean', + }, + { + 'type': 'field_dropdown', + 'name': 'OP', + 'options': [ + ['%{BKY_LOGIC_OPERATION_AND}', 'AND'], + ['%{BKY_LOGIC_OPERATION_OR}', 'OR'], + ], + }, + { + 'type': 'input_value', + 'name': 'B', + 'check': 'Boolean', + }, + ], + 'inputsInline': true, + 'output': 'Boolean', + 'style': 'logic_blocks', + 'helpUrl': '%{BKY_LOGIC_OPERATION_HELPURL}', + 'extensions': ['logic_op_tooltip'], + }, + // Block for negation. + { + 'type': 'logic_negate', + 'message0': '%{BKY_LOGIC_NEGATE_TITLE}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'BOOL', + 'check': 'Boolean', + }, + ], + 'output': 'Boolean', + 'style': 'logic_blocks', + 'tooltip': '%{BKY_LOGIC_NEGATE_TOOLTIP}', + 'helpUrl': '%{BKY_LOGIC_NEGATE_HELPURL}', + }, + // Block for null data type. + { + 'type': 'logic_null', + 'message0': '%{BKY_LOGIC_NULL}', + 'output': null, + 'style': 'logic_blocks', + 'tooltip': '%{BKY_LOGIC_NULL_TOOLTIP}', + 'helpUrl': '%{BKY_LOGIC_NULL_HELPURL}', + }, + // Block for ternary operator. + { + 'type': 'logic_ternary', + 'message0': '%{BKY_LOGIC_TERNARY_CONDITION} %1', + 'args0': [ + { + 'type': 'input_value', + 'name': 'IF', + 'check': 'Boolean', + }, + ], + 'message1': '%{BKY_LOGIC_TERNARY_IF_TRUE} %1', + 'args1': [ + { + 'type': 'input_value', + 'name': 'THEN', + }, + ], + 'message2': '%{BKY_LOGIC_TERNARY_IF_FALSE} %1', + 'args2': [ + { + 'type': 'input_value', + 'name': 'ELSE', + }, + ], + 'output': null, + 'style': 'logic_blocks', + 'tooltip': '%{BKY_LOGIC_TERNARY_TOOLTIP}', + 'helpUrl': '%{BKY_LOGIC_TERNARY_HELPURL}', + 'extensions': ['logic_ternary'], + }, + // Block representing the if statement in the controls_if mutator. + { + 'type': 'controls_if_if', + 'message0': '%{BKY_CONTROLS_IF_IF_TITLE_IF}', + 'nextStatement': null, + 'enableContextMenu': false, + 'style': 'logic_blocks', + 'tooltip': '%{BKY_CONTROLS_IF_IF_TOOLTIP}', + }, + // Block representing the else-if statement in the controls_if mutator. + { + 'type': 'controls_if_elseif', + 'message0': '%{BKY_CONTROLS_IF_ELSEIF_TITLE_ELSEIF}', + 'previousStatement': null, + 'nextStatement': null, + 'enableContextMenu': false, + 'style': 'logic_blocks', + 'tooltip': '%{BKY_CONTROLS_IF_ELSEIF_TOOLTIP}', + }, + // Block representing the else statement in the controls_if mutator. + { + 'type': 'controls_if_else', + 'message0': '%{BKY_CONTROLS_IF_ELSE_TITLE_ELSE}', + 'previousStatement': null, + 'enableContextMenu': false, + 'style': 'logic_blocks', + 'tooltip': '%{BKY_CONTROLS_IF_ELSE_TOOLTIP}', + }, +]); + +/** + * Tooltip text, keyed by block OP value. Used by logic_compare and + * logic_operation blocks. + * + * @see {Extensions#buildTooltipForDropdown} + */ +const TOOLTIPS_BY_OP = { + // logic_compare + 'EQ': '%{BKY_LOGIC_COMPARE_TOOLTIP_EQ}', + 'NEQ': '%{BKY_LOGIC_COMPARE_TOOLTIP_NEQ}', + 'LT': '%{BKY_LOGIC_COMPARE_TOOLTIP_LT}', + 'LTE': '%{BKY_LOGIC_COMPARE_TOOLTIP_LTE}', + 'GT': '%{BKY_LOGIC_COMPARE_TOOLTIP_GT}', + 'GTE': '%{BKY_LOGIC_COMPARE_TOOLTIP_GTE}', + + // logic_operation + 'AND': '%{BKY_LOGIC_OPERATION_TOOLTIP_AND}', + 'OR': '%{BKY_LOGIC_OPERATION_TOOLTIP_OR}', +}; + +Extensions.register( + '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. + */ +const CONTROLS_IF_MUTATOR_MIXIN = { + elseifCount_: 0, + elseCount_: 0, + + /** + * Create XML to represent the number of else-if and else inputs. + * Backwards compatible serialization implementation. + * + * @returns XML storage element. + */ + mutationToDom: function (this: IfBlock): Element | null { + if (!this.elseifCount_ && !this.elseCount_) { + return null; + } + const container = xmlUtils.createElement('mutation'); + if (this.elseifCount_) { + container.setAttribute('elseif', String(this.elseifCount_)); + } + if (this.elseCount_) { + container.setAttribute('else', '1'); + } + return container; + }, + /** + * Parse XML to restore the else-if and else inputs. + * Backwards compatible serialization implementation. + * + * @param xmlElement XML storage element. + */ + 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. + * + * @returns The state of this block, ie the else if count and else state. + */ + saveExtraState: function (this: IfBlock): IfExtraState | null { + if (!this.elseifCount_ && !this.elseCount_) { + return null; + } + const state = Object.create(null); + if (this.elseifCount_) { + state['elseIfCount'] = this.elseifCount_; + } + if (this.elseCount_) { + state['hasElse'] = true; + } + return state; + }, + /** + * Applies the given state to this block. + * + * @param state The state to apply to this block, ie the else if count + and + * else 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 MutatorIcon's workspace. + * @returns Root block in mutator. + */ + decompose: function (this: IfBlock, workspace: Workspace): ContainerBlock { + const containerBlock = workspace.newBlock('controls_if_if'); + (containerBlock as BlockSvg).initSvg(); + let connection = containerBlock.nextConnection!; + for (let i = 1; i <= this.elseifCount_; i++) { + const elseifBlock = workspace.newBlock('controls_if_elseif'); + (elseifBlock as BlockSvg).initSvg(); + connection.connect(elseifBlock.previousConnection!); + connection = elseifBlock.nextConnection!; + } + if (this.elseCount_) { + const elseBlock = workspace.newBlock('controls_if_else'); + (elseBlock as BlockSvg).initSvg(); + connection.connect(elseBlock.previousConnection!); + } + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * + * @param containerBlock Root block in mutator. + */ + compose: function (this: IfBlock, containerBlock: ContainerBlock) { + let clauseBlock = + containerBlock.nextConnection!.targetBlock() as ClauseBlock | null; + // Count number of inputs. + this.elseifCount_ = 0; + this.elseCount_ = 0; + // 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 = [null]; + const statementConnections: Array = [null]; + let elseStatementConnection: Connection | null = null; + while (clauseBlock) { + if (clauseBlock.isInsertionMarker()) { + clauseBlock = clauseBlock.getNextBlock() as ClauseBlock | null; + continue; + } + switch (clauseBlock.type) { + case 'controls_if_elseif': + this.elseifCount_++; + // 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_ as Connection | null; + break; + default: + throw TypeError('Unknown block type: ' + clauseBlock.type); + } + clauseBlock = clauseBlock.getNextBlock() as ClauseBlock | null; + } + this.updateShape_(); + // Reconnect any child blocks. + this.reconnectChildBlocks_( + valueConnections, + statementConnections, + elseStatementConnection, + ); + }, + /** + * Store pointers to any connected child blocks. + * + * @param containerBlock Root block in mutator. + */ + 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() as ClauseBlock | null; + continue; + } + switch (clauseBlock.type) { + case 'controls_if_elseif': { + const inputIf = this.getInput('IF' + i); + const inputDo = this.getInput('DO' + i); + clauseBlock.valueConnection_ = + inputIf && inputIf.connection!.targetConnection; + clauseBlock.statementConnection_ = + inputDo && inputDo.connection!.targetConnection; + i++; + break; + } + case 'controls_if_else': { + const inputDo = this.getInput('ELSE'); + clauseBlock.statementConnection_ = + inputDo && inputDo.connection!.targetConnection; + break; + } + default: + throw TypeError('Unknown block type: ' + clauseBlock.type); + } + clauseBlock = clauseBlock.getNextBlock() as ClauseBlock | null; + } + }, + /** + * Reconstructs the block with all child blocks attached. + */ + rebuildShape_: function (this: IfBlock) { + const valueConnections: Array = [null]; + const statementConnections: Array = [null]; + let elseStatementConnection: Connection | null = null; + + if (this.getInput('ELSE')) { + elseStatementConnection = + 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); + } + this.updateShape_(); + this.reconnectChildBlocks_( + valueConnections, + statementConnections, + elseStatementConnection, + ); + }, + /** + * Modify this block to have the correct number of inputs. + * + * @internal + */ + updateShape_: function (this: IfBlock) { + // Delete everything. + if (this.getInput('ELSE')) { + this.removeInput('ELSE'); + } + for (let i = 1; this.getInput('IF' + i); i++) { + this.removeInput('IF' + i); + this.removeInput('DO' + i); + } + // Rebuild block. + for (let i = 1; i <= this.elseifCount_; i++) { + this.appendValueInput('IF' + i) + .setCheck('Boolean') + .appendField(Msg['CONTROLS_IF_MSG_ELSEIF']); + this.appendStatementInput('DO' + i).appendField( + Msg['CONTROLS_IF_MSG_THEN'], + ); + } + if (this.elseCount_) { + this.appendStatementInput('ELSE').appendField( + Msg['CONTROLS_IF_MSG_ELSE'], + ); + } + }, + /** + * Reconnects child blocks. + * + * @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 ( + this: IfBlock, + valueConnections: Array, + statementConnections: Array, + elseStatementConnection: Connection | null, + ) { + for (let i = 1; i <= this.elseifCount_; i++) { + valueConnections[i]?.reconnect(this, 'IF' + i); + statementConnections[i]?.reconnect(this, 'DO' + i); + } + elseStatementConnection?.reconnect(this, 'ELSE'); + }, +}; + +Extensions.registerMutator( + '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. + */ +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; +} +type CompareMixinType = typeof LOGIC_COMPARE_ONCHANGE_MIXIN; + +/** + * 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 e Change event. + */ + 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!, + ) + ) { + // Mismatch between two inputs. Revert the block connections, + // bumping away the newly connected block(s). + Events.setGroup(e.group); + const prevA = this.prevBlocks_[0]; + if (prevA !== blockA) { + blockA.unplug(); + if (prevA && !prevA.isDisposed() && !prevA.isShadow()) { + // The shadow block is automatically replaced during unplug(). + this.getInput('A')!.connection!.connect(prevA.outputConnection!); + } + } + const prevB = this.prevBlocks_[1]; + if (prevB !== blockB) { + blockB.unplug(); + if (prevB && !prevB.isDisposed() && !prevB.isShadow()) { + // The shadow block is automatically replaced during unplug(). + this.getInput('B')!.connection!.connect(prevB.outputConnection!); + } + } + this.bumpNeighbours(); + Events.setGroup(false); + } + this.prevBlocks_[0] = this.getInputTargetBlock('A'); + this.prevBlocks_[1] = this.getInputTargetBlock('B'); + }, +}; + +/** + * "logic_compare" extension function. Adds type left and right side type + * checking to "logic_compare" blocks. + */ +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. + */ +const LOGIC_TERNARY_ONCHANGE_MIXIN = { + prevParentConnection_: null as Connection | null, + + /** + * Called whenever anything on the workspace changes. + * Prevent mismatched types. + */ + 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. + 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. + Events.setGroup(e.group); + if (parentConnection === this.prevParentConnection_) { + this.unplug(); + parentConnection.getSourceBlock().bumpNeighbours(); + } else { + block.unplug(); + block.bumpNeighbours(); + } + Events.setGroup(false); + } + } + } + this.prevParentConnection_ = parentConnection; + }, +}; + +Extensions.registerMixin('logic_ternary', LOGIC_TERNARY_ONCHANGE_MIXIN); + +// Register provided blocks. +defineBlocks(blocks); diff --git a/blocks/loops.js b/blocks/loops.js deleted file mode 100644 index d6fb2f2f679..00000000000 --- a/blocks/loops.js +++ /dev/null @@ -1,357 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Loop blocks for Blockly. - * - * This file is scraped to extract a .json file of block definitions. The array - * passed to defineBlocksWithJsonArray(..) must be strict JSON: double quotes - * only, no outside references, no functions, no trailing commas, etc. The one - * exception is end-of-line comments, which the scraper will remove. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Blocks.loops'); // Deprecated -goog.provide('Blockly.Constants.Loops'); - -goog.require('Blockly'); -goog.require('Blockly.Blocks'); -goog.require('Blockly.FieldDropdown'); -goog.require('Blockly.FieldLabel'); -goog.require('Blockly.FieldNumber'); -goog.require('Blockly.FieldVariable'); -goog.require('Blockly.Warning'); - - -/** - * Unused constant for the common HSV hue for all blocks in this category. - * @deprecated Use Blockly.Msg['LOOPS_HUE']. (2018 April 5) - */ -Blockly.Constants.Loops.HUE = 120; - -Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT - // Block for repeat n times (external number). - { - "type": "controls_repeat_ext", - "message0": "%{BKY_CONTROLS_REPEAT_TITLE}", - "args0": [{ - "type": "input_value", - "name": "TIMES", - "check": "Number" - }], - "message1": "%{BKY_CONTROLS_REPEAT_INPUT_DO} %1", - "args1": [{ - "type": "input_statement", - "name": "DO" - }], - "previousStatement": null, - "nextStatement": null, - "style": "loop_blocks", - "tooltip": "%{BKY_CONTROLS_REPEAT_TOOLTIP}", - "helpUrl": "%{BKY_CONTROLS_REPEAT_HELPURL}" - }, - // Block for repeat n times (internal number). - // The 'controls_repeat_ext' block is preferred as it is more flexible. - { - "type": "controls_repeat", - "message0": "%{BKY_CONTROLS_REPEAT_TITLE}", - "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" - }], - "previousStatement": null, - "nextStatement": null, - "style": "loop_blocks", - "tooltip": "%{BKY_CONTROLS_REPEAT_TOOLTIP}", - "helpUrl": "%{BKY_CONTROLS_REPEAT_HELPURL}" - }, - // Block for 'do while/until' loop. - { - "type": "controls_whileUntil", - "message0": "%1 %2", - "args0": [ - { - "type": "field_dropdown", - "name": "MODE", - "options": [ - ["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}", "WHILE"], - ["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}", "UNTIL"] - ] - }, - { - "type": "input_value", - "name": "BOOL", - "check": "Boolean" - } - ], - "message1": "%{BKY_CONTROLS_REPEAT_INPUT_DO} %1", - "args1": [{ - "type": "input_statement", - "name": "DO" - }], - "previousStatement": null, - "nextStatement": null, - "style": "loop_blocks", - "helpUrl": "%{BKY_CONTROLS_WHILEUNTIL_HELPURL}", - "extensions": ["controls_whileUntil_tooltip"] - }, - // Block for 'for' loop. - { - "type": "controls_for", - "message0": "%{BKY_CONTROLS_FOR_TITLE}", - "args0": [ - { - "type": "field_variable", - "name": "VAR", - "variable": null - }, - { - "type": "input_value", - "name": "FROM", - "check": "Number", - "align": "RIGHT" - }, - { - "type": "input_value", - "name": "TO", - "check": "Number", - "align": "RIGHT" - }, - { - "type": "input_value", - "name": "BY", - "check": "Number", - "align": "RIGHT" - } - ], - "message1": "%{BKY_CONTROLS_REPEAT_INPUT_DO} %1", - "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" - ] - }, - // Block for 'for each' loop. - { - "type": "controls_forEach", - "message0": "%{BKY_CONTROLS_FOREACH_TITLE}", - "args0": [ - { - "type": "field_variable", - "name": "VAR", - "variable": null - }, - { - "type": "input_value", - "name": "LIST", - "check": "Array" - } - ], - "message1": "%{BKY_CONTROLS_REPEAT_INPUT_DO} %1", - "args1": [{ - "type": "input_statement", - "name": "DO" - }], - "previousStatement": null, - "nextStatement": null, - "style": "loop_blocks", - "helpUrl": "%{BKY_CONTROLS_FOREACH_HELPURL}", - "extensions": [ - "contextMenu_newGetVariableBlock", - "controls_forEach_tooltip" - ] - }, - // Block for flow statements: continue, break. - { - "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"] - ] - }], - "previousStatement": null, - "style": "loop_blocks", - "helpUrl": "%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}", - "extensions": [ - "controls_flow_tooltip", - "controls_flow_in_loop_check" - ] - } -]); // END JSON EXTRACT (Do not delete this comment.) - -/** - * Tooltips for the 'controls_whileUntil' block, keyed by MODE value. - * @see {Blockly.Extensions#buildTooltipForDropdown} - * @package - * @readonly - */ -Blockly.Constants.Loops.WHILE_UNTIL_TOOLTIPS = { - 'WHILE': '%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_WHILE}', - 'UNTIL': '%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL}' -}; - -Blockly.Extensions.register('controls_whileUntil_tooltip', - Blockly.Extensions.buildTooltipForDropdown( - 'MODE', Blockly.Constants.Loops.WHILE_UNTIL_TOOLTIPS)); - -/** - * Tooltips for the 'controls_flow_statements' block, keyed by FLOW value. - * @see {Blockly.Extensions#buildTooltipForDropdown} - * @package - * @readonly - */ -Blockly.Constants.Loops.BREAK_CONTINUE_TOOLTIPS = { - 'BREAK': '%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK}', - 'CONTINUE': '%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE}' -}; - -Blockly.Extensions.register('controls_flow_tooltip', - Blockly.Extensions.buildTooltipForDropdown( - 'FLOW', Blockly.Constants.Loops.BREAK_CONTINUE_TOOLTIPS)); - -/** - * Mixin to add a context menu item to create a 'variables_get' block. - * Used by blocks 'controls_for' and 'controls_forEach'. - * @mixin - * @augments Blockly.Block - * @package - * @readonly - */ -Blockly.Constants.Loops.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 {Blockly.Block} - */ - customContextMenu: function(options) { - if (this.isInFlyout) { - return; - } - var variable = this.getField('VAR').getVariable(); - var varName = variable.name; - if (!this.isCollapsed() && varName != null) { - var option = {enabled: true}; - option.text = - Blockly.Msg['VARIABLES_SET_CREATE_GET'].replace('%1', varName); - var xmlField = Blockly.Variables.generateVariableFieldDom(variable); - var xmlBlock = Blockly.utils.xml.createElement('block'); - xmlBlock.setAttribute('type', 'variables_get'); - xmlBlock.appendChild(xmlField); - option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); - options.push(option); - } - } -}; - -Blockly.Extensions.registerMixin('contextMenu_newGetVariableBlock', - Blockly.Constants.Loops.CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN); - -Blockly.Extensions.register('controls_for_tooltip', - Blockly.Extensions.buildTooltipWithFieldText( - '%{BKY_CONTROLS_FOR_TOOLTIP}', 'VAR')); - -Blockly.Extensions.register('controls_forEach_tooltip', - Blockly.Extensions.buildTooltipWithFieldText( - '%{BKY_CONTROLS_FOREACH_TOOLTIP}', 'VAR')); - -/** - * 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 Blockly.Block - * @package - * @readonly - */ -Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = { - /** - * List of block types that are loops and thus do not need warnings. - * To add a new loop type add this to your code: - * Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN.LOOP_TYPES.push('custom_loop'); - */ - LOOP_TYPES: [ - 'controls_repeat', - 'controls_repeat_ext', - 'controls_forEach', - 'controls_for', - 'controls_whileUntil' - ], - - /** - * Don't automatically add STATEMENT_PREFIX and STATEMENT_SUFFIX to generated - * code. These will be handled manually in this block's generators. - */ - suppressPrefixSuffix: true, - - /** - * Is the given block enclosed (at any level) by a loop? - * @param {!Blockly.Block} block Current block. - * @return {Blockly.Block} The nearest surrounding loop, or null if none. - */ - getSurroundLoop: function(block) { - // Is the block nested in a loop? - do { - if (Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN.LOOP_TYPES - .indexOf(block.type) != -1) { - return block; - } - block = block.getSurroundParent(); - } while (block); - return null; - }, - - /** - * Called whenever anything on the workspace changes. - * Add warning if this flow block is not nested inside a loop. - * @param {!Blockly.Events.Abstract} e Change event. - * @this {Blockly.Block} - */ - onchange: function(e) { - // Don't change state if: - // * It's at the start of a drag. - // * It's not a move event. - // * Or the moving block is not this block. - if (!this.workspace.isDragging || this.workspace.isDragging() || - e.type != Blockly.Events.BLOCK_MOVE || e.blockId != this.id) { - return; - } - var enabled = Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN - .getSurroundLoop(this); - this.setWarningText(enabled ? null : - Blockly.Msg['CONTROLS_FLOW_STATEMENTS_WARNING']); - if (!this.isInFlyout) { - var group = Blockly.Events.getGroup(); - // Makes it so the move and the disable event get undone together. - Blockly.Events.setGroup(e.group); - this.setEnabled(enabled); - Blockly.Events.setGroup(group); - } - } -}; - -Blockly.Extensions.registerMixin('controls_flow_in_loop_check', - Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN); diff --git a/blocks/loops.ts b/blocks/loops.ts new file mode 100644 index 00000000000..c7cb710d770 --- /dev/null +++ b/blocks/loops.ts @@ -0,0 +1,408 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.libraryBlocks.loops + +import type {Abstract as AbstractEvent} from '../core/events/events_abstract.js'; +import type {Block} from '../core/block.js'; +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 {Msg} from '../core/msg.js'; +import { + createBlockDefinitionsFromJsonArray, + defineBlocks, +} from '../core/common.js'; +import * as eventUtils from '../core/events/utils.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. + */ +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', + }, + ], + 'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1', + 'args1': [ + { + 'type': 'input_statement', + 'name': 'DO', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'style': 'loop_blocks', + 'tooltip': '%{BKY_CONTROLS_REPEAT_TOOLTIP}', + 'helpUrl': '%{BKY_CONTROLS_REPEAT_HELPURL}', + }, + // Block for repeat n times (internal number). + // The 'controls_repeat_ext' block is preferred as it is more flexible. + { + 'type': 'controls_repeat', + 'message0': '%{BKY_CONTROLS_REPEAT_TITLE}', + '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', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'style': 'loop_blocks', + 'tooltip': '%{BKY_CONTROLS_REPEAT_TOOLTIP}', + 'helpUrl': '%{BKY_CONTROLS_REPEAT_HELPURL}', + }, + // Block for 'do while/until' loop. + { + 'type': 'controls_whileUntil', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'field_dropdown', + 'name': 'MODE', + 'options': [ + ['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}', 'WHILE'], + ['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}', 'UNTIL'], + ], + }, + { + 'type': 'input_value', + 'name': 'BOOL', + 'check': 'Boolean', + }, + ], + 'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1', + 'args1': [ + { + 'type': 'input_statement', + 'name': 'DO', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'style': 'loop_blocks', + 'helpUrl': '%{BKY_CONTROLS_WHILEUNTIL_HELPURL}', + 'extensions': ['controls_whileUntil_tooltip'], + }, + // Block for 'for' loop. + { + 'type': 'controls_for', + 'message0': '%{BKY_CONTROLS_FOR_TITLE}', + 'args0': [ + { + 'type': 'field_variable', + 'name': 'VAR', + 'variable': null, + }, + { + 'type': 'input_value', + 'name': 'FROM', + 'check': 'Number', + 'align': 'RIGHT', + }, + { + 'type': 'input_value', + 'name': 'TO', + 'check': 'Number', + 'align': 'RIGHT', + }, + { + 'type': 'input_value', + 'name': 'BY', + 'check': 'Number', + 'align': 'RIGHT', + }, + ], + 'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1', + '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'], + }, + // Block for 'for each' loop. + { + 'type': 'controls_forEach', + 'message0': '%{BKY_CONTROLS_FOREACH_TITLE}', + 'args0': [ + { + 'type': 'field_variable', + 'name': 'VAR', + 'variable': null, + }, + { + 'type': 'input_value', + 'name': 'LIST', + 'check': 'Array', + }, + ], + 'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1', + 'args1': [ + { + 'type': 'input_statement', + 'name': 'DO', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'style': 'loop_blocks', + 'helpUrl': '%{BKY_CONTROLS_FOREACH_HELPURL}', + 'extensions': [ + 'contextMenu_newGetVariableBlock', + 'controls_forEach_tooltip', + ], + }, + // Block for flow statements: continue, break. + { + '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'], + ], + }, + ], + 'previousStatement': null, + 'style': 'loop_blocks', + 'helpUrl': '%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}', + 'suppressPrefixSuffix': true, + 'extensions': ['controls_flow_tooltip', 'controls_flow_in_loop_check'], + }, +]); + +/** + * Tooltips for the 'controls_whileUntil' block, keyed by MODE value. + * + * @see {Extensions#buildTooltipForDropdown} + */ +const WHILE_UNTIL_TOOLTIPS = { + 'WHILE': '%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_WHILE}', + 'UNTIL': '%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL}', +}; + +Extensions.register( + 'controls_whileUntil_tooltip', + Extensions.buildTooltipForDropdown('MODE', WHILE_UNTIL_TOOLTIPS), +); + +/** + * Tooltips for the 'controls_flow_statements' block, keyed by FLOW value. + * + * @see {Extensions#buildTooltipForDropdown} + */ +const BREAK_CONTINUE_TOOLTIPS = { + 'BREAK': '%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK}', + 'CONTINUE': '%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE}', +}; + +Extensions.register( + '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'. + */ +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 options List of menu options to add to. + */ + customContextMenu: function ( + this: CustomContextMenuBlock, + options: Array, + ) { + if (this.isInFlyout) { + return; + } + const varField = this.getField('VAR') as FieldVariable; + const variable = varField.getVariable()!; + const varName = variable.name; + if (!this.isCollapsed() && varName !== null) { + const getVarBlockState = { + type: 'variables_get', + fields: {VAR: varField.saveState(true)}, + }; + + options.push({ + enabled: true, + text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', varName), + callback: ContextMenu.callbackFactory(this, getVarBlockState), + }); + } + }, +}; + +Extensions.registerMixin( + 'contextMenu_newGetVariableBlock', + CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN, +); + +Extensions.register( + 'controls_for_tooltip', + Extensions.buildTooltipWithFieldText('%{BKY_CONTROLS_FOR_TOOLTIP}', 'VAR'), +); + +Extensions.register( + 'controls_forEach_tooltip', + Extensions.buildTooltipWithFieldText( + '%{BKY_CONTROLS_FOREACH_TOOLTIP}', + 'VAR', + ), +); + +/** + * List of block types that are loops and thus do not need warnings. + * To add a new loop type add this to your code: + * + * // If using the Blockly npm package and es6 import syntax: + * import {loops} from 'blockly/blocks'; + * loops.loopTypes.add('custom_loop'); + * + * // Else if using Closure Compiler and goog.modules: + * const {loopTypes} = goog.require('Blockly.libraryBlocks.loops'); + * loopTypes.add('custom_loop'); + * + * // Else if using blockly_compressed + blockss_compressed.js in browser: + * Blockly.libraryBlocks.loopTypes.add('custom_loop'); + */ +export const loopTypes: Set = new Set([ + 'controls_repeat', + 'controls_repeat_ext', + 'controls_forEach', + 'controls_for', + 'controls_whileUntil', +]); + +/** + * Type of a block that has CONTROL_FLOW_IN_LOOP_CHECK_MIXIN + * + * @internal + */ +export type ControlFlowInLoopBlock = Block & ControlFlowInLoopMixin; +interface ControlFlowInLoopMixin extends ControlFlowInLoopMixinType {} +type ControlFlowInLoopMixinType = typeof CONTROL_FLOW_IN_LOOP_CHECK_MIXIN; + +/** + * The language-neutral ID for when the reason why a block is disabled is + * because the block is only valid inside of a loop. + */ +const CONTROL_FLOW_NOT_IN_LOOP_DISABLED_REASON = 'CONTROL_FLOW_NOT_IN_LOOP'; +/** + * 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. + */ +const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = { + /** + * Is this block enclosed (at any level) by a loop? + * + * @returns The nearest surrounding loop, or null if none. + */ + 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; + } + block = block.getSurroundParent(); + } while (block); + return null; + }, + + /** + * Called whenever anything on the workspace changes. + * Add warning if this flow block is not nested inside a loop. + */ + 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 ( + !ws.isDragging || + ws.isDragging() || + (e.type !== Events.BLOCK_MOVE && e.type !== Events.BLOCK_CREATE) + ) { + return; + } + const enabled = !!this.getSurroundLoop(); + this.setWarningText( + enabled ? null : Msg['CONTROLS_FLOW_STATEMENTS_WARNING'], + ); + + if (!this.isInFlyout) { + try { + // There is no need to record the enable/disable change on the undo/redo + // list since the change will be automatically recreated when replayed. + eventUtils.setRecordUndo(false); + this.setDisabledReason( + !enabled, + CONTROL_FLOW_NOT_IN_LOOP_DISABLED_REASON, + ); + } finally { + eventUtils.setRecordUndo(true); + } + } + }, +}; + +Extensions.registerMixin( + 'controls_flow_in_loop_check', + CONTROL_FLOW_IN_LOOP_CHECK_MIXIN, +); + +// Register provided blocks. +defineBlocks(blocks); diff --git a/blocks/math.js b/blocks/math.js deleted file mode 100644 index b6827c5b121..00000000000 --- a/blocks/math.js +++ /dev/null @@ -1,566 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Math blocks for Blockly. - * - * This file is scraped to extract a .json file of block definitions. The array - * passed to defineBlocksWithJsonArray(..) must be strict JSON: double quotes - * only, no outside references, no functions, no trailing commas, etc. The one - * exception is end-of-line comments, which the scraper will remove. - * @author q.neutron@gmail.com (Quynh Neutron) - */ -'use strict'; - -goog.provide('Blockly.Blocks.math'); // Deprecated -goog.provide('Blockly.Constants.Math'); - -goog.require('Blockly'); -goog.require('Blockly.Blocks'); -goog.require('Blockly.FieldDropdown'); -goog.require('Blockly.FieldLabel'); -goog.require('Blockly.FieldNumber'); -goog.require('Blockly.FieldVariable'); - - -/** - * Unused constant for the common HSV hue for all blocks in this category. - * @deprecated Use Blockly.Msg['MATH_HUE']. (2018 April 5) - */ -Blockly.Constants.Math.HUE = 230; - -Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT - // Block for numeric value. - { - "type": "math_number", - "message0": "%1", - "args0": [{ - "type": "field_number", - "name": "NUM", - "value": 0 - }], - "output": "Number", - "helpUrl": "%{BKY_MATH_NUMBER_HELPURL}", - "style": "math_blocks", - "tooltip": "%{BKY_MATH_NUMBER_TOOLTIP}", - "extensions": ["parent_tooltip_when_inline"] - }, - - // Block for basic arithmetic operator. - { - "type": "math_arithmetic", - "message0": "%1 %2 %3", - "args0": [ - { - "type": "input_value", - "name": "A", - "check": "Number" - }, - { - "type": "field_dropdown", - "name": "OP", - "options": [ - ["%{BKY_MATH_ADDITION_SYMBOL}", "ADD"], - ["%{BKY_MATH_SUBTRACTION_SYMBOL}", "MINUS"], - ["%{BKY_MATH_MULTIPLICATION_SYMBOL}", "MULTIPLY"], - ["%{BKY_MATH_DIVISION_SYMBOL}", "DIVIDE"], - ["%{BKY_MATH_POWER_SYMBOL}", "POWER"] - ] - }, - { - "type": "input_value", - "name": "B", - "check": "Number" - } - ], - "inputsInline": true, - "output": "Number", - "style": "math_blocks", - "helpUrl": "%{BKY_MATH_ARITHMETIC_HELPURL}", - "extensions": ["math_op_tooltip"] - }, - - // Block for advanced math operators with single operand. - { - "type": "math_single", - "message0": "%1 %2", - "args0": [ - { - "type": "field_dropdown", - "name": "OP", - "options": [ - ["%{BKY_MATH_SINGLE_OP_ROOT}", 'ROOT'], - ["%{BKY_MATH_SINGLE_OP_ABSOLUTE}", 'ABS'], - ['-', 'NEG'], - ['ln', 'LN'], - ['log10', 'LOG10'], - ['e^', 'EXP'], - ['10^', 'POW10'] - ] - }, - { - "type": "input_value", - "name": "NUM", - "check": "Number" - } - ], - "output": "Number", - "style": "math_blocks", - "helpUrl": "%{BKY_MATH_SINGLE_HELPURL}", - "extensions": ["math_op_tooltip"] - }, - - // Block for trigonometry operators. - { - "type": "math_trig", - "message0": "%1 %2", - "args0": [ - { - "type": "field_dropdown", - "name": "OP", - "options": [ - ["%{BKY_MATH_TRIG_SIN}", "SIN"], - ["%{BKY_MATH_TRIG_COS}", "COS"], - ["%{BKY_MATH_TRIG_TAN}", "TAN"], - ["%{BKY_MATH_TRIG_ASIN}", "ASIN"], - ["%{BKY_MATH_TRIG_ACOS}", "ACOS"], - ["%{BKY_MATH_TRIG_ATAN}", "ATAN"] - ] - }, - { - "type": "input_value", - "name": "NUM", - "check": "Number" - } - ], - "output": "Number", - "style": "math_blocks", - "helpUrl": "%{BKY_MATH_TRIG_HELPURL}", - "extensions": ["math_op_tooltip"] - }, - - // Block for constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. - { - "type": "math_constant", - "message0": "%1", - "args0": [ - { - "type": "field_dropdown", - "name": "CONSTANT", - "options": [ - ["\u03c0", "PI"], - ["e", "E"], - ["\u03c6", "GOLDEN_RATIO"], - ["sqrt(2)", "SQRT2"], - ["sqrt(\u00bd)", "SQRT1_2"], - ["\u221e", "INFINITY"] - ] - } - ], - "output": "Number", - "style": "math_blocks", - "tooltip": "%{BKY_MATH_CONSTANT_TOOLTIP}", - "helpUrl": "%{BKY_MATH_CONSTANT_HELPURL}" - }, - - // Block for checking if a number is even, odd, prime, whole, positive, - // negative or if it is divisible by certain number. - { - "type": "math_number_property", - "message0": "%1 %2", - "args0": [ - { - "type": "input_value", - "name": "NUMBER_TO_CHECK", - "check": "Number" - }, - { - "type": "field_dropdown", - "name": "PROPERTY", - "options": [ - ["%{BKY_MATH_IS_EVEN}", "EVEN"], - ["%{BKY_MATH_IS_ODD}", "ODD"], - ["%{BKY_MATH_IS_PRIME}", "PRIME"], - ["%{BKY_MATH_IS_WHOLE}", "WHOLE"], - ["%{BKY_MATH_IS_POSITIVE}", "POSITIVE"], - ["%{BKY_MATH_IS_NEGATIVE}", "NEGATIVE"], - ["%{BKY_MATH_IS_DIVISIBLE_BY}", "DIVISIBLE_BY"] - ] - } - ], - "inputsInline": true, - "output": "Boolean", - "style": "math_blocks", - "tooltip": "%{BKY_MATH_IS_TOOLTIP}", - "mutator": "math_is_divisibleby_mutator" - }, - - // Block for adding to a variable in place. - { - "type": "math_change", - "message0": "%{BKY_MATH_CHANGE_TITLE}", - "args0": [ - { - "type": "field_variable", - "name": "VAR", - "variable": "%{BKY_MATH_CHANGE_TITLE_ITEM}" - }, - { - "type": "input_value", - "name": "DELTA", - "check": "Number" - } - ], - "previousStatement": null, - "nextStatement": null, - "style": "variable_blocks", - "helpUrl": "%{BKY_MATH_CHANGE_HELPURL}", - "extensions": ["math_change_tooltip"] - }, - - // Block for rounding functions. - { - "type": "math_round", - "message0": "%1 %2", - "args0": [ - { - "type": "field_dropdown", - "name": "OP", - "options": [ - ["%{BKY_MATH_ROUND_OPERATOR_ROUND}", "ROUND"], - ["%{BKY_MATH_ROUND_OPERATOR_ROUNDUP}", "ROUNDUP"], - ["%{BKY_MATH_ROUND_OPERATOR_ROUNDDOWN}", "ROUNDDOWN"] - ] - }, - { - "type": "input_value", - "name": "NUM", - "check": "Number" - } - ], - "output": "Number", - "style": "math_blocks", - "helpUrl": "%{BKY_MATH_ROUND_HELPURL}", - "tooltip": "%{BKY_MATH_ROUND_TOOLTIP}" - }, - - // Block for evaluating a list of numbers to return sum, average, min, max, - // etc. Some functions also work on text (min, max, mode, median). - { - "type": "math_on_list", - "message0": "%1 %2", - "args0": [ - { - "type": "field_dropdown", - "name": "OP", - "options": [ - ["%{BKY_MATH_ONLIST_OPERATOR_SUM}", "SUM"], - ["%{BKY_MATH_ONLIST_OPERATOR_MIN}", "MIN"], - ["%{BKY_MATH_ONLIST_OPERATOR_MAX}", "MAX"], - ["%{BKY_MATH_ONLIST_OPERATOR_AVERAGE}", "AVERAGE"], - ["%{BKY_MATH_ONLIST_OPERATOR_MEDIAN}", "MEDIAN"], - ["%{BKY_MATH_ONLIST_OPERATOR_MODE}", "MODE"], - ["%{BKY_MATH_ONLIST_OPERATOR_STD_DEV}", "STD_DEV"], - ["%{BKY_MATH_ONLIST_OPERATOR_RANDOM}", "RANDOM"] - ] - }, - { - "type": "input_value", - "name": "LIST", - "check": "Array" - } - ], - "output": "Number", - "style": "math_blocks", - "helpUrl": "%{BKY_MATH_ONLIST_HELPURL}", - "mutator": "math_modes_of_list_mutator", - "extensions": ["math_op_tooltip"] - }, - - // Block for remainder of a division. - { - "type": "math_modulo", - "message0": "%{BKY_MATH_MODULO_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "DIVIDEND", - "check": "Number" - }, - { - "type": "input_value", - "name": "DIVISOR", - "check": "Number" - } - ], - "inputsInline": true, - "output": "Number", - "style": "math_blocks", - "tooltip": "%{BKY_MATH_MODULO_TOOLTIP}", - "helpUrl": "%{BKY_MATH_MODULO_HELPURL}" - }, - - // Block for constraining a number between two limits. - { - "type": "math_constrain", - "message0": "%{BKY_MATH_CONSTRAIN_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "VALUE", - "check": "Number" - }, - { - "type": "input_value", - "name": "LOW", - "check": "Number" - }, - { - "type": "input_value", - "name": "HIGH", - "check": "Number" - } - ], - "inputsInline": true, - "output": "Number", - "style": "math_blocks", - "tooltip": "%{BKY_MATH_CONSTRAIN_TOOLTIP}", - "helpUrl": "%{BKY_MATH_CONSTRAIN_HELPURL}" - }, - - // Block for random integer between [X] and [Y]. - { - "type": "math_random_int", - "message0": "%{BKY_MATH_RANDOM_INT_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "FROM", - "check": "Number" - }, - { - "type": "input_value", - "name": "TO", - "check": "Number" - } - ], - "inputsInline": true, - "output": "Number", - "style": "math_blocks", - "tooltip": "%{BKY_MATH_RANDOM_INT_TOOLTIP}", - "helpUrl": "%{BKY_MATH_RANDOM_INT_HELPURL}" - }, - - // Block for random integer between [X] and [Y]. - { - "type": "math_random_float", - "message0": "%{BKY_MATH_RANDOM_FLOAT_TITLE_RANDOM}", - "output": "Number", - "style": "math_blocks", - "tooltip": "%{BKY_MATH_RANDOM_FLOAT_TOOLTIP}", - "helpUrl": "%{BKY_MATH_RANDOM_FLOAT_HELPURL}" - }, - - // Block for calculating atan2 of [X] and [Y]. - { - "type": "math_atan2", - "message0": "%{BKY_MATH_ATAN2_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "X", - "check": "Number" - }, - { - "type": "input_value", - "name": "Y", - "check": "Number" - } - ], - "inputsInline": true, - "output": "Number", - "style": "math_blocks", - "tooltip": "%{BKY_MATH_ATAN2_TOOLTIP}", - "helpUrl": "%{BKY_MATH_ATAN2_HELPURL}" - } -]); // END JSON EXTRACT (Do not delete this comment.) - -/** - * Mapping of math block OP value to tooltip message for blocks - * math_arithmetic, math_simple, math_trig, and math_on_lists. - * @see {Blockly.Extensions#buildTooltipForDropdown} - * @package - * @readonly - */ -Blockly.Constants.Math.TOOLTIPS_BY_OP = { - // math_arithmetic - 'ADD': '%{BKY_MATH_ARITHMETIC_TOOLTIP_ADD}', - 'MINUS': '%{BKY_MATH_ARITHMETIC_TOOLTIP_MINUS}', - 'MULTIPLY': '%{BKY_MATH_ARITHMETIC_TOOLTIP_MULTIPLY}', - 'DIVIDE': '%{BKY_MATH_ARITHMETIC_TOOLTIP_DIVIDE}', - 'POWER': '%{BKY_MATH_ARITHMETIC_TOOLTIP_POWER}', - - // math_simple - 'ROOT': '%{BKY_MATH_SINGLE_TOOLTIP_ROOT}', - 'ABS': '%{BKY_MATH_SINGLE_TOOLTIP_ABS}', - 'NEG': '%{BKY_MATH_SINGLE_TOOLTIP_NEG}', - 'LN': '%{BKY_MATH_SINGLE_TOOLTIP_LN}', - 'LOG10': '%{BKY_MATH_SINGLE_TOOLTIP_LOG10}', - 'EXP': '%{BKY_MATH_SINGLE_TOOLTIP_EXP}', - 'POW10': '%{BKY_MATH_SINGLE_TOOLTIP_POW10}', - - // math_trig - 'SIN': '%{BKY_MATH_TRIG_TOOLTIP_SIN}', - 'COS': '%{BKY_MATH_TRIG_TOOLTIP_COS}', - 'TAN': '%{BKY_MATH_TRIG_TOOLTIP_TAN}', - 'ASIN': '%{BKY_MATH_TRIG_TOOLTIP_ASIN}', - 'ACOS': '%{BKY_MATH_TRIG_TOOLTIP_ACOS}', - 'ATAN': '%{BKY_MATH_TRIG_TOOLTIP_ATAN}', - - // math_on_lists - 'SUM': '%{BKY_MATH_ONLIST_TOOLTIP_SUM}', - 'MIN': '%{BKY_MATH_ONLIST_TOOLTIP_MIN}', - 'MAX': '%{BKY_MATH_ONLIST_TOOLTIP_MAX}', - 'AVERAGE': '%{BKY_MATH_ONLIST_TOOLTIP_AVERAGE}', - 'MEDIAN': '%{BKY_MATH_ONLIST_TOOLTIP_MEDIAN}', - 'MODE': '%{BKY_MATH_ONLIST_TOOLTIP_MODE}', - 'STD_DEV': '%{BKY_MATH_ONLIST_TOOLTIP_STD_DEV}', - 'RANDOM': '%{BKY_MATH_ONLIST_TOOLTIP_RANDOM}' -}; - -Blockly.Extensions.register('math_op_tooltip', - Blockly.Extensions.buildTooltipForDropdown( - 'OP', Blockly.Constants.Math.TOOLTIPS_BY_OP)); - - -/** - * Mixin for mutator functions in the 'math_is_divisibleby_mutator' - * extension. - * @mixin - * @augments Blockly.Block - * @package - */ -Blockly.Constants.Math.IS_DIVISIBLEBY_MUTATOR_MIXIN = { - /** - * Create XML to represent whether the 'divisorInput' should be present. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - var divisorInput = (this.getFieldValue('PROPERTY') == 'DIVISIBLE_BY'); - container.setAttribute('divisor_input', divisorInput); - return container; - }, - /** - * Parse XML to restore the 'divisorInput'. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - var divisorInput = (xmlElement.getAttribute('divisor_input') == 'true'); - this.updateShape_(divisorInput); - }, - /** - * Modify this block to have (or not have) an input for 'is divisible by'. - * @param {boolean} divisorInput True if this block has a divisor input. - * @private - * @this {Blockly.Block} - */ - updateShape_: function(divisorInput) { - // Add or remove a Value Input. - var inputExists = this.getInput('DIVISOR'); - if (divisorInput) { - if (!inputExists) { - this.appendValueInput('DIVISOR') - .setCheck('Number'); - } - } else if (inputExists) { - this.removeInput('DIVISOR'); - } - } -}; - -/** - * 'math_is_divisibleby_mutator' extension to the 'math_property' block that - * can update the block shape (add/remove divisor input) based on whether - * property is "divisible by". - * @this {Blockly.Block} - * @package - */ -Blockly.Constants.Math.IS_DIVISIBLE_MUTATOR_EXTENSION = function() { - this.getField('PROPERTY').setValidator(function(option) { - var divisorInput = (option == 'DIVISIBLE_BY'); - this.getSourceBlock().updateShape_(divisorInput); - }); -}; - -Blockly.Extensions.registerMutator('math_is_divisibleby_mutator', - Blockly.Constants.Math.IS_DIVISIBLEBY_MUTATOR_MIXIN, - Blockly.Constants.Math.IS_DIVISIBLE_MUTATOR_EXTENSION); - -// Update the tooltip of 'math_change' block to reference the variable. -Blockly.Extensions.register('math_change_tooltip', - Blockly.Extensions.buildTooltipWithFieldText( - '%{BKY_MATH_CHANGE_TOOLTIP}', 'VAR')); - -/** - * Mixin with mutator methods to support alternate output based if the - * 'math_on_list' block uses the 'MODE' operation. - * @mixin - * @augments Blockly.Block - * @package - * @readonly - */ -Blockly.Constants.Math.LIST_MODES_MUTATOR_MIXIN = { - /** - * Modify this block to have the correct output type. - * @param {string} newOp Either 'MODE' or some op than returns a number. - * @private - * @this {Blockly.Block} - */ - updateType_: function(newOp) { - if (newOp == 'MODE') { - this.outputConnection.setCheck('Array'); - } else { - this.outputConnection.setCheck('Number'); - } - }, - /** - * Create XML to represent the output type. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('op', this.getFieldValue('OP')); - return container; - }, - /** - * Parse XML to restore the output type. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - this.updateType_(xmlElement.getAttribute('op')); - } -}; - -/** - * Extension to 'math_on_list' blocks that allows support of - * modes operation (outputs a list of numbers). - * @this {Blockly.Block} - * @package - */ -Blockly.Constants.Math.LIST_MODES_MUTATOR_EXTENSION = function() { - this.getField('OP').setValidator(function(newOp) { - this.updateType_(newOp); - }.bind(this)); -}; - -Blockly.Extensions.registerMutator('math_modes_of_list_mutator', - Blockly.Constants.Math.LIST_MODES_MUTATOR_MIXIN, - Blockly.Constants.Math.LIST_MODES_MUTATOR_EXTENSION); diff --git a/blocks/math.ts b/blocks/math.ts new file mode 100644 index 00000000000..c96aef3c23b --- /dev/null +++ b/blocks/math.ts @@ -0,0 +1,591 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.libraryBlocks.math + +import * as Extensions from '../core/extensions.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 { + 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. + */ +export const blocks = createBlockDefinitionsFromJsonArray([ + // Block for numeric value. + { + 'type': 'math_number', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_number', + 'name': 'NUM', + 'value': 0, + }, + ], + 'output': 'Number', + 'helpUrl': '%{BKY_MATH_NUMBER_HELPURL}', + 'style': 'math_blocks', + 'tooltip': '%{BKY_MATH_NUMBER_TOOLTIP}', + 'extensions': ['parent_tooltip_when_inline'], + }, + + // Block for basic arithmetic operator. + { + 'type': 'math_arithmetic', + 'message0': '%1 %2 %3', + 'args0': [ + { + 'type': 'input_value', + 'name': 'A', + 'check': 'Number', + }, + { + 'type': 'field_dropdown', + 'name': 'OP', + 'options': [ + ['%{BKY_MATH_ADDITION_SYMBOL}', 'ADD'], + ['%{BKY_MATH_SUBTRACTION_SYMBOL}', 'MINUS'], + ['%{BKY_MATH_MULTIPLICATION_SYMBOL}', 'MULTIPLY'], + ['%{BKY_MATH_DIVISION_SYMBOL}', 'DIVIDE'], + ['%{BKY_MATH_POWER_SYMBOL}', 'POWER'], + ], + }, + { + 'type': 'input_value', + 'name': 'B', + 'check': 'Number', + }, + ], + 'inputsInline': true, + 'output': 'Number', + 'style': 'math_blocks', + 'helpUrl': '%{BKY_MATH_ARITHMETIC_HELPURL}', + 'extensions': ['math_op_tooltip'], + }, + + // Block for advanced math operators with single operand. + { + 'type': 'math_single', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'field_dropdown', + 'name': 'OP', + 'options': [ + ['%{BKY_MATH_SINGLE_OP_ROOT}', 'ROOT'], + ['%{BKY_MATH_SINGLE_OP_ABSOLUTE}', 'ABS'], + ['-', 'NEG'], + ['ln', 'LN'], + ['log10', 'LOG10'], + ['e^', 'EXP'], + ['10^', 'POW10'], + ], + }, + { + 'type': 'input_value', + 'name': 'NUM', + 'check': 'Number', + }, + ], + 'output': 'Number', + 'style': 'math_blocks', + 'helpUrl': '%{BKY_MATH_SINGLE_HELPURL}', + 'extensions': ['math_op_tooltip'], + }, + + // Block for trigonometry operators. + { + 'type': 'math_trig', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'field_dropdown', + 'name': 'OP', + 'options': [ + ['%{BKY_MATH_TRIG_SIN}', 'SIN'], + ['%{BKY_MATH_TRIG_COS}', 'COS'], + ['%{BKY_MATH_TRIG_TAN}', 'TAN'], + ['%{BKY_MATH_TRIG_ASIN}', 'ASIN'], + ['%{BKY_MATH_TRIG_ACOS}', 'ACOS'], + ['%{BKY_MATH_TRIG_ATAN}', 'ATAN'], + ], + }, + { + 'type': 'input_value', + 'name': 'NUM', + 'check': 'Number', + }, + ], + 'output': 'Number', + 'style': 'math_blocks', + 'helpUrl': '%{BKY_MATH_TRIG_HELPURL}', + 'extensions': ['math_op_tooltip'], + }, + + // Block for constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. + { + 'type': 'math_constant', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_dropdown', + 'name': 'CONSTANT', + 'options': [ + ['\u03c0', 'PI'], + ['e', 'E'], + ['\u03c6', 'GOLDEN_RATIO'], + ['sqrt(2)', 'SQRT2'], + ['sqrt(\u00bd)', 'SQRT1_2'], + ['\u221e', 'INFINITY'], + ], + }, + ], + 'output': 'Number', + 'style': 'math_blocks', + 'tooltip': '%{BKY_MATH_CONSTANT_TOOLTIP}', + 'helpUrl': '%{BKY_MATH_CONSTANT_HELPURL}', + }, + + // Block for checking if a number is even, odd, prime, whole, positive, + // negative or if it is divisible by certain number. + { + 'type': 'math_number_property', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'input_value', + 'name': 'NUMBER_TO_CHECK', + 'check': 'Number', + }, + { + 'type': 'field_dropdown', + 'name': 'PROPERTY', + 'options': [ + ['%{BKY_MATH_IS_EVEN}', 'EVEN'], + ['%{BKY_MATH_IS_ODD}', 'ODD'], + ['%{BKY_MATH_IS_PRIME}', 'PRIME'], + ['%{BKY_MATH_IS_WHOLE}', 'WHOLE'], + ['%{BKY_MATH_IS_POSITIVE}', 'POSITIVE'], + ['%{BKY_MATH_IS_NEGATIVE}', 'NEGATIVE'], + ['%{BKY_MATH_IS_DIVISIBLE_BY}', 'DIVISIBLE_BY'], + ], + }, + ], + 'inputsInline': true, + 'output': 'Boolean', + 'style': 'math_blocks', + 'tooltip': '%{BKY_MATH_IS_TOOLTIP}', + 'mutator': 'math_is_divisibleby_mutator', + }, + + // Block for adding to a variable in place. + { + 'type': 'math_change', + 'message0': '%{BKY_MATH_CHANGE_TITLE}', + 'args0': [ + { + 'type': 'field_variable', + 'name': 'VAR', + 'variable': '%{BKY_MATH_CHANGE_TITLE_ITEM}', + }, + { + 'type': 'input_value', + 'name': 'DELTA', + 'check': 'Number', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'style': 'variable_blocks', + 'helpUrl': '%{BKY_MATH_CHANGE_HELPURL}', + 'extensions': ['math_change_tooltip'], + }, + + // Block for rounding functions. + { + 'type': 'math_round', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'field_dropdown', + 'name': 'OP', + 'options': [ + ['%{BKY_MATH_ROUND_OPERATOR_ROUND}', 'ROUND'], + ['%{BKY_MATH_ROUND_OPERATOR_ROUNDUP}', 'ROUNDUP'], + ['%{BKY_MATH_ROUND_OPERATOR_ROUNDDOWN}', 'ROUNDDOWN'], + ], + }, + { + 'type': 'input_value', + 'name': 'NUM', + 'check': 'Number', + }, + ], + 'output': 'Number', + 'style': 'math_blocks', + 'helpUrl': '%{BKY_MATH_ROUND_HELPURL}', + 'tooltip': '%{BKY_MATH_ROUND_TOOLTIP}', + }, + + // Block for evaluating a list of numbers to return sum, average, min, max, + // etc. Some functions also work on text (min, max, mode, median). + { + 'type': 'math_on_list', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'field_dropdown', + 'name': 'OP', + 'options': [ + ['%{BKY_MATH_ONLIST_OPERATOR_SUM}', 'SUM'], + ['%{BKY_MATH_ONLIST_OPERATOR_MIN}', 'MIN'], + ['%{BKY_MATH_ONLIST_OPERATOR_MAX}', 'MAX'], + ['%{BKY_MATH_ONLIST_OPERATOR_AVERAGE}', 'AVERAGE'], + ['%{BKY_MATH_ONLIST_OPERATOR_MEDIAN}', 'MEDIAN'], + ['%{BKY_MATH_ONLIST_OPERATOR_MODE}', 'MODE'], + ['%{BKY_MATH_ONLIST_OPERATOR_STD_DEV}', 'STD_DEV'], + ['%{BKY_MATH_ONLIST_OPERATOR_RANDOM}', 'RANDOM'], + ], + }, + { + 'type': 'input_value', + 'name': 'LIST', + 'check': 'Array', + }, + ], + 'output': 'Number', + 'style': 'math_blocks', + 'helpUrl': '%{BKY_MATH_ONLIST_HELPURL}', + 'mutator': 'math_modes_of_list_mutator', + 'extensions': ['math_op_tooltip'], + }, + + // Block for remainder of a division. + { + 'type': 'math_modulo', + 'message0': '%{BKY_MATH_MODULO_TITLE}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'DIVIDEND', + 'check': 'Number', + }, + { + 'type': 'input_value', + 'name': 'DIVISOR', + 'check': 'Number', + }, + ], + 'inputsInline': true, + 'output': 'Number', + 'style': 'math_blocks', + 'tooltip': '%{BKY_MATH_MODULO_TOOLTIP}', + 'helpUrl': '%{BKY_MATH_MODULO_HELPURL}', + }, + + // Block for constraining a number between two limits. + { + 'type': 'math_constrain', + 'message0': '%{BKY_MATH_CONSTRAIN_TITLE}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'VALUE', + 'check': 'Number', + }, + { + 'type': 'input_value', + 'name': 'LOW', + 'check': 'Number', + }, + { + 'type': 'input_value', + 'name': 'HIGH', + 'check': 'Number', + }, + ], + 'inputsInline': true, + 'output': 'Number', + 'style': 'math_blocks', + 'tooltip': '%{BKY_MATH_CONSTRAIN_TOOLTIP}', + 'helpUrl': '%{BKY_MATH_CONSTRAIN_HELPURL}', + }, + + // Block for random integer between [X] and [Y]. + { + 'type': 'math_random_int', + 'message0': '%{BKY_MATH_RANDOM_INT_TITLE}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'FROM', + 'check': 'Number', + }, + { + 'type': 'input_value', + 'name': 'TO', + 'check': 'Number', + }, + ], + 'inputsInline': true, + 'output': 'Number', + 'style': 'math_blocks', + 'tooltip': '%{BKY_MATH_RANDOM_INT_TOOLTIP}', + 'helpUrl': '%{BKY_MATH_RANDOM_INT_HELPURL}', + }, + + // Block for random integer between [X] and [Y]. + { + 'type': 'math_random_float', + 'message0': '%{BKY_MATH_RANDOM_FLOAT_TITLE_RANDOM}', + 'output': 'Number', + 'style': 'math_blocks', + 'tooltip': '%{BKY_MATH_RANDOM_FLOAT_TOOLTIP}', + 'helpUrl': '%{BKY_MATH_RANDOM_FLOAT_HELPURL}', + }, + + // Block for calculating atan2 of [X] and [Y]. + { + 'type': 'math_atan2', + 'message0': '%{BKY_MATH_ATAN2_TITLE}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'X', + 'check': 'Number', + }, + { + 'type': 'input_value', + 'name': 'Y', + 'check': 'Number', + }, + ], + 'inputsInline': true, + 'output': 'Number', + 'style': 'math_blocks', + 'tooltip': '%{BKY_MATH_ATAN2_TOOLTIP}', + 'helpUrl': '%{BKY_MATH_ATAN2_HELPURL}', + }, +]); + +/** + * Mapping of math block OP value to tooltip message for blocks + * math_arithmetic, math_simple, math_trig, and math_on_lists. + * + * @see {Extensions#buildTooltipForDropdown} + * @package + * @readonly + */ +const TOOLTIPS_BY_OP = { + // math_arithmetic + 'ADD': '%{BKY_MATH_ARITHMETIC_TOOLTIP_ADD}', + 'MINUS': '%{BKY_MATH_ARITHMETIC_TOOLTIP_MINUS}', + 'MULTIPLY': '%{BKY_MATH_ARITHMETIC_TOOLTIP_MULTIPLY}', + 'DIVIDE': '%{BKY_MATH_ARITHMETIC_TOOLTIP_DIVIDE}', + 'POWER': '%{BKY_MATH_ARITHMETIC_TOOLTIP_POWER}', + + // math_simple + 'ROOT': '%{BKY_MATH_SINGLE_TOOLTIP_ROOT}', + 'ABS': '%{BKY_MATH_SINGLE_TOOLTIP_ABS}', + 'NEG': '%{BKY_MATH_SINGLE_TOOLTIP_NEG}', + 'LN': '%{BKY_MATH_SINGLE_TOOLTIP_LN}', + 'LOG10': '%{BKY_MATH_SINGLE_TOOLTIP_LOG10}', + 'EXP': '%{BKY_MATH_SINGLE_TOOLTIP_EXP}', + 'POW10': '%{BKY_MATH_SINGLE_TOOLTIP_POW10}', + + // math_trig + 'SIN': '%{BKY_MATH_TRIG_TOOLTIP_SIN}', + 'COS': '%{BKY_MATH_TRIG_TOOLTIP_COS}', + 'TAN': '%{BKY_MATH_TRIG_TOOLTIP_TAN}', + 'ASIN': '%{BKY_MATH_TRIG_TOOLTIP_ASIN}', + 'ACOS': '%{BKY_MATH_TRIG_TOOLTIP_ACOS}', + 'ATAN': '%{BKY_MATH_TRIG_TOOLTIP_ATAN}', + + // math_on_lists + 'SUM': '%{BKY_MATH_ONLIST_TOOLTIP_SUM}', + 'MIN': '%{BKY_MATH_ONLIST_TOOLTIP_MIN}', + 'MAX': '%{BKY_MATH_ONLIST_TOOLTIP_MAX}', + 'AVERAGE': '%{BKY_MATH_ONLIST_TOOLTIP_AVERAGE}', + 'MEDIAN': '%{BKY_MATH_ONLIST_TOOLTIP_MEDIAN}', + 'MODE': '%{BKY_MATH_ONLIST_TOOLTIP_MODE}', + 'STD_DEV': '%{BKY_MATH_ONLIST_TOOLTIP_STD_DEV}', + 'RANDOM': '%{BKY_MATH_ONLIST_TOOLTIP_RANDOM}', +}; + +Extensions.register( + 'math_op_tooltip', + Extensions.buildTooltipForDropdown('OP', TOOLTIPS_BY_OP), +); + +/** Type of a block that has IS_DIVISBLEBY_MUTATOR_MIXIN */ +type DivisiblebyBlock = Block & DivisiblebyMixin; +interface DivisiblebyMixin extends DivisiblebyMixinType {} +type DivisiblebyMixinType = typeof IS_DIVISIBLEBY_MUTATOR_MIXIN; + +/** + * Mixin for mutator functions in the 'math_is_divisibleby_mutator' + * extension. + * + * @mixin + * @augments Block + * @package + */ +const IS_DIVISIBLEBY_MUTATOR_MIXIN = { + /** + * Create XML to represent whether the 'divisorInput' should be present. + * Backwards compatible serialization implementation. + * + * @returns XML storage element. + */ + mutationToDom: function (this: DivisiblebyBlock): Element { + const container = xmlUtils.createElement('mutation'); + const divisorInput = this.getFieldValue('PROPERTY') === 'DIVISIBLE_BY'; + container.setAttribute('divisor_input', String(divisorInput)); + return container; + }, + /** + * Parse XML to restore the 'divisorInput'. + * Backwards compatible serialization implementation. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: DivisiblebyBlock, xmlElement: Element) { + const divisorInput = xmlElement.getAttribute('divisor_input') === 'true'; + this.updateShape_(divisorInput); + }, + + // This block does not need JSO serialization hooks (saveExtraState and + // loadExtraState) because the state of this object is already encoded in the + // dropdown values. + // XML hooks are kept for backwards compatibility. + + /** + * Modify this block to have (or not have) an input for 'is divisible by'. + * + * @param divisorInput True if this block has a divisor input. + */ + updateShape_: function (this: DivisiblebyBlock, divisorInput: boolean) { + // Add or remove a Value Input. + const inputExists = this.getInput('DIVISOR'); + if (divisorInput) { + if (!inputExists) { + this.appendValueInput('DIVISOR').setCheck('Number'); + } + } else if (inputExists) { + this.removeInput('DIVISOR'); + } + }, +}; + +/** + * 'math_is_divisibleby_mutator' extension to the 'math_property' block that + * can update the block shape (add/remove divisor input) based on whether + * property is "divisible by". + */ +const IS_DIVISIBLE_MUTATOR_EXTENSION = function (this: DivisiblebyBlock) { + this.getField('PROPERTY')!.setValidator( + /** @param option The selected dropdown option. */ + function (this: FieldDropdown, option: string) { + const divisorInput = option === 'DIVISIBLE_BY'; + (this.getSourceBlock() as DivisiblebyBlock).updateShape_(divisorInput); + return undefined; // FieldValidators can't be void. Use option as-is. + }, + ); +}; + +Extensions.registerMutator( + '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'), +); + +/** Type of a block that has LIST_MODES_MUTATOR_MIXIN */ +type ListModesBlock = Block & ListModesMixin; +interface ListModesMixin extends ListModesMixinType {} +type ListModesMixinType = typeof LIST_MODES_MUTATOR_MIXIN; + +/** + * Mixin with mutator methods to support alternate output based if the + * 'math_on_list' block uses the 'MODE' operation. + */ +const LIST_MODES_MUTATOR_MIXIN = { + /** + * Modify this block to have the correct output type. + * + * @param newOp Either 'MODE' or some op than returns a number. + */ + updateType_: function (this: ListModesBlock, newOp: string) { + if (newOp === 'MODE') { + this.outputConnection!.setCheck('Array'); + } else { + this.outputConnection!.setCheck('Number'); + } + }, + /** + * Create XML to represent the output type. + * Backwards compatible serialization implementation. + * + * @returns XML storage element. + */ + mutationToDom: function (this: ListModesBlock): Element { + const container = xmlUtils.createElement('mutation'); + container.setAttribute('op', this.getFieldValue('OP')); + return container; + }, + /** + * Parse XML to restore the output type. + * Backwards compatible serialization implementation. + * + * @param xmlElement XML storage 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); + }, + + // This block does not need JSO serialization hooks (saveExtraState and + // loadExtraState) because the state of this object is already encoded in the + // dropdown values. + // XML hooks are kept for backwards compatibility. +}; + +/** + * 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) { + this.getField('OP')!.setValidator( + 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, +); + +// Register provided blocks. +defineBlocks(blocks); diff --git a/blocks/procedures.js b/blocks/procedures.js deleted file mode 100644 index d918d92d67e..00000000000 --- a/blocks/procedures.js +++ /dev/null @@ -1,1072 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Procedure blocks for Blockly. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Blocks.procedures'); - -goog.require('Blockly'); -goog.require('Blockly.Blocks'); -goog.require('Blockly.Comment'); -goog.require('Blockly.FieldCheckbox'); -goog.require('Blockly.FieldLabel'); -goog.require('Blockly.FieldTextInput'); -goog.require('Blockly.Mutator'); -goog.require('Blockly.Warning'); - - -Blockly.Blocks['procedures_defnoreturn'] = { - /** - * Block for defining a procedure with no return value. - * @this {Blockly.Block} - */ - init: function() { - var nameField = new Blockly.FieldTextInput('', - Blockly.Procedures.rename); - nameField.setSpellcheck(false); - this.appendDummyInput() - .appendField(Blockly.Msg['PROCEDURES_DEFNORETURN_TITLE']) - .appendField(nameField, 'NAME') - .appendField('', 'PARAMS'); - this.setMutator(new Blockly.Mutator(['procedures_mutatorarg'])); - if ((this.workspace.options.comments || - (this.workspace.options.parentWorkspace && - this.workspace.options.parentWorkspace.options.comments)) && - Blockly.Msg['PROCEDURES_DEFNORETURN_COMMENT']) { - this.setCommentText(Blockly.Msg['PROCEDURES_DEFNORETURN_COMMENT']); - } - this.setStyle('procedure_blocks'); - this.setTooltip(Blockly.Msg['PROCEDURES_DEFNORETURN_TOOLTIP']); - this.setHelpUrl(Blockly.Msg['PROCEDURES_DEFNORETURN_HELPURL']); - this.arguments_ = []; - this.argumentVarModels_ = []; - this.setStatements_(true); - this.statementConnection_ = null; - }, - /** - * Add or remove the statement block from this function definition. - * @param {boolean} hasStatements True if a statement block is needed. - * @this {Blockly.Block} - */ - setStatements_: function(hasStatements) { - if (this.hasStatements_ === hasStatements) { - return; - } - if (hasStatements) { - this.appendStatementInput('STACK') - .appendField(Blockly.Msg['PROCEDURES_DEFNORETURN_DO']); - if (this.getInput('RETURN')) { - this.moveInputBefore('STACK', 'RETURN'); - } - } else { - this.removeInput('STACK', true); - } - this.hasStatements_ = hasStatements; - }, - /** - * Update the display of parameters for this procedure definition block. - * @private - * @this {Blockly.Block} - */ - updateParams_: function() { - - // Merge the arguments into a human-readable list. - var paramString = ''; - if (this.arguments_.length) { - paramString = Blockly.Msg['PROCEDURES_BEFORE_PARAMS'] + - ' ' + this.arguments_.join(', '); - } - // The params field is deterministic based on the mutation, - // no need to fire a change event. - Blockly.Events.disable(); - try { - this.setFieldValue(paramString, 'PARAMS'); - } finally { - Blockly.Events.enable(); - } - }, - /** - * Create XML to represent the argument inputs. - * @param {boolean=} opt_paramIds If true include the IDs of the parameter - * quarks. Used by Blockly.Procedures.mutateCallers for reconnection. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function(opt_paramIds) { - var container = Blockly.utils.xml.createElement('mutation'); - if (opt_paramIds) { - container.setAttribute('name', this.getFieldValue('NAME')); - } - for (var i = 0; i < this.argumentVarModels_.length; i++) { - var parameter = Blockly.utils.xml.createElement('arg'); - var argModel = this.argumentVarModels_[i]; - parameter.setAttribute('name', argModel.name); - parameter.setAttribute('varid', argModel.getId()); - if (opt_paramIds && this.paramIds_) { - parameter.setAttribute('paramId', this.paramIds_[i]); - } - container.appendChild(parameter); - } - - // Save whether the statement input is visible. - if (!this.hasStatements_) { - container.setAttribute('statements', 'false'); - } - return container; - }, - /** - * Parse XML to restore the argument inputs. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - this.arguments_ = []; - this.argumentVarModels_ = []; - for (var i = 0, childNode; (childNode = xmlElement.childNodes[i]); i++) { - if (childNode.nodeName.toLowerCase() == 'arg') { - var varName = childNode.getAttribute('name'); - var varId = childNode.getAttribute('varid') || childNode.getAttribute('varId'); - this.arguments_.push(varName); - var variable = Blockly.Variables.getOrCreateVariablePackage( - this.workspace, varId, varName, ''); - if (variable != null) { - this.argumentVarModels_.push(variable); - } else { - console.log('Failed to create a variable with name ' + varName + ', ignoring.'); - } - } - } - this.updateParams_(); - Blockly.Procedures.mutateCallers(this); - - // Show or hide the statement input. - this.setStatements_(xmlElement.getAttribute('statements') !== 'false'); - }, - /** - * Populate the mutator's dialog with this block's components. - * @param {!Blockly.Workspace} workspace Mutator's workspace. - * @return {!Blockly.Block} Root block in mutator. - * @this {Blockly.Block} - */ - decompose: function(workspace) { - /* - * Creates the following XML: - * - * - * - * arg1_name - * etc... - * - * - * - */ - - var containerBlockNode = Blockly.utils.xml.createElement('block'); - containerBlockNode.setAttribute('type', 'procedures_mutatorcontainer'); - var statementNode = Blockly.utils.xml.createElement('statement'); - statementNode.setAttribute('name', 'STACK'); - containerBlockNode.appendChild(statementNode); - - var node = statementNode; - for (var i = 0; i < this.arguments_.length; i++) { - var argBlockNode = Blockly.utils.xml.createElement('block'); - argBlockNode.setAttribute('type', 'procedures_mutatorarg'); - var fieldNode = Blockly.utils.xml.createElement('field'); - fieldNode.setAttribute('name', 'NAME'); - var argumentName = Blockly.utils.xml.createTextNode(this.arguments_[i]); - fieldNode.appendChild(argumentName); - argBlockNode.appendChild(fieldNode); - var nextNode = Blockly.utils.xml.createElement('next'); - argBlockNode.appendChild(nextNode); - - node.appendChild(argBlockNode); - node = nextNode; - } - - var containerBlock = Blockly.Xml.domToBlock(containerBlockNode, workspace); - - if (this.type == 'procedures_defreturn') { - containerBlock.setFieldValue(this.hasStatements_, 'STATEMENTS'); - } else { - containerBlock.removeInput('STATEMENT_INPUT'); - } - - // Initialize procedure's callers with blank IDs. - Blockly.Procedures.mutateCallers(this); - return containerBlock; - }, - /** - * Reconfigure this block based on the mutator dialog's components. - * @param {!Blockly.Block} containerBlock Root block in mutator. - * @this {Blockly.Block} - */ - compose: function(containerBlock) { - // Parameter list. - this.arguments_ = []; - this.paramIds_ = []; - this.argumentVarModels_ = []; - var paramBlock = containerBlock.getInputTargetBlock('STACK'); - while (paramBlock) { - var varName = paramBlock.getFieldValue('NAME'); - this.arguments_.push(varName); - var variable = this.workspace.getVariable(varName, ''); - this.argumentVarModels_.push(variable); - - this.paramIds_.push(paramBlock.id); - paramBlock = paramBlock.nextConnection && - paramBlock.nextConnection.targetBlock(); - } - this.updateParams_(); - Blockly.Procedures.mutateCallers(this); - - // Show/hide the statement input. - var hasStatements = containerBlock.getFieldValue('STATEMENTS'); - if (hasStatements !== null) { - hasStatements = hasStatements == 'TRUE'; - if (this.hasStatements_ != hasStatements) { - if (hasStatements) { - this.setStatements_(true); - // Restore the stack, if one was saved. - Blockly.Mutator.reconnect(this.statementConnection_, this, 'STACK'); - this.statementConnection_ = null; - } else { - // Save the stack, then disconnect it. - var stackConnection = this.getInput('STACK').connection; - this.statementConnection_ = stackConnection.targetConnection; - if (this.statementConnection_) { - var stackBlock = stackConnection.targetBlock(); - stackBlock.unplug(); - stackBlock.bumpNeighbours(); - } - this.setStatements_(false); - } - } - } - }, - /** - * Return the signature of this procedure definition. - * @return {!Array} Tuple containing three elements: - * - the name of the defined procedure, - * - a list of all its arguments, - * - that it DOES NOT have a return value. - * @this {Blockly.Block} - */ - getProcedureDef: function() { - return [this.getFieldValue('NAME'), this.arguments_, false]; - }, - /** - * Return all variables referenced by this block. - * @return {!Array.} List of variable names. - * @this {Blockly.Block} - */ - getVars: function() { - return this.arguments_; - }, - /** - * Return all variables referenced by this block. - * @return {!Array.} List of variable models. - * @this {Blockly.Block} - */ - getVarModels: function() { - return this.argumentVarModels_; - }, - /** - * Notification that a variable is renaming. - * If the ID matches one of this block's variables, rename it. - * @param {string} oldId ID of variable to rename. - * @param {string} newId ID of new variable. May be the same as oldId, but - * with an updated name. Guaranteed to be the same type as the old - * variable. - * @override - * @this {Blockly.Block} - */ - renameVarById: function(oldId, newId) { - var oldVariable = this.workspace.getVariableById(oldId); - if (oldVariable.type != '') { - // Procedure arguments always have the empty type. - return; - } - var oldName = oldVariable.name; - var newVar = this.workspace.getVariableById(newId); - - var change = false; - for (var i = 0; i < this.argumentVarModels_.length; i++) { - if (this.argumentVarModels_[i].getId() == oldId) { - this.arguments_[i] = newVar.name; - this.argumentVarModels_[i] = newVar; - change = true; - } - } - if (change) { - this.displayRenamedVar_(oldName, newVar.name); - Blockly.Procedures.mutateCallers(this); - } - }, - /** - * Notification that a variable is renaming but keeping the same ID. If the - * variable is in use on this block, rerender to show the new name. - * @param {!Blockly.VariableModel} variable The variable being renamed. - * @package - * @override - * @this {Blockly.Block} - */ - updateVarName: function(variable) { - var newName = variable.name; - var change = false; - for (var i = 0; i < this.argumentVarModels_.length; i++) { - if (this.argumentVarModels_[i].getId() == variable.getId()) { - var oldName = this.arguments_[i]; - this.arguments_[i] = newName; - change = true; - } - } - if (change) { - this.displayRenamedVar_(oldName, newName); - Blockly.Procedures.mutateCallers(this); - } - }, - /** - * Update the display to reflect a newly renamed argument. - * @param {string} oldName The old display name of the argument. - * @param {string} newName The new display name of the argument. - * @private - * @this {Blockly.Block} - */ - displayRenamedVar_: function(oldName, newName) { - this.updateParams_(); - // Update the mutator's variables if the mutator is open. - if (this.mutator && this.mutator.isVisible()) { - var blocks = this.mutator.workspace_.getAllBlocks(false); - for (var i = 0, block; (block = blocks[i]); i++) { - if (block.type == 'procedures_mutatorarg' && - Blockly.Names.equals(oldName, block.getFieldValue('NAME'))) { - block.setFieldValue(newName, 'NAME'); - } - } - } - }, - /** - * Add custom menu options to this block's context menu. - * @param {!Array} options List of menu options to add to. - * @this {Blockly.Block} - */ - customContextMenu: function(options) { - if (this.isInFlyout) { - return; - } - // Add option to create caller. - var option = {enabled: true}; - var name = this.getFieldValue('NAME'); - option.text = Blockly.Msg['PROCEDURES_CREATE_DO'].replace('%1', name); - var xmlMutation = Blockly.utils.xml.createElement('mutation'); - xmlMutation.setAttribute('name', name); - for (var i = 0; i < this.arguments_.length; i++) { - var xmlArg = Blockly.utils.xml.createElement('arg'); - xmlArg.setAttribute('name', this.arguments_[i]); - xmlMutation.appendChild(xmlArg); - } - var xmlBlock = Blockly.utils.xml.createElement('block'); - xmlBlock.setAttribute('type', this.callType_); - xmlBlock.appendChild(xmlMutation); - option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); - options.push(option); - - // Add options to create getters for each parameter. - if (!this.isCollapsed()) { - for (var i = 0; i < this.argumentVarModels_.length; i++) { - var argOption = {enabled: true}; - var argVar = this.argumentVarModels_[i]; - argOption.text = Blockly.Msg['VARIABLES_SET_CREATE_GET'] - .replace('%1', argVar.name); - - var argXmlField = Blockly.Variables.generateVariableFieldDom(argVar); - var argXmlBlock = Blockly.utils.xml.createElement('block'); - argXmlBlock.setAttribute('type', 'variables_get'); - argXmlBlock.appendChild(argXmlField); - argOption.callback = - Blockly.ContextMenu.callbackFactory(this, argXmlBlock); - options.push(argOption); - } - } - }, - callType_: 'procedures_callnoreturn' -}; - -Blockly.Blocks['procedures_defreturn'] = { - /** - * Block for defining a procedure with a return value. - * @this {Blockly.Block} - */ - init: function() { - var nameField = new Blockly.FieldTextInput('', - Blockly.Procedures.rename); - nameField.setSpellcheck(false); - this.appendDummyInput() - .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_TITLE']) - .appendField(nameField, 'NAME') - .appendField('', 'PARAMS'); - this.appendValueInput('RETURN') - .setAlign(Blockly.ALIGN_RIGHT) - .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']); - this.setMutator(new Blockly.Mutator(['procedures_mutatorarg'])); - if ((this.workspace.options.comments || - (this.workspace.options.parentWorkspace && - this.workspace.options.parentWorkspace.options.comments)) && - Blockly.Msg['PROCEDURES_DEFRETURN_COMMENT']) { - this.setCommentText(Blockly.Msg['PROCEDURES_DEFRETURN_COMMENT']); - } - this.setStyle('procedure_blocks'); - this.setTooltip(Blockly.Msg['PROCEDURES_DEFRETURN_TOOLTIP']); - this.setHelpUrl(Blockly.Msg['PROCEDURES_DEFRETURN_HELPURL']); - this.arguments_ = []; - this.argumentVarModels_ = []; - this.setStatements_(true); - this.statementConnection_ = null; - }, - setStatements_: Blockly.Blocks['procedures_defnoreturn'].setStatements_, - updateParams_: Blockly.Blocks['procedures_defnoreturn'].updateParams_, - mutationToDom: Blockly.Blocks['procedures_defnoreturn'].mutationToDom, - domToMutation: Blockly.Blocks['procedures_defnoreturn'].domToMutation, - decompose: Blockly.Blocks['procedures_defnoreturn'].decompose, - compose: Blockly.Blocks['procedures_defnoreturn'].compose, - /** - * Return the signature of this procedure definition. - * @return {!Array} Tuple containing three elements: - * - the name of the defined procedure, - * - a list of all its arguments, - * - that it DOES have a return value. - * @this {Blockly.Block} - */ - getProcedureDef: function() { - return [this.getFieldValue('NAME'), this.arguments_, true]; - }, - getVars: Blockly.Blocks['procedures_defnoreturn'].getVars, - getVarModels: Blockly.Blocks['procedures_defnoreturn'].getVarModels, - renameVarById: Blockly.Blocks['procedures_defnoreturn'].renameVarById, - updateVarName: Blockly.Blocks['procedures_defnoreturn'].updateVarName, - displayRenamedVar_: Blockly.Blocks['procedures_defnoreturn'].displayRenamedVar_, - customContextMenu: Blockly.Blocks['procedures_defnoreturn'].customContextMenu, - callType_: 'procedures_callreturn' -}; - -Blockly.Blocks['procedures_mutatorcontainer'] = { - /** - * Mutator block for procedure container. - * @this {Blockly.Block} - */ - init: function() { - this.appendDummyInput() - .appendField(Blockly.Msg['PROCEDURES_MUTATORCONTAINER_TITLE']); - this.appendStatementInput('STACK'); - this.appendDummyInput('STATEMENT_INPUT') - .appendField(Blockly.Msg['PROCEDURES_ALLOW_STATEMENTS']) - .appendField(new Blockly.FieldCheckbox('TRUE'), 'STATEMENTS'); - this.setStyle('procedure_blocks'); - this.setTooltip(Blockly.Msg['PROCEDURES_MUTATORCONTAINER_TOOLTIP']); - this.contextMenu = false; - }, -}; - -Blockly.Blocks['procedures_mutatorarg'] = { - /** - * Mutator block for procedure argument. - * @this {Blockly.Block} - */ - init: function() { - var field = new Blockly.FieldTextInput( - Blockly.Procedures.DEFAULT_ARG, this.validator_); - // Hack: override showEditor to do just a little bit more work. - // We don't have a good place to hook into the start of a text edit. - field.oldShowEditorFn_ = field.showEditor_; - var newShowEditorFn = function() { - this.createdVariables_ = []; - this.oldShowEditorFn_(); - }; - field.showEditor_ = newShowEditorFn; - - this.appendDummyInput() - .appendField(Blockly.Msg['PROCEDURES_MUTATORARG_TITLE']) - .appendField(field, 'NAME'); - this.setPreviousStatement(true); - this.setNextStatement(true); - this.setStyle('procedure_blocks'); - this.setTooltip(Blockly.Msg['PROCEDURES_MUTATORARG_TOOLTIP']); - this.contextMenu = false; - - // Create the default variable when we drag the block in from the flyout. - // Have to do this after installing the field on the block. - field.onFinishEditing_ = this.deleteIntermediateVars_; - // Create an empty list so onFinishEditing_ has something to look at, even - // though the editor was never opened. - field.createdVariables_ = []; - field.onFinishEditing_('x'); - }, - - /** - * Obtain a valid name for the procedure argument. Create a variable if - * necessary. - * Merge runs of whitespace. Strip leading and trailing whitespace. - * Beyond this, all names are legal. - * @param {string} varName User-supplied name. - * @return {?string} Valid name, or null if a name was not specified. - * @private - * @this Blockly.FieldTextInput - */ - validator_: function(varName) { - var sourceBlock = this.getSourceBlock(); - var outerWs = Blockly.Mutator.findParentWs(sourceBlock.workspace); - varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); - if (!varName) { - return null; - } - - // Prevents duplicate parameter names in functions - var workspace = sourceBlock.workspace.targetWorkspace || - sourceBlock.workspace; - var blocks = workspace.getAllBlocks(false); - var caselessName = varName.toLowerCase(); - for (var i = 0; i < blocks.length; i++) { - if (blocks[i].id == this.getSourceBlock().id) { - continue; - } - // Other blocks values may not be set yet when this is loaded. - var otherVar = blocks[i].getFieldValue('NAME'); - if (otherVar && otherVar.toLowerCase() == caselessName) { - return null; - } - } - - // Don't create variables for arg blocks that - // only exist in the mutator's flyout. - if (sourceBlock.isInFlyout) { - return varName; - } - - var model = outerWs.getVariable(varName, ''); - if (model && model.name != varName) { - // Rename the variable (case change) - outerWs.renameVariableById(model.getId(), varName); - } - if (!model) { - model = outerWs.createVariable(varName, ''); - if (model && this.createdVariables_) { - this.createdVariables_.push(model); - } - } - return varName; - }, - - /** - * Called when focusing away from the text field. - * Deletes all variables that were created as the user typed their intended - * variable name. - * @param {string} newText The new variable name. - * @private - * @this Blockly.FieldTextInput - */ - deleteIntermediateVars_: function(newText) { - var outerWs = Blockly.Mutator.findParentWs(this.getSourceBlock().workspace); - if (!outerWs) { - return; - } - for (var i = 0; i < this.createdVariables_.length; i++) { - var model = this.createdVariables_[i]; - if (model.name != newText) { - outerWs.deleteVariableById(model.getId()); - } - } - } -}; - -Blockly.Blocks['procedures_callnoreturn'] = { - /** - * Block for calling a procedure with no return value. - * @this {Blockly.Block} - */ - init: function() { - this.appendDummyInput('TOPROW') - .appendField(this.id, 'NAME'); - this.setPreviousStatement(true); - this.setNextStatement(true); - this.setStyle('procedure_blocks'); - // Tooltip is set in renameProcedure. - this.setHelpUrl(Blockly.Msg['PROCEDURES_CALLNORETURN_HELPURL']); - this.arguments_ = []; - this.argumentVarModels_ = []; - this.quarkConnections_ = {}; - this.quarkIds_ = null; - this.previousEnabledState_ = true; - }, - - /** - * Returns the name of the procedure this block calls. - * @return {string} Procedure name. - * @this {Blockly.Block} - */ - getProcedureCall: function() { - // The NAME field is guaranteed to exist, null will never be returned. - return /** @type {string} */ (this.getFieldValue('NAME')); - }, - /** - * Notification that a procedure is renaming. - * If the name matches this block's procedure, rename it. - * @param {string} oldName Previous name of procedure. - * @param {string} newName Renamed procedure. - * @this {Blockly.Block} - */ - renameProcedure: function(oldName, newName) { - if (Blockly.Names.equals(oldName, this.getProcedureCall())) { - this.setFieldValue(newName, 'NAME'); - var baseMsg = this.outputConnection ? - Blockly.Msg['PROCEDURES_CALLRETURN_TOOLTIP'] : - Blockly.Msg['PROCEDURES_CALLNORETURN_TOOLTIP']; - this.setTooltip(baseMsg.replace('%1', newName)); - } - }, - /** - * Notification that the procedure's parameters have changed. - * @param {!Array.} paramNames New param names, e.g. ['x', 'y', 'z']. - * @param {!Array.} paramIds IDs of params (consistent for each - * parameter through the life of a mutator, regardless of param renaming), - * e.g. ['piua', 'f8b_', 'oi.o']. - * @private - * @this {Blockly.Block} - */ - setProcedureParameters_: function(paramNames, paramIds) { - // Data structures: - // this.arguments = ['x', 'y'] - // Existing param names. - // this.quarkConnections_ {piua: null, f8b_: Blockly.Connection} - // Look-up of paramIds to connections plugged into the call block. - // this.quarkIds_ = ['piua', 'f8b_'] - // Existing param IDs. - // Note that quarkConnections_ may include IDs that no longer exist, but - // which might reappear if a param is reattached in the mutator. - var defBlock = Blockly.Procedures.getDefinition(this.getProcedureCall(), - this.workspace); - var mutatorOpen = defBlock && defBlock.mutator && - defBlock.mutator.isVisible(); - if (!mutatorOpen) { - this.quarkConnections_ = {}; - this.quarkIds_ = null; - } - if (!paramIds) { - // Reset the quarks (a mutator is about to open). - return; - } - // Test arguments (arrays of strings) for changes. '\n' is not a valid - // argument name character, so it is a valid delimiter here. - if (paramNames.join('\n') == this.arguments_.join('\n')) { - // No change. - this.quarkIds_ = paramIds; - return; - } - if (paramIds.length != paramNames.length) { - throw RangeError('paramNames and paramIds must be the same length.'); - } - this.setCollapsed(false); - if (!this.quarkIds_) { - // Initialize tracking for this block. - this.quarkConnections_ = {}; - this.quarkIds_ = []; - } - // Switch off rendering while the block is rebuilt. - var savedRendered = this.rendered; - this.rendered = false; - // Update the quarkConnections_ with existing connections. - for (var i = 0; i < this.arguments_.length; i++) { - var input = this.getInput('ARG' + i); - if (input) { - var connection = input.connection.targetConnection; - this.quarkConnections_[this.quarkIds_[i]] = connection; - if (mutatorOpen && connection && - paramIds.indexOf(this.quarkIds_[i]) == -1) { - // This connection should no longer be attached to this block. - connection.disconnect(); - connection.getSourceBlock().bumpNeighbours(); - } - } - } - // Rebuild the block's arguments. - this.arguments_ = [].concat(paramNames); - // And rebuild the argument model list. - this.argumentVarModels_ = []; - for (var i = 0; i < this.arguments_.length; i++) { - var variable = Blockly.Variables.getOrCreateVariablePackage( - this.workspace, null, this.arguments_[i], ''); - this.argumentVarModels_.push(variable); - } - - this.updateShape_(); - this.quarkIds_ = paramIds; - // Reconnect any child blocks. - if (this.quarkIds_) { - for (var i = 0; i < this.arguments_.length; i++) { - var quarkId = this.quarkIds_[i]; - if (quarkId in this.quarkConnections_) { - var connection = this.quarkConnections_[quarkId]; - if (!Blockly.Mutator.reconnect(connection, this, 'ARG' + i)) { - // Block no longer exists or has been attached elsewhere. - delete this.quarkConnections_[quarkId]; - } - } - } - } - // Restore rendering and show the changes. - this.rendered = savedRendered; - if (this.rendered) { - this.render(); - } - }, - /** - * Modify this block to have the correct number of arguments. - * @private - * @this {Blockly.Block} - */ - updateShape_: function() { - for (var i = 0; i < this.arguments_.length; i++) { - var field = this.getField('ARGNAME' + i); - if (field) { - // Ensure argument name is up to date. - // The argument name field is deterministic based on the mutation, - // no need to fire a change event. - Blockly.Events.disable(); - try { - field.setValue(this.arguments_[i]); - } finally { - Blockly.Events.enable(); - } - } else { - // Add new input. - field = new Blockly.FieldLabel(this.arguments_[i]); - var input = this.appendValueInput('ARG' + i) - .setAlign(Blockly.ALIGN_RIGHT) - .appendField(field, 'ARGNAME' + i); - input.init(); - } - } - // Remove deleted inputs. - while (this.getInput('ARG' + i)) { - this.removeInput('ARG' + i); - i++; - } - // Add 'with:' if there are parameters, remove otherwise. - var topRow = this.getInput('TOPROW'); - if (topRow) { - if (this.arguments_.length) { - if (!this.getField('WITH')) { - topRow.appendField(Blockly.Msg['PROCEDURES_CALL_BEFORE_PARAMS'], 'WITH'); - topRow.init(); - } - } else { - if (this.getField('WITH')) { - topRow.removeField('WITH'); - } - } - } - }, - /** - * Create XML to represent the (non-editable) name and arguments. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('name', this.getProcedureCall()); - for (var i = 0; i < this.arguments_.length; i++) { - var parameter = Blockly.utils.xml.createElement('arg'); - parameter.setAttribute('name', this.arguments_[i]); - container.appendChild(parameter); - } - return container; - }, - /** - * Parse XML to restore the (non-editable) name and parameters. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - var name = xmlElement.getAttribute('name'); - this.renameProcedure(this.getProcedureCall(), name); - var args = []; - var paramIds = []; - for (var i = 0, childNode; (childNode = xmlElement.childNodes[i]); i++) { - if (childNode.nodeName.toLowerCase() == 'arg') { - args.push(childNode.getAttribute('name')); - paramIds.push(childNode.getAttribute('paramId')); - } - } - this.setProcedureParameters_(args, paramIds); - }, - /** - * Return all variables referenced by this block. - * @return {!Array.} List of variable names. - * @this {Blockly.Block} - */ - getVars: function() { - return this.arguments_; - }, - /** - * Return all variables referenced by this block. - * @return {!Array.} List of variable models. - * @this {Blockly.Block} - */ - getVarModels: function() { - return this.argumentVarModels_; - }, - /** - * Procedure calls cannot exist without the corresponding procedure - * definition. Enforce this link whenever an event is fired. - * @param {!Blockly.Events.Abstract} event Change event. - * @this {Blockly.Block} - */ - onchange: function(event) { - if (!this.workspace || this.workspace.isFlyout) { - // Block is deleted or is in a flyout. - return; - } - if (!event.recordUndo) { - // Events not generated by user. Skip handling. - return; - } - if (event.type == Blockly.Events.BLOCK_CREATE && - event.ids.indexOf(this.id) != -1) { - // Look for the case where a procedure call was created (usually through - // paste) and there is no matching definition. In this case, create - // an empty definition block with the correct signature. - var name = this.getProcedureCall(); - var def = Blockly.Procedures.getDefinition(name, this.workspace); - if (def && (def.type != this.defType_ || - JSON.stringify(def.getVars()) != JSON.stringify(this.arguments_))) { - // The signatures don't match. - def = null; - } - if (!def) { - Blockly.Events.setGroup(event.group); - /** - * Create matching definition block. - * - * - * - * - * - * test - * - * - */ - var xml = Blockly.utils.xml.createElement('xml'); - var block = Blockly.utils.xml.createElement('block'); - block.setAttribute('type', this.defType_); - var xy = this.getRelativeToSurfaceXY(); - var x = xy.x + Blockly.SNAP_RADIUS * (this.RTL ? -1 : 1); - var y = xy.y + Blockly.SNAP_RADIUS * 2; - block.setAttribute('x', x); - block.setAttribute('y', y); - var mutation = this.mutationToDom(); - block.appendChild(mutation); - var field = Blockly.utils.xml.createElement('field'); - field.setAttribute('name', 'NAME'); - field.appendChild(Blockly.utils.xml.createTextNode( - this.getProcedureCall())); - block.appendChild(field); - xml.appendChild(block); - Blockly.Xml.domToWorkspace(xml, this.workspace); - Blockly.Events.setGroup(false); - } - } else if (event.type == Blockly.Events.BLOCK_DELETE) { - // Look for the case where a procedure definition has been deleted, - // leaving this block (a procedure call) orphaned. In this case, delete - // the orphan. - var name = this.getProcedureCall(); - var def = Blockly.Procedures.getDefinition(name, this.workspace); - if (!def) { - Blockly.Events.setGroup(event.group); - this.dispose(true); - Blockly.Events.setGroup(false); - } - } else if (event.type == Blockly.Events.CHANGE && event.element == 'disabled') { - var name = this.getProcedureCall(); - var def = Blockly.Procedures.getDefinition(name, this.workspace); - if (def && def.id == event.blockId) { - // in most cases the old group should be '' - var oldGroup = Blockly.Events.getGroup(); - if (oldGroup) { - // This should only be possible programmatically and may indicate a problem - // with event grouping. If you see this message please investigate. If the - // use ends up being valid we may need to reorder events in the undo stack. - console.log('Saw an existing group while responding to a definition change'); - } - Blockly.Events.setGroup(event.group); - if (event.newValue) { - this.previousEnabledState_ = this.isEnabled(); - this.setEnabled(false); - } else { - this.setEnabled(this.previousEnabledState_); - } - Blockly.Events.setGroup(oldGroup); - } - } - }, - /** - * Add menu option to find the definition block for this call. - * @param {!Array} options List of menu options to add to. - * @this {Blockly.Block} - */ - customContextMenu: function(options) { - if (!this.workspace.isMovable()) { - // If we center on the block and the workspace isn't movable we could - // loose blocks at the edges of the workspace. - return; - } - - var option = {enabled: true}; - option.text = Blockly.Msg['PROCEDURES_HIGHLIGHT_DEF']; - var name = this.getProcedureCall(); - var workspace = this.workspace; - option.callback = function() { - var def = Blockly.Procedures.getDefinition(name, workspace); - if (def) { - workspace.centerOnBlock(def.id); - def.select(); - } - }; - options.push(option); - }, - defType_: 'procedures_defnoreturn' -}; - -Blockly.Blocks['procedures_callreturn'] = { - /** - * Block for calling a procedure with a return value. - * @this {Blockly.Block} - */ - init: function() { - this.appendDummyInput('TOPROW') - .appendField('', 'NAME'); - this.setOutput(true); - this.setStyle('procedure_blocks'); - // Tooltip is set in domToMutation. - this.setHelpUrl(Blockly.Msg['PROCEDURES_CALLRETURN_HELPURL']); - this.arguments_ = []; - this.quarkConnections_ = {}; - this.quarkIds_ = null; - this.previousEnabledState_ = true; - }, - - getProcedureCall: Blockly.Blocks['procedures_callnoreturn'].getProcedureCall, - renameProcedure: Blockly.Blocks['procedures_callnoreturn'].renameProcedure, - setProcedureParameters_: - Blockly.Blocks['procedures_callnoreturn'].setProcedureParameters_, - updateShape_: Blockly.Blocks['procedures_callnoreturn'].updateShape_, - mutationToDom: Blockly.Blocks['procedures_callnoreturn'].mutationToDom, - domToMutation: Blockly.Blocks['procedures_callnoreturn'].domToMutation, - getVars: Blockly.Blocks['procedures_callnoreturn'].getVars, - getVarModels: Blockly.Blocks['procedures_callnoreturn'].getVarModels, - onchange: Blockly.Blocks['procedures_callnoreturn'].onchange, - customContextMenu: - Blockly.Blocks['procedures_callnoreturn'].customContextMenu, - defType_: 'procedures_defreturn' -}; - -Blockly.Blocks['procedures_ifreturn'] = { - /** - * Block for conditionally returning a value from a procedure. - * @this {Blockly.Block} - */ - init: function() { - this.appendValueInput('CONDITION') - .setCheck('Boolean') - .appendField(Blockly.Msg['CONTROLS_IF_MSG_IF']); - this.appendValueInput('VALUE') - .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']); - this.setInputsInline(true); - this.setPreviousStatement(true); - this.setNextStatement(true); - this.setStyle('procedure_blocks'); - this.setTooltip(Blockly.Msg['PROCEDURES_IFRETURN_TOOLTIP']); - this.setHelpUrl(Blockly.Msg['PROCEDURES_IFRETURN_HELPURL']); - this.hasReturnValue_ = true; - }, - /** - * Create XML to represent whether this block has a return value. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('value', Number(this.hasReturnValue_)); - return container; - }, - /** - * Parse XML to restore whether this block has a return value. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - var value = xmlElement.getAttribute('value'); - this.hasReturnValue_ = (value == 1); - if (!this.hasReturnValue_) { - this.removeInput('VALUE'); - this.appendDummyInput('VALUE') - .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']); - } - }, - /** - * Called whenever anything on the workspace changes. - * Add warning if this flow block is not nested inside a loop. - * @param {!Blockly.Events.Abstract} _e Change event. - * @this {Blockly.Block} - */ - onchange: function(_e) { - if (!this.workspace.isDragging || this.workspace.isDragging()) { - return; // Don't change state at the start of a drag. - } - var legal = false; - // Is the block nested in a procedure? - var block = this; - do { - if (this.FUNCTION_TYPES.indexOf(block.type) != -1) { - legal = true; - break; - } - block = block.getSurroundParent(); - } while (block); - if (legal) { - // If needed, toggle whether this block has a return value. - if (block.type == 'procedures_defnoreturn' && this.hasReturnValue_) { - this.removeInput('VALUE'); - this.appendDummyInput('VALUE') - .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']); - this.hasReturnValue_ = false; - } else if (block.type == 'procedures_defreturn' && - !this.hasReturnValue_) { - this.removeInput('VALUE'); - this.appendValueInput('VALUE') - .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']); - this.hasReturnValue_ = true; - } - this.setWarningText(null); - if (!this.isInFlyout) { - this.setEnabled(true); - } - } else { - this.setWarningText(Blockly.Msg['PROCEDURES_IFRETURN_WARNING']); - if (!this.isInFlyout && !this.getInheritedDisabled()) { - this.setEnabled(false); - } - } - }, - /** - * List of block types that are functions and thus do not need warnings. - * To add a new function type add this to your code: - * Blockly.Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func'); - */ - FUNCTION_TYPES: ['procedures_defnoreturn', 'procedures_defreturn'] -}; diff --git a/blocks/procedures.ts b/blocks/procedures.ts new file mode 100644 index 00000000000..1214eb55eda --- /dev/null +++ b/blocks/procedures.ts @@ -0,0 +1,1355 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.libraryBlocks.procedures + +import * as ContextMenu from '../core/contextmenu.js'; +import * as Events from '../core/events/events.js'; +import * as Procedures from '../core/procedures.js'; +import * as Variables from '../core/variables.js'; +import * as Xml from '../core/xml.js'; +import * as fieldRegistry from '../core/field_registry.js'; +import * as xmlUtils from '../core/utils/xml.js'; +import type {Abstract as AbstractEvent} from '../core/events/events_abstract.js'; +import {Align} from '../core/inputs/align.js'; +import type {Block} from '../core/block.js'; +import type {BlockSvg} from '../core/block_svg.js'; +import type {BlockCreate} from '../core/events/events_block_create.js'; +import type {BlockChange} from '../core/events/events_block_change.js'; +import type {BlockDefinition} from '../core/blocks.js'; +import type {Connection} from '../core/connection.js'; +import type { + ContextMenuOption, + LegacyContextMenuOption, +} from '../core/contextmenu_registry.js'; +import * as eventUtils from '../core/events/utils.js'; +import {FieldCheckbox} from '../core/field_checkbox.js'; +import {FieldLabel} from '../core/field_label.js'; +import {FieldTextInput} from '../core/field_textinput.js'; +import {Msg} from '../core/msg.js'; +import {MutatorIcon as Mutator} from '../core/icons/mutator_icon.js'; +import {Names} from '../core/names.js'; +import type {VariableModel} from '../core/variable_model.js'; +import type {Workspace} from '../core/workspace.js'; +import type {WorkspaceSvg} from '../core/workspace_svg.js'; +import {config} from '../core/config.js'; +import {defineBlocks} from '../core/common.js'; +import '../core/icons/comment_icon.js'; +import '../core/icons/warning_icon.js'; +import * as common from '../core/common.js'; + +/** A dictionary of the block definitions provided by this module. */ +export const blocks: {[key: string]: BlockDefinition} = {}; + +/** Type of a block using the PROCEDURE_DEF_COMMON mixin. */ +type ProcedureBlock = Block & ProcedureMixin; +interface ProcedureMixin extends ProcedureMixinType { + arguments_: string[]; + argumentVarModels_: VariableModel[]; + callType_: string; + paramIds_: string[]; + hasStatements_: boolean; + statementConnection_: Connection | null; +} +type ProcedureMixinType = typeof PROCEDURE_DEF_COMMON; + +/** Extra state for serialising procedure blocks. */ +type ProcedureExtraState = { + params?: Array<{name: string; id: string}>; + hasStatements: boolean; +}; + +/** + * Common properties for the procedure_defnoreturn and + * procedure_defreturn blocks. + */ +const PROCEDURE_DEF_COMMON = { + /** + * Add or remove the statement block from this function definition. + * + * @param hasStatements True if a statement block is needed. + */ + setStatements_: function (this: ProcedureBlock, hasStatements: boolean) { + if (this.hasStatements_ === hasStatements) { + return; + } + if (hasStatements) { + this.appendStatementInput('STACK').appendField( + Msg['PROCEDURES_DEFNORETURN_DO'], + ); + if (this.getInput('RETURN')) { + this.moveInputBefore('STACK', 'RETURN'); + } + } else { + this.removeInput('STACK', true); + } + this.hasStatements_ = hasStatements; + }, + /** + * Update the display of parameters for this procedure definition block. + * + * @internal + */ + updateParams_: function (this: ProcedureBlock) { + // Merge the arguments into a human-readable list. + let paramString = ''; + if (this.arguments_.length) { + paramString = + Msg['PROCEDURES_BEFORE_PARAMS'] + ' ' + this.arguments_.join(', '); + } + // The params field is deterministic based on the mutation, + // no need to fire a change event. + Events.disable(); + try { + this.setFieldValue(paramString, 'PARAMS'); + } finally { + Events.enable(); + } + }, + /** + * Create XML to represent the argument inputs. + * Backwards compatible serialization implementation. + * + * @param opt_paramIds If true include the IDs of the parameter + * quarks. Used by Procedures.mutateCallers for reconnection. + * @returns XML storage element. + */ + mutationToDom: function ( + this: ProcedureBlock, + opt_paramIds: boolean, + ): Element { + const container = xmlUtils.createElement('mutation'); + if (opt_paramIds) { + container.setAttribute('name', this.getFieldValue('NAME')); + } + for (let i = 0; i < this.argumentVarModels_.length; i++) { + const parameter = xmlUtils.createElement('arg'); + const argModel = this.argumentVarModels_[i]; + parameter.setAttribute('name', argModel.name); + parameter.setAttribute('varid', argModel.getId()); + if (opt_paramIds && this.paramIds_) { + parameter.setAttribute('paramId', this.paramIds_[i]); + } + container.appendChild(parameter); + } + + // Save whether the statement input is visible. + if (!this.hasStatements_) { + container.setAttribute('statements', 'false'); + } + return container; + }, + /** + * Parse XML to restore the argument inputs. + * Backwards compatible serialization implementation. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: ProcedureBlock, xmlElement: Element) { + this.arguments_ = []; + this.argumentVarModels_ = []; + for (let i = 0, childNode; (childNode = xmlElement.childNodes[i]); i++) { + if (childNode.nodeName.toLowerCase() === 'arg') { + const childElement = childNode as Element; + const varName = childElement.getAttribute('name')!; + const varId = + childElement.getAttribute('varid') || + childElement.getAttribute('varId'); + this.arguments_.push(varName); + const variable = Variables.getOrCreateVariablePackage( + this.workspace, + varId, + varName, + '', + ); + if (variable !== null) { + this.argumentVarModels_.push(variable); + } else { + console.log( + `Failed to create a variable named "${varName}", ignoring.`, + ); + } + } + } + this.updateParams_(); + Procedures.mutateCallers(this); + + // Show or hide the statement input. + this.setStatements_(xmlElement.getAttribute('statements') !== 'false'); + }, + /** + * Returns the state of this block as a JSON serializable object. + * + * @returns The state of this block, eg the parameters and statements. + */ + saveExtraState: function (this: ProcedureBlock): ProcedureExtraState | null { + if (!this.argumentVarModels_.length && this.hasStatements_) { + return null; + } + const state = Object.create(null); + if (this.argumentVarModels_.length) { + state['params'] = []; + for (let i = 0; i < this.argumentVarModels_.length; i++) { + state['params'].push({ + // We don't need to serialize the name, but just in case we decide + // to separate params from variables. + 'name': this.argumentVarModels_[i].name, + 'id': this.argumentVarModels_[i].getId(), + }); + } + } + if (!this.hasStatements_) { + state['hasStatements'] = false; + } + return state; + }, + /** + * Applies the given state to this block. + * + * @param state The state to apply to this block, eg the parameters + * and statements. + */ + loadExtraState: function (this: ProcedureBlock, state: ProcedureExtraState) { + this.arguments_ = []; + this.argumentVarModels_ = []; + if (state['params']) { + for (let i = 0; i < state['params'].length; i++) { + const param = state['params'][i]; + const variable = Variables.getOrCreateVariablePackage( + this.workspace, + param['id'], + param['name'], + '', + ); + this.arguments_.push(variable.name); + this.argumentVarModels_.push(variable); + } + } + this.updateParams_(); + Procedures.mutateCallers(this); + this.setStatements_(state['hasStatements'] === false ? false : true); + }, + /** + * Populate the mutator's dialog with this block's components. + * + * @param workspace Mutator's workspace. + * @returns Root block in mutator. + */ + decompose: function ( + this: ProcedureBlock, + workspace: Workspace, + ): ContainerBlock { + /* + * Creates the following XML: + * + * + * + * arg1_name + * etc... + * + * + * + */ + + const containerBlockNode = xmlUtils.createElement('block'); + containerBlockNode.setAttribute('type', 'procedures_mutatorcontainer'); + const statementNode = xmlUtils.createElement('statement'); + statementNode.setAttribute('name', 'STACK'); + containerBlockNode.appendChild(statementNode); + + let node = statementNode; + for (let i = 0; i < this.arguments_.length; i++) { + const argBlockNode = xmlUtils.createElement('block'); + argBlockNode.setAttribute('type', 'procedures_mutatorarg'); + const fieldNode = xmlUtils.createElement('field'); + fieldNode.setAttribute('name', 'NAME'); + const argumentName = xmlUtils.createTextNode(this.arguments_[i]); + fieldNode.appendChild(argumentName); + argBlockNode.appendChild(fieldNode); + const nextNode = xmlUtils.createElement('next'); + argBlockNode.appendChild(nextNode); + + node.appendChild(argBlockNode); + node = nextNode; + } + + const containerBlock = Xml.domToBlock( + containerBlockNode, + workspace, + ) as ContainerBlock; + + if (this.type === 'procedures_defreturn') { + containerBlock.setFieldValue(this.hasStatements_, 'STATEMENTS'); + } else { + containerBlock.removeInput('STATEMENT_INPUT'); + } + + // Initialize procedure's callers with blank IDs. + Procedures.mutateCallers(this); + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * + * @param containerBlock Root block in mutator. + */ + compose: function (this: ProcedureBlock, containerBlock: ContainerBlock) { + // Parameter list. + this.arguments_ = []; + this.paramIds_ = []; + this.argumentVarModels_ = []; + let paramBlock = containerBlock.getInputTargetBlock('STACK'); + while (paramBlock && !paramBlock.isInsertionMarker()) { + const varName = paramBlock.getFieldValue('NAME'); + this.arguments_.push(varName); + const variable = this.workspace.getVariable(varName, '')!; + this.argumentVarModels_.push(variable); + + this.paramIds_.push(paramBlock.id); + paramBlock = + paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); + } + this.updateParams_(); + Procedures.mutateCallers(this); + + // Show/hide the statement input. + let hasStatements = containerBlock.getFieldValue('STATEMENTS'); + if (hasStatements !== null) { + hasStatements = hasStatements === 'TRUE'; + if (this.hasStatements_ !== hasStatements) { + if (hasStatements) { + this.setStatements_(true); + // Restore the stack, if one was saved. + this.statementConnection_?.reconnect(this, 'STACK'); + this.statementConnection_ = null; + } else { + // Save the stack, then disconnect it. + const stackConnection = this.getInput('STACK')!.connection; + this.statementConnection_ = stackConnection!.targetConnection; + if (this.statementConnection_) { + const stackBlock = stackConnection!.targetBlock()!; + stackBlock.unplug(); + stackBlock.bumpNeighbours(); + } + this.setStatements_(false); + } + } + } + }, + /** + * Return all variables referenced by this block. + * + * @returns List of variable names. + */ + getVars: function (this: ProcedureBlock): string[] { + return this.arguments_; + }, + /** + * Return all variables referenced by this block. + * + * @returns List of variable models. + */ + getVarModels: function (this: ProcedureBlock): VariableModel[] { + return this.argumentVarModels_; + }, + /** + * Notification that a variable is renaming. + * If the ID matches one of this block's variables, rename it. + * + * @param oldId ID of variable to rename. + * @param newId ID of new variable. May be the same as oldId, but + * with an updated name. Guaranteed to be the same type as the + * old variable. + */ + renameVarById: function ( + this: ProcedureBlock & BlockSvg, + oldId: string, + newId: string, + ) { + const oldVariable = this.workspace.getVariableById(oldId)!; + if (oldVariable.type !== '') { + // Procedure arguments always have the empty type. + return; + } + const oldName = oldVariable.name; + const newVar = this.workspace.getVariableById(newId)!; + + let change = false; + for (let i = 0; i < this.argumentVarModels_.length; i++) { + if (this.argumentVarModels_[i].getId() === oldId) { + this.arguments_[i] = newVar.name; + this.argumentVarModels_[i] = newVar; + change = true; + } + } + if (change) { + this.displayRenamedVar_(oldName, newVar.name); + Procedures.mutateCallers(this); + } + }, + /** + * Notification that a variable is renaming but keeping the same ID. If the + * variable is in use on this block, rerender to show the new name. + * + * @param variable The variable being renamed. + */ + updateVarName: function ( + this: ProcedureBlock & BlockSvg, + variable: VariableModel, + ) { + const newName = variable.name; + let change = false; + let oldName; + for (let i = 0; i < this.argumentVarModels_.length; i++) { + if (this.argumentVarModels_[i].getId() === variable.getId()) { + oldName = this.arguments_[i]; + this.arguments_[i] = newName; + change = true; + } + } + if (change) { + this.displayRenamedVar_(oldName as string, newName); + Procedures.mutateCallers(this); + } + }, + /** + * Update the display to reflect a newly renamed argument. + * + * @internal + * @param oldName The old display name of the argument. + * @param newName The new display name of the argument. + */ + displayRenamedVar_: function ( + this: ProcedureBlock & BlockSvg, + oldName: string, + newName: string, + ) { + this.updateParams_(); + // Update the mutator's variables if the mutator is open. + const mutator = this.getIcon(Mutator.TYPE); + if (mutator && mutator.bubbleIsVisible()) { + const blocks = mutator.getWorkspace()!.getAllBlocks(false); + for (let i = 0, block; (block = blocks[i]); i++) { + if ( + block.type === 'procedures_mutatorarg' && + Names.equals(oldName, block.getFieldValue('NAME')) + ) { + block.setFieldValue(newName, 'NAME'); + } + } + } + }, + /** + * Add custom menu options to this block's context menu. + * + * @param options List of menu options to add to. + */ + customContextMenu: function ( + this: ProcedureBlock, + options: Array, + ) { + if (this.isInFlyout) { + return; + } + // Add option to create caller. + const name = this.getFieldValue('NAME'); + const callProcedureBlockState = { + type: (this as AnyDuringMigration).callType_, + extraState: {name: name, params: this.arguments_}, + }; + options.push({ + enabled: true, + text: Msg['PROCEDURES_CREATE_DO'].replace('%1', name), + callback: ContextMenu.callbackFactory(this, callProcedureBlockState), + }); + + // Add options to create getters for each parameter. + if (!this.isCollapsed()) { + for (let i = 0; i < this.argumentVarModels_.length; i++) { + const argVar = this.argumentVarModels_[i]; + const getVarBlockState = { + type: 'variables_get', + fields: { + VAR: {name: argVar.name, id: argVar.getId(), type: argVar.type}, + }, + }; + options.push({ + enabled: true, + text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', argVar.name), + callback: ContextMenu.callbackFactory(this, getVarBlockState), + }); + } + } + }, +}; + +blocks['procedures_defnoreturn'] = { + ...PROCEDURE_DEF_COMMON, + /** + * Block for defining a procedure with no return value. + */ + init: function (this: ProcedureBlock & BlockSvg) { + const initName = Procedures.findLegalName('', this); + const nameField = fieldRegistry.fromJson({ + type: 'field_input', + text: initName, + }) as FieldTextInput; + nameField!.setValidator(Procedures.rename); + nameField.setSpellcheck(false); + this.appendDummyInput() + .appendField(Msg['PROCEDURES_DEFNORETURN_TITLE']) + .appendField(nameField, 'NAME') + .appendField('', 'PARAMS'); + this.setMutator(new Mutator(['procedures_mutatorarg'], this)); + if ( + (this.workspace.options.comments || + (this.workspace.options.parentWorkspace && + this.workspace.options.parentWorkspace.options.comments)) && + Msg['PROCEDURES_DEFNORETURN_COMMENT'] + ) { + this.setCommentText(Msg['PROCEDURES_DEFNORETURN_COMMENT']); + } + this.setStyle('procedure_blocks'); + this.setTooltip(Msg['PROCEDURES_DEFNORETURN_TOOLTIP']); + this.setHelpUrl(Msg['PROCEDURES_DEFNORETURN_HELPURL']); + this.arguments_ = []; + this.argumentVarModels_ = []; + this.setStatements_(true); + this.statementConnection_ = null; + }, + /** + * Return the signature of this procedure definition. + * + * @returns Tuple containing three elements: + * - the name of the defined procedure, + * - a list of all its arguments, + * - that it DOES NOT have a return value. + */ + getProcedureDef: function (this: ProcedureBlock): [string, string[], false] { + return [this.getFieldValue('NAME'), this.arguments_, false]; + }, + callType_: 'procedures_callnoreturn', +}; + +blocks['procedures_defreturn'] = { + ...PROCEDURE_DEF_COMMON, + /** + * Block for defining a procedure with a return value. + */ + init: function (this: ProcedureBlock & BlockSvg) { + const initName = Procedures.findLegalName('', this); + const nameField = fieldRegistry.fromJson({ + type: 'field_input', + text: initName, + }) as FieldTextInput; + nameField.setValidator(Procedures.rename); + nameField.setSpellcheck(false); + this.appendDummyInput() + .appendField(Msg['PROCEDURES_DEFRETURN_TITLE']) + .appendField(nameField, 'NAME') + .appendField('', 'PARAMS'); + this.appendValueInput('RETURN') + .setAlign(Align.RIGHT) + .appendField(Msg['PROCEDURES_DEFRETURN_RETURN']); + this.setMutator(new Mutator(['procedures_mutatorarg'], this)); + if ( + (this.workspace.options.comments || + (this.workspace.options.parentWorkspace && + this.workspace.options.parentWorkspace.options.comments)) && + Msg['PROCEDURES_DEFRETURN_COMMENT'] + ) { + this.setCommentText(Msg['PROCEDURES_DEFRETURN_COMMENT']); + } + this.setStyle('procedure_blocks'); + this.setTooltip(Msg['PROCEDURES_DEFRETURN_TOOLTIP']); + this.setHelpUrl(Msg['PROCEDURES_DEFRETURN_HELPURL']); + this.arguments_ = []; + this.argumentVarModels_ = []; + this.setStatements_(true); + this.statementConnection_ = null; + }, + /** + * Return the signature of this procedure definition. + * + * @returns Tuple containing three elements: + * - the name of the defined procedure, + * - a list of all its arguments, + * - that it DOES have a return value. + */ + getProcedureDef: function (this: ProcedureBlock): [string, string[], true] { + return [this.getFieldValue('NAME'), this.arguments_, true]; + }, + callType_: 'procedures_callreturn', +}; + +/** Type of a procedures_mutatorcontainer block. */ +type ContainerBlock = Block & ContainerMixin; +interface ContainerMixin extends ContainerMixinType {} +type ContainerMixinType = typeof PROCEDURES_MUTATORCONTAINER; + +const PROCEDURES_MUTATORCONTAINER = { + /** + * Mutator block for procedure container. + */ + init: function (this: ContainerBlock) { + this.appendDummyInput().appendField( + Msg['PROCEDURES_MUTATORCONTAINER_TITLE'], + ); + this.appendStatementInput('STACK'); + this.appendDummyInput('STATEMENT_INPUT') + .appendField(Msg['PROCEDURES_ALLOW_STATEMENTS']) + .appendField( + fieldRegistry.fromJson({ + type: 'field_checkbox', + checked: true, + }) as FieldCheckbox, + 'STATEMENTS', + ); + this.setStyle('procedure_blocks'); + this.setTooltip(Msg['PROCEDURES_MUTATORCONTAINER_TOOLTIP']); + this.contextMenu = false; + }, +}; +blocks['procedures_mutatorcontainer'] = PROCEDURES_MUTATORCONTAINER; + +/** Type of a procedures_mutatorarg block. */ +type ArgumentBlock = Block & ArgumentMixin; +interface ArgumentMixin extends ArgumentMixinType {} +type ArgumentMixinType = typeof PROCEDURES_MUTATORARGUMENT; + +// TODO(#6920): This is kludgy. +type FieldTextInputForArgument = FieldTextInput & { + oldShowEditorFn_(_e?: Event, quietInput?: boolean): void; + createdVariables_: VariableModel[]; +}; + +const PROCEDURES_MUTATORARGUMENT = { + /** + * Mutator block for procedure argument. + */ + init: function (this: ArgumentBlock) { + const field = fieldRegistry.fromJson({ + type: 'field_input', + text: Procedures.DEFAULT_ARG, + }) as FieldTextInputForArgument; + field.setValidator(this.validator_); + // Hack: override showEditor to do just a little bit more work. + // We don't have a good place to hook into the start of a text edit. + field.oldShowEditorFn_ = (field as AnyDuringMigration).showEditor_; + const newShowEditorFn = function (this: typeof field) { + this.createdVariables_ = []; + this.oldShowEditorFn_(); + }; + (field as AnyDuringMigration).showEditor_ = newShowEditorFn; + + this.appendDummyInput() + .appendField(Msg['PROCEDURES_MUTATORARG_TITLE']) + .appendField(field, 'NAME'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle('procedure_blocks'); + this.setTooltip(Msg['PROCEDURES_MUTATORARG_TOOLTIP']); + this.contextMenu = false; + + // Create the default variable when we drag the block in from the flyout. + // Have to do this after installing the field on the block. + field.onFinishEditing_ = this.deleteIntermediateVars_; + // Create an empty list so onFinishEditing_ has something to look at, even + // though the editor was never opened. + field.createdVariables_ = []; + field.onFinishEditing_('x'); + }, + + /** + * Obtain a valid name for the procedure argument. Create a variable if + * necessary. + * Merge runs of whitespace. Strip leading and trailing whitespace. + * Beyond this, all names are legal. + * + * @internal + * @param varName User-supplied name. + * @returns Valid name, or null if a name was not specified. + */ + validator_: function ( + this: FieldTextInputForArgument, + varName: string, + ): string | null { + const sourceBlock = this.getSourceBlock()!; + const outerWs = sourceBlock!.workspace.getRootWorkspace()!; + varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); + if (!varName) { + return null; + } + + // Prevents duplicate parameter names in functions + const workspace = + (sourceBlock.workspace as WorkspaceSvg).targetWorkspace || + sourceBlock.workspace; + const blocks = workspace.getAllBlocks(false); + const caselessName = varName.toLowerCase(); + for (let i = 0; i < blocks.length; i++) { + if (blocks[i].id === this.getSourceBlock()!.id) { + continue; + } + // Other blocks values may not be set yet when this is loaded. + const otherVar = blocks[i].getFieldValue('NAME'); + if (otherVar && otherVar.toLowerCase() === caselessName) { + return null; + } + } + + // Don't create variables for arg blocks that + // only exist in the mutator's flyout. + if (sourceBlock.isInFlyout) { + return varName; + } + + let model = outerWs.getVariable(varName, ''); + if (model && model.name !== varName) { + // Rename the variable (case change) + outerWs.renameVariableById(model.getId(), varName); + } + if (!model) { + model = outerWs.createVariable(varName, ''); + if (model && this.createdVariables_) { + this.createdVariables_.push(model); + } + } + return varName; + }, + + /** + * Called when focusing away from the text field. + * Deletes all variables that were created as the user typed their intended + * variable name. + * + * @internal + * @param newText The new variable name. + */ + deleteIntermediateVars_: function ( + this: FieldTextInputForArgument, + newText: string, + ) { + const outerWs = this.getSourceBlock()!.workspace.getRootWorkspace(); + if (!outerWs) { + return; + } + for (let i = 0; i < this.createdVariables_.length; i++) { + const model = this.createdVariables_[i]; + if (model.name !== newText) { + outerWs.deleteVariableById(model.getId()); + } + } + }, +}; +blocks['procedures_mutatorarg'] = PROCEDURES_MUTATORARGUMENT; + +/** Type of a block using the PROCEDURE_CALL_COMMON mixin. */ +type CallBlock = Block & CallMixin; +interface CallMixin extends CallMixinType { + argumentVarModels_: VariableModel[]; + arguments_: string[]; + defType_: string; + quarkIds_: string[] | null; + quarkConnections_: {[id: string]: Connection}; +} +type CallMixinType = typeof PROCEDURE_CALL_COMMON; + +/** Extra state for serialising call blocks. */ +type CallExtraState = { + name: string; + params?: string[]; +}; + +/** + * The language-neutral ID for when the reason why a block is disabled is + * because the block's corresponding procedure definition is disabled. + */ +const DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON = + 'DISABLED_PROCEDURE_DEFINITION'; + +/** + * Common properties for the procedure_callnoreturn and + * procedure_callreturn blocks. + */ +const PROCEDURE_CALL_COMMON = { + /** + * Returns the name of the procedure this block calls. + * + * @returns Procedure name. + */ + getProcedureCall: function (this: CallBlock): string { + // The NAME field is guaranteed to exist, null will never be returned. + return this.getFieldValue('NAME'); + }, + /** + * Notification that a procedure is renaming. + * If the name matches this block's procedure, rename it. + * + * @param oldName Previous name of procedure. + * @param newName Renamed procedure. + */ + renameProcedure: function ( + this: CallBlock, + oldName: string, + newName: string, + ) { + if (Names.equals(oldName, this.getProcedureCall())) { + this.setFieldValue(newName, 'NAME'); + const baseMsg = this.outputConnection + ? Msg['PROCEDURES_CALLRETURN_TOOLTIP'] + : Msg['PROCEDURES_CALLNORETURN_TOOLTIP']; + this.setTooltip(baseMsg.replace('%1', newName)); + } + }, + /** + * Notification that the procedure's parameters have changed. + * + * @internal + * @param paramNames New param names, e.g. ['x', 'y', 'z']. + * @param paramIds IDs of params (consistent for each parameter + * through the life of a mutator, regardless of param renaming), + * e.g. ['piua', 'f8b_', 'oi.o']. + */ + setProcedureParameters_: function ( + this: CallBlock, + paramNames: string[], + paramIds: string[], + ) { + // Data structures: + // this.arguments = ['x', 'y'] + // Existing param names. + // this.quarkConnections_ {piua: null, f8b_: Connection} + // Look-up of paramIds to connections plugged into the call block. + // this.quarkIds_ = ['piua', 'f8b_'] + // Existing param IDs. + // Note that quarkConnections_ may include IDs that no longer exist, but + // which might reappear if a param is reattached in the mutator. + const defBlock = Procedures.getDefinition( + this.getProcedureCall(), + this.workspace, + ); + const mutatorIcon = defBlock && defBlock.getIcon(Mutator.TYPE); + const mutatorOpen = mutatorIcon && mutatorIcon.bubbleIsVisible(); + if (!mutatorOpen) { + this.quarkConnections_ = {}; + this.quarkIds_ = null; + } else { + // fix #6091 - this call could cause an error when outside if-else + // expanding block while mutating prevents another error (ancient fix) + this.setCollapsed(false); + } + // Test arguments (arrays of strings) for changes. '\n' is not a valid + // argument name character, so it is a valid delimiter here. + if (paramNames.join('\n') === this.arguments_.join('\n')) { + // No change. + this.quarkIds_ = paramIds; + return; + } + if (paramIds.length !== paramNames.length) { + throw RangeError('paramNames and paramIds must be the same length.'); + } + if (!this.quarkIds_) { + // Initialize tracking for this block. + this.quarkConnections_ = {}; + this.quarkIds_ = []; + } + // Update the quarkConnections_ with existing connections. + for (let i = 0; i < this.arguments_.length; i++) { + const input = this.getInput('ARG' + i); + if (input) { + const connection = input.connection!.targetConnection!; + this.quarkConnections_[this.quarkIds_[i]] = connection; + if ( + mutatorOpen && + connection && + !paramIds.includes(this.quarkIds_[i]) + ) { + // This connection should no longer be attached to this block. + connection.disconnect(); + connection.getSourceBlock().bumpNeighbours(); + } + } + } + // Rebuild the block's arguments. + this.arguments_ = ([] as string[]).concat(paramNames); + // And rebuild the argument model list. + this.argumentVarModels_ = []; + for (let i = 0; i < this.arguments_.length; i++) { + const variable = Variables.getOrCreateVariablePackage( + this.workspace, + null, + this.arguments_[i], + '', + ); + this.argumentVarModels_.push(variable); + } + + this.updateShape_(); + this.quarkIds_ = paramIds; + // Reconnect any child blocks. + if (this.quarkIds_) { + for (let i = 0; i < this.arguments_.length; i++) { + const quarkId: string = this.quarkIds_[i]; // TODO(#6920) + if (quarkId in this.quarkConnections_) { + // TODO(#6920): investigate claimed circular initialisers. + const connection: Connection = this.quarkConnections_[quarkId]; + if (!connection?.reconnect(this, 'ARG' + i)) { + // Block no longer exists or has been attached elsewhere. + delete this.quarkConnections_[quarkId]; + } + } + } + } + }, + /** + * Modify this block to have the correct number of arguments. + * + * @internal + */ + updateShape_: function (this: CallBlock) { + for (let i = 0; i < this.arguments_.length; i++) { + const argField = this.getField('ARGNAME' + i); + if (argField) { + // Ensure argument name is up to date. + // The argument name field is deterministic based on the mutation, + // no need to fire a change event. + Events.disable(); + try { + argField.setValue(this.arguments_[i]); + } finally { + Events.enable(); + } + } else { + // Add new input. + const newField = fieldRegistry.fromJson({ + type: 'field_label', + text: this.arguments_[i], + }) as FieldLabel; + this.appendValueInput('ARG' + i) + .setAlign(Align.RIGHT) + .appendField(newField, 'ARGNAME' + i); + } + } + // Remove deleted inputs. + for (let i = this.arguments_.length; this.getInput('ARG' + i); i++) { + this.removeInput('ARG' + i); + } + // Add 'with:' if there are parameters, remove otherwise. + const topRow = this.getInput('TOPROW'); + if (topRow) { + if (this.arguments_.length) { + if (!this.getField('WITH')) { + topRow.appendField(Msg['PROCEDURES_CALL_BEFORE_PARAMS'], 'WITH'); + } + } else { + if (this.getField('WITH')) { + topRow.removeField('WITH'); + } + } + } + }, + /** + * Create XML to represent the (non-editable) name and arguments. + * Backwards compatible serialization implementation. + * + * @returns XML storage element. + */ + mutationToDom: function (this: CallBlock): Element { + const container = xmlUtils.createElement('mutation'); + container.setAttribute('name', this.getProcedureCall()); + for (let i = 0; i < this.arguments_.length; i++) { + const parameter = xmlUtils.createElement('arg'); + parameter.setAttribute('name', this.arguments_[i]); + container.appendChild(parameter); + } + return container; + }, + /** + * Parse XML to restore the (non-editable) name and parameters. + * Backwards compatible serialization implementation. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: CallBlock, xmlElement: Element) { + const name = xmlElement.getAttribute('name')!; + this.renameProcedure(this.getProcedureCall(), name); + const args: string[] = []; + const paramIds = []; + for (let i = 0, childNode; (childNode = xmlElement.childNodes[i]); i++) { + if (childNode.nodeName.toLowerCase() === 'arg') { + args.push((childNode as Element).getAttribute('name')!); + paramIds.push((childNode as Element).getAttribute('paramId')!); + } + } + this.setProcedureParameters_(args, paramIds); + }, + /** + * Returns the state of this block as a JSON serializable object. + * + * @returns The state of this block, ie the params and procedure name. + */ + saveExtraState: function (this: CallBlock): CallExtraState { + const state = Object.create(null); + state['name'] = this.getProcedureCall(); + if (this.arguments_.length) { + state['params'] = this.arguments_; + } + return state; + }, + /** + * Applies the given state to this block. + * + * @param state The state to apply to this block, ie the params and + * procedure name. + */ + loadExtraState: function (this: CallBlock, state: CallExtraState) { + this.renameProcedure(this.getProcedureCall(), state['name']); + const params = state['params']; + if (params) { + const ids: string[] = []; + ids.length = params.length; + ids.fill(null as unknown as string); // TODO(#6920) + this.setProcedureParameters_(params, ids); + } + }, + /** + * Return all variables referenced by this block. + * + * @returns List of variable names. + */ + getVars: function (this: CallBlock): string[] { + return this.arguments_; + }, + /** + * Return all variables referenced by this block. + * + * @returns List of variable models. + */ + getVarModels: function (this: CallBlock): VariableModel[] { + return this.argumentVarModels_; + }, + /** + * Procedure calls cannot exist without the corresponding procedure + * definition. Enforce this link whenever an event is fired. + * + * @param event Change event. + */ + onchange: function (this: CallBlock, event: AbstractEvent) { + if (!this.workspace || this.workspace.isFlyout) { + // Block is deleted or is in a flyout. + return; + } + if (!event.recordUndo) { + // Events not generated by user. Skip handling. + return; + } + if ( + event.type === Events.BLOCK_CREATE && + (event as BlockCreate).ids!.includes(this.id) + ) { + // Look for the case where a procedure call was created (usually through + // paste) and there is no matching definition. In this case, create + // an empty definition block with the correct signature. + const name = this.getProcedureCall(); + let def = Procedures.getDefinition(name, this.workspace); + if ( + def && + (def.type !== this.defType_ || + JSON.stringify(def.getVars()) !== JSON.stringify(this.arguments_)) + ) { + // The signatures don't match. + def = null; + } + if (!def) { + Events.setGroup(event.group); + /** + * Create matching definition block. + * + * + * + * + * + * test + * + * + */ + const xml = xmlUtils.createElement('xml'); + const block = xmlUtils.createElement('block'); + block.setAttribute('type', this.defType_); + const xy = this.getRelativeToSurfaceXY(); + const x = xy.x + config.snapRadius * (this.RTL ? -1 : 1); + const y = xy.y + config.snapRadius * 2; + block.setAttribute('x', `${x}`); + block.setAttribute('y', `${y}`); + const mutation = this.mutationToDom(); + block.appendChild(mutation); + const field = xmlUtils.createElement('field'); + field.setAttribute('name', 'NAME'); + const callName = this.getProcedureCall(); + const newName = Procedures.findLegalName(callName, this); + if (callName !== newName) { + this.renameProcedure(callName, newName); + } + field.appendChild(xmlUtils.createTextNode(callName)); + block.appendChild(field); + xml.appendChild(block); + Xml.domToWorkspace(xml, this.workspace); + Events.setGroup(false); + } + } else if (event.type === Events.BLOCK_DELETE) { + // Look for the case where a procedure definition has been deleted, + // leaving this block (a procedure call) orphaned. In this case, delete + // the orphan. + const name = this.getProcedureCall(); + const def = Procedures.getDefinition(name, this.workspace); + if (!def) { + Events.setGroup(event.group); + this.dispose(true); + Events.setGroup(false); + } + } else if ( + event.type === Events.BLOCK_CHANGE && + (event as BlockChange).element === 'disabled' + ) { + const blockChangeEvent = event as BlockChange; + const name = this.getProcedureCall(); + const def = Procedures.getDefinition(name, this.workspace); + if (def && def.id === blockChangeEvent.blockId) { + // in most cases the old group should be '' + const oldGroup = Events.getGroup(); + if (oldGroup) { + // This should only be possible programmatically and may indicate a + // problem with event grouping. If you see this message please + // investigate. If the use ends up being valid we may need to reorder + // events in the undo stack. + console.log( + 'Saw an existing group while responding to a definition change', + ); + } + Events.setGroup(event.group); + const valid = def.isEnabled(); + this.setDisabledReason( + !valid, + DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON, + ); + this.setWarningText( + valid + ? null + : Msg['PROCEDURES_CALL_DISABLED_DEF_WARNING'].replace('%1', name), + ); + Events.setGroup(oldGroup); + } + } + }, + /** + * Add menu option to find the definition block for this call. + * + * @param options List of menu options to add to. + */ + customContextMenu: function ( + this: CallBlock, + options: Array, + ) { + if (!(this.workspace as WorkspaceSvg).isMovable()) { + // If we center on the block and the workspace isn't movable we could + // loose blocks at the edges of the workspace. + return; + } + + const name = this.getProcedureCall(); + const workspace = this.workspace; + options.push({ + enabled: true, + text: Msg['PROCEDURES_HIGHLIGHT_DEF'], + callback: function () { + const def = Procedures.getDefinition(name, workspace); + if (def) { + (workspace as WorkspaceSvg).centerOnBlock(def.id); + common.setSelected(def as BlockSvg); + } + }, + }); + }, +}; + +blocks['procedures_callnoreturn'] = { + ...PROCEDURE_CALL_COMMON, + /** + * Block for calling a procedure with no return value. + */ + init: function (this: CallBlock) { + this.appendDummyInput('TOPROW').appendField('', 'NAME'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle('procedure_blocks'); + // Tooltip is set in renameProcedure. + this.setHelpUrl(Msg['PROCEDURES_CALLNORETURN_HELPURL']); + this.arguments_ = []; + this.argumentVarModels_ = []; + this.quarkConnections_ = {}; + this.quarkIds_ = null; + }, + + defType_: 'procedures_defnoreturn', +}; + +blocks['procedures_callreturn'] = { + ...PROCEDURE_CALL_COMMON, + /** + * Block for calling a procedure with a return value. + */ + init: function (this: CallBlock) { + this.appendDummyInput('TOPROW').appendField('', 'NAME'); + this.setOutput(true); + this.setStyle('procedure_blocks'); + // Tooltip is set in domToMutation. + this.setHelpUrl(Msg['PROCEDURES_CALLRETURN_HELPURL']); + this.arguments_ = []; + this.argumentVarModels_ = []; + this.quarkConnections_ = {}; + this.quarkIds_ = null; + }, + + defType_: 'procedures_defreturn', +}; + +/** + * Type of a procedures_ifreturn block. + * + * @internal + */ +export type IfReturnBlock = Block & IfReturnMixin; +interface IfReturnMixin extends IfReturnMixinType { + hasReturnValue_: boolean; +} +type IfReturnMixinType = typeof PROCEDURES_IFRETURN; + +/** + * The language-neutral ID for when the reason why a block is disabled is + * because the block is only valid inside of a procedure body. + */ +const UNPARENTED_IFRETURN_DISABLED_REASON = 'UNPARENTED_IFRETURN'; + +const PROCEDURES_IFRETURN = { + /** + * Block for conditionally returning a value from a procedure. + */ + init: function (this: IfReturnBlock) { + this.appendValueInput('CONDITION') + .setCheck('Boolean') + .appendField(Msg['CONTROLS_IF_MSG_IF']); + this.appendValueInput('VALUE').appendField( + Msg['PROCEDURES_DEFRETURN_RETURN'], + ); + this.setInputsInline(true); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle('procedure_blocks'); + this.setTooltip(Msg['PROCEDURES_IFRETURN_TOOLTIP']); + this.setHelpUrl(Msg['PROCEDURES_IFRETURN_HELPURL']); + this.hasReturnValue_ = true; + }, + /** + * Create XML to represent whether this block has a return value. + * + * @returns XML storage element. + */ + mutationToDom: function (this: IfReturnBlock): Element { + const container = xmlUtils.createElement('mutation'); + container.setAttribute('value', String(Number(this.hasReturnValue_))); + return container; + }, + /** + * Parse XML to restore whether this block has a return value. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: IfReturnBlock, xmlElement: Element) { + const value = xmlElement.getAttribute('value'); + this.hasReturnValue_ = value === '1'; + if (!this.hasReturnValue_) { + this.removeInput('VALUE'); + this.appendDummyInput('VALUE').appendField( + Msg['PROCEDURES_DEFRETURN_RETURN'], + ); + } + }, + + // This block does not need JSO serialization hooks (saveExtraState and + // loadExtraState) because the state of this block is already encoded in the + // block's position in the workspace. + // XML hooks are kept for backwards compatibility. + + /** + * Called whenever anything on the workspace changes. + * Add warning if this flow block is not nested inside a loop. + * + * @param e Move event. + */ + onchange: function (this: IfReturnBlock, e: AbstractEvent) { + if ( + ((this.workspace as WorkspaceSvg).isDragging && + (this.workspace as WorkspaceSvg).isDragging()) || + (e.type !== Events.BLOCK_MOVE && e.type !== Events.BLOCK_CREATE) + ) { + return; // Don't change state at the start of a drag. + } + let legal = false; + // Is the block nested in a procedure? + let block = this; // eslint-disable-line @typescript-eslint/no-this-alias + do { + if (this.FUNCTION_TYPES.includes(block.type)) { + legal = true; + break; + } + block = block.getSurroundParent()!; + } while (block); + if (legal) { + // If needed, toggle whether this block has a return value. + if (block.type === 'procedures_defnoreturn' && this.hasReturnValue_) { + this.removeInput('VALUE'); + this.appendDummyInput('VALUE').appendField( + Msg['PROCEDURES_DEFRETURN_RETURN'], + ); + this.hasReturnValue_ = false; + } else if ( + block.type === 'procedures_defreturn' && + !this.hasReturnValue_ + ) { + this.removeInput('VALUE'); + this.appendValueInput('VALUE').appendField( + Msg['PROCEDURES_DEFRETURN_RETURN'], + ); + this.hasReturnValue_ = true; + } + this.setWarningText(null); + } else { + this.setWarningText(Msg['PROCEDURES_IFRETURN_WARNING']); + } + + if (!this.isInFlyout) { + try { + // There is no need to record the enable/disable change on the undo/redo + // list since the change will be automatically recreated when replayed. + eventUtils.setRecordUndo(false); + this.setDisabledReason(!legal, UNPARENTED_IFRETURN_DISABLED_REASON); + } finally { + eventUtils.setRecordUndo(true); + } + } + }, + /** + * List of block types that are functions and thus do not need warnings. + * To add a new function type add this to your code: + * Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func'); + */ + FUNCTION_TYPES: ['procedures_defnoreturn', 'procedures_defreturn'], +}; +blocks['procedures_ifreturn'] = PROCEDURES_IFRETURN; + +// Register provided blocks. +defineBlocks(blocks); diff --git a/blocks/text.js b/blocks/text.js deleted file mode 100644 index 65710f1aa88..00000000000 --- a/blocks/text.js +++ /dev/null @@ -1,923 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Text blocks for Blockly. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Blocks.texts'); // Deprecated -goog.provide('Blockly.Constants.Text'); - -goog.require('Blockly'); -goog.require('Blockly.Blocks'); -goog.require('Blockly.FieldDropdown'); -goog.require('Blockly.FieldImage'); -goog.require('Blockly.FieldMultilineInput'); -goog.require('Blockly.FieldTextInput'); -goog.require('Blockly.FieldVariable'); -goog.require('Blockly.Mutator'); - - -/** - * Unused constant for the common HSV hue for all blocks in this category. - * @deprecated Use Blockly.Msg['TEXTS_HUE']. (2018 April 5) - */ -Blockly.Constants.Text.HUE = 160; - -Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT - // Block for text value - { - "type": "text", - "message0": "%1", - "args0": [{ - "type": "field_input", - "name": "TEXT", - "text": "" - }], - "output": "String", - "style": "text_blocks", - "helpUrl": "%{BKY_TEXT_TEXT_HELPURL}", - "tooltip": "%{BKY_TEXT_TEXT_TOOLTIP}", - "extensions": [ - "text_quotes", - "parent_tooltip_when_inline" - ] - }, - { - "type": "text_multiline", - "message0": "%1 %2", - "args0": [{ - "type": "field_image", - "src": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAARCAYAAADpP' + - 'U2iAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAdhgAAHYYBXaITgQAAABh0RVh0' + - 'U29mdHdhcmUAcGFpbnQubmV0IDQuMS42/U4J6AAAAP1JREFUOE+Vks0KQUEYhjm' + - 'RIja4ABtZ2dm5A3t3Ia6AUm7CylYuQRaUhZSlLZJiQbFAyRnPN33y01HOW08z88' + - '73zpwzM4F3GWOCruvGIE4/rLaV+Nq1hVGMBqzhqlxgCys4wJA65xnogMHsQ5luj' + - 'nYHTejBBCK2mE4abjCgMGhNxHgDFWjDSG07kdfVa2pZMf4ZyMAdWmpZMfYOsLiD' + - 'MYMjlMB+K613QISRhTnITnsYg5yUd0DETmEoMlkFOeIT/A58iyK5E18BuTBfgYX' + - 'fwNJv4P9/oEBerLylOnRhygmGdPpTTBZAPkde61lbQe4moWUvYUZYLfUNftIY4z' + - 'wA5X2Z9AYnQrEAAAAASUVORK5CYII=', - "width": 12, - "height": 17, - "alt": '\u00B6' - },{ - "type": "field_multilinetext", - "name": "TEXT", - "text": "" - }], - "output": "String", - "style": "text_blocks", - "helpUrl": "%{BKY_TEXT_TEXT_HELPURL}", - "tooltip": "%{BKY_TEXT_TEXT_TOOLTIP}", - "extensions": [ - "parent_tooltip_when_inline" - ] - }, - { - "type": "text_join", - "message0": "", - "output": "String", - "style": "text_blocks", - "helpUrl": "%{BKY_TEXT_JOIN_HELPURL}", - "tooltip": "%{BKY_TEXT_JOIN_TOOLTIP}", - "mutator": "text_join_mutator" - - }, - { - "type": "text_create_join_container", - "message0": "%{BKY_TEXT_CREATE_JOIN_TITLE_JOIN} %1 %2", - "args0": [{ - "type": "input_dummy" - }, - { - "type": "input_statement", - "name": "STACK" - }], - "style": "text_blocks", - "tooltip": "%{BKY_TEXT_CREATE_JOIN_TOOLTIP}", - "enableContextMenu": false - }, - { - "type": "text_create_join_item", - "message0": "%{BKY_TEXT_CREATE_JOIN_ITEM_TITLE_ITEM}", - "previousStatement": null, - "nextStatement": null, - "style": "text_blocks", - "tooltip": "%{BKY_TEXT_CREATE_JOIN_ITEM_TOOLTIP}", - "enableContextMenu": false - }, - { - "type": "text_append", - "message0": "%{BKY_TEXT_APPEND_TITLE}", - "args0": [{ - "type": "field_variable", - "name": "VAR", - "variable": "%{BKY_TEXT_APPEND_VARIABLE}" - }, - { - "type": "input_value", - "name": "TEXT" - }], - "previousStatement": null, - "nextStatement": null, - "style": "text_blocks", - "extensions": [ - "text_append_tooltip" - ] - }, - { - "type": "text_length", - "message0": "%{BKY_TEXT_LENGTH_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "VALUE", - "check": ['String', 'Array'] - } - ], - "output": 'Number', - "style": "text_blocks", - "tooltip": "%{BKY_TEXT_LENGTH_TOOLTIP}", - "helpUrl": "%{BKY_TEXT_LENGTH_HELPURL}" - }, - { - "type": "text_isEmpty", - "message0": "%{BKY_TEXT_ISEMPTY_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "VALUE", - "check": ['String', 'Array'] - } - ], - "output": 'Boolean', - "style": "text_blocks", - "tooltip": "%{BKY_TEXT_ISEMPTY_TOOLTIP}", - "helpUrl": "%{BKY_TEXT_ISEMPTY_HELPURL}" - }, - { - "type": "text_indexOf", - "message0": "%{BKY_TEXT_INDEXOF_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "VALUE", - "check": "String" - }, - { - "type": "field_dropdown", - "name": "END", - "options": [ - [ - "%{BKY_TEXT_INDEXOF_OPERATOR_FIRST}", - "FIRST" - ], - [ - "%{BKY_TEXT_INDEXOF_OPERATOR_LAST}", - "LAST" - ] - ] - }, - { - "type": "input_value", - "name": "FIND", - "check": "String" - } - ], - "output": "Number", - "style": "text_blocks", - "helpUrl": "%{BKY_TEXT_INDEXOF_HELPURL}", - "inputsInline": true, - "extensions": [ - "text_indexOf_tooltip" - ] - }, - { - "type": "text_charAt", - "message0": "%{BKY_TEXT_CHARAT_TITLE}", // "in text %1 %2" - "args0": [ - { - "type":"input_value", - "name": "VALUE", - "check": "String" - }, - { - "type": "field_dropdown", - "name": "WHERE", - "options": [ - ["%{BKY_TEXT_CHARAT_FROM_START}", "FROM_START"], - ["%{BKY_TEXT_CHARAT_FROM_END}", "FROM_END"], - ["%{BKY_TEXT_CHARAT_FIRST}", "FIRST"], - ["%{BKY_TEXT_CHARAT_LAST}", "LAST"], - ["%{BKY_TEXT_CHARAT_RANDOM}", "RANDOM"] - ] - } - ], - "output": "String", - "style": "text_blocks", - "helpUrl": "%{BKY_TEXT_CHARAT_HELPURL}", - "inputsInline": true, - "mutator": "text_charAt_mutator" - } -]); // END JSON EXTRACT (Do not delete this comment.) - -Blockly.Blocks['text_getSubstring'] = { - /** - * Block for getting substring. - * @this {Blockly.Block} - */ - init: function() { - this['WHERE_OPTIONS_1'] = [ - [Blockly.Msg['TEXT_GET_SUBSTRING_START_FROM_START'], 'FROM_START'], - [Blockly.Msg['TEXT_GET_SUBSTRING_START_FROM_END'], 'FROM_END'], - [Blockly.Msg['TEXT_GET_SUBSTRING_START_FIRST'], 'FIRST'] - ]; - this['WHERE_OPTIONS_2'] = [ - [Blockly.Msg['TEXT_GET_SUBSTRING_END_FROM_START'], 'FROM_START'], - [Blockly.Msg['TEXT_GET_SUBSTRING_END_FROM_END'], 'FROM_END'], - [Blockly.Msg['TEXT_GET_SUBSTRING_END_LAST'], 'LAST'] - ]; - this.setHelpUrl(Blockly.Msg['TEXT_GET_SUBSTRING_HELPURL']); - this.setStyle('text_blocks'); - this.appendValueInput('STRING') - .setCheck('String') - .appendField(Blockly.Msg['TEXT_GET_SUBSTRING_INPUT_IN_TEXT']); - this.appendDummyInput('AT1'); - this.appendDummyInput('AT2'); - if (Blockly.Msg['TEXT_GET_SUBSTRING_TAIL']) { - this.appendDummyInput('TAIL') - .appendField(Blockly.Msg['TEXT_GET_SUBSTRING_TAIL']); - } - this.setInputsInline(true); - this.setOutput(true, 'String'); - this.updateAt_(1, true); - this.updateAt_(2, true); - this.setTooltip(Blockly.Msg['TEXT_GET_SUBSTRING_TOOLTIP']); - }, - /** - * Create XML to represent whether there are 'AT' inputs. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - var isAt1 = this.getInput('AT1').type == Blockly.INPUT_VALUE; - container.setAttribute('at1', isAt1); - var isAt2 = this.getInput('AT2').type == Blockly.INPUT_VALUE; - container.setAttribute('at2', isAt2); - return container; - }, - /** - * Parse XML to restore the 'AT' inputs. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - var isAt1 = (xmlElement.getAttribute('at1') == 'true'); - var isAt2 = (xmlElement.getAttribute('at2') == 'true'); - this.updateAt_(1, isAt1); - this.updateAt_(2, isAt2); - }, - /** - * Create or delete an input for a numeric index. - * This block has two such inputs, independent of each other. - * @param {number} n Specify first or second input (1 or 2). - * @param {boolean} isAt True if the input should exist. - * @private - * @this {Blockly.Block} - */ - updateAt_: function(n, isAt) { - // Create or delete an input for the numeric index. - // Destroy old 'AT' and 'ORDINAL' inputs. - this.removeInput('AT' + n); - this.removeInput('ORDINAL' + n, true); - // Create either a value 'AT' input or a dummy input. - if (isAt) { - this.appendValueInput('AT' + n).setCheck('Number'); - if (Blockly.Msg['ORDINAL_NUMBER_SUFFIX']) { - this.appendDummyInput('ORDINAL' + n) - .appendField(Blockly.Msg['ORDINAL_NUMBER_SUFFIX']); - } - } else { - this.appendDummyInput('AT' + n); - } - // Move tail, if present, to end of block. - if (n == 2 && Blockly.Msg['TEXT_GET_SUBSTRING_TAIL']) { - this.removeInput('TAIL', true); - this.appendDummyInput('TAIL') - .appendField(Blockly.Msg['TEXT_GET_SUBSTRING_TAIL']); - } - var menu = new Blockly.FieldDropdown(this['WHERE_OPTIONS_' + n], - function(value) { - var newAt = (value == 'FROM_START') || (value == 'FROM_END'); - // The 'isAt' variable is available due to this function being a - // closure. - if (newAt != isAt) { - var block = this.getSourceBlock(); - block.updateAt_(n, newAt); - // This menu has been destroyed and replaced. - // Update the replacement. - block.setFieldValue(value, 'WHERE' + n); - return null; - } - return undefined; - }); - - this.getInput('AT' + n) - .appendField(menu, 'WHERE' + n); - if (n == 1) { - this.moveInputBefore('AT1', 'AT2'); - if (this.getInput('ORDINAL1')) { - this.moveInputBefore('ORDINAL1', 'AT2'); - } - } - } -}; - -Blockly.Blocks['text_changeCase'] = { - /** - * Block for changing capitalization. - * @this {Blockly.Block} - */ - init: function() { - var OPERATORS = [ - [Blockly.Msg['TEXT_CHANGECASE_OPERATOR_UPPERCASE'], 'UPPERCASE'], - [Blockly.Msg['TEXT_CHANGECASE_OPERATOR_LOWERCASE'], 'LOWERCASE'], - [Blockly.Msg['TEXT_CHANGECASE_OPERATOR_TITLECASE'], 'TITLECASE'] - ]; - this.setHelpUrl(Blockly.Msg['TEXT_CHANGECASE_HELPURL']); - this.setStyle('text_blocks'); - this.appendValueInput('TEXT') - .setCheck('String') - .appendField(new Blockly.FieldDropdown(OPERATORS), 'CASE'); - this.setOutput(true, 'String'); - this.setTooltip(Blockly.Msg['TEXT_CHANGECASE_TOOLTIP']); - } -}; - -Blockly.Blocks['text_trim'] = { - /** - * Block for trimming spaces. - * @this {Blockly.Block} - */ - init: function() { - var OPERATORS = [ - [Blockly.Msg['TEXT_TRIM_OPERATOR_BOTH'], 'BOTH'], - [Blockly.Msg['TEXT_TRIM_OPERATOR_LEFT'], 'LEFT'], - [Blockly.Msg['TEXT_TRIM_OPERATOR_RIGHT'], 'RIGHT'] - ]; - this.setHelpUrl(Blockly.Msg['TEXT_TRIM_HELPURL']); - this.setStyle('text_blocks'); - this.appendValueInput('TEXT') - .setCheck('String') - .appendField(new Blockly.FieldDropdown(OPERATORS), 'MODE'); - this.setOutput(true, 'String'); - this.setTooltip(Blockly.Msg['TEXT_TRIM_TOOLTIP']); - } -}; - -Blockly.Blocks['text_print'] = { - /** - * Block for print statement. - * @this {Blockly.Block} - */ - init: function() { - this.jsonInit({ - "message0": Blockly.Msg['TEXT_PRINT_TITLE'], - "args0": [ - { - "type": "input_value", - "name": "TEXT" - } - ], - "previousStatement": null, - "nextStatement": null, - "style": "text_blocks", - "tooltip": Blockly.Msg['TEXT_PRINT_TOOLTIP'], - "helpUrl": Blockly.Msg['TEXT_PRINT_HELPURL'] - }); - } -}; - -Blockly.Blocks['text_prompt_ext'] = { - /** - * Block for prompt function (external message). - * @this {Blockly.Block} - */ - init: function() { - var TYPES = [ - [Blockly.Msg['TEXT_PROMPT_TYPE_TEXT'], 'TEXT'], - [Blockly.Msg['TEXT_PROMPT_TYPE_NUMBER'], 'NUMBER'] - ]; - this.setHelpUrl(Blockly.Msg['TEXT_PROMPT_HELPURL']); - this.setStyle('text_blocks'); - // Assign 'this' to a variable for use in the closures below. - var thisBlock = this; - var dropdown = new Blockly.FieldDropdown(TYPES, function(newOp) { - thisBlock.updateType_(newOp); - }); - this.appendValueInput('TEXT') - .appendField(dropdown, 'TYPE'); - this.setOutput(true, 'String'); - this.setTooltip(function() { - return (thisBlock.getFieldValue('TYPE') == 'TEXT') ? - Blockly.Msg['TEXT_PROMPT_TOOLTIP_TEXT'] : - Blockly.Msg['TEXT_PROMPT_TOOLTIP_NUMBER']; - }); - }, - /** - * Modify this block to have the correct output type. - * @param {string} newOp Either 'TEXT' or 'NUMBER'. - * @private - * @this {Blockly.Block} - */ - updateType_: function(newOp) { - this.outputConnection.setCheck(newOp == 'NUMBER' ? 'Number' : 'String'); - }, - /** - * Create XML to represent the output type. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('type', this.getFieldValue('TYPE')); - return container; - }, - /** - * Parse XML to restore the output type. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - this.updateType_(xmlElement.getAttribute('type')); - } -}; - -Blockly.Blocks['text_prompt'] = { - /** - * Block for prompt function (internal message). - * The 'text_prompt_ext' block is preferred as it is more flexible. - * @this {Blockly.Block} - */ - init: function() { - this.mixin(Blockly.Constants.Text.QUOTE_IMAGE_MIXIN); - var TYPES = [ - [Blockly.Msg['TEXT_PROMPT_TYPE_TEXT'], 'TEXT'], - [Blockly.Msg['TEXT_PROMPT_TYPE_NUMBER'], 'NUMBER'] - ]; - - // Assign 'this' to a variable for use in the closures below. - var thisBlock = this; - this.setHelpUrl(Blockly.Msg['TEXT_PROMPT_HELPURL']); - this.setStyle('text_blocks'); - var dropdown = new Blockly.FieldDropdown(TYPES, function(newOp) { - thisBlock.updateType_(newOp); - }); - this.appendDummyInput() - .appendField(dropdown, 'TYPE') - .appendField(this.newQuote_(true)) - .appendField(new Blockly.FieldTextInput(''), 'TEXT') - .appendField(this.newQuote_(false)); - this.setOutput(true, 'String'); - this.setTooltip(function() { - return (thisBlock.getFieldValue('TYPE') == 'TEXT') ? - Blockly.Msg['TEXT_PROMPT_TOOLTIP_TEXT'] : - Blockly.Msg['TEXT_PROMPT_TOOLTIP_NUMBER']; - }); - }, - updateType_: Blockly.Blocks['text_prompt_ext'].updateType_, - mutationToDom: Blockly.Blocks['text_prompt_ext'].mutationToDom, - domToMutation: Blockly.Blocks['text_prompt_ext'].domToMutation -}; - -Blockly.Blocks['text_count'] = { - /** - * Block for counting how many times one string appears within another string. - * @this {Blockly.Block} - */ - init: function() { - this.jsonInit({ - "message0": Blockly.Msg['TEXT_COUNT_MESSAGE0'], - "args0": [ - { - "type": "input_value", - "name": "SUB", - "check": "String" - }, - { - "type": "input_value", - "name": "TEXT", - "check": "String" - } - ], - "output": "Number", - "inputsInline": true, - "style": "text_blocks", - "tooltip": Blockly.Msg['TEXT_COUNT_TOOLTIP'], - "helpUrl": Blockly.Msg['TEXT_COUNT_HELPURL'] - }); - } -}; - -Blockly.Blocks['text_replace'] = { - /** - * Block for replacing one string with another in the text. - * @this {Blockly.Block} - */ - init: function() { - this.jsonInit({ - "message0": Blockly.Msg['TEXT_REPLACE_MESSAGE0'], - "args0": [ - { - "type": "input_value", - "name": "FROM", - "check": "String" - }, - { - "type": "input_value", - "name": "TO", - "check": "String" - }, - { - "type": "input_value", - "name": "TEXT", - "check": "String" - } - ], - "output": "String", - "inputsInline": true, - "style": "text_blocks", - "tooltip": Blockly.Msg['TEXT_REPLACE_TOOLTIP'], - "helpUrl": Blockly.Msg['TEXT_REPLACE_HELPURL'] - }); - } -}; - -Blockly.Blocks['text_reverse'] = { - /** - * Block for reversing a string. - * @this {Blockly.Block} - */ - init: function() { - this.jsonInit({ - "message0": Blockly.Msg['TEXT_REVERSE_MESSAGE0'], - "args0": [ - { - "type": "input_value", - "name": "TEXT", - "check": "String" - } - ], - "output": "String", - "inputsInline": true, - "style": "text_blocks", - "tooltip": Blockly.Msg['TEXT_REVERSE_TOOLTIP'], - "helpUrl": Blockly.Msg['TEXT_REVERSE_HELPURL'] - }); - } -}; - -/** - * - * @mixin - * @package - * @readonly - */ -Blockly.Constants.Text.QUOTE_IMAGE_MIXIN = { - /** - * Image data URI of an LTR opening double quote (same as RTL closing double quote). - * @readonly - */ - QUOTE_IMAGE_LEFT_DATAURI: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAA' + - 'n0lEQVQI1z3OMa5BURSF4f/cQhAKjUQhuQmFNwGJEUi0RKN5rU7FHKhpjEH3TEMtkdBSCY' + - '1EIv8r7nFX9e29V7EBAOvu7RPjwmWGH/VuF8CyN9/OAdvqIXYLvtRaNjx9mMTDyo+NjAN1' + - 'HNcl9ZQ5oQMM3dgDUqDo1l8DzvwmtZN7mnD+PkmLa+4mhrxVA9fRowBWmVBhFy5gYEjKMf' + - 'z9AylsaRRgGzvZAAAAAElFTkSuQmCC', - /** - * Image data URI of an LTR closing double quote (same as RTL opening double quote). - * @readonly - */ - QUOTE_IMAGE_RIGHT_DATAURI: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAA' + - 'qUlEQVQI1z3KvUpCcRiA8ef9E4JNHhI0aFEacm1o0BsI0Slx8wa8gLauoDnoBhq7DcfWhg' + - 'gONDmJJgqCPA7neJ7p934EOOKOnM8Q7PDElo/4x4lFb2DmuUjcUzS3URnGib9qaPNbuXvB' + - 'O3sGPHJDRG6fGVdMSeWDP2q99FQdFrz26Gu5Tq7dFMzUvbXy8KXeAj57cOklgA+u1B5Aos' + - 'lLtGIHQMaCVnwDnADZIFIrXsoXrgAAAABJRU5ErkJggg==', - /** - * Pixel width of QUOTE_IMAGE_LEFT_DATAURI and QUOTE_IMAGE_RIGHT_DATAURI. - * @readonly - */ - QUOTE_IMAGE_WIDTH: 12, - /** - * Pixel height of QUOTE_IMAGE_LEFT_DATAURI and QUOTE_IMAGE_RIGHT_DATAURI. - * @readonly - */ - QUOTE_IMAGE_HEIGHT: 12, - - /** - * Inserts appropriate quote images before and after the named field. - * @param {string} fieldName The name of the field to wrap with quotes. - * @this {Blockly.Block} - */ - quoteField_: function(fieldName) { - for (var i = 0, input; (input = this.inputList[i]); i++) { - for (var j = 0, field; (field = input.fieldRow[j]); j++) { - if (fieldName == field.name) { - input.insertFieldAt(j, this.newQuote_(true)); - input.insertFieldAt(j + 2, this.newQuote_(false)); - return; - } - } - } - console.warn('field named "' + fieldName + '" not found in ' + this.toDevString()); - }, - - /** - * A helper function that generates a FieldImage of an opening or - * closing double quote. The selected quote will be adapted for RTL blocks. - * @param {boolean} open If the image should be open quote (“ in LTR). - * Otherwise, a closing quote is used (” in LTR). - * @return {!Blockly.FieldImage} The new field. - * @this {Blockly.Block} - */ - newQuote_: function(open) { - var isLeft = this.RTL ? !open : open; - var dataUri = isLeft ? - this.QUOTE_IMAGE_LEFT_DATAURI : - this.QUOTE_IMAGE_RIGHT_DATAURI; - return new Blockly.FieldImage( - dataUri, - this.QUOTE_IMAGE_WIDTH, - this.QUOTE_IMAGE_HEIGHT, - isLeft ? '\u201C' : '\u201D'); - } -}; - -/** - * Wraps TEXT field with images of double quote characters. - * @this {Blockly.Block} - */ -Blockly.Constants.Text.TEXT_QUOTES_EXTENSION = function() { - this.mixin(Blockly.Constants.Text.QUOTE_IMAGE_MIXIN); - this.quoteField_('TEXT'); -}; - -/** - * Mixin for mutator functions in the 'text_join_mutator' extension. - * @mixin - * @augments Blockly.Block - * @package - */ -Blockly.Constants.Text.TEXT_JOIN_MUTATOR_MIXIN = { - /** - * Create XML to represent number of text inputs. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('items', this.itemCount_); - return container; - }, - /** - * Parse XML to restore the text inputs. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10); - this.updateShape_(); - }, - /** - * Populate the mutator's dialog with this block's components. - * @param {!Blockly.Workspace} workspace Mutator's workspace. - * @return {!Blockly.Block} Root block in mutator. - * @this {Blockly.Block} - */ - decompose: function(workspace) { - var containerBlock = workspace.newBlock('text_create_join_container'); - containerBlock.initSvg(); - var connection = containerBlock.getInput('STACK').connection; - for (var i = 0; i < this.itemCount_; i++) { - var itemBlock = workspace.newBlock('text_create_join_item'); - itemBlock.initSvg(); - connection.connect(itemBlock.previousConnection); - connection = itemBlock.nextConnection; - } - return containerBlock; - }, - /** - * Reconfigure this block based on the mutator dialog's components. - * @param {!Blockly.Block} containerBlock Root block in mutator. - * @this {Blockly.Block} - */ - compose: function(containerBlock) { - var itemBlock = containerBlock.getInputTargetBlock('STACK'); - // Count number of inputs. - var connections = []; - while (itemBlock) { - connections.push(itemBlock.valueConnection_); - itemBlock = itemBlock.nextConnection && - itemBlock.nextConnection.targetBlock(); - } - // Disconnect any children that don't belong. - for (var i = 0; i < this.itemCount_; i++) { - var connection = this.getInput('ADD' + i).connection.targetConnection; - if (connection && connections.indexOf(connection) == -1) { - connection.disconnect(); - } - } - this.itemCount_ = connections.length; - this.updateShape_(); - // Reconnect any child blocks. - for (var i = 0; i < this.itemCount_; i++) { - Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i); - } - }, - /** - * Store pointers to any connected child blocks. - * @param {!Blockly.Block} containerBlock Root block in mutator. - * @this {Blockly.Block} - */ - saveConnections: function(containerBlock) { - var itemBlock = containerBlock.getInputTargetBlock('STACK'); - var i = 0; - while (itemBlock) { - var input = this.getInput('ADD' + i); - itemBlock.valueConnection_ = input && input.connection.targetConnection; - i++; - itemBlock = itemBlock.nextConnection && - itemBlock.nextConnection.targetBlock(); - } - }, - /** - * Modify this block to have the correct number of inputs. - * @private - * @this {Blockly.Block} - */ - updateShape_: function() { - if (this.itemCount_ && this.getInput('EMPTY')) { - this.removeInput('EMPTY'); - } else if (!this.itemCount_ && !this.getInput('EMPTY')) { - this.appendDummyInput('EMPTY') - .appendField(this.newQuote_(true)) - .appendField(this.newQuote_(false)); - } - // Add new inputs. - for (var i = 0; i < this.itemCount_; i++) { - if (!this.getInput('ADD' + i)) { - var input = this.appendValueInput('ADD' + i) - .setAlign(Blockly.ALIGN_RIGHT); - if (i == 0) { - input.appendField(Blockly.Msg['TEXT_JOIN_TITLE_CREATEWITH']); - } - } - } - // Remove deleted inputs. - while (this.getInput('ADD' + i)) { - this.removeInput('ADD' + i); - i++; - } - } -}; - -/** - * Performs final setup of a text_join block. - * @this {Blockly.Block} - */ -Blockly.Constants.Text.TEXT_JOIN_EXTENSION = function() { - // Add the quote mixin for the itemCount_ = 0 case. - this.mixin(Blockly.Constants.Text.QUOTE_IMAGE_MIXIN); - // Initialize the mutator values. - this.itemCount_ = 2; - this.updateShape_(); - // Configure the mutator UI. - this.setMutator(new Blockly.Mutator(['text_create_join_item'])); -}; - -// Update the tooltip of 'text_append' block to reference the variable. -Blockly.Extensions.register('text_append_tooltip', - Blockly.Extensions.buildTooltipWithFieldText( - '%{BKY_TEXT_APPEND_TOOLTIP}', 'VAR')); - -/** - * Update the tooltip of 'text_append' block to reference the variable. - * @this {Blockly.Block} - */ -Blockly.Constants.Text.TEXT_INDEXOF_TOOLTIP_EXTENSION = function() { - // Assign 'this' to a variable for use in the tooltip closure below. - var thisBlock = this; - this.setTooltip(function() { - return Blockly.Msg['TEXT_INDEXOF_TOOLTIP'].replace('%1', - thisBlock.workspace.options.oneBasedIndex ? '0' : '-1'); - }); -}; - -/** - * Mixin for mutator functions in the 'text_charAt_mutator' extension. - * @mixin - * @augments Blockly.Block - * @package - */ -Blockly.Constants.Text.TEXT_CHARAT_MUTATOR_MIXIN = { - /** - * Create XML to represent whether there is an 'AT' input. - * @return {!Element} XML storage element. - * @this {Blockly.Block} - */ - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('at', !!this.isAt_); - return container; - }, - /** - * Parse XML to restore the 'AT' input. - * @param {!Element} xmlElement XML storage element. - * @this {Blockly.Block} - */ - domToMutation: function(xmlElement) { - // Note: Until January 2013 this block did not have mutations, - // so 'at' defaults to true. - var isAt = (xmlElement.getAttribute('at') != 'false'); - this.updateAt_(isAt); - }, - /** - * Create or delete an input for the numeric index. - * @param {boolean} isAt True if the input should exist. - * @private - * @this {Blockly.Block} - */ - updateAt_: function(isAt) { - // Destroy old 'AT' and 'ORDINAL' inputs. - this.removeInput('AT', true); - this.removeInput('ORDINAL', true); - // Create either a value 'AT' input or a dummy input. - if (isAt) { - this.appendValueInput('AT').setCheck('Number'); - if (Blockly.Msg['ORDINAL_NUMBER_SUFFIX']) { - this.appendDummyInput('ORDINAL') - .appendField(Blockly.Msg['ORDINAL_NUMBER_SUFFIX']); - } - } - if (Blockly.Msg['TEXT_CHARAT_TAIL']) { - this.removeInput('TAIL', true); - this.appendDummyInput('TAIL') - .appendField(Blockly.Msg['TEXT_CHARAT_TAIL']); - } - - this.isAt_ = isAt; - } -}; - -/** - * Does the initial mutator update of text_charAt and adds the tooltip - * @this {Blockly.Block} - */ -Blockly.Constants.Text.TEXT_CHARAT_EXTENSION = function() { - var dropdown = this.getField('WHERE'); - dropdown.setValidator(function(value) { - var newAt = (value == 'FROM_START') || (value == 'FROM_END'); - if (newAt != this.isAt_) { - var block = this.getSourceBlock(); - block.updateAt_(newAt); - } - }); - this.updateAt_(true); - // Assign 'this' to a variable for use in the tooltip closure below. - var thisBlock = this; - this.setTooltip(function() { - var where = thisBlock.getFieldValue('WHERE'); - var tooltip = Blockly.Msg['TEXT_CHARAT_TOOLTIP']; - if (where == 'FROM_START' || where == 'FROM_END') { - var msg = (where == 'FROM_START') ? - Blockly.Msg['LISTS_INDEX_FROM_START_TOOLTIP'] : - Blockly.Msg['LISTS_INDEX_FROM_END_TOOLTIP']; - if (msg) { - tooltip += ' ' + msg.replace('%1', - thisBlock.workspace.options.oneBasedIndex ? '#1' : '#0'); - } - } - return tooltip; - }); -}; - -Blockly.Extensions.register('text_indexOf_tooltip', - Blockly.Constants.Text.TEXT_INDEXOF_TOOLTIP_EXTENSION); - -Blockly.Extensions.register('text_quotes', - Blockly.Constants.Text.TEXT_QUOTES_EXTENSION); - -Blockly.Extensions.registerMutator('text_join_mutator', - Blockly.Constants.Text.TEXT_JOIN_MUTATOR_MIXIN, - Blockly.Constants.Text.TEXT_JOIN_EXTENSION); - -Blockly.Extensions.registerMutator('text_charAt_mutator', - Blockly.Constants.Text.TEXT_CHARAT_MUTATOR_MIXIN, - Blockly.Constants.Text.TEXT_CHARAT_EXTENSION); diff --git a/blocks/text.ts b/blocks/text.ts new file mode 100644 index 00000000000..91a27005a92 --- /dev/null +++ b/blocks/text.ts @@ -0,0 +1,1007 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.libraryBlocks.texts + +import * as Extensions from '../core/extensions.js'; +import * as fieldRegistry from '../core/field_registry.js'; +import * as xmlUtils from '../core/utils/xml.js'; +import {Align} from '../core/inputs/align.js'; +import type {Block} from '../core/block.js'; +import type {BlockSvg} from '../core/block_svg.js'; +import {Connection} from '../core/connection.js'; +import {FieldImage} from '../core/field_image.js'; +import {FieldDropdown} from '../core/field_dropdown.js'; +import {FieldTextInput} from '../core/field_textinput.js'; +import {Msg} from '../core/msg.js'; +import {MutatorIcon} from '../core/icons/mutator_icon.js'; +import type {Workspace} from '../core/workspace.js'; +import { + createBlockDefinitionsFromJsonArray, + defineBlocks, +} from '../core/common.js'; +import '../core/field_variable.js'; +import {ValueInput} from '../core/inputs/value_input.js'; + +/** + * A dictionary of the block definitions provided by this module. + */ +export const blocks = createBlockDefinitionsFromJsonArray([ + // Block for text value + { + 'type': 'text', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_input', + 'name': 'TEXT', + 'text': '', + }, + ], + 'output': 'String', + 'style': 'text_blocks', + 'helpUrl': '%{BKY_TEXT_TEXT_HELPURL}', + 'tooltip': '%{BKY_TEXT_TEXT_TOOLTIP}', + 'extensions': ['text_quotes', 'parent_tooltip_when_inline'], + }, + { + 'type': 'text_join', + 'message0': '', + 'output': 'String', + 'style': 'text_blocks', + 'helpUrl': '%{BKY_TEXT_JOIN_HELPURL}', + 'tooltip': '%{BKY_TEXT_JOIN_TOOLTIP}', + 'mutator': 'text_join_mutator', + }, + { + 'type': 'text_create_join_container', + 'message0': '%{BKY_TEXT_CREATE_JOIN_TITLE_JOIN} %1 %2', + 'args0': [ + { + 'type': 'input_dummy', + }, + { + 'type': 'input_statement', + 'name': 'STACK', + }, + ], + 'style': 'text_blocks', + 'tooltip': '%{BKY_TEXT_CREATE_JOIN_TOOLTIP}', + 'enableContextMenu': false, + }, + { + 'type': 'text_create_join_item', + 'message0': '%{BKY_TEXT_CREATE_JOIN_ITEM_TITLE_ITEM}', + 'previousStatement': null, + 'nextStatement': null, + 'style': 'text_blocks', + 'tooltip': '%{BKY_TEXT_CREATE_JOIN_ITEM_TOOLTIP}', + 'enableContextMenu': false, + }, + { + 'type': 'text_append', + 'message0': '%{BKY_TEXT_APPEND_TITLE}', + 'args0': [ + { + 'type': 'field_variable', + 'name': 'VAR', + 'variable': '%{BKY_TEXT_APPEND_VARIABLE}', + }, + { + 'type': 'input_value', + 'name': 'TEXT', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'style': 'text_blocks', + 'extensions': ['text_append_tooltip'], + }, + { + 'type': 'text_length', + 'message0': '%{BKY_TEXT_LENGTH_TITLE}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'VALUE', + 'check': ['String', 'Array'], + }, + ], + 'output': 'Number', + 'style': 'text_blocks', + 'tooltip': '%{BKY_TEXT_LENGTH_TOOLTIP}', + 'helpUrl': '%{BKY_TEXT_LENGTH_HELPURL}', + }, + { + 'type': 'text_isEmpty', + 'message0': '%{BKY_TEXT_ISEMPTY_TITLE}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'VALUE', + 'check': ['String', 'Array'], + }, + ], + 'output': 'Boolean', + 'style': 'text_blocks', + 'tooltip': '%{BKY_TEXT_ISEMPTY_TOOLTIP}', + 'helpUrl': '%{BKY_TEXT_ISEMPTY_HELPURL}', + }, + { + 'type': 'text_indexOf', + 'message0': '%{BKY_TEXT_INDEXOF_TITLE}', + 'args0': [ + { + 'type': 'input_value', + 'name': 'VALUE', + 'check': 'String', + }, + { + 'type': 'field_dropdown', + 'name': 'END', + 'options': [ + ['%{BKY_TEXT_INDEXOF_OPERATOR_FIRST}', 'FIRST'], + ['%{BKY_TEXT_INDEXOF_OPERATOR_LAST}', 'LAST'], + ], + }, + { + 'type': 'input_value', + 'name': 'FIND', + 'check': 'String', + }, + ], + 'output': 'Number', + 'style': 'text_blocks', + 'helpUrl': '%{BKY_TEXT_INDEXOF_HELPURL}', + 'inputsInline': true, + 'extensions': ['text_indexOf_tooltip'], + }, + { + 'type': 'text_charAt', + 'message0': '%{BKY_TEXT_CHARAT_TITLE}', // "in text %1 %2" + 'args0': [ + { + 'type': 'input_value', + 'name': 'VALUE', + 'check': 'String', + }, + { + 'type': 'field_dropdown', + 'name': 'WHERE', + 'options': [ + ['%{BKY_TEXT_CHARAT_FROM_START}', 'FROM_START'], + ['%{BKY_TEXT_CHARAT_FROM_END}', 'FROM_END'], + ['%{BKY_TEXT_CHARAT_FIRST}', 'FIRST'], + ['%{BKY_TEXT_CHARAT_LAST}', 'LAST'], + ['%{BKY_TEXT_CHARAT_RANDOM}', 'RANDOM'], + ], + }, + ], + 'output': 'String', + 'style': 'text_blocks', + 'helpUrl': '%{BKY_TEXT_CHARAT_HELPURL}', + 'inputsInline': true, + 'mutator': 'text_charAt_mutator', + }, +]); + +/** Type of a 'text_get_substring' block. */ +type GetSubstringBlock = Block & GetSubstringMixin; +interface GetSubstringMixin extends GetSubstringType { + WHERE_OPTIONS_1: Array<[string, string]>; + WHERE_OPTIONS_2: Array<[string, string]>; +} +type GetSubstringType = typeof GET_SUBSTRING_BLOCK; + +const GET_SUBSTRING_BLOCK = { + /** + * Block for getting substring. + */ + init: function (this: GetSubstringBlock) { + this['WHERE_OPTIONS_1'] = [ + [Msg['TEXT_GET_SUBSTRING_START_FROM_START'], 'FROM_START'], + [Msg['TEXT_GET_SUBSTRING_START_FROM_END'], 'FROM_END'], + [Msg['TEXT_GET_SUBSTRING_START_FIRST'], 'FIRST'], + ]; + this['WHERE_OPTIONS_2'] = [ + [Msg['TEXT_GET_SUBSTRING_END_FROM_START'], 'FROM_START'], + [Msg['TEXT_GET_SUBSTRING_END_FROM_END'], 'FROM_END'], + [Msg['TEXT_GET_SUBSTRING_END_LAST'], 'LAST'], + ]; + this.setHelpUrl(Msg['TEXT_GET_SUBSTRING_HELPURL']); + this.setStyle('text_blocks'); + this.appendValueInput('STRING') + .setCheck('String') + .appendField(Msg['TEXT_GET_SUBSTRING_INPUT_IN_TEXT']); + this.appendDummyInput('AT1'); + this.appendDummyInput('AT2'); + if (Msg['TEXT_GET_SUBSTRING_TAIL']) { + this.appendDummyInput('TAIL').appendField(Msg['TEXT_GET_SUBSTRING_TAIL']); + } + this.setInputsInline(true); + this.setOutput(true, 'String'); + this.updateAt_(1, true); + this.updateAt_(2, true); + this.setTooltip(Msg['TEXT_GET_SUBSTRING_TOOLTIP']); + }, + /** + * Create XML to represent whether there are 'AT' inputs. + * Backwards compatible serialization implementation. + * + * @returns XML storage element. + */ + mutationToDom: function (this: GetSubstringBlock): Element { + const container = xmlUtils.createElement('mutation'); + const isAt1 = this.getInput('AT1') instanceof ValueInput; + container.setAttribute('at1', `${isAt1}`); + const isAt2 = this.getInput('AT2') instanceof ValueInput; + container.setAttribute('at2', `${isAt2}`); + return container; + }, + /** + * Parse XML to restore the 'AT' inputs. + * Backwards compatible serialization implementation. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: GetSubstringBlock, xmlElement: Element) { + const isAt1 = xmlElement.getAttribute('at1') === 'true'; + const isAt2 = xmlElement.getAttribute('at2') === 'true'; + this.updateAt_(1, isAt1); + this.updateAt_(2, isAt2); + }, + + // This block does not need JSO serialization hooks (saveExtraState and + // loadExtraState) because the state of this object is already encoded in the + // dropdown values. + // XML hooks are kept for backwards compatibility. + + /** + * Create or delete an input for a numeric index. + * This block has two such inputs, independent of each other. + * + * @internal + * @param n Which input to modify (either 1 or 2). + * @param isAt True if the input includes a value connection, false otherwise. + */ + updateAt_: function (this: GetSubstringBlock, n: 1 | 2, isAt: boolean) { + // Create or delete an input for the numeric index. + // Destroy old 'AT' and 'ORDINAL' inputs. + this.removeInput('AT' + n); + this.removeInput('ORDINAL' + n, true); + // Create either a value 'AT' input or a dummy input. + if (isAt) { + this.appendValueInput('AT' + n).setCheck('Number'); + if (Msg['ORDINAL_NUMBER_SUFFIX']) { + this.appendDummyInput('ORDINAL' + n).appendField( + Msg['ORDINAL_NUMBER_SUFFIX'], + ); + } + } else { + this.appendDummyInput('AT' + n); + } + // Move tail, if present, to end of block. + if (n === 2 && Msg['TEXT_GET_SUBSTRING_TAIL']) { + this.removeInput('TAIL', true); + this.appendDummyInput('TAIL').appendField(Msg['TEXT_GET_SUBSTRING_TAIL']); + } + const menu = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: + this[('WHERE_OPTIONS_' + n) as 'WHERE_OPTIONS_1' | 'WHERE_OPTIONS_2'], + }) as FieldDropdown; + menu.setValidator( + /** + * @param value The input value. + * @returns Null if the field has been replaced; otherwise undefined. + */ + function (this: FieldDropdown, value: any): null | undefined { + const newAt = value === 'FROM_START' || value === 'FROM_END'; + // The 'isAt' variable is available due to this function being a + // closure. + if (newAt !== isAt) { + const block = this.getSourceBlock() as GetSubstringBlock; + block.updateAt_(n, newAt); + // This menu has been destroyed and replaced. + // Update the replacement. + block.setFieldValue(value, 'WHERE' + n); + return null; + } + return undefined; + }, + ); + + this.getInput('AT' + n)!.appendField(menu, 'WHERE' + n); + if (n === 1) { + this.moveInputBefore('AT1', 'AT2'); + if (this.getInput('ORDINAL1')) { + this.moveInputBefore('ORDINAL1', 'AT2'); + } + } + }, +}; + +blocks['text_getSubstring'] = GET_SUBSTRING_BLOCK; + +blocks['text_changeCase'] = { + /** + * Block for changing capitalization. + */ + init: function (this: Block) { + const OPERATORS = [ + [Msg['TEXT_CHANGECASE_OPERATOR_UPPERCASE'], 'UPPERCASE'], + [Msg['TEXT_CHANGECASE_OPERATOR_LOWERCASE'], 'LOWERCASE'], + [Msg['TEXT_CHANGECASE_OPERATOR_TITLECASE'], 'TITLECASE'], + ]; + this.setHelpUrl(Msg['TEXT_CHANGECASE_HELPURL']); + this.setStyle('text_blocks'); + this.appendValueInput('TEXT') + .setCheck('String') + .appendField( + fieldRegistry.fromJson({ + type: 'field_dropdown', + options: OPERATORS, + }) as FieldDropdown, + 'CASE', + ); + this.setOutput(true, 'String'); + this.setTooltip(Msg['TEXT_CHANGECASE_TOOLTIP']); + }, +}; + +blocks['text_trim'] = { + /** + * Block for trimming spaces. + */ + init: function (this: Block) { + const OPERATORS = [ + [Msg['TEXT_TRIM_OPERATOR_BOTH'], 'BOTH'], + [Msg['TEXT_TRIM_OPERATOR_LEFT'], 'LEFT'], + [Msg['TEXT_TRIM_OPERATOR_RIGHT'], 'RIGHT'], + ]; + this.setHelpUrl(Msg['TEXT_TRIM_HELPURL']); + this.setStyle('text_blocks'); + this.appendValueInput('TEXT') + .setCheck('String') + .appendField( + fieldRegistry.fromJson({ + type: 'field_dropdown', + options: OPERATORS, + }) as FieldDropdown, + 'MODE', + ); + this.setOutput(true, 'String'); + this.setTooltip(Msg['TEXT_TRIM_TOOLTIP']); + }, +}; + +blocks['text_print'] = { + /** + * Block for print statement. + */ + init: function (this: Block) { + this.jsonInit({ + 'message0': Msg['TEXT_PRINT_TITLE'], + 'args0': [ + { + 'type': 'input_value', + 'name': 'TEXT', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'style': 'text_blocks', + 'tooltip': Msg['TEXT_PRINT_TOOLTIP'], + 'helpUrl': Msg['TEXT_PRINT_HELPURL'], + }); + }, +}; + +type PromptCommonBlock = Block & PromptCommonMixin; +interface PromptCommonMixin extends PromptCommonType {} +type PromptCommonType = typeof PROMPT_COMMON; + +/** + * Common properties for the text_prompt_ext and text_prompt blocks + * definitions. + */ +const PROMPT_COMMON = { + /** + * Modify this block to have the correct output type. + * + * @internal + * @param newOp The new output type. Should be either 'TEXT' or 'NUMBER'. + */ + updateType_: function (this: PromptCommonBlock, newOp: string) { + this.outputConnection!.setCheck(newOp === 'NUMBER' ? 'Number' : 'String'); + }, + /** + * Create XML to represent the output type. + * Backwards compatible serialization implementation. + * + * @returns XML storage element. + */ + mutationToDom: function (this: PromptCommonBlock): Element { + const container = xmlUtils.createElement('mutation'); + container.setAttribute('type', this.getFieldValue('TYPE')); + return container; + }, + /** + * Parse XML to restore the output type. + * Backwards compatible serialization implementation. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: PromptCommonBlock, xmlElement: Element) { + this.updateType_(xmlElement.getAttribute('type')!); + }, +}; + +blocks['text_prompt_ext'] = { + ...PROMPT_COMMON, + /** + * Block for prompt function (external message). + */ + init: function (this: PromptCommonBlock) { + const TYPES = [ + [Msg['TEXT_PROMPT_TYPE_TEXT'], 'TEXT'], + [Msg['TEXT_PROMPT_TYPE_NUMBER'], 'NUMBER'], + ]; + this.setHelpUrl(Msg['TEXT_PROMPT_HELPURL']); + this.setStyle('text_blocks'); + const dropdown = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: TYPES, + }) as FieldDropdown; + dropdown.setValidator((newOp: string) => { + this.updateType_(newOp); + return undefined; // FieldValidators can't be void. Use option as-is. + }); + this.appendValueInput('TEXT').appendField(dropdown, 'TYPE'); + this.setOutput(true, 'String'); + this.setTooltip(() => { + return this.getFieldValue('TYPE') === 'TEXT' + ? Msg['TEXT_PROMPT_TOOLTIP_TEXT'] + : Msg['TEXT_PROMPT_TOOLTIP_NUMBER']; + }); + }, + + // This block does not need JSO serialization hooks (saveExtraState and + // loadExtraState) because the state of this object is already encoded in the + // dropdown values. + // XML hooks are kept for backwards compatibility. +}; + +type PromptBlock = Block & PromptCommonMixin & QuoteImageMixin; + +const TEXT_PROMPT_BLOCK = { + ...PROMPT_COMMON, + /** + * Block for prompt function (internal message). + * The 'text_prompt_ext' block is preferred as it is more flexible. + */ + init: function (this: PromptBlock) { + this.mixin(QUOTE_IMAGE_MIXIN); + const TYPES = [ + [Msg['TEXT_PROMPT_TYPE_TEXT'], 'TEXT'], + [Msg['TEXT_PROMPT_TYPE_NUMBER'], 'NUMBER'], + ]; + + this.setHelpUrl(Msg['TEXT_PROMPT_HELPURL']); + this.setStyle('text_blocks'); + const dropdown = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: TYPES, + }) as FieldDropdown; + dropdown.setValidator((newOp: string) => { + this.updateType_(newOp); + return undefined; // FieldValidators can't be void. Use option as-is. + }); + this.appendDummyInput() + .appendField(dropdown, 'TYPE') + .appendField(this.newQuote_(true)) + .appendField( + fieldRegistry.fromJson({ + type: 'field_input', + text: '', + }) as FieldTextInput, + 'TEXT', + ) + .appendField(this.newQuote_(false)); + this.setOutput(true, 'String'); + this.setTooltip(() => { + return this.getFieldValue('TYPE') === 'TEXT' + ? Msg['TEXT_PROMPT_TOOLTIP_TEXT'] + : Msg['TEXT_PROMPT_TOOLTIP_NUMBER']; + }); + }, +}; + +blocks['text_prompt'] = TEXT_PROMPT_BLOCK; + +blocks['text_count'] = { + /** + * Block for counting how many times one string appears within another string. + */ + init: function (this: Block) { + this.jsonInit({ + 'message0': Msg['TEXT_COUNT_MESSAGE0'], + 'args0': [ + { + 'type': 'input_value', + 'name': 'SUB', + 'check': 'String', + }, + { + 'type': 'input_value', + 'name': 'TEXT', + 'check': 'String', + }, + ], + 'output': 'Number', + 'inputsInline': true, + 'style': 'text_blocks', + 'tooltip': Msg['TEXT_COUNT_TOOLTIP'], + 'helpUrl': Msg['TEXT_COUNT_HELPURL'], + }); + }, +}; + +blocks['text_replace'] = { + /** + * Block for replacing one string with another in the text. + */ + init: function (this: Block) { + this.jsonInit({ + 'message0': Msg['TEXT_REPLACE_MESSAGE0'], + 'args0': [ + { + 'type': 'input_value', + 'name': 'FROM', + 'check': 'String', + }, + { + 'type': 'input_value', + 'name': 'TO', + 'check': 'String', + }, + { + 'type': 'input_value', + 'name': 'TEXT', + 'check': 'String', + }, + ], + 'output': 'String', + 'inputsInline': true, + 'style': 'text_blocks', + 'tooltip': Msg['TEXT_REPLACE_TOOLTIP'], + 'helpUrl': Msg['TEXT_REPLACE_HELPURL'], + }); + }, +}; + +blocks['text_reverse'] = { + /** + * Block for reversing a string. + */ + init: function (this: Block) { + this.jsonInit({ + 'message0': Msg['TEXT_REVERSE_MESSAGE0'], + 'args0': [ + { + 'type': 'input_value', + 'name': 'TEXT', + 'check': 'String', + }, + ], + 'output': 'String', + 'inputsInline': true, + 'style': 'text_blocks', + 'tooltip': Msg['TEXT_REVERSE_TOOLTIP'], + 'helpUrl': Msg['TEXT_REVERSE_HELPURL'], + }); + }, +}; + +/** Type of a block that has QUOTE_IMAGE_MIXIN */ +type QuoteImageBlock = Block & QuoteImageMixin; +interface QuoteImageMixin extends QuoteImageMixinType {} +type QuoteImageMixinType = typeof QUOTE_IMAGE_MIXIN; + +const QUOTE_IMAGE_MIXIN = { + /** + * Image data URI of an LTR opening double quote (same as RTL closing double + * quote). + */ + QUOTE_IMAGE_LEFT_DATAURI: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAA' + + 'n0lEQVQI1z3OMa5BURSF4f/cQhAKjUQhuQmFNwGJEUi0RKN5rU7FHKhpjEH3TEMtkdBSCY' + + '1EIv8r7nFX9e29V7EBAOvu7RPjwmWGH/VuF8CyN9/OAdvqIXYLvtRaNjx9mMTDyo+NjAN1' + + 'HNcl9ZQ5oQMM3dgDUqDo1l8DzvwmtZN7mnD+PkmLa+4mhrxVA9fRowBWmVBhFy5gYEjKMf' + + 'z9AylsaRRgGzvZAAAAAElFTkSuQmCC', + /** + * Image data URI of an LTR closing double quote (same as RTL opening double + * quote). + */ + QUOTE_IMAGE_RIGHT_DATAURI: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAA' + + 'qUlEQVQI1z3KvUpCcRiA8ef9E4JNHhI0aFEacm1o0BsI0Slx8wa8gLauoDnoBhq7DcfWhg' + + 'gONDmJJgqCPA7neJ7p934EOOKOnM8Q7PDElo/4x4lFb2DmuUjcUzS3URnGib9qaPNbuXvB' + + 'O3sGPHJDRG6fGVdMSeWDP2q99FQdFrz26Gu5Tq7dFMzUvbXy8KXeAj57cOklgA+u1B5Aos' + + 'lLtGIHQMaCVnwDnADZIFIrXsoXrgAAAABJRU5ErkJggg==', + /** + * Pixel width of QUOTE_IMAGE_LEFT_DATAURI and QUOTE_IMAGE_RIGHT_DATAURI. + */ + QUOTE_IMAGE_WIDTH: 12, + /** + * Pixel height of QUOTE_IMAGE_LEFT_DATAURI and QUOTE_IMAGE_RIGHT_DATAURI. + */ + QUOTE_IMAGE_HEIGHT: 12, + + /** + * Inserts appropriate quote images before and after the named field. + * + * @param fieldName The name of the field to wrap with quotes. + */ + quoteField_: function (this: QuoteImageBlock, fieldName: string) { + for (let i = 0, input; (input = this.inputList[i]); i++) { + for (let j = 0, field; (field = input.fieldRow[j]); j++) { + if (fieldName === field.name) { + input.insertFieldAt(j, this.newQuote_(true)); + input.insertFieldAt(j + 2, this.newQuote_(false)); + return; + } + } + } + console.warn( + 'field named "' + fieldName + '" not found in ' + this.toDevString(), + ); + }, + + /** + * A helper function that generates a FieldImage of an opening or + * closing double quote. The selected quote will be adapted for RTL blocks. + * + * @param open If the image should be open quote (“ in LTR). + * Otherwise, a closing quote is used (” in LTR). + * @returns The new field. + */ + newQuote_: function (this: QuoteImageBlock, open: boolean): FieldImage { + const isLeft = this.RTL ? !open : open; + const dataUri = isLeft + ? this.QUOTE_IMAGE_LEFT_DATAURI + : this.QUOTE_IMAGE_RIGHT_DATAURI; + return fieldRegistry.fromJson({ + type: 'field_image', + src: dataUri, + width: this.QUOTE_IMAGE_WIDTH, + height: this.QUOTE_IMAGE_HEIGHT, + alt: isLeft ? '\u201C' : '\u201D', + }) as FieldImage; + }, +}; + +/** + * Wraps TEXT field with images of double quote characters. + */ +const QUOTES_EXTENSION = function (this: QuoteImageBlock) { + this.mixin(QUOTE_IMAGE_MIXIN); + this.quoteField_('TEXT'); +}; + +/** + * Type of a block that has TEXT_JOIN_MUTATOR_MIXIN + * + * @internal + */ +export type JoinMutatorBlock = BlockSvg & JoinMutatorMixin & QuoteImageMixin; +interface JoinMutatorMixin extends JoinMutatorMixinType {} +type JoinMutatorMixinType = typeof JOIN_MUTATOR_MIXIN; + +/** Type of a item block in the text_join_mutator bubble. */ +type JoinItemBlock = BlockSvg & JoinItemMixin; +interface JoinItemMixin { + valueConnection_: Connection | null; +} + +/** + * Mixin for mutator functions in the 'text_join_mutator' extension. + */ +const JOIN_MUTATOR_MIXIN = { + itemCount_: 0, + /** + * Create XML to represent number of text inputs. + * Backwards compatible serialization implementation. + * + * @returns XML storage element. + */ + mutationToDom: function (this: JoinMutatorBlock): Element { + const container = xmlUtils.createElement('mutation'); + container.setAttribute('items', `${this.itemCount_}`); + return container; + }, + /** + * Parse XML to restore the text inputs. + * Backwards compatible serialization implementation. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: JoinMutatorBlock, xmlElement: Element) { + this.itemCount_ = parseInt(xmlElement.getAttribute('items')!, 10); + this.updateShape_(); + }, + /** + * Returns the state of this block as a JSON serializable object. + * + * @returns The state of this block, ie the item count. + */ + saveExtraState: function (this: JoinMutatorBlock): {itemCount: number} { + return { + 'itemCount': this.itemCount_, + }; + }, + /** + * Applies the given state to this block. + * + * @param state The state to apply to this block, ie the item count. + */ + loadExtraState: function (this: JoinMutatorBlock, state: {[x: string]: any}) { + this.itemCount_ = state['itemCount']; + this.updateShape_(); + }, + /** + * Populate the mutator's dialog with this block's components. + * + * @param workspace Mutator's workspace. + * @returns Root block in mutator. + */ + decompose: function (this: JoinMutatorBlock, workspace: Workspace): Block { + const containerBlock = workspace.newBlock( + 'text_create_join_container', + ) as BlockSvg; + containerBlock.initSvg(); + let connection = containerBlock.getInput('STACK')!.connection!; + for (let i = 0; i < this.itemCount_; i++) { + const itemBlock = workspace.newBlock( + 'text_create_join_item', + ) as JoinItemBlock; + itemBlock.initSvg(); + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * + * @param containerBlock Root block in mutator. + */ + compose: function (this: JoinMutatorBlock, containerBlock: Block) { + let itemBlock = containerBlock.getInputTargetBlock( + 'STACK', + ) as JoinItemBlock; + // Count number of inputs. + const connections = []; + while (itemBlock) { + if (itemBlock.isInsertionMarker()) { + itemBlock = itemBlock.getNextBlock() as JoinItemBlock; + continue; + } + connections.push(itemBlock.valueConnection_); + itemBlock = itemBlock.getNextBlock() as JoinItemBlock; + } + // Disconnect any children that don't belong. + for (let i = 0; i < this.itemCount_; i++) { + const connection = this.getInput('ADD' + i)!.connection!.targetConnection; + if (connection && !connections.includes(connection)) { + connection.disconnect(); + } + } + this.itemCount_ = connections.length; + this.updateShape_(); + // Reconnect any child blocks. + for (let i = 0; i < this.itemCount_; i++) { + connections[i]?.reconnect(this, 'ADD' + i); + } + }, + /** + * Store pointers to any connected child blocks. + * + * @param containerBlock Root block in mutator. + */ + saveConnections: function (this: JoinMutatorBlock, containerBlock: Block) { + let itemBlock = containerBlock.getInputTargetBlock('STACK'); + let i = 0; + while (itemBlock) { + if (itemBlock.isInsertionMarker()) { + itemBlock = itemBlock.getNextBlock(); + continue; + } + const input = this.getInput('ADD' + i); + (itemBlock as JoinItemBlock).valueConnection_ = + input && input.connection!.targetConnection; + itemBlock = itemBlock.getNextBlock(); + i++; + } + }, + /** + * Modify this block to have the correct number of inputs. + * + */ + updateShape_: function (this: JoinMutatorBlock) { + if (this.itemCount_ && this.getInput('EMPTY')) { + this.removeInput('EMPTY'); + } else if (!this.itemCount_ && !this.getInput('EMPTY')) { + this.appendDummyInput('EMPTY') + .appendField(this.newQuote_(true)) + .appendField(this.newQuote_(false)); + } + // Add new inputs. + for (let i = 0; i < this.itemCount_; i++) { + if (!this.getInput('ADD' + i)) { + const input = this.appendValueInput('ADD' + i).setAlign(Align.RIGHT); + if (i === 0) { + input.appendField(Msg['TEXT_JOIN_TITLE_CREATEWITH']); + } + } + } + // Remove deleted inputs. + for (let i = this.itemCount_; this.getInput('ADD' + i); i++) { + this.removeInput('ADD' + i); + } + }, +}; + +/** + * Performs final setup of a text_join block. + */ +const JOIN_EXTENSION = function (this: JoinMutatorBlock) { + // Add the quote mixin for the itemCount_ = 0 case. + this.mixin(QUOTE_IMAGE_MIXIN); + // Initialize the mutator values. + this.itemCount_ = 2; + this.updateShape_(); + // Configure the mutator UI. + this.setMutator(new MutatorIcon(['text_create_join_item'], this)); +}; + +// Update the tooltip of 'text_append' block to reference the variable. +Extensions.register( + 'text_append_tooltip', + Extensions.buildTooltipWithFieldText('%{BKY_TEXT_APPEND_TOOLTIP}', 'VAR'), +); + +/** + * Update the tooltip of 'text_append' block to reference the variable. + */ +const INDEXOF_TOOLTIP_EXTENSION = function (this: Block) { + this.setTooltip(() => { + return Msg['TEXT_INDEXOF_TOOLTIP'].replace( + '%1', + this.workspace.options.oneBasedIndex ? '0' : '-1', + ); + }); +}; + +/** Type of a block that has TEXT_CHARAT_MUTATOR_MIXIN */ +type CharAtBlock = Block & CharAtMixin; +interface CharAtMixin extends CharAtMixinType {} +type CharAtMixinType = typeof CHARAT_MUTATOR_MIXIN; + +/** + * Mixin for mutator functions in the 'text_charAt_mutator' extension. + */ +const CHARAT_MUTATOR_MIXIN = { + isAt_: false, + /** + * Create XML to represent whether there is an 'AT' input. + * Backwards compatible serialization implementation. + * + * @returns XML storage element. + */ + mutationToDom: function (this: CharAtBlock): Element { + const container = xmlUtils.createElement('mutation'); + container.setAttribute('at', `${this.isAt_}`); + return container; + }, + /** + * Parse XML to restore the 'AT' input. + * Backwards compatible serialization implementation. + * + * @param xmlElement XML storage element. + */ + domToMutation: function (this: CharAtBlock, xmlElement: Element) { + // Note: Until January 2013 this block did not have mutations, + // so 'at' defaults to true. + const isAt = xmlElement.getAttribute('at') !== 'false'; + this.updateAt_(isAt); + }, + + // This block does not need JSO serialization hooks (saveExtraState and + // loadExtraState) because the state of this object is already encoded in the + // dropdown values. + // XML hooks are kept for backwards compatibility. + + /** + * Create or delete an input for the numeric index. + * + * @internal + * @param isAt True if the input should exist. + */ + updateAt_: function (this: CharAtBlock, isAt: boolean) { + // Destroy old 'AT' and 'ORDINAL' inputs. + this.removeInput('AT', true); + this.removeInput('ORDINAL', true); + // Create either a value 'AT' input or a dummy input. + if (isAt) { + this.appendValueInput('AT').setCheck('Number'); + if (Msg['ORDINAL_NUMBER_SUFFIX']) { + this.appendDummyInput('ORDINAL').appendField( + Msg['ORDINAL_NUMBER_SUFFIX'], + ); + } + } + if (Msg['TEXT_CHARAT_TAIL']) { + this.removeInput('TAIL', true); + this.appendDummyInput('TAIL').appendField(Msg['TEXT_CHARAT_TAIL']); + } + + this.isAt_ = isAt; + }, +}; + +/** + * Does the initial mutator update of text_charAt and adds the tooltip + */ +const CHARAT_EXTENSION = function (this: CharAtBlock) { + const dropdown = this.getField('WHERE') as FieldDropdown; + dropdown.setValidator(function (this: FieldDropdown, value: any) { + const newAt = value === 'FROM_START' || value === 'FROM_END'; + const block = this.getSourceBlock() as CharAtBlock; + if (newAt !== block.isAt_) { + block.updateAt_(newAt); + } + return undefined; // FieldValidators can't be void. Use option as-is. + }); + this.updateAt_(true); + this.setTooltip(() => { + const where = this.getFieldValue('WHERE'); + let tooltip = Msg['TEXT_CHARAT_TOOLTIP']; + if (where === 'FROM_START' || where === 'FROM_END') { + const msg = + where === 'FROM_START' + ? Msg['LISTS_INDEX_FROM_START_TOOLTIP'] + : Msg['LISTS_INDEX_FROM_END_TOOLTIP']; + if (msg) { + tooltip += + ' ' + + msg.replace('%1', this.workspace.options.oneBasedIndex ? '#1' : '#0'); + } + } + return tooltip; + }); +}; + +Extensions.register('text_indexOf_tooltip', INDEXOF_TOOLTIP_EXTENSION); + +Extensions.register('text_quotes', QUOTES_EXTENSION); + +Extensions.registerMixin('quote_image_mixin', QUOTE_IMAGE_MIXIN); + +Extensions.registerMutator( + 'text_join_mutator', + JOIN_MUTATOR_MIXIN, + JOIN_EXTENSION, +); + +Extensions.registerMutator( + 'text_charAt_mutator', + CHARAT_MUTATOR_MIXIN, + CHARAT_EXTENSION, +); + +// Register provided blocks. +defineBlocks(blocks); diff --git a/blocks/variables.js b/blocks/variables.js deleted file mode 100644 index 65967f921b1..00000000000 --- a/blocks/variables.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Variable blocks for Blockly. - - * This file is scraped to extract a .json file of block definitions. The array - * passed to defineBlocksWithJsonArray(..) must be strict JSON: double quotes - * only, no outside references, no functions, no trailing commas, etc. The one - * exception is end-of-line comments, which the scraper will remove. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Blocks.variables'); // Deprecated. -goog.provide('Blockly.Constants.Variables'); - -goog.require('Blockly'); -goog.require('Blockly.Blocks'); -goog.require('Blockly.FieldLabel'); -goog.require('Blockly.FieldVariable'); - - -/** - * Unused constant for the common HSV hue for all blocks in this category. - * @deprecated Use Blockly.Msg['VARIABLES_HUE']. (2018 April 5) - */ -Blockly.Constants.Variables.HUE = 330; - -Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT - // Block for variable getter. - { - "type": "variables_get", - "message0": "%1", - "args0": [ - { - "type": "field_variable", - "name": "VAR", - "variable": "%{BKY_VARIABLES_DEFAULT_NAME}" - } - ], - "output": null, - "style": "variable_blocks", - "helpUrl": "%{BKY_VARIABLES_GET_HELPURL}", - "tooltip": "%{BKY_VARIABLES_GET_TOOLTIP}", - "extensions": ["contextMenu_variableSetterGetter"] - }, - // Block for variable setter. - { - "type": "variables_set", - "message0": "%{BKY_VARIABLES_SET}", - "args0": [ - { - "type": "field_variable", - "name": "VAR", - "variable": "%{BKY_VARIABLES_DEFAULT_NAME}" - }, - { - "type": "input_value", - "name": "VALUE" - } - ], - "previousStatement": null, - "nextStatement": null, - "style": "variable_blocks", - "tooltip": "%{BKY_VARIABLES_SET_TOOLTIP}", - "helpUrl": "%{BKY_VARIABLES_SET_HELPURL}", - "extensions": ["contextMenu_variableSetterGetter"] - } -]); // END JSON EXTRACT (Do not delete this comment.) - -/** - * 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 Blockly.Block - * @package - * @readonly - */ -Blockly.Constants.Variables.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 {Blockly.Block} - */ - customContextMenu: function(options) { - if (!this.isInFlyout) { - // Getter blocks have the option to create a setter block, and vice versa. - if (this.type == 'variables_get') { - var opposite_type = 'variables_set'; - var contextMenuMsg = Blockly.Msg['VARIABLES_GET_CREATE_SET']; - } else { - var opposite_type = 'variables_get'; - var contextMenuMsg = Blockly.Msg['VARIABLES_SET_CREATE_GET']; - } - - var option = {enabled: this.workspace.remainingCapacity() > 0}; - var name = this.getField('VAR').getText(); - option.text = contextMenuMsg.replace('%1', name); - var xmlField = Blockly.utils.xml.createElement('field'); - xmlField.setAttribute('name', 'VAR'); - xmlField.appendChild(Blockly.utils.xml.createTextNode(name)); - var xmlBlock = Blockly.utils.xml.createElement('block'); - xmlBlock.setAttribute('type', opposite_type); - xmlBlock.appendChild(xmlField); - option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); - options.push(option); - // Getter blocks have the option to rename or delete that variable. - } else { - if (this.type == 'variables_get' || this.type == 'variables_get_reporter') { - var renameOption = { - text: Blockly.Msg.RENAME_VARIABLE, - enabled: true, - callback: Blockly.Constants.Variables.RENAME_OPTION_CALLBACK_FACTORY(this) - }; - var name = this.getField('VAR').getText(); - var deleteOption = { - text: Blockly.Msg.DELETE_VARIABLE.replace('%1', name), - enabled: true, - callback: Blockly.Constants.Variables.DELETE_OPTION_CALLBACK_FACTORY(this) - }; - options.unshift(renameOption); - options.unshift(deleteOption); - } - } - } -}; - -/** - * Callback for rename variable dropdown menu option associated with a - * variable getter block. - * @param {!Blockly.Block} block The block with the variable to rename. - * @return {!function()} A function that renames the variable. - */ -Blockly.Constants.Variables.RENAME_OPTION_CALLBACK_FACTORY = function(block) { - return function() { - var workspace = block.workspace; - var variable = block.getField('VAR').getVariable(); - Blockly.Variables.renameVariable(workspace, variable); - }; -}; - -/** - * Callback for delete variable dropdown menu option associated with a - * variable getter block. - * @param {!Blockly.Block} block The block with the variable to delete. - * @return {!function()} A function that deletes the variable. - */ -Blockly.Constants.Variables.DELETE_OPTION_CALLBACK_FACTORY = function(block) { - return function() { - var workspace = block.workspace; - var variable = block.getField('VAR').getVariable(); - workspace.deleteVariableById(variable.getId()); - workspace.refreshToolboxSelection(); - }; -}; - -Blockly.Extensions.registerMixin('contextMenu_variableSetterGetter', - Blockly.Constants.Variables.CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN); diff --git a/blocks/variables.ts b/blocks/variables.ts new file mode 100644 index 00000000000..8ac038fb2ce --- /dev/null +++ b/blocks/variables.ts @@ -0,0 +1,182 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.libraryBlocks.variables + +import * as ContextMenu from '../core/contextmenu.js'; +import * as Extensions from '../core/extensions.js'; +import * as Variables from '../core/variables.js'; +import 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. + */ +export const blocks = createBlockDefinitionsFromJsonArray([ + // Block for variable getter. + { + 'type': 'variables_get', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_variable', + 'name': 'VAR', + 'variable': '%{BKY_VARIABLES_DEFAULT_NAME}', + }, + ], + 'output': null, + 'style': 'variable_blocks', + 'helpUrl': '%{BKY_VARIABLES_GET_HELPURL}', + 'tooltip': '%{BKY_VARIABLES_GET_TOOLTIP}', + 'extensions': ['contextMenu_variableSetterGetter'], + }, + // Block for variable setter. + { + 'type': 'variables_set', + 'message0': '%{BKY_VARIABLES_SET}', + 'args0': [ + { + 'type': 'field_variable', + 'name': 'VAR', + 'variable': '%{BKY_VARIABLES_DEFAULT_NAME}', + }, + { + 'type': 'input_value', + 'name': 'VALUE', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'style': 'variable_blocks', + 'tooltip': '%{BKY_VARIABLES_SET_TOOLTIP}', + 'helpUrl': '%{BKY_VARIABLES_SET_HELPURL}', + 'extensions': ['contextMenu_variableSetterGetter'], + }, +]); + +/** 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'. + */ +const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = { + /** + * Add menu option to create getter/setter block for this setter/getter. + * + * @param options List of menu options to add to. + */ + customContextMenu: function ( + this: VariableBlock, + options: Array, + ) { + if (!this.isInFlyout) { + let oppositeType; + let contextMenuMsg; + // Getter blocks have the option to create a setter block, and vice versa. + if (this.type === 'variables_get') { + oppositeType = 'variables_set'; + contextMenuMsg = Msg['VARIABLES_GET_CREATE_SET']; + } else { + oppositeType = 'variables_get'; + contextMenuMsg = Msg['VARIABLES_SET_CREATE_GET']; + } + + const varField = this.getField('VAR')!; + const newVarBlockState = { + type: oppositeType, + fields: {VAR: varField.saveState(true)}, + }; + + options.push({ + enabled: this.workspace.remainingCapacity() > 0, + text: contextMenuMsg.replace('%1', varField.getText()), + callback: ContextMenu.callbackFactory(this, newVarBlockState), + }); + // Getter blocks have the option to rename or delete that variable. + } else { + 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 deleteOption = { + text: Msg['DELETE_VARIABLE'].replace('%1', name), + enabled: true, + callback: deleteOptionCallbackFactory(this), + }; + options.unshift(renameOption); + options.unshift(deleteOption); + } + } + }, +}; + +/** + * Factory for callbacks for rename variable dropdown menu option + * associated with a variable getter block. + * + * @param block The block with the variable to rename. + * @returns A function that renames the variable. + */ +const renameOptionCallbackFactory = function ( + block: VariableBlock, +): () => void { + return function () { + const workspace = block.workspace; + const variableField = block.getField('VAR') as FieldVariable; + const variable = variableField.getVariable()!; + Variables.renameVariable(workspace, variable); + }; +}; + +/** + * Factory for callbacks for delete variable dropdown menu option + * associated with a variable getter block. + * + * @param block The block with the variable to delete. + * @returns A function that deletes the variable. + */ +const deleteOptionCallbackFactory = function ( + block: VariableBlock, +): () => void { + return function () { + const workspace = block.workspace; + const variableField = block.getField('VAR') as FieldVariable; + const variable = variableField.getVariable()!; + workspace.deleteVariableById(variable.getId()); + (workspace as WorkspaceSvg).refreshToolboxSelection(); + }; +}; + +Extensions.registerMixin( + 'contextMenu_variableSetterGetter', + CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN, +); + +// Register provided blocks. +defineBlocks(blocks); diff --git a/blocks/variables_dynamic.js b/blocks/variables_dynamic.js deleted file mode 100644 index de9a364fdd3..00000000000 --- a/blocks/variables_dynamic.js +++ /dev/null @@ -1,180 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Variable blocks for Blockly. - - * This file is scraped to extract a .json file of block definitions. The array - * passed to defineBlocksWithJsonArray(..) must be strict JSON: double quotes - * only, no outside references, no functions, no trailing commas, etc. The one - * exception is end-of-line comments, which the scraper will remove. - * @author duzc2dtw@gmail.com (Du Tian Wei) - */ -'use strict'; - -goog.provide('Blockly.Constants.VariablesDynamic'); - -goog.require('Blockly'); -goog.require('Blockly.Blocks'); -goog.require('Blockly.FieldLabel'); -goog.require('Blockly.FieldVariable'); - - -/** - * Unused constant for the common HSV hue for all blocks in this category. - * @deprecated Use Blockly.Msg['VARIABLES_DYNAMIC_HUE']. (2018 April 5) - */ -Blockly.Constants.VariablesDynamic.HUE = 310; - -Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT - // Block for variable getter. - { - "type": "variables_get_dynamic", - "message0": "%1", - "args0": [{ - "type": "field_variable", - "name": "VAR", - "variable": "%{BKY_VARIABLES_DEFAULT_NAME}" - }], - "output": null, - "style": "variable_dynamic_blocks", - "helpUrl": "%{BKY_VARIABLES_GET_HELPURL}", - "tooltip": "%{BKY_VARIABLES_GET_TOOLTIP}", - "extensions": ["contextMenu_variableDynamicSetterGetter"] - }, - // Block for variable setter. - { - "type": "variables_set_dynamic", - "message0": "%{BKY_VARIABLES_SET}", - "args0": [{ - "type": "field_variable", - "name": "VAR", - "variable": "%{BKY_VARIABLES_DEFAULT_NAME}" - }, - { - "type": "input_value", - "name": "VALUE" - } - ], - "previousStatement": null, - "nextStatement": null, - "style": "variable_dynamic_blocks", - "tooltip": "%{BKY_VARIABLES_SET_TOOLTIP}", - "helpUrl": "%{BKY_VARIABLES_SET_HELPURL}", - "extensions": ["contextMenu_variableDynamicSetterGetter"] - } -]); // END JSON EXTRACT (Do not delete this comment.) - -/** - * 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 Blockly.Block - * @package - * @readonly - */ -Blockly.Constants.VariablesDynamic.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 {Blockly.Block} - */ - customContextMenu: function(options) { - // Getter blocks have the option to create a setter block, and vice versa. - if (!this.isInFlyout) { - var opposite_type; - var contextMenuMsg; - var id = this.getFieldValue('VAR'); - var variableModel = this.workspace.getVariableById(id); - var varType = variableModel.type; - if (this.type == 'variables_get_dynamic') { - opposite_type = 'variables_set_dynamic'; - contextMenuMsg = Blockly.Msg['VARIABLES_GET_CREATE_SET']; - } else { - opposite_type = 'variables_get_dynamic'; - contextMenuMsg = Blockly.Msg['VARIABLES_SET_CREATE_GET']; - } - - var option = {enabled: this.workspace.remainingCapacity() > 0}; - var name = this.getField('VAR').getText(); - option.text = contextMenuMsg.replace('%1', name); - var xmlField = Blockly.utils.xml.createElement('field'); - xmlField.setAttribute('name', 'VAR'); - xmlField.setAttribute('variabletype', varType); - xmlField.appendChild(Blockly.utils.xml.createTextNode(name)); - var xmlBlock = Blockly.utils.xml.createElement('block'); - xmlBlock.setAttribute('type', opposite_type); - xmlBlock.appendChild(xmlField); - option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); - options.push(option); - } else { - if (this.type == 'variables_get_dynamic' || - this.type == 'variables_get_reporter_dynamic') { - var renameOption = { - text: Blockly.Msg.RENAME_VARIABLE, - enabled: true, - callback: Blockly.Constants.Variables.RENAME_OPTION_CALLBACK_FACTORY(this) - }; - var name = this.getField('VAR').getText(); - var deleteOption = { - text: Blockly.Msg.DELETE_VARIABLE.replace('%1', name), - enabled: true, - callback: Blockly.Constants.Variables.DELETE_OPTION_CALLBACK_FACTORY(this) - }; - options.unshift(renameOption); - options.unshift(deleteOption); - } - } - }, - /** - * Called whenever anything on the workspace changes. - * Set the connection type for this block. - * @param {!Blockly.Events.Abstract} _e Change event. - * @this {Blockly.Block} - */ - onchange: function(_e) { - var id = this.getFieldValue('VAR'); - var variableModel = Blockly.Variables.getVariable(this.workspace, id); - if (this.type == 'variables_get_dynamic') { - this.outputConnection.setCheck(variableModel.type); - } else { - this.getInput('VALUE').connection.setCheck(variableModel.type); - } - } -}; - -/** - * Callback for rename variable dropdown menu option associated with a - * variable getter block. - * @param {!Blockly.Block} block The block with the variable to rename. - * @return {!function()} A function that renames the variable. - */ -Blockly.Constants.VariablesDynamic.RENAME_OPTION_CALLBACK_FACTORY = function(block) { - return function() { - var workspace = block.workspace; - var variable = block.getField('VAR').getVariable(); - Blockly.Variables.renameVariable(workspace, variable); - }; -}; - -/** - * Callback for delete variable dropdown menu option associated with a - * variable getter block. - * @param {!Blockly.Block} block The block with the variable to delete. - * @return {!function()} A function that deletes the variable. - */ -Blockly.Constants.VariablesDynamic.DELETE_OPTION_CALLBACK_FACTORY = function(block) { - return function() { - var workspace = block.workspace; - var variable = block.getField('VAR').getVariable(); - workspace.deleteVariableById(variable.getId()); - workspace.refreshToolboxSelection(); - }; -}; - -Blockly.Extensions.registerMixin('contextMenu_variableDynamicSetterGetter', - Blockly.Constants.VariablesDynamic.CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN); diff --git a/blocks/variables_dynamic.ts b/blocks/variables_dynamic.ts new file mode 100644 index 00000000000..e74cae423ab --- /dev/null +++ b/blocks/variables_dynamic.ts @@ -0,0 +1,193 @@ +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.libraryBlocks.variablesDynamic + +import * as ContextMenu from '../core/contextmenu.js'; +import * as Extensions from '../core/extensions.js'; +import * as Variables from '../core/variables.js'; +import {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. + */ +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}', + }, + ], + 'output': null, + 'style': 'variable_dynamic_blocks', + 'helpUrl': '%{BKY_VARIABLES_GET_HELPURL}', + 'tooltip': '%{BKY_VARIABLES_GET_TOOLTIP}', + 'extensions': ['contextMenu_variableDynamicSetterGetter'], + }, + // Block for variable setter. + { + 'type': 'variables_set_dynamic', + 'message0': '%{BKY_VARIABLES_SET}', + 'args0': [ + { + 'type': 'field_variable', + 'name': 'VAR', + 'variable': '%{BKY_VARIABLES_DEFAULT_NAME}', + }, + { + 'type': 'input_value', + 'name': 'VALUE', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'style': 'variable_dynamic_blocks', + 'tooltip': '%{BKY_VARIABLES_SET_TOOLTIP}', + 'helpUrl': '%{BKY_VARIABLES_SET_HELPURL}', + 'extensions': ['contextMenu_variableDynamicSetterGetter'], + }, +]); + +/** 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'. + */ +const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = { + /** + * Add menu option to create getter/setter block for this setter/getter. + * + * @param options List of menu options to add to. + */ + customContextMenu: function ( + this: VariableBlock, + options: Array, + ) { + // Getter blocks have the option to create a setter block, and vice versa. + if (!this.isInFlyout) { + let oppositeType; + let contextMenuMsg; + if (this.type === 'variables_get_dynamic') { + oppositeType = 'variables_set_dynamic'; + contextMenuMsg = Msg['VARIABLES_GET_CREATE_SET']; + } else { + oppositeType = 'variables_get_dynamic'; + contextMenuMsg = Msg['VARIABLES_SET_CREATE_GET']; + } + + const varField = this.getField('VAR')!; + const newVarBlockState = { + type: oppositeType, + fields: {VAR: varField.saveState(true)}, + }; + + options.push({ + enabled: this.workspace.remainingCapacity() > 0, + text: contextMenuMsg.replace('%1', varField.getText()), + callback: ContextMenu.callbackFactory(this, newVarBlockState), + }); + } else { + 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 deleteOption = { + text: Msg['DELETE_VARIABLE'].replace('%1', name), + enabled: true, + callback: deleteOptionCallbackFactory(this), + }; + options.unshift(renameOption); + options.unshift(deleteOption); + } + } + }, + /** + * Called whenever anything on the workspace changes. + * Set the connection type for this block. + * + * @param _e Change event. + */ + onchange: function (this: VariableBlock, _e: AbstractEvent) { + const id = this.getFieldValue('VAR'); + const variableModel = Variables.getVariable(this.workspace, id)!; + if (this.type === 'variables_get_dynamic') { + this.outputConnection!.setCheck(variableModel.type); + } else { + this.getInput('VALUE')!.connection!.setCheck(variableModel.type); + } + }, +}; + +/** + * Factory for callbacks for rename variable dropdown menu option + * associated with a variable getter block. + * + * @param block The block with the variable to rename. + * @returns A function that renames the variable. + */ +const renameOptionCallbackFactory = function (block: VariableBlock) { + return function () { + const workspace = block.workspace; + const variableField = block.getField('VAR') as FieldVariable; + const variable = variableField.getVariable()!; + Variables.renameVariable(workspace, variable); + }; +}; + +/** + * Factory for callbacks for delete variable dropdown menu option + * associated with a variable getter block. + * + * @param block The block with the variable to delete. + * @returns A function that deletes the variable. + */ +const deleteOptionCallbackFactory = function (block: VariableBlock) { + return function () { + const workspace = block.workspace; + const variableField = block.getField('VAR') as FieldVariable; + const variable = variableField.getVariable()!; + workspace.deleteVariableById(variable.getId()); + (workspace as WorkspaceSvg).refreshToolboxSelection(); + }; +}; + +Extensions.registerMixin( + 'contextMenu_variableDynamicSetterGetter', + CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN, +); + +// Register provided blocks. +defineBlocks(blocks); diff --git a/blocks_compressed.js b/blocks_compressed.js deleted file mode 100644 index f8a8457ee11..00000000000 --- a/blocks_compressed.js +++ /dev/null @@ -1,182 +0,0 @@ -// Do not edit this file; automatically generated by gulp. - -/* eslint-disable */ -;(function(root, factory) { - if (typeof define === 'function' && define.amd) { // AMD - define(['./blockly_compressed.js'], factory); - } else if (typeof exports === 'object') { // Node.js - module.exports = factory(require('./blockly_compressed.js')); - } else { // Browser - root.Blockly.Blocks = factory(root.Blockly); - } -}(this, function(Blockly) { - 'use strict';Blockly.Blocks.colour={};Blockly.Constants={};Blockly.Constants.Colour={};Blockly.Constants.Colour.HUE=20; -Blockly.defineBlocksWithJsonArray([{type:"colour_picker",message0:"%1",args0:[{type:"field_colour",name:"COLOUR",colour:"#ff0000"}],output:"Colour",helpUrl:"%{BKY_COLOUR_PICKER_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_PICKER_TOOLTIP}",extensions:["parent_tooltip_when_inline"]},{type:"colour_random",message0:"%{BKY_COLOUR_RANDOM_TITLE}",output:"Colour",helpUrl:"%{BKY_COLOUR_RANDOM_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_RANDOM_TOOLTIP}"},{type:"colour_rgb",message0:"%{BKY_COLOUR_RGB_TITLE} %{BKY_COLOUR_RGB_RED} %1 %{BKY_COLOUR_RGB_GREEN} %2 %{BKY_COLOUR_RGB_BLUE} %3", -args0:[{type:"input_value",name:"RED",check:"Number",align:"RIGHT"},{type:"input_value",name:"GREEN",check:"Number",align:"RIGHT"},{type:"input_value",name:"BLUE",check:"Number",align:"RIGHT"}],output:"Colour",helpUrl:"%{BKY_COLOUR_RGB_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_RGB_TOOLTIP}"},{type:"colour_blend",message0:"%{BKY_COLOUR_BLEND_TITLE} %{BKY_COLOUR_BLEND_COLOUR1} %1 %{BKY_COLOUR_BLEND_COLOUR2} %2 %{BKY_COLOUR_BLEND_RATIO} %3",args0:[{type:"input_value",name:"COLOUR1",check:"Colour", -align:"RIGHT"},{type:"input_value",name:"COLOUR2",check:"Colour",align:"RIGHT"},{type:"input_value",name:"RATIO",check:"Number",align:"RIGHT"}],output:"Colour",helpUrl:"%{BKY_COLOUR_BLEND_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_BLEND_TOOLTIP}"}]);Blockly.Blocks.lists={};Blockly.Constants.Lists={};Blockly.Constants.Lists.HUE=260; -Blockly.defineBlocksWithJsonArray([{type:"lists_create_empty",message0:"%{BKY_LISTS_CREATE_EMPTY_TITLE}",output:"Array",style:"list_blocks",tooltip:"%{BKY_LISTS_CREATE_EMPTY_TOOLTIP}",helpUrl:"%{BKY_LISTS_CREATE_EMPTY_HELPURL}"},{type:"lists_repeat",message0:"%{BKY_LISTS_REPEAT_TITLE}",args0:[{type:"input_value",name:"ITEM"},{type:"input_value",name:"NUM",check:"Number"}],output:"Array",style:"list_blocks",tooltip:"%{BKY_LISTS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_LISTS_REPEAT_HELPURL}"},{type:"lists_reverse", -message0:"%{BKY_LISTS_REVERSE_MESSAGE0}",args0:[{type:"input_value",name:"LIST",check:"Array"}],output:"Array",inputsInline:!0,style:"list_blocks",tooltip:"%{BKY_LISTS_REVERSE_TOOLTIP}",helpUrl:"%{BKY_LISTS_REVERSE_HELPURL}"},{type:"lists_isEmpty",message0:"%{BKY_LISTS_ISEMPTY_TITLE}",args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Boolean",style:"list_blocks",tooltip:"%{BKY_LISTS_ISEMPTY_TOOLTIP}",helpUrl:"%{BKY_LISTS_ISEMPTY_HELPURL}"},{type:"lists_length",message0:"%{BKY_LISTS_LENGTH_TITLE}", -args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Number",style:"list_blocks",tooltip:"%{BKY_LISTS_LENGTH_TOOLTIP}",helpUrl:"%{BKY_LISTS_LENGTH_HELPURL}"}]); -Blockly.Blocks.lists_create_with={init:function(){this.setHelpUrl(Blockly.Msg.LISTS_CREATE_WITH_HELPURL);this.setStyle("list_blocks");this.itemCount_=3;this.updateShape_();this.setOutput(!0,"Array");this.setMutator(new Blockly.Mutator(["lists_create_with_item"]));this.setTooltip(Blockly.Msg.LISTS_CREATE_WITH_TOOLTIP)},mutationToDom:function(){var a=Blockly.utils.xml.createElement("mutation");a.setAttribute("items",this.itemCount_);return a},domToMutation:function(a){this.itemCount_=parseInt(a.getAttribute("items"), -10);this.updateShape_()},decompose:function(a){var b=a.newBlock("lists_create_with_container");b.initSvg();for(var c=b.getInput("STACK").connection,d=0;d","GT"],["\u200f\u2265","GTE"]]},{type:"input_value",name:"B"}],inputsInline:!0,output:"Boolean",style:"logic_blocks",helpUrl:"%{BKY_LOGIC_COMPARE_HELPURL}",extensions:["logic_compare", -"logic_op_tooltip"]},{type:"logic_operation",message0:"%1 %2 %3",args0:[{type:"input_value",name:"A",check:"Boolean"},{type:"field_dropdown",name:"OP",options:[["%{BKY_LOGIC_OPERATION_AND}","AND"],["%{BKY_LOGIC_OPERATION_OR}","OR"]]},{type:"input_value",name:"B",check:"Boolean"}],inputsInline:!0,output:"Boolean",style:"logic_blocks",helpUrl:"%{BKY_LOGIC_OPERATION_HELPURL}",extensions:["logic_op_tooltip"]},{type:"logic_negate",message0:"%{BKY_LOGIC_NEGATE_TITLE}",args0:[{type:"input_value",name:"BOOL", -check:"Boolean"}],output:"Boolean",style:"logic_blocks",tooltip:"%{BKY_LOGIC_NEGATE_TOOLTIP}",helpUrl:"%{BKY_LOGIC_NEGATE_HELPURL}"},{type:"logic_null",message0:"%{BKY_LOGIC_NULL}",output:null,style:"logic_blocks",tooltip:"%{BKY_LOGIC_NULL_TOOLTIP}",helpUrl:"%{BKY_LOGIC_NULL_HELPURL}"},{type:"logic_ternary",message0:"%{BKY_LOGIC_TERNARY_CONDITION} %1",args0:[{type:"input_value",name:"IF",check:"Boolean"}],message1:"%{BKY_LOGIC_TERNARY_IF_TRUE} %1",args1:[{type:"input_value",name:"THEN"}],message2:"%{BKY_LOGIC_TERNARY_IF_FALSE} %1", -args2:[{type:"input_value",name:"ELSE"}],output:null,style:"logic_blocks",tooltip:"%{BKY_LOGIC_TERNARY_TOOLTIP}",helpUrl:"%{BKY_LOGIC_TERNARY_HELPURL}",extensions:["logic_ternary"]}]); -Blockly.defineBlocksWithJsonArray([{type:"controls_if_if",message0:"%{BKY_CONTROLS_IF_IF_TITLE_IF}",nextStatement:null,enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_IF_TOOLTIP}"},{type:"controls_if_elseif",message0:"%{BKY_CONTROLS_IF_ELSEIF_TITLE_ELSEIF}",previousStatement:null,nextStatement:null,enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_ELSEIF_TOOLTIP}"},{type:"controls_if_else",message0:"%{BKY_CONTROLS_IF_ELSE_TITLE_ELSE}",previousStatement:null, -enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_ELSE_TOOLTIP}"}]);Blockly.Constants.Logic.TOOLTIPS_BY_OP={EQ:"%{BKY_LOGIC_COMPARE_TOOLTIP_EQ}",NEQ:"%{BKY_LOGIC_COMPARE_TOOLTIP_NEQ}",LT:"%{BKY_LOGIC_COMPARE_TOOLTIP_LT}",LTE:"%{BKY_LOGIC_COMPARE_TOOLTIP_LTE}",GT:"%{BKY_LOGIC_COMPARE_TOOLTIP_GT}",GTE:"%{BKY_LOGIC_COMPARE_TOOLTIP_GTE}",AND:"%{BKY_LOGIC_OPERATION_TOOLTIP_AND}",OR:"%{BKY_LOGIC_OPERATION_TOOLTIP_OR}"}; -Blockly.Extensions.register("logic_op_tooltip",Blockly.Extensions.buildTooltipForDropdown("OP",Blockly.Constants.Logic.TOOLTIPS_BY_OP)); -Blockly.Constants.Logic.CONTROLS_IF_MUTATOR_MIXIN={elseifCount_:0,elseCount_:0,suppressPrefixSuffix:!0,mutationToDom:function(){if(!this.elseifCount_&&!this.elseCount_)return null;var a=Blockly.utils.xml.createElement("mutation");this.elseifCount_&&a.setAttribute("elseif",this.elseifCount_);this.elseCount_&&a.setAttribute("else",1);return a},domToMutation:function(a){this.elseifCount_=parseInt(a.getAttribute("elseif"),10)||0;this.elseCount_=parseInt(a.getAttribute("else"),10)||0;this.rebuildShape_()}, -decompose:function(a){var b=a.newBlock("controls_if_if");b.initSvg();for(var c=b.nextConnection,d=1;d<=this.elseifCount_;d++){var e=a.newBlock("controls_if_elseif");e.initSvg();c.connect(e.previousConnection);c=e.nextConnection}this.elseCount_&&(a=a.newBlock("controls_if_else"),a.initSvg(),c.connect(a.previousConnection));return b},compose:function(a){a=a.nextConnection.targetBlock();this.elseCount_=this.elseifCount_=0;for(var b=[null],c=[null],d=null;a;){switch(a.type){case "controls_if_elseif":this.elseifCount_++; -b.push(a.valueConnection_);c.push(a.statementConnection_);break;case "controls_if_else":this.elseCount_++;d=a.statementConnection_;break;default:throw TypeError("Unknown block type: "+a.type);}a=a.nextConnection&&a.nextConnection.targetBlock()}this.updateShape_();this.reconnectChildBlocks_(b,c,d)},saveConnections:function(a){a=a.nextConnection.targetBlock();for(var b=1;a;){switch(a.type){case "controls_if_elseif":var c=this.getInput("IF"+b),d=this.getInput("DO"+b);a.valueConnection_=c&&c.connection.targetConnection; -a.statementConnection_=d&&d.connection.targetConnection;b++;break;case "controls_if_else":d=this.getInput("ELSE");a.statementConnection_=d&&d.connection.targetConnection;break;default:throw TypeError("Unknown block type: "+a.type);}a=a.nextConnection&&a.nextConnection.targetBlock()}},rebuildShape_:function(){var a=[null],b=[null],c=null;this.getInput("ELSE")&&(c=this.getInput("ELSE").connection.targetConnection);for(var d=1;this.getInput("IF"+d);){var e=this.getInput("IF"+d),f=this.getInput("DO"+ -d);a.push(e.connection.targetConnection);b.push(f.connection.targetConnection);d++}this.updateShape_();this.reconnectChildBlocks_(a,b,c)},updateShape_:function(){this.getInput("ELSE")&&this.removeInput("ELSE");for(var a=1;this.getInput("IF"+a);)this.removeInput("IF"+a),this.removeInput("DO"+a),a++;for(a=1;a<=this.elseifCount_;a++)this.appendValueInput("IF"+a).setCheck("Boolean").appendField(Blockly.Msg.CONTROLS_IF_MSG_ELSEIF),this.appendStatementInput("DO"+a).appendField(Blockly.Msg.CONTROLS_IF_MSG_THEN); -this.elseCount_&&this.appendStatementInput("ELSE").appendField(Blockly.Msg.CONTROLS_IF_MSG_ELSE)},reconnectChildBlocks_:function(a,b,c){for(var d=1;d<=this.elseifCount_;d++)Blockly.Mutator.reconnect(a[d],this,"IF"+d),Blockly.Mutator.reconnect(b[d],this,"DO"+d);Blockly.Mutator.reconnect(c,this,"ELSE")}};Blockly.Extensions.registerMutator("controls_if_mutator",Blockly.Constants.Logic.CONTROLS_IF_MUTATOR_MIXIN,null,["controls_if_elseif","controls_if_else"]); -Blockly.Constants.Logic.CONTROLS_IF_TOOLTIP_EXTENSION=function(){this.setTooltip(function(){if(this.elseifCount_||this.elseCount_){if(!this.elseifCount_&&this.elseCount_)return Blockly.Msg.CONTROLS_IF_TOOLTIP_2;if(this.elseifCount_&&!this.elseCount_)return Blockly.Msg.CONTROLS_IF_TOOLTIP_3;if(this.elseifCount_&&this.elseCount_)return Blockly.Msg.CONTROLS_IF_TOOLTIP_4}else return Blockly.Msg.CONTROLS_IF_TOOLTIP_1;return""}.bind(this))};Blockly.Extensions.register("controls_if_tooltip",Blockly.Constants.Logic.CONTROLS_IF_TOOLTIP_EXTENSION); -Blockly.Constants.Logic.LOGIC_COMPARE_ONCHANGE_MIXIN={onchange:function(a){this.prevBlocks_||(this.prevBlocks_=[null,null]);var b=this.getInputTargetBlock("A"),c=this.getInputTargetBlock("B");b&&c&&!this.workspace.connectionChecker.doTypeChecks(b.outputConnection,c.outputConnection)&&(Blockly.Events.setGroup(a.group),a=this.prevBlocks_[0],a!==b&&(b.unplug(),!a||a.isDisposed()||a.isShadow()||this.getInput("A").connection.connect(a.outputConnection)),b=this.prevBlocks_[1],b!==c&&(c.unplug(),!b||b.isDisposed()|| -b.isShadow()||this.getInput("B").connection.connect(b.outputConnection)),this.bumpNeighbours(),Blockly.Events.setGroup(!1));this.prevBlocks_[0]=this.getInputTargetBlock("A");this.prevBlocks_[1]=this.getInputTargetBlock("B")}};Blockly.Constants.Logic.LOGIC_COMPARE_EXTENSION=function(){this.mixin(Blockly.Constants.Logic.LOGIC_COMPARE_ONCHANGE_MIXIN)};Blockly.Extensions.register("logic_compare",Blockly.Constants.Logic.LOGIC_COMPARE_EXTENSION); -Blockly.Constants.Logic.LOGIC_TERNARY_ONCHANGE_MIXIN={prevParentConnection_:null,onchange:function(a){var b=this.getInputTargetBlock("THEN"),c=this.getInputTargetBlock("ELSE"),d=this.outputConnection.targetConnection;if((b||c)&&d)for(var e=0;2>e;e++){var f=1==e?b:c;f&&!f.workspace.connectionChecker.doTypeChecks(f.outputConnection,d)&&(Blockly.Events.setGroup(a.group),d===this.prevParentConnection_?(this.unplug(),d.getSourceBlock().bumpNeighbours()):(f.unplug(),f.bumpNeighbours()),Blockly.Events.setGroup(!1))}this.prevParentConnection_= -d}};Blockly.Extensions.registerMixin("logic_ternary",Blockly.Constants.Logic.LOGIC_TERNARY_ONCHANGE_MIXIN);Blockly.Blocks.loops={};Blockly.Constants.Loops={};Blockly.Constants.Loops.HUE=120; -Blockly.defineBlocksWithJsonArray([{type:"controls_repeat_ext",message0:"%{BKY_CONTROLS_REPEAT_TITLE}",args0:[{type:"input_value",name:"TIMES",check:"Number"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",tooltip:"%{BKY_CONTROLS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_CONTROLS_REPEAT_HELPURL}"},{type:"controls_repeat",message0:"%{BKY_CONTROLS_REPEAT_TITLE}",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"}],previousStatement:null,nextStatement:null,style:"loop_blocks",tooltip:"%{BKY_CONTROLS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_CONTROLS_REPEAT_HELPURL}"},{type:"controls_whileUntil",message0:"%1 %2",args0:[{type:"field_dropdown",name:"MODE",options:[["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}","WHILE"],["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}","UNTIL"]]},{type:"input_value",name:"BOOL",check:"Boolean"}], -message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_WHILEUNTIL_HELPURL}",extensions:["controls_whileUntil_tooltip"]},{type:"controls_for",message0:"%{BKY_CONTROLS_FOR_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:null},{type:"input_value",name:"FROM",check:"Number",align:"RIGHT"},{type:"input_value",name:"TO",check:"Number",align:"RIGHT"},{type:"input_value",name:"BY", -check:"Number",align:"RIGHT"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],inputsInline:!0,previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FOR_HELPURL}",extensions:["contextMenu_newGetVariableBlock","controls_for_tooltip"]},{type:"controls_forEach",message0:"%{BKY_CONTROLS_FOREACH_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:null},{type:"input_value",name:"LIST",check:"Array"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1", -args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FOREACH_HELPURL}",extensions:["contextMenu_newGetVariableBlock","controls_forEach_tooltip"]},{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"]]}],previousStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}", -extensions:["controls_flow_tooltip","controls_flow_in_loop_check"]}]);Blockly.Constants.Loops.WHILE_UNTIL_TOOLTIPS={WHILE:"%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_WHILE}",UNTIL:"%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL}"};Blockly.Extensions.register("controls_whileUntil_tooltip",Blockly.Extensions.buildTooltipForDropdown("MODE",Blockly.Constants.Loops.WHILE_UNTIL_TOOLTIPS));Blockly.Constants.Loops.BREAK_CONTINUE_TOOLTIPS={BREAK:"%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK}",CONTINUE:"%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE}"}; -Blockly.Extensions.register("controls_flow_tooltip",Blockly.Extensions.buildTooltipForDropdown("FLOW",Blockly.Constants.Loops.BREAK_CONTINUE_TOOLTIPS)); -Blockly.Constants.Loops.CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN={customContextMenu:function(a){if(!this.isInFlyout){var b=this.getField("VAR").getVariable(),c=b.name;if(!this.isCollapsed()&&null!=c){var d={enabled:!0};d.text=Blockly.Msg.VARIABLES_SET_CREATE_GET.replace("%1",c);b=Blockly.Variables.generateVariableFieldDom(b);c=Blockly.utils.xml.createElement("block");c.setAttribute("type","variables_get");c.appendChild(b);d.callback=Blockly.ContextMenu.callbackFactory(this,c);a.push(d)}}}}; -Blockly.Extensions.registerMixin("contextMenu_newGetVariableBlock",Blockly.Constants.Loops.CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN);Blockly.Extensions.register("controls_for_tooltip",Blockly.Extensions.buildTooltipWithFieldText("%{BKY_CONTROLS_FOR_TOOLTIP}","VAR"));Blockly.Extensions.register("controls_forEach_tooltip",Blockly.Extensions.buildTooltipWithFieldText("%{BKY_CONTROLS_FOREACH_TOOLTIP}","VAR")); -Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN={LOOP_TYPES:["controls_repeat","controls_repeat_ext","controls_forEach","controls_for","controls_whileUntil"],suppressPrefixSuffix:!0,getSurroundLoop:function(a){do{if(-1!=Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN.LOOP_TYPES.indexOf(a.type))return a;a=a.getSurroundParent()}while(a);return null},onchange:function(a){if(this.workspace.isDragging&&!this.workspace.isDragging()&&a.type==Blockly.Events.BLOCK_MOVE&&a.blockId==this.id){var b= -Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN.getSurroundLoop(this);this.setWarningText(b?null:Blockly.Msg.CONTROLS_FLOW_STATEMENTS_WARNING);if(!this.isInFlyout){var c=Blockly.Events.getGroup();Blockly.Events.setGroup(a.group);this.setEnabled(b);Blockly.Events.setGroup(c)}}}};Blockly.Extensions.registerMixin("controls_flow_in_loop_check",Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN);Blockly.Blocks.math={};Blockly.Constants.Math={};Blockly.Constants.Math.HUE=230; -Blockly.defineBlocksWithJsonArray([{type:"math_number",message0:"%1",args0:[{type:"field_number",name:"NUM",value:0}],output:"Number",helpUrl:"%{BKY_MATH_NUMBER_HELPURL}",style:"math_blocks",tooltip:"%{BKY_MATH_NUMBER_TOOLTIP}",extensions:["parent_tooltip_when_inline"]},{type:"math_arithmetic",message0:"%1 %2 %3",args0:[{type:"input_value",name:"A",check:"Number"},{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ADDITION_SYMBOL}","ADD"],["%{BKY_MATH_SUBTRACTION_SYMBOL}","MINUS"],["%{BKY_MATH_MULTIPLICATION_SYMBOL}", -"MULTIPLY"],["%{BKY_MATH_DIVISION_SYMBOL}","DIVIDE"],["%{BKY_MATH_POWER_SYMBOL}","POWER"]]},{type:"input_value",name:"B",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ARITHMETIC_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_single",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_SINGLE_OP_ROOT}","ROOT"],["%{BKY_MATH_SINGLE_OP_ABSOLUTE}","ABS"],["-","NEG"],["ln","LN"],["log10","LOG10"],["e^","EXP"],["10^","POW10"]]}, -{type:"input_value",name:"NUM",check:"Number"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_SINGLE_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_trig",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_TRIG_SIN}","SIN"],["%{BKY_MATH_TRIG_COS}","COS"],["%{BKY_MATH_TRIG_TAN}","TAN"],["%{BKY_MATH_TRIG_ASIN}","ASIN"],["%{BKY_MATH_TRIG_ACOS}","ACOS"],["%{BKY_MATH_TRIG_ATAN}","ATAN"]]},{type:"input_value",name:"NUM",check:"Number"}],output:"Number",style:"math_blocks", -helpUrl:"%{BKY_MATH_TRIG_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_constant",message0:"%1",args0:[{type:"field_dropdown",name:"CONSTANT",options:[["\u03c0","PI"],["e","E"],["\u03c6","GOLDEN_RATIO"],["sqrt(2)","SQRT2"],["sqrt(\u00bd)","SQRT1_2"],["\u221e","INFINITY"]]}],output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_CONSTANT_TOOLTIP}",helpUrl:"%{BKY_MATH_CONSTANT_HELPURL}"},{type:"math_number_property",message0:"%1 %2",args0:[{type:"input_value",name:"NUMBER_TO_CHECK",check:"Number"}, -{type:"field_dropdown",name:"PROPERTY",options:[["%{BKY_MATH_IS_EVEN}","EVEN"],["%{BKY_MATH_IS_ODD}","ODD"],["%{BKY_MATH_IS_PRIME}","PRIME"],["%{BKY_MATH_IS_WHOLE}","WHOLE"],["%{BKY_MATH_IS_POSITIVE}","POSITIVE"],["%{BKY_MATH_IS_NEGATIVE}","NEGATIVE"],["%{BKY_MATH_IS_DIVISIBLE_BY}","DIVISIBLE_BY"]]}],inputsInline:!0,output:"Boolean",style:"math_blocks",tooltip:"%{BKY_MATH_IS_TOOLTIP}",mutator:"math_is_divisibleby_mutator"},{type:"math_change",message0:"%{BKY_MATH_CHANGE_TITLE}",args0:[{type:"field_variable", -name:"VAR",variable:"%{BKY_MATH_CHANGE_TITLE_ITEM}"},{type:"input_value",name:"DELTA",check:"Number"}],previousStatement:null,nextStatement:null,style:"variable_blocks",helpUrl:"%{BKY_MATH_CHANGE_HELPURL}",extensions:["math_change_tooltip"]},{type:"math_round",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ROUND_OPERATOR_ROUND}","ROUND"],["%{BKY_MATH_ROUND_OPERATOR_ROUNDUP}","ROUNDUP"],["%{BKY_MATH_ROUND_OPERATOR_ROUNDDOWN}","ROUNDDOWN"]]},{type:"input_value",name:"NUM", -check:"Number"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ROUND_HELPURL}",tooltip:"%{BKY_MATH_ROUND_TOOLTIP}"},{type:"math_on_list",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ONLIST_OPERATOR_SUM}","SUM"],["%{BKY_MATH_ONLIST_OPERATOR_MIN}","MIN"],["%{BKY_MATH_ONLIST_OPERATOR_MAX}","MAX"],["%{BKY_MATH_ONLIST_OPERATOR_AVERAGE}","AVERAGE"],["%{BKY_MATH_ONLIST_OPERATOR_MEDIAN}","MEDIAN"],["%{BKY_MATH_ONLIST_OPERATOR_MODE}","MODE"],["%{BKY_MATH_ONLIST_OPERATOR_STD_DEV}", -"STD_DEV"],["%{BKY_MATH_ONLIST_OPERATOR_RANDOM}","RANDOM"]]},{type:"input_value",name:"LIST",check:"Array"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ONLIST_HELPURL}",mutator:"math_modes_of_list_mutator",extensions:["math_op_tooltip"]},{type:"math_modulo",message0:"%{BKY_MATH_MODULO_TITLE}",args0:[{type:"input_value",name:"DIVIDEND",check:"Number"},{type:"input_value",name:"DIVISOR",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_MODULO_TOOLTIP}", -helpUrl:"%{BKY_MATH_MODULO_HELPURL}"},{type:"math_constrain",message0:"%{BKY_MATH_CONSTRAIN_TITLE}",args0:[{type:"input_value",name:"VALUE",check:"Number"},{type:"input_value",name:"LOW",check:"Number"},{type:"input_value",name:"HIGH",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_CONSTRAIN_TOOLTIP}",helpUrl:"%{BKY_MATH_CONSTRAIN_HELPURL}"},{type:"math_random_int",message0:"%{BKY_MATH_RANDOM_INT_TITLE}",args0:[{type:"input_value",name:"FROM",check:"Number"}, -{type:"input_value",name:"TO",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_RANDOM_INT_TOOLTIP}",helpUrl:"%{BKY_MATH_RANDOM_INT_HELPURL}"},{type:"math_random_float",message0:"%{BKY_MATH_RANDOM_FLOAT_TITLE_RANDOM}",output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_RANDOM_FLOAT_TOOLTIP}",helpUrl:"%{BKY_MATH_RANDOM_FLOAT_HELPURL}"},{type:"math_atan2",message0:"%{BKY_MATH_ATAN2_TITLE}",args0:[{type:"input_value",name:"X",check:"Number"},{type:"input_value", -name:"Y",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_ATAN2_TOOLTIP}",helpUrl:"%{BKY_MATH_ATAN2_HELPURL}"}]); -Blockly.Constants.Math.TOOLTIPS_BY_OP={ADD:"%{BKY_MATH_ARITHMETIC_TOOLTIP_ADD}",MINUS:"%{BKY_MATH_ARITHMETIC_TOOLTIP_MINUS}",MULTIPLY:"%{BKY_MATH_ARITHMETIC_TOOLTIP_MULTIPLY}",DIVIDE:"%{BKY_MATH_ARITHMETIC_TOOLTIP_DIVIDE}",POWER:"%{BKY_MATH_ARITHMETIC_TOOLTIP_POWER}",ROOT:"%{BKY_MATH_SINGLE_TOOLTIP_ROOT}",ABS:"%{BKY_MATH_SINGLE_TOOLTIP_ABS}",NEG:"%{BKY_MATH_SINGLE_TOOLTIP_NEG}",LN:"%{BKY_MATH_SINGLE_TOOLTIP_LN}",LOG10:"%{BKY_MATH_SINGLE_TOOLTIP_LOG10}",EXP:"%{BKY_MATH_SINGLE_TOOLTIP_EXP}",POW10:"%{BKY_MATH_SINGLE_TOOLTIP_POW10}", -SIN:"%{BKY_MATH_TRIG_TOOLTIP_SIN}",COS:"%{BKY_MATH_TRIG_TOOLTIP_COS}",TAN:"%{BKY_MATH_TRIG_TOOLTIP_TAN}",ASIN:"%{BKY_MATH_TRIG_TOOLTIP_ASIN}",ACOS:"%{BKY_MATH_TRIG_TOOLTIP_ACOS}",ATAN:"%{BKY_MATH_TRIG_TOOLTIP_ATAN}",SUM:"%{BKY_MATH_ONLIST_TOOLTIP_SUM}",MIN:"%{BKY_MATH_ONLIST_TOOLTIP_MIN}",MAX:"%{BKY_MATH_ONLIST_TOOLTIP_MAX}",AVERAGE:"%{BKY_MATH_ONLIST_TOOLTIP_AVERAGE}",MEDIAN:"%{BKY_MATH_ONLIST_TOOLTIP_MEDIAN}",MODE:"%{BKY_MATH_ONLIST_TOOLTIP_MODE}",STD_DEV:"%{BKY_MATH_ONLIST_TOOLTIP_STD_DEV}",RANDOM:"%{BKY_MATH_ONLIST_TOOLTIP_RANDOM}"}; -Blockly.Extensions.register("math_op_tooltip",Blockly.Extensions.buildTooltipForDropdown("OP",Blockly.Constants.Math.TOOLTIPS_BY_OP)); -Blockly.Constants.Math.IS_DIVISIBLEBY_MUTATOR_MIXIN={mutationToDom:function(){var a=Blockly.utils.xml.createElement("mutation"),b="DIVISIBLE_BY"==this.getFieldValue("PROPERTY");a.setAttribute("divisor_input",b);return a},domToMutation:function(a){a="true"==a.getAttribute("divisor_input");this.updateShape_(a)},updateShape_:function(a){var b=this.getInput("DIVISOR");a?b||this.appendValueInput("DIVISOR").setCheck("Number"):b&&this.removeInput("DIVISOR")}}; -Blockly.Constants.Math.IS_DIVISIBLE_MUTATOR_EXTENSION=function(){this.getField("PROPERTY").setValidator(function(a){a="DIVISIBLE_BY"==a;this.getSourceBlock().updateShape_(a)})};Blockly.Extensions.registerMutator("math_is_divisibleby_mutator",Blockly.Constants.Math.IS_DIVISIBLEBY_MUTATOR_MIXIN,Blockly.Constants.Math.IS_DIVISIBLE_MUTATOR_EXTENSION);Blockly.Extensions.register("math_change_tooltip",Blockly.Extensions.buildTooltipWithFieldText("%{BKY_MATH_CHANGE_TOOLTIP}","VAR")); -Blockly.Constants.Math.LIST_MODES_MUTATOR_MIXIN={updateType_:function(a){"MODE"==a?this.outputConnection.setCheck("Array"):this.outputConnection.setCheck("Number")},mutationToDom:function(){var a=Blockly.utils.xml.createElement("mutation");a.setAttribute("op",this.getFieldValue("OP"));return a},domToMutation:function(a){this.updateType_(a.getAttribute("op"))}};Blockly.Constants.Math.LIST_MODES_MUTATOR_EXTENSION=function(){this.getField("OP").setValidator(function(a){this.updateType_(a)}.bind(this))}; -Blockly.Extensions.registerMutator("math_modes_of_list_mutator",Blockly.Constants.Math.LIST_MODES_MUTATOR_MIXIN,Blockly.Constants.Math.LIST_MODES_MUTATOR_EXTENSION);Blockly.Blocks.procedures={}; -Blockly.Blocks.procedures_defnoreturn={init:function(){var a=new Blockly.FieldTextInput("",Blockly.Procedures.rename);a.setSpellcheck(!1);this.appendDummyInput().appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_TITLE).appendField(a,"NAME").appendField("","PARAMS");this.setMutator(new Blockly.Mutator(["procedures_mutatorarg"]));(this.workspace.options.comments||this.workspace.options.parentWorkspace&&this.workspace.options.parentWorkspace.options.comments)&&Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT&&this.setCommentText(Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT); -this.setStyle("procedure_blocks");this.setTooltip(Blockly.Msg.PROCEDURES_DEFNORETURN_TOOLTIP);this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFNORETURN_HELPURL);this.arguments_=[];this.argumentVarModels_=[];this.setStatements_(!0);this.statementConnection_=null},setStatements_:function(a){this.hasStatements_!==a&&(a?(this.appendStatementInput("STACK").appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_DO),this.getInput("RETURN")&&this.moveInputBefore("STACK","RETURN")):this.removeInput("STACK",!0),this.hasStatements_= -a)},updateParams_:function(){var a="";this.arguments_.length&&(a=Blockly.Msg.PROCEDURES_BEFORE_PARAMS+" "+this.arguments_.join(", "));Blockly.Events.disable();try{this.setFieldValue(a,"PARAMS")}finally{Blockly.Events.enable()}},mutationToDom:function(a){var b=Blockly.utils.xml.createElement("mutation");a&&b.setAttribute("name",this.getFieldValue("NAME"));for(var c=0;c -# build.py with no parameters builds all files. -# core builds blockly_compressed, blockly_uncompressed, and blocks_compressed. -# generators builds every _compressed.js. -# langfiles builds every msg/js/.js file. - -# This script generates two versions of Blockly's core files: -# blockly_compressed.js -# blockly_uncompressed.js -# The compressed file is a concatenation of all of Blockly's core files which -# have been run through Google's Closure Compiler. This is done using the -# online API (which takes a few seconds and requires an Internet connection). -# The uncompressed file is a script that loads in each of Blockly's core files -# one by one. This takes much longer for a browser to load, but is useful -# when debugging code since line numbers are meaningful and variables haven't -# been renamed. The uncompressed file also allows for a faster development -# cycle since there is no need to rebuild or recompile, just reload. -# -# This script also generates: -# blocks_compressed.js: The compressed Blockly language blocks. -# javascript_compressed.js: The compressed JavaScript generator. -# python_compressed.js: The compressed Python generator. -# php_compressed.js: The compressed PHP generator. -# lua_compressed.js: The compressed Lua generator. -# dart_compressed.js: The compressed Dart generator. -# msg/js/.js for every language defined in msg/js/.json. - -import argparse -import codecs -import glob -import json -import os -import re -import subprocess -import sys -import threading - -if sys.version_info[0] == 2: - import httplib - from urllib import urlencode -else: - import http.client as httplib - from urllib.parse import urlencode - from importlib import reload - -# Read package.json and extract the current Blockly version. -blocklyVersion = json.loads(open('package.json', 'r').read())['version'] - -def import_path(fullpath): - """Import a file with full path specification. - Allows one to import from any directory, something __import__ does not do. - - Args: - fullpath: Path and filename of import. - - Returns: - An imported module. - """ - path, filename = os.path.split(fullpath) - filename, ext = os.path.splitext(filename) - sys.path.append(path) - module = __import__(filename) - reload(module) # Might be out of date. - del sys.path[-1] - return module - - -HEADER = ("// Do not edit this file; automatically generated by build.py.\n" - "'use strict';\n") - - -class Gen_uncompressed(threading.Thread): - """Generate a JavaScript file that loads Blockly's raw files. - Runs in a separate thread. - """ - def __init__(self, search_paths, target_filename): - threading.Thread.__init__(self) - self.search_paths = search_paths - self.target_filename = target_filename - - def run(self): - f = open(self.target_filename, 'w') - f.write(HEADER) - f.write(""" -this.IS_NODE_JS = !!(typeof module !== 'undefined' && module.exports); - -this.BLOCKLY_DIR = (function(root) { - if (!root.IS_NODE_JS) { - // Find name of current directory. - var scripts = document.getElementsByTagName('script'); - var re = new RegExp('(.+)[\/]blockly_(.*)uncompressed\.js$'); - for (var i = 0, script; script = scripts[i]; i++) { - var match = re.exec(script.src); - if (match) { - return match[1]; - } - } - alert('Could not detect Blockly\\'s directory name.'); - } - return ''; -})(this); - -this.BLOCKLY_BOOT = function(root) { - // Execute after Closure has loaded. -""") - add_dependency = [] - base_path = calcdeps.FindClosureBasePath(self.search_paths) - for dep in calcdeps.BuildDependenciesFromFiles(self.search_paths): - add_dependency.append(calcdeps.GetDepsLine(dep, base_path)) - add_dependency.sort() # Deterministic build. - add_dependency = '\n'.join(add_dependency) - f.write(add_dependency + '\n') - - f.write('\n') - f.write('// Load Blockly.\n') - f.write('goog.require(\'Blockly.requires\')\n') - - f.write(""" -delete root.BLOCKLY_DIR; -delete root.BLOCKLY_BOOT; -delete root.IS_NODE_JS; -}; - -if (this.IS_NODE_JS) { - this.BLOCKLY_BOOT(this); - module.exports = Blockly; -} else { - document.write(''); - document.write(''); -} -""") - f.close() - print("SUCCESS: " + self.target_filename) - - -class Gen_compressed(threading.Thread): - """Generate a JavaScript file that contains all of Blockly's core and all - required parts of Closure, compiled together. - Uses the Closure Compiler's online API. - Runs in a separate thread. - """ - def __init__(self, search_paths, bundles): - threading.Thread.__init__(self) - self.search_paths = search_paths - self.bundles = bundles - - def run(self): - if (self.bundles.core): - self.gen_core() - self.gen_blocks() - - if (self.bundles.generators): - self.gen_generator("javascript") - self.gen_generator("python") - self.gen_generator("php") - self.gen_generator("lua") - self.gen_generator("dart") - - def gen_core(self): - target_filename = "blockly_compressed.js" - # Define the parameters for the POST request. - params = [ - ("compilation_level", "SIMPLE_OPTIMIZATIONS"), - ("use_closure_library", "false"), - ("output_format", "json"), - ("output_info", "compiled_code"), - ("output_info", "warnings"), - ("output_info", "errors"), - ("output_info", "statistics"), - ("warning_level", "DEFAULT"), - ] - - # Read in all the source files. - filenames = calcdeps.CalculateDependencies(self.search_paths, - [os.path.join("core", "requires.js")]) - filenames.sort() # Deterministic build. - for filename in filenames: - # Filter out the Closure files (the compiler will add them). - if filename.startswith("closure"): - continue - f = codecs.open(filename, encoding="utf-8") - code = "".join(f.readlines()) - # Inject the Blockly version. - if filename == "core/blockly.js": - code = code.replace("Blockly.VERSION = 'uncompiled';", - "Blockly.VERSION = '%s';" % blocklyVersion) - # Strip out all requireType calls. - code = re.sub(r"goog.requireType(.*)", "", code) - params.append(("js_code", code.encode("utf-8"))) - f.close() - - self.do_compile(params, target_filename, filenames, "") - - def gen_blocks(self): - target_filename = "blocks_compressed.js" - # Define the parameters for the POST request. - params = [ - ("compilation_level", "SIMPLE_OPTIMIZATIONS"), - ("output_format", "json"), - ("output_info", "compiled_code"), - ("output_info", "warnings"), - ("output_info", "errors"), - ("output_info", "statistics"), - ("warning_level", "DEFAULT"), - ] - - # Add Blockly, Blockly.Blocks, and all fields to be compatible with the compiler. - params.append(("js_code", """ -goog.provide('Blockly'); -goog.provide('Blockly.Blocks'); -goog.provide('Blockly.Comment'); -goog.provide('Blockly.FieldCheckbox'); -goog.provide('Blockly.FieldColour'); -goog.provide('Blockly.FieldDropdown'); -goog.provide('Blockly.FieldImage'); -goog.provide('Blockly.FieldLabel'); -goog.provide('Blockly.FieldMultilineInput'); -goog.provide('Blockly.FieldNumber'); -goog.provide('Blockly.FieldTextInput'); -goog.provide('Blockly.FieldVariable'); -goog.provide('Blockly.Mutator'); -goog.provide('Blockly.Warning'); -""")) - # Read in all the source files. - filenames = glob.glob(os.path.join("blocks", "*.js")) - filenames.sort() # Deterministic build. - for filename in filenames: - f = codecs.open(filename, encoding="utf-8") - params.append(("js_code", "".join(f.readlines()).encode("utf-8"))) - f.close() - - # Remove Blockly, Blockly.Blocks and all fields to be compatible with Blockly. - remove = r"var Blockly=\{[^;]*\};\n?" - self.do_compile(params, target_filename, filenames, remove) - - def gen_generator(self, language): - target_filename = language + "_compressed.js" - # Define the parameters for the POST request. - params = [ - ("compilation_level", "SIMPLE_OPTIMIZATIONS"), - ("output_format", "json"), - ("output_info", "compiled_code"), - ("output_info", "warnings"), - ("output_info", "errors"), - ("output_info", "statistics"), - ("warning_level", "DEFAULT"), - ] - - # Read in all the source files. - # Add Blockly.Generator and Blockly.utils.string to be compatible - # with the compiler. - params.append(("js_code", """ -goog.provide('Blockly.Generator'); -goog.provide('Blockly.utils.global'); -goog.provide('Blockly.utils.string'); -""")) - filenames = glob.glob( - os.path.join("generators", language, "*.js")) - filenames.sort() # Deterministic build. - filenames.insert(0, os.path.join("generators", language + ".js")) - for filename in filenames: - f = codecs.open(filename, encoding="utf-8") - params.append(("js_code", "".join(f.readlines()).encode("utf-8"))) - f.close() - filenames.insert(0, "[goog.provide]") - - # Remove Blockly.Generator and Blockly.utils.string to be compatible - # with Blockly. - remove = r"var Blockly=\{[^;]*\};\s*Blockly.utils.global={};\s*Blockly.utils.string={};\n?" - self.do_compile(params, target_filename, filenames, remove) - - def do_compile(self, params, target_filename, filenames, remove): - # Send the request to Google. - headers = {"Content-type": "application/x-www-form-urlencoded"} - conn = httplib.HTTPSConnection("closure-compiler.appspot.com") - conn.request("POST", "/compile", urlencode(params), headers) - response = conn.getresponse() - - # Decode is necessary for Python 3.4 compatibility - json_str = response.read().decode("utf-8") - conn.close() - - # Parse the JSON response. - try: - json_data = json.loads(json_str) - except ValueError: - print("ERROR: Could not parse JSON for %s. Raw data:" % target_filename) - print(json_str) - return - - def file_lookup(name): - if not name.startswith("Input_"): - return "???" - n = int(name[6:]) - return filenames[n] - - if "serverErrors" in json_data: - errors = json_data["serverErrors"] - for error in errors: - print("SERVER ERROR: %s" % target_filename) - print(error["error"]) - elif "errors" in json_data: - errors = json_data["errors"] - for error in errors: - print("FATAL ERROR") - print(error["error"]) - if error["file"]: - print("%s at line %d:" % ( - file_lookup(error["file"]), error["lineno"])) - print(error["line"]) - print((" " * error["charno"]) + "^") - sys.exit(1) - else: - if "warnings" in json_data: - warnings = json_data["warnings"] - for warning in warnings: - print("WARNING") - print(warning["warning"]) - if warning["file"]: - print("%s at line %d:" % ( - file_lookup(warning["file"]), warning["lineno"])) - print(warning["line"]) - print((" " * warning["charno"]) + "^") - print() - - if not "compiledCode" in json_data: - print("FATAL ERROR: Compiler did not return compiledCode.") - sys.exit(1) - - code = HEADER + "\n" + json_data["compiledCode"] - # Remove Blockly definitions to be compatible with Blockly. - code = re.sub(remove, "", code) - code = self.trim_licence(code) - - stats = json_data["statistics"] - original_b = stats["originalSize"] - compressed_b = stats["compressedSize"] - if original_b > 0 and compressed_b > 0: - f = open(target_filename, "w") - f.write(code) - f.close() - - original_kb = int(original_b / 1024 + 0.5) - compressed_kb = int(compressed_b / 1024 + 0.5) - ratio = int(float(compressed_b) / float(original_b) * 100 + 0.5) - print("SUCCESS: " + target_filename) - print("Size changed from %d KB to %d KB (%d%%)." % ( - original_kb, compressed_kb, ratio)) - else: - print("UNKNOWN ERROR") - - def trim_licence(self, code): - """Strip out Google's and MIT's Apache licences. - - JS Compiler preserves dozens of Apache licences in the Blockly code. - Remove these if they belong to Google or MIT. - MIT's permission to do this is logged in Blockly issue 2412. - - Args: - code: Large blob of compiled source code. - - Returns: - Code with Google's and MIT's Apache licences trimmed. - """ - apache2 = re.compile("""/\\* - - (Copyright \\d+ (Google LLC|Massachusetts Institute of Technology)) -( All rights reserved. -)? SPDX-License-Identifier: Apache-2.0 -\\*/""") - return re.sub(apache2, "", code) - - -class Gen_langfiles(threading.Thread): - """Generate JavaScript file for each natural language supported. - - Runs in a separate thread. - """ - - def __init__(self): - threading.Thread.__init__(self) - - def run(self): - # The files msg/json/{en,qqq,synonyms}.json depend on msg/messages.js. - try: - subprocess.check_call([ - "python", - os.path.join("scripts", "i18n", "js_to_json.py"), - "--input_file", "msg/messages.js", - "--output_dir", "msg/json/", - "--quiet"]) - except (subprocess.CalledProcessError, OSError) as e: - # Documentation for subprocess.check_call says that CalledProcessError - # will be raised on failure, but I found that OSError is also possible. - print("Error running scripts/i18n/js_to_json.py: ", e) - sys.exit(1) - - # Checking whether it is necessary to rebuild the js files would be a lot of - # work since we would have to compare each .json file with each - # .js file. Rebuilding is easy and cheap, so just go ahead and do it. - try: - # Use create_messages.py to create .js files from .json files. - cmd = [ - "python", - os.path.join("scripts", "i18n", "create_messages.py"), - "--source_lang_file", os.path.join("msg", "json", "en.json"), - "--source_synonym_file", os.path.join("msg", "json", "synonyms.json"), - "--source_constants_file", os.path.join("msg", "json", "constants.json"), - "--key_file", os.path.join("msg", "json", "keys.json"), - "--output_dir", os.path.join("msg", "js"), - "--quiet"] - json_files = glob.glob(os.path.join("msg", "json", "*.json")) - json_files = [file for file in json_files if not - (file.endswith(("keys.json", "synonyms.json", "qqq.json", "constants.json")))] - cmd.extend(json_files) - subprocess.check_call(cmd) - except (subprocess.CalledProcessError, OSError) as e: - print("Error running scripts/i18n/create_messages.py: ", e) - sys.exit(1) - - # Output list of .js files created. - for f in json_files: - # This assumes the path to the current directory does not contain "json". - f = f.replace("json", "js") - if os.path.isfile(f): - print("SUCCESS: " + f) - else: - print("FAILED to create " + f) - -# Class to hold arguments if user passes in old argument style. -class Arguments: - def __init__(self): - self.core = False - self.generators = False - self.langfiles = False - -# Gets the command line arguments. -def get_args(): - parser = argparse.ArgumentParser(description="Decide which files to build.") - parser.add_argument('-core', action="store_true", default=False, help="Build core") - parser.add_argument('-generators', action="store_true", default=False, help="Build the generators") - parser.add_argument('-langfiles', action="store_true", default=False, help="Build all the language files") - - # New argument style: ./build.py -core - # Old argument style: ./build.py core - # Changed as of July 2019. - try: - args = parser.parse_args() - if (not args.core) and (not args.generators) and (not args.langfiles): - # No arguments, use these defaults: - args.core = True - args.generators = True - args.langfiles = True - except SystemExit: - # Fall back to old argument style. - args = Arguments() - args.core = 'core' in sys.argv - args.generators = 'generators' in sys.argv - args.langfiles = 'langfiles' in sys.argv - if 'accessible' in sys.argv: - print("The Blockly accessibility demo has moved to https://github.com/google/blockly-experimental") - return args - -if __name__ == "__main__": - args = get_args() - calcdeps = import_path(os.path.join("closure", "bin", "calcdeps.py")) - full_search_paths = calcdeps.ExpandDirectories(["core", "closure"]) - full_search_paths = sorted(full_search_paths) # Deterministic build. - - print("Deprecation Warning: (July 2020)\n This build script has been " + - "deprecated, please use 'npm run build' instead. \n The script will be " + - "removed from Blockly core in Q4 of 2020.\n") - - # Uncompressed and compressed are run in parallel threads. - # Uncompressed is limited by processor speed. - if (args.core): - Gen_uncompressed(full_search_paths, 'blockly_uncompressed.js').start() - - # Compressed is limited by network and server speed. - Gen_compressed(full_search_paths, args).start() - - # This is run locally in a separate thread - if (args.langfiles): - Gen_langfiles().start() diff --git a/closure/bin/calcdeps.py b/closure/bin/calcdeps.py deleted file mode 100644 index 67e5e8d1574..00000000000 --- a/closure/bin/calcdeps.py +++ /dev/null @@ -1,325 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2006 The Closure Library Authors. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS-IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -"""Calculates JavaScript dependencies without requiring Google's build system. - -It iterates over a number of search paths and builds a dependency tree. With -the inputs provided, it walks the dependency tree and outputs all the files -required for compilation. -""" - - -import logging -import os -import re -import sys - - -_BASE_REGEX_STRING = '^\s*goog\.%s\(\s*[\'"](.+)[\'"]\s*\)' -req_regex = re.compile(_BASE_REGEX_STRING % 'require') -prov_regex = re.compile(_BASE_REGEX_STRING % 'provide') -ns_regex = re.compile('^ns:((\w+\.)*(\w+))$') - - -def IsValidFile(ref): - """Returns true if the provided reference is a file and exists.""" - return os.path.isfile(ref) - - -def IsJsFile(ref): - """Returns true if the provided reference is a JavaScript file.""" - return ref.endswith('.js') - - -def IsNamespace(ref): - """Returns true if the provided reference is a namespace.""" - return re.match(ns_regex, ref) is not None - - -def IsDirectory(ref): - """Returns true if the provided reference is a directory.""" - return os.path.isdir(ref) - - -def ExpandDirectories(refs): - """Expands any directory references into inputs. - - Description: - Looks for any directories in the provided references. Found directories - are recursively searched for .js files, which are then added to the result - list. - - Args: - refs: a list of references such as files, directories, and namespaces - - Returns: - A list of references with directories removed and replaced by any - .js files that are found in them. Also, the paths will be normalized. - """ - result = [] - for ref in refs: - if IsDirectory(ref): - # Disable 'Unused variable' for subdirs - # pylint: disable=unused-variable - for (directory, subdirs, filenames) in os.walk(ref): - for filename in filenames: - if IsJsFile(filename): - result.append(os.path.join(directory, filename)) - else: - result.append(ref) - return map(os.path.normpath, result) - - -class DependencyInfo(object): - """Represents a dependency that is used to build and walk a tree.""" - - def __init__(self, filename): - self.filename = filename - self.provides = [] - self.requires = [] - - def __str__(self): - return '%s Provides: %s Requires: %s' % (self.filename, - repr(self.provides), - repr(self.requires)) - - -def BuildDependenciesFromFiles(files): - """Build a list of dependencies from a list of files. - - Description: - Takes a list of files, extracts their provides and requires, and builds - out a list of dependency objects. - - Args: - files: a list of files to be parsed for goog.provides and goog.requires. - - Returns: - A list of dependency objects, one for each file in the files argument. - """ - result = [] - filenames = set() - for filename in files: - if filename in filenames: - continue - - # Python 3 requires the file encoding to be specified - if (sys.version_info[0] < 3): - file_handle = open(filename, 'r') - else: - file_handle = open(filename, 'r', encoding='utf8') - - try: - dep = CreateDependencyInfo(filename, file_handle) - result.append(dep) - finally: - file_handle.close() - - filenames.add(filename) - - return result - - -def CreateDependencyInfo(filename, source): - """Create dependency info. - - Args: - filename: Filename for source. - source: File-like object containing source. - - Returns: - A DependencyInfo object with provides and requires filled. - """ - dep = DependencyInfo(filename) - for line in source: - if re.match(req_regex, line): - dep.requires.append(re.search(req_regex, line).group(1)) - if re.match(prov_regex, line): - dep.provides.append(re.search(prov_regex, line).group(1)) - return dep - - -def BuildDependencyHashFromDependencies(deps): - """Builds a hash for searching dependencies by the namespaces they provide. - - Description: - Dependency objects can provide multiple namespaces. This method enumerates - the provides of each dependency and adds them to a hash that can be used - to easily resolve a given dependency by a namespace it provides. - - Args: - deps: a list of dependency objects used to build the hash. - - Raises: - Exception: If a multiple files try to provide the same namepace. - - Returns: - A hash table { namespace: dependency } that can be used to resolve a - dependency by a namespace it provides. - """ - dep_hash = {} - for dep in deps: - for provide in dep.provides: - if provide in dep_hash: - raise Exception('Duplicate provide (%s) in (%s, %s)' % ( - provide, - dep_hash[provide].filename, - dep.filename)) - dep_hash[provide] = dep - return dep_hash - - -def CalculateDependencies(paths, inputs): - """Calculates the dependencies for given inputs. - - Description: - This method takes a list of paths (files, directories) and builds a - searchable data structure based on the namespaces that each .js file - provides. It then parses through each input, resolving dependencies - against this data structure. The final output is a list of files, - including the inputs, that represent all of the code that is needed to - compile the given inputs. - - Args: - paths: the references (files, directories) that are used to build the - dependency hash. - inputs: the inputs (files, directories, namespaces) that have dependencies - that need to be calculated. - - Raises: - Exception: if a provided input is invalid. - - Returns: - A list of all files, including inputs, that are needed to compile the given - inputs. - """ - deps = BuildDependenciesFromFiles(paths + inputs) - search_hash = BuildDependencyHashFromDependencies(deps) - result_list = [] - seen_list = [] - for input_file in inputs: - if IsNamespace(input_file): - namespace = re.search(ns_regex, input_file).group(1) - if namespace not in search_hash: - raise Exception('Invalid namespace (%s)' % namespace) - input_file = search_hash[namespace].filename - if not IsValidFile(input_file) or not IsJsFile(input_file): - raise Exception('Invalid file (%s)' % input_file) - seen_list.append(input_file) - file_handle = open(input_file, 'r') - try: - for line in file_handle: - if re.match(req_regex, line): - require = re.search(req_regex, line).group(1) - ResolveDependencies(require, search_hash, result_list, seen_list) - finally: - file_handle.close() - result_list.append(input_file) - - return result_list - - -def FindClosureBasePath(paths): - """Given a list of file paths, return Closure base.js path, if any. - - Args: - paths: A list of paths. - - Returns: - The path to Closure's base.js file including filename, if found. - """ - - for path in paths: - pathname, filename = os.path.split(path) - - if filename == 'base.js': - f = open(path) - - is_base = False - - # Sanity check that this is the Closure base file. Check that this - # is where goog is defined. This is determined by the @provideGoog - # flag. - for line in f: - if '@provideGoog' in line: - is_base = True - break - - f.close() - - if is_base: - return path - -def ResolveDependencies(require, search_hash, result_list, seen_list): - """Takes a given requirement and resolves all of the dependencies for it. - - Description: - A given requirement may require other dependencies. This method - recursively resolves all dependencies for the given requirement. - - Raises: - Exception: when require does not exist in the search_hash. - - Args: - require: the namespace to resolve dependencies for. - search_hash: the data structure used for resolving dependencies. - result_list: a list of filenames that have been calculated as dependencies. - This variable is the output for this function. - seen_list: a list of filenames that have been 'seen'. This is required - for the dependency->dependent ordering. - """ - if require not in search_hash: - raise Exception('Missing provider for (%s)' % require) - - dep = search_hash[require] - if not dep.filename in seen_list: - seen_list.append(dep.filename) - for sub_require in dep.requires: - ResolveDependencies(sub_require, search_hash, result_list, seen_list) - result_list.append(dep.filename) - - -def GetDepsLine(dep, base_path): - """Returns a JS string for a dependency statement in the deps.js file. - - Args: - dep: The dependency that we're printing. - base_path: The path to Closure's base.js including filename. - """ - return 'goog.addDependency("%s", %s, %s);' % ( - GetRelpath(dep.filename, base_path), dep.provides, dep.requires) - - -def GetRelpath(path, start): - """Return a relative path to |path| from |start|.""" - # NOTE: Python 2.6 provides os.path.relpath, which has almost the same - # functionality as this function. Since we want to support 2.4, we have - # to implement it manually. :( - path_list = os.path.abspath(os.path.normpath(path)).split(os.sep) - start_list = os.path.abspath( - os.path.normpath(os.path.dirname(start))).split(os.sep) - - common_prefix_count = 0 - for i in range(0, min(len(path_list), len(start_list))): - if path_list[i] != start_list[i]: - break - common_prefix_count += 1 - - # Always use forward slashes, because this will get expanded to a url, - # not a file path. - return '/'.join(['..'] * (len(start_list) - common_prefix_count) + - path_list[common_prefix_count:]) diff --git a/closure/goog/base.js b/closure/goog/base.js deleted file mode 100644 index 52a15dbcbdc..00000000000 --- a/closure/goog/base.js +++ /dev/null @@ -1,531 +0,0 @@ -// Copyright 2006 The Closure Library Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS-IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileoverview Bootstrap for the Google JS Library (Closure). - * - * In uncompiled mode base.js will attempt to load Closure's deps file, unless - * the global CLOSURE_NO_DEPS is set to true. This allows projects - * to include their own deps file(s) from different locations. - * - * Avoid including base.js more than once. This is strictly discouraged and not - * supported. goog.require(...) won't work properly in that case. - * - * @provideGoog - */ - - -/** - * Base namespace for the Closure library. Checks to see goog is already - * defined in the current scope before assigning to prevent clobbering if - * base.js is loaded more than once. - * - * @const - */ -var goog = goog || {}; - -/** - * Reference to the global object. - * https://www.ecma-international.org/ecma-262/9.0/index.html#sec-global-object - * - * More info on this implementation here: - * https://docs.google.com/document/d/1NAeW4Wk7I7FV0Y2tcUFvQdGMc89k2vdgSXInw8_nvCI/edit - * - * @const - * @suppress {undefinedVars} self won't be referenced unless `this` is falsy. - * @type {!Global} - */ -goog.global = - // Check `this` first for backwards compatibility. - // Valid unless running as an ES module or in a function wrapper called - // without setting `this` properly. - // Note that base.js can't usefully be imported as an ES module, but it may - // be compiled into bundles that are loadable as ES modules. - this || - // https://developer.mozilla.org/en-US/docs/Web/API/Window/self - // For in-page browser environments and workers. - self; - -/** - * Builds an object structure for the provided namespace path, ensuring that - * names that already exist are not overwritten. For example: - * "a.b.c" -> a = {};a.b={};a.b.c={}; - * Used by goog.provide and goog.exportSymbol. - * @param {string} name name of the object that this file defines. - * @private - */ -goog.exportPath_ = function(name) { - var parts = name.split('.'); - var cur = goog.global; - - // Internet Explorer exhibits strange behavior when throwing errors from - // methods externed in this manner. See the testExportSymbolExceptions in - // base_test.html for an example. - if (!(parts[0] in cur) && typeof cur.execScript != 'undefined') { - cur.execScript('var ' + parts[0]); - } - - for (var part; parts.length && (part = parts.shift());) { - if (cur[part] && cur[part] !== Object.prototype[part]) { - cur = cur[part]; - } else { - cur = cur[part] = {}; - } - } -}; - -/** - * Defines a namespace in Closure. - * - * A namespace may only be defined once in a codebase. It may be defined using - * goog.provide() or goog.module(). - * - * The presence of one or more goog.provide() calls in a file indicates - * that the file defines the given objects/namespaces. - * Provided symbols must not be null or undefined. - * - * In addition, goog.provide() creates the object stubs for a namespace - * (for example, goog.provide("goog.foo.bar") will create the object - * goog.foo.bar if it does not already exist). - * - * Build tools also scan for provide/require/module statements - * to discern dependencies, build dependency files (see deps.js), etc. - * - * @see goog.require - * @see goog.module - * @param {string} name Namespace provided by this file in the form - * "goog.package.part". - */ -goog.provide = function(name) { - // Ensure that the same namespace isn't provided twice. - // A goog.module/goog.provide maps a goog.require to a specific file - if (goog.isProvided_(name)) { - throw Error('Namespace "' + name + '" already declared.'); - } - - delete goog.implicitNamespaces_[name]; - - var namespace = name; - while ((namespace = namespace.substring(0, namespace.lastIndexOf('.')))) { - if (goog.getObjectByName(namespace)) { - break; - } - goog.implicitNamespaces_[namespace] = true; - } - - goog.exportPath_(name); -}; - -/** - * @private {?{ - * moduleName: (string|undefined), - * declareLegacyNamespace:boolean, - * type: ?goog.ModuleType - * }} - */ -goog.moduleLoaderState_ = null; - -/** - * Check if the given name has been goog.provided. This will return false for - * names that are available only as implicit namespaces. - * @param {string} name name of the object to look for. - * @return {boolean} Whether the name has been provided. - * @private - */ -goog.isProvided_ = function(name) { - return (!goog.implicitNamespaces_[name] && - goog.isDefAndNotNull(goog.getObjectByName(name))); -}; - -/** - * Namespaces implicitly defined by goog.provide. For example, - * goog.provide('goog.events.Event') implicitly declares that 'goog' and - * 'goog.events' must be namespaces. - * - * @type {!Object} - * @private - */ -goog.implicitNamespaces_ = {}; - -// NOTE: We add goog.module as an implicit namespace as goog.module is defined -// here and because the existing module package has not been moved yet out of -// the goog.module namespace. This satisfies both the debug loader and -// ahead-of-time dependency management. - - -/** - * Returns an object based on its fully qualified external name. The object - * is not found if null or undefined. If you are using a compilation pass that - * renames property names beware that using this function will not find renamed - * properties. - * - * @param {string} name The fully qualified name. - * @param {Object=} opt_obj The object within which to look; default is - * |goog.global|. - * @return {?} The value (object or primitive) or, if not found, null. - */ -goog.getObjectByName = function(name, opt_obj) { - var parts = name.split('.'); - var cur = opt_obj || goog.global; - for (var i = 0; i < parts.length; i++) { - cur = cur[parts[i]]; - if (!goog.isDefAndNotNull(cur)) { - return null; - } - } - return cur; -}; - - -/** - * Adds a dependency from a file to the files it requires. - * @param {string} relPath The path to the js file. - * @param {!Array} provides An array of strings with - * the names of the objects this file provides. - * @param {!Array} requires An array of strings with - * the names of the objects this file requires. - */ -goog.addDependency = function(relPath, provides, requires) { - goog.debugLoader_.addDependency(relPath, provides, requires); -}; - - - - -// NOTE(nnaze): The debug DOM loader was included in base.js as an original way -// to do "debug-mode" development. The dependency system can sometimes be -// confusing, as can the debug DOM loader's asynchronous nature. -// -// With the DOM loader, a call to goog.require() is not blocking -- the script -// will not load until some point after the current script. If a namespace is -// needed at runtime, it needs to be defined in a previous script, or loaded via -// require() with its registered dependencies. -// -// User-defined namespaces may need their own deps file. For a reference on -// creating a deps file, see: -// Externally: https://developers.google.com/closure/library/docs/depswriter -// -// Because of legacy clients, the DOM loader can't be easily removed from -// base.js. Work was done to make it disableable or replaceable for -// different environments (DOM-less JavaScript interpreters like Rhino or V8, -// for example). See bootstrap/ for more information. - - -/** - * Implements a system for the dynamic resolution of dependencies that works in - * parallel with the BUILD system. - * - * Note that all calls to goog.require will be stripped by the compiler. - * - * @see goog.provide - * @param {string} namespace Namespace (as was given in goog.provide, - * goog.module, or goog.declareModuleId) in the form - * "goog.package.part". - * @return {?} If called within a goog.module or ES6 module file, the associated - * namespace or module otherwise null. - */ -goog.require = function(namespace) { - // If the object already exists we do not need to do anything. - if (!goog.isProvided_(namespace)) { - var moduleLoaderState = goog.moduleLoaderState_; - goog.moduleLoaderState_ = null; - try { - goog.debugLoader_.load_(namespace); - } finally { - goog.moduleLoaderState_ = moduleLoaderState; - } - } - - return null; -}; - -/** - * Requires a symbol for its type information. This is an indication to the - * compiler that the symbol may appear in type annotations, yet it is not - * referenced at runtime. - * - * When called within a goog.module or ES6 module file, the return value may be - * assigned to or destructured into a variable, but it may not be otherwise used - * in code outside of a type annotation. - * - * Note that all calls to goog.requireType will be stripped by the compiler. - * - * @param {string} namespace Namespace (as was given in goog.provide, - * goog.module, or goog.declareModuleId) in the form - * "goog.package.part". - * @return {?} - */ -goog.requireType = function(namespace) { - // Return an empty object so that single-level destructuring of the return - // value doesn't crash at runtime when using the debug loader. Multi-level - // destructuring isn't supported. - return {}; -}; - -/** - * Path for included scripts. - * @type {string} - */ -goog.basePath = ''; - -/** - * Normalize a file path by removing redundant ".." and extraneous "." file - * path components. - * @param {string} path - * @return {string} - * @private - */ -goog.normalizePath_ = function(path) { - var components = path.split('/'); - var i = 0; - while (i < components.length) { - if (components[i] == '.') { - components.splice(i, 1); - } else if ( - i && components[i] == '..' && components[i - 1] && - components[i - 1] != '..') { - components.splice(--i, 2); - } else { - i++; - } - } - return components.join('/'); -}; - - -//============================================================================== -// Language Enhancements -//============================================================================== - - -/** - * Returns true if the specified value is defined and not null. - * @param {?} val Variable to test. - * @return {boolean} Whether variable is defined and not null. - */ -goog.isDefAndNotNull = function(val) { - // Note that undefined == null. - return val != null; -}; - -//============================================================================== -// goog.defineClass implementation -//============================================================================== - - -// There's a bug in the compiler where without collapse properties the -// Closure namespace defines do not guard code correctly. To help reduce code -// size also check for !COMPILED even though it redundant until this is fixed. - -/** - * Tries to detect the base path of base.js script that bootstraps Closure. - * @private - */ -goog.findBasePath_ = function() { - /** @type {!Document} */ - var doc = goog.global.document; - // If we have a currentScript available, use it exclusively. - var currentScript = doc.currentScript; - if (currentScript) { - var scripts = [currentScript]; - } else { - var scripts = doc.getElementsByTagName('SCRIPT'); - } - // Search backwards since the current script is in almost all cases the one - // that has base.js. - for (var i = scripts.length - 1; i >= 0; --i) { - var script = /** @type {!HTMLScriptElement} */ (scripts[i]); - var src = script.src; - var qmark = src.lastIndexOf('?'); - var l = qmark == -1 ? src.length : qmark; - if (src.substr(l - 7, 7) == 'base.js') { - goog.basePath = src.substr(0, l - 7); - return; - } - } -}; - -goog.findBasePath_(); - -/** - * A debug loader is responsible for downloading and executing javascript - * files in an unbundled, uncompiled environment. - * - * @struct @constructor @final @private - */ -goog.DebugLoader_ = function() { - /** @private @const {!Object} */ - this.dependencies_ = {}; - /** @private @const {!Object} */ - this.idToPath_ = {}; - /** @private @const {!Object} */ - this.written_ = {}; - /** @private {!Array} */ - this.depsToLoad_ = []; -}; - - -/** - * Travserses the dependency graph and queues the given dependency, and all of - * its transitive dependencies, for loading and then starts loading if not - * paused. - * - * @param {string} namespace - * @private - */ -goog.DebugLoader_.prototype.load_ = function(namespace) { - if (!this.getPathFromDeps_(namespace)) { - throw Error('goog.require could not find: ' + namespace); - } else { - var loader = this; - - var deps = []; - - /** @param {string} namespace */ - var visit = function(namespace) { - var path = loader.getPathFromDeps_(namespace); - - if (!path) { - throw Error('Bad dependency path or symbol: ' + namespace); - } - - if (loader.written_[path]) { - return; - } - - loader.written_[path] = true; - - var dep = loader.dependencies_[path]; - for (var i = 0; i < dep.requires.length; i++) { - if (!goog.isProvided_(dep.requires[i])) { - visit(dep.requires[i]); - } - } - - deps.push(dep); - }; - - visit(namespace); - - var wasLoading = !!this.depsToLoad_.length; - this.depsToLoad_ = this.depsToLoad_.concat(deps); - - if (!wasLoading) { - this.loadDeps_(); - } - } -}; - - -/** - * Loads any queued dependencies until they are all loaded or paused. - * - * @private - */ -goog.DebugLoader_.prototype.loadDeps_ = function() { - var loader = this; - - while (this.depsToLoad_.length) { - (function() { - var loadCallDone = false; - var dep = loader.depsToLoad_.shift(); - - try { - dep.load(); - } finally { - loadCallDone = true; - } - })(); - } -}; - - -/** - * @param {string} absPathOrId - * @return {?string} - * @private - */ -goog.DebugLoader_.prototype.getPathFromDeps_ = function(absPathOrId) { - return this.idToPath_[absPathOrId]; -}; - - -/** - * Basic super class for all dependencies Closure Library can load. - * - * This default implementation is designed to load untranspiled, non-module - * scripts in a web broswer. - * - * For transpiled non-goog.module files {@see goog.TranspiledDependency}. - * For goog.modules see {@see goog.GoogModuleDependency}. - * For untranspiled ES6 modules {@see goog.Es6ModuleDependency}. - * - * @param {string} path Absolute path of this script. - * @param {!Array} requires goog symbols or relative paths to Closure - * this depends on. - * @struct @constructor - */ -goog.Dependency = function(path, requires) { - /** @const */ - this.path = path; - /** @const */ - this.requires = requires; -}; - -/** - * Map of script ready / state change callbacks. Old IE cannot handle putting - * these properties on goog.global. - * - * @private @const {!Object} - */ -goog.Dependency.callbackMap_ = {}; - - -/** - * Starts loading this dependency. This dependency can pause loading if it - * needs to and resume it later via the controller interface. - * - * When this is loaded it should call controller.loaded(). Note that this will - * end up calling the loaded method of this dependency; there is no need to - * call it explicitly. - */ -goog.Dependency.prototype.load = function() { - /** @type {!HTMLDocument} */ - var doc = goog.global.document; - doc.write(' - - - + + + + @@ -31,7 +31,7 @@ - + - - - - - - - - - - - - - - - - - -
-

Blockly > - Demos > Block Factory

-
- - - - - -
-

Preview: - -

-
- - - -
-
-
-
-
- - - - - - - - - - - - - - - - -
-
-
-

Language code: - -

-
-

-              
-            
-

Generator stub: - -

-
-

-            
-
- - - diff --git a/demos/blockfactory_old/link.png b/demos/blockfactory_old/link.png deleted file mode 100644 index 11dfd82845e..00000000000 Binary files a/demos/blockfactory_old/link.png and /dev/null differ diff --git a/demos/code/code.js b/demos/code/code.js index 0c27653252f..c264ff2dc9d 100644 --- a/demos/code/code.js +++ b/demos/code/code.js @@ -6,7 +6,6 @@ /** * @fileoverview JavaScript for Blockly's Code demo. - * @author fraser@google.com (Neil Fraser) */ 'use strict'; @@ -33,6 +32,7 @@ Code.LANGUAGE_NAME = { 'fa': 'فارسی', 'fr': 'Français', 'he': 'עברית', + 'hr': 'Hrvatski', 'hrx': 'Hunsrik', 'hu': 'Magyar', 'ia': 'Interlingua', @@ -106,7 +106,7 @@ Code.getLang = function() { * @return {boolean} True if RTL, false if LTR. */ Code.isRtl = function() { - return Code.LANGUAGE_RTL.indexOf(Code.LANG) != -1; + return Code.LANGUAGE_RTL.includes(Code.LANG); }; /** @@ -127,11 +127,11 @@ Code.loadBlocks = function(defaultXml) { } else if (loadOnce) { // Language switching stores the blocks during the reload. delete window.sessionStorage.loadOnceBlocks; - var xml = Blockly.Xml.textToDom(loadOnce); + var xml = Blockly.utils.xml.textToDom(loadOnce); Blockly.Xml.domToWorkspace(xml, Code.workspace); } else if (defaultXml) { // Load the editor with default starting blocks. - var xml = Blockly.Xml.textToDom(defaultXml); + var xml = Blockly.utils.xml.textToDom(defaultXml); Blockly.Xml.domToWorkspace(xml, Code.workspace); } else if ('BlocklyStorage' in window) { // Restore saved blocks in a separate thread so that subsequent @@ -184,11 +184,16 @@ Code.changeCodingLanguage = function() { * @param {!Function} func Event handler to bind. */ Code.bindClick = function(el, func) { - if (typeof el == 'string') { + if (typeof el === 'string') { el = document.getElementById(el); } el.addEventListener('click', func, true); - el.addEventListener('touchend', func, true); + function touchFunc(e) { + // Prevent code from being executed twice on touchscreens. + e.preventDefault(); + func(e); + } + el.addEventListener('touchend', touchFunc, true); }; /** @@ -196,7 +201,7 @@ Code.bindClick = function(el, func) { */ Code.importPrettify = function() { var script = document.createElement('script'); - script.setAttribute('src', 'https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js'); + script.setAttribute('src', 'https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js'); document.head.appendChild(script); }; @@ -234,14 +239,16 @@ Code.LANG = Code.getLang(); * List of tab names. * @private */ -Code.TABS_ = ['blocks', 'javascript', 'php', 'python', 'dart', 'lua', 'xml']; +Code.TABS_ = [ + 'blocks', 'javascript', 'php', 'python', 'dart', 'lua', 'xml', 'json' +]; /** * List of tab names with casing, for display in the UI. * @private */ Code.TABS_DISPLAY_ = [ - 'Blocks', 'JavaScript', 'PHP', 'Python', 'Dart', 'Lua', 'XML', + 'Blocks', 'JavaScript', 'PHP', 'Python', 'Dart', 'Lua', 'XML', 'JSON' ]; Code.selected = 'blocks'; @@ -257,10 +264,10 @@ Code.tabClick = function(clickedName) { var xmlText = xmlTextarea.value; var xmlDom = null; try { - xmlDom = Blockly.Xml.textToDom(xmlText); + xmlDom = Blockly.utils.xml.textToDom(xmlText); } catch (e) { - var q = - window.confirm(MSG['badXml'].replace('%1', e)); + var q = window.confirm( + MSG['parseError'].replace(/%1/g, 'XML').replace('%2', e)); if (!q) { // Leave the user on the XML tab. return; @@ -272,6 +279,25 @@ Code.tabClick = function(clickedName) { } } + if (document.getElementById('tab_json').classList.contains('tabon')) { + var jsonTextarea = document.getElementById('content_json'); + var jsonText = jsonTextarea.value; + var json = null; + try { + json = JSON.parse(jsonText); + } catch (e) { + var q = window.confirm( + MSG['parseError'].replace(/%1/g, 'JSON').replace('%2', e)); + if (!q) { + // Leave the user on the JSON tab. + return; + } + } + if (json) { + Blockly.serialization.workspaces.load(json, Code.workspace); + } + } + if (document.getElementById('tab_blocks').classList.contains('tabon')) { Code.workspace.setVisible(false); } @@ -295,7 +321,7 @@ Code.tabClick = function(clickedName) { Code.renderContent(); // The code menu tab is on if the blocks tab is off. var codeMenuTab = document.getElementById('tab_code'); - if (clickedName == 'blocks') { + if (clickedName === 'blocks') { Code.workspace.setVisible(true); codeMenuTab.className = 'taboff'; } else { @@ -304,7 +330,7 @@ Code.tabClick = function(clickedName) { // Sync the menu's value with the clicked tab value if needed. var codeMenu = document.getElementById('code_menu'); for (var i = 0; i < codeMenu.options.length; i++) { - if (codeMenu.options[i].value == clickedName) { + if (codeMenu.options[i].value === clickedName) { codeMenu.selectedIndex = i; break; } @@ -318,31 +344,36 @@ Code.tabClick = function(clickedName) { Code.renderContent = function() { var content = document.getElementById('content_' + Code.selected); // Initialize the pane. - if (content.id == 'content_xml') { + if (content.id === 'content_xml') { var xmlTextarea = document.getElementById('content_xml'); var xmlDom = Blockly.Xml.workspaceToDom(Code.workspace); var xmlText = Blockly.Xml.domToPrettyText(xmlDom); xmlTextarea.value = xmlText; xmlTextarea.focus(); - } else if (content.id == 'content_javascript') { - Code.attemptCodeGeneration(Blockly.JavaScript); - } else if (content.id == 'content_python') { - Code.attemptCodeGeneration(Blockly.Python); - } else if (content.id == 'content_php') { - Code.attemptCodeGeneration(Blockly.PHP); - } else if (content.id == 'content_dart') { - Code.attemptCodeGeneration(Blockly.Dart); - } else if (content.id == 'content_lua') { - Code.attemptCodeGeneration(Blockly.Lua); + } else if (content.id === 'content_json') { + var jsonTextarea = document.getElementById('content_json'); + jsonTextarea.value = JSON.stringify( + Blockly.serialization.workspaces.save(Code.workspace), null, 2); + jsonTextarea.focus(); + } else if (content.id === 'content_javascript') { + Code.attemptCodeGeneration(javascript.javascriptGenerator); + } else if (content.id === 'content_python') { + Code.attemptCodeGeneration(python.pythonGenerator); + } else if (content.id === 'content_php') { + Code.attemptCodeGeneration(php.phpGenerator); + } else if (content.id === 'content_dart') { + Code.attemptCodeGeneration(dart.dartGenerator); + } else if (content.id === 'content_lua') { + Code.attemptCodeGeneration(lua.luaGenerator); } - if (typeof PR == 'object') { + if (typeof PR === 'object') { PR.prettyPrint(); } }; /** * Attempt to generate the code and display it in the UI, pretty printed. - * @param generator {!Blockly.Generator} The generator to use. + * @param generator {!Blockly.CodeGenerator} The generator to use. */ Code.attemptCodeGeneration = function(generator) { var content = document.getElementById('content_' + Code.selected); @@ -357,25 +388,25 @@ Code.attemptCodeGeneration = function(generator) { /** * Check whether all blocks in use have generator functions. - * @param generator {!Blockly.Generator} The generator to use. + * @param generator {!Blockly.CodeGenerator} The generator to use. */ Code.checkAllGeneratorFunctionsDefined = function(generator) { var blocks = Code.workspace.getAllBlocks(false); var missingBlockGenerators = []; for (var i = 0; i < blocks.length; i++) { var blockType = blocks[i].type; - if (!generator[blockType]) { - if (missingBlockGenerators.indexOf(blockType) == -1) { + if (!generator.forBlock[blockType]) { + if (!missingBlockGenerators.includes(blockType)) { missingBlockGenerators.push(blockType); } } } - var valid = missingBlockGenerators.length == 0; + var valid = missingBlockGenerators.length === 0; if (!valid) { var msg = 'The generator code for the following blocks not specified for ' + generator.name_ + ':\n - ' + missingBlockGenerators.join('\n - '); - Blockly.alert(msg); // Assuming synchronous. No callback. + Blockly.dialog.alert(msg); // Assuming synchronous. No callback. } return valid; }; @@ -419,7 +450,7 @@ Code.init = function() { // TODO: Clean up the message files so this is done explicitly instead of // through this for-loop. for (var messageKey in MSG) { - if (messageKey.indexOf('cat') == 0) { + if (messageKey.startsWith('cat')) { Blockly.Msg[messageKey.toUpperCase()] = MSG[messageKey]; } } @@ -428,7 +459,7 @@ Code.init = function() { var toolboxText = document.getElementById('toolbox').outerHTML; toolboxText = toolboxText.replace(/(^|[^%]){(\w+)}/g, function(m, p1, p2) {return p1 + MSG[p2];}); - var toolboxXml = Blockly.Xml.textToDom(toolboxText); + var toolboxXml = Blockly.utils.xml.textToDom(toolboxText); Code.workspace = Blockly.inject('content_blocks', {grid: @@ -446,7 +477,7 @@ Code.init = function() { // Add to reserved word list: Local variables in execution environment (runJS) // and the infinite loop detection function. - Blockly.JavaScript.addReservedWords('code,timeouts,checkTimeout'); + javascript.javascriptGenerator.addReservedWords('code,timeouts,checkTimeout'); Code.loadBlocks(''); @@ -466,7 +497,7 @@ Code.init = function() { BlocklyStorage['HTTPREQUEST_ERROR'] = MSG['httpRequestError']; BlocklyStorage['LINK_ALERT'] = MSG['linkAlert']; BlocklyStorage['HASH_ERROR'] = MSG['hashError']; - BlocklyStorage['XML_ERROR'] = MSG['xmlError']; + BlocklyStorage['XML_ERROR'] = MSG['loadError']; Code.bindClick(linkButton, function() {BlocklyStorage.link(Code.workspace);}); } else if (linkButton) { @@ -521,7 +552,7 @@ Code.initLanguage = function() { var tuple = languages[i]; var lang = tuple[tuple.length - 1]; var option = new Option(tuple[0], lang); - if (lang == Code.LANG) { + if (lang === Code.LANG) { option.selected = true; } languageMenu.options.add(option); @@ -549,17 +580,23 @@ Code.initLanguage = function() { /** * Execute the user's code. * Just a quick and dirty eval. Catch infinite loops. + * @param {Event} event Event created from listener bound to the function. */ -Code.runJS = function() { - Blockly.JavaScript.INFINITE_LOOP_TRAP = 'checkTimeout();\n'; +Code.runJS = function(event) { + // Prevent code from being executed twice on touchscreens. + if (event.type === 'touchend') { + event.preventDefault(); + } + + javascript.javascriptGenerator.INFINITE_LOOP_TRAP = 'checkTimeout();\n'; var timeouts = 0; var checkTimeout = function() { if (timeouts++ > 1000000) { throw MSG['timeout']; } }; - var code = Blockly.JavaScript.workspaceToCode(Code.workspace); - Blockly.JavaScript.INFINITE_LOOP_TRAP = null; + var code = javascript.javascriptGenerator.workspaceToCode(Code.workspace); + javascript.javascriptGenerator.INFINITE_LOOP_TRAP = null; try { eval(code); } catch (e) { @@ -584,6 +621,6 @@ Code.discard = function() { // Load the Code demo's language strings. document.write('\n'); // Load Blockly's language strings. -document.write('\n'); +document.write('\n'); window.addEventListener('load', Code.init); diff --git a/demos/code/index.html b/demos/code/index.html index 9fd858aeffa..d8f894607f7 100644 --- a/demos/code/index.html +++ b/demos/code/index.html @@ -7,13 +7,13 @@ Blockly Demo: - - - - - - - + + + + + + + @@ -47,6 +47,8 @@

Blockly‏ > Dart   XML +   + JSON   @@ -78,6 +80,7 @@

Blockly‏ >

   

   
+