diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 5131e620167..00000000000 --- a/.eslintignore +++ /dev/null @@ -1,10 +0,0 @@ -*_compressed*.js -*_uncompressed*.js -/msg/* -/core/css.js -/tests/jsunit/* -/tests/generators/* -/generators/* -/demos/* -/accessible/* -/appengine/* \ No newline at end of file diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index bba9e1ba20e..00000000000 --- a/.eslintrc +++ /dev/null @@ -1,28 +0,0 @@ -{ - "rules": { - "curly": ["error", "multi-line"], - "eol-last": ["error"], - "indent": ["error", 2, {"SwitchCase": 1}], # Blockly/Google use 2-space indents - "linebreak-style": ["error", "unix"], - "max-len": ["error", 120, 4], - "no-trailing-spaces": ["error", { "skipBlankLines": true }], - "no-unused-vars": ["error", {"args": "after-used", "varsIgnorePattern": "^_"}], - "no-use-before-define": ["error"], - "quotes": ["off"], # Blockly mixes single and double quotes - "semi": ["error", "always"], - "space-before-function-paren": ["error", "never"], # Blockly doesn't have space before function paren - "strict": ["off"], # Blockly uses 'use strict' in files - "no-cond-assign": ["off"], # Blockly often uses cond-assignment in loops - "no-redeclare": ["off"], # Closure style allows redeclarations - "valid-jsdoc": ["error", {"requireReturn": false}], - "no-console": ["off"] - }, - "env": { - "browser": true - }, - "globals": { - "Blockly": true, # Blockly global - "goog": true # goog closure libraries/includes - }, - "extends": "eslint:recommended" -} 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/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 87% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index fcd866043f5..634b59bfad4 100644 --- a/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.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 new file mode 100644 index 00000000000..aa4bd749d99 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +contact_links: + - name: Ask a question ❓ + url: https://groups.google.com/forum/#!forum/blockly + 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.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.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 new file mode 100644 index 00000000000..6a62a4d9d92 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,42 @@ + + +## The basics + + + +- [ ] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) + +## The details +### Resolves + + +Fixes + +### Proposed Changes + + + +### Reason for Changes + + + +### Test Coverage + + + +### Documentation + + + +### Additional Information + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..42f0d297aea --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: 'npm' # See documentation for possible values + directory: '/' # Location of package manifests + target-branch: 'develop' + schedule: + 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..938a16fc714 --- /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.4 + # 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..c4ab688f8fd --- /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, 22.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 Clean Install + run: npm ci + + - 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 53eebc859d0..3c1938f17d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,22 @@ node_modules npm-debug.log +build-debug.log .DS_Store .settings .project +*.gz *.pyc *.komodoproject -/nbproject/private/ \ No newline at end of file +/nbproject/private/ +tsdoc-metadata.json + +tests/compile/main_compressed.js +tests/compile/main_compressed.js.map +tests/compile/*compiler*.jar +tests/screenshot/outputs/* +local_build/*compiler*.jar +local_build/local_*_compressed.js +chromedriver +build/ +dist/ +temp/ diff --git a/.jshintignore b/.jshintignore deleted file mode 100644 index 9cc962747a8..00000000000 --- a/.jshintignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -tests/ -demos/ -**/*_compressed.js -**/*_uncompressed.js -**/*_test.js \ No newline at end of file diff --git a/.npmrc b/.npmrc index 214c29d1395..a4af34998fa 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ registry=https://registry.npmjs.org/ +legacy-peer-deps=true 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..84a85c1159e --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,15 @@ +// 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, + // Organise imports using a plugin. + 'plugins': ['prettier-plugin-organize-imports'], +}; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bbc6cfcb887..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -language: node_js -matrix: - include: - - os: linux - dist: trusty - node_js: stable - sudo: required - addons: - apt: - packages: - - google-chrome-stable - - os: osx - node_js: stable - osx_image: xcode8.3 - -before_install: -- npm install google-closure-library -- npm install webdriverio -# Symlink closure library -- ln -s $(npm root)/google-closure-library ../closure-library - -before_script: - - export DISPLAY=:99.0 - - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then ( scripts/setup_linux_env.sh ) fi - - if [ "${TRAVIS_OS_NAME}" == "osx" ]; then ( scripts/setup_osx_env.sh ) fi - - sleep 2 - -script: - - set -x - - npm test 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/LICENSE b/LICENSE index 6a1992987fe..d6456956733 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache License - Version 2.0, January 2011 + Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -175,3 +175,28 @@ of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md index 457fb810f72..5a0f3b8f27b 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,80 @@ -# 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. +- [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 a few minutes and will help us better support the Blockly community. -Want to contribute? Great! First, read [our guidelines for contributors](https://developers.google.com/blockly/guides/modify/contributing). +### Installing Blockly + +Blockly is [available on npm](https://www.npmjs.com/package/blockly). + +```bash +npm install blockly +``` + +For more information on installing and using Blockly, see the [Getting Started article](https://developers.google.com/blockly/guides/get-started/web). + +### Getting Help + +- [Report a bug](https://developers.google.com/blockly/guides/modify/contribute/write_a_good_issue) or file a feature request on GitHub +- Ask a question, or search others' questions, on our [developer forum](https://groups.google.com/forum/#!forum/blockly). You can also drop by to say hello and show us your prototypes; collectively we have a lot of experience and can offer hints which will save you time. We actively monitor the forums and typically respond to questions within 2 working days. + +### 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 + +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 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: + +```bash +npm install blockly@beta +``` + +As it is a beta channel, it may be less stable, and the APIs there are subject to change. + +### Branches + +There are two main branches for Blockly. + +**[master](https://github.com/google/blockly)** - This is the (mostly) stable current release of Blockly. + +**[develop](https://github.com/google/blockly/tree/develop)** - This is where most of our work happens. Pull requests should always be made against develop. This branch will generally be usable, but may be less stable than the master branch. Once something is in develop we expect it to merge to master in the next release. + +**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. + +### 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. + +## Issues and Milestones + +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. + +## Good to Know + +- Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com) +- We test browsers using [BrowserStack](https://browserstack.com) 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/accessible/README.md b/accessible/README.md deleted file mode 100644 index a69fa6667cf..00000000000 --- a/accessible/README.md +++ /dev/null @@ -1,54 +0,0 @@ -Accessible Blockly -================== - -Google's Blockly is a web-based, visual programming editor that is accessible -to blind users. - -The code in this directory renders a version of the Blockly toolbox and -workspace that is fully keyboard-navigable, and compatible with most screen -readers. It is optimized for NVDA on Firefox. - -In the future, Accessible Blockly may be modified to suit accessibility needs -other than visual impairments. Note that deaf users are expected to continue -using Blockly over Accessible Blockly. - - -Using Accessible Blockly in Your Web App ----------------------------------------- -The demo at blockly/demos/accessible covers the absolute minimum required to -import Accessible Blockly into your web app. You will need to import the files -in the same order as in the demo: utils.service.js will need to be the first -Angular file imported. - -When the DOMContentLoaded event fires, call ng.platform.browser.bootstrap() on -the main component to be loaded. This will usually be blocklyApp.AppComponent, -but if you have another component that wraps it, use that one instead. - - -Customizing the Sidebar and Audio ---------------------------------- -The Accessible Blockly workspace comes with a customizable sidebar. - -To customize the sidebar, you will need to declare an ACCESSIBLE_GLOBALS object -in the global scope that looks like this: - - var ACCESSIBLE_GLOBALS = { - mediaPathPrefix: null, - customSidebarButtons: [] - }; - -The value of mediaPathPrefix should be the location of the accessible/media -folder. - -The value of 'customSidebarButtons' should be a list of objects, each -representing buttons on the sidebar. Each of these objects should have the -following keys: - - 'text' (the text to display on the button) - - 'action' (the function that gets run when the button is clicked) - - 'id' (optional; the id of the button) - - -Limitations ------------ -- We do not support having multiple Accessible Blockly apps in a single webpage. -- Accessible Blockly does not support the use of shadow blocks. diff --git a/accessible/app.component.js b/accessible/app.component.js deleted file mode 100644 index c31fd4259bb..00000000000 --- a/accessible/app.component.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Top-level component for the Accessible Blockly application. - * @author madeeha@google.com (Madeeha Ghori) - */ - -goog.provide('blocklyApp.AppComponent'); - -goog.require('Blockly'); - -goog.require('blocklyApp.AudioService'); -goog.require('blocklyApp.BlockConnectionService'); -goog.require('blocklyApp.BlockOptionsModalComponent'); -goog.require('blocklyApp.BlockOptionsModalService'); -goog.require('blocklyApp.KeyboardInputService'); -goog.require('blocklyApp.NotificationsService'); -goog.require('blocklyApp.SidebarComponent'); -goog.require('blocklyApp.ToolboxModalComponent'); -goog.require('blocklyApp.ToolboxModalService'); -goog.require('blocklyApp.TranslatePipe'); -goog.require('blocklyApp.TreeService'); -goog.require('blocklyApp.UtilsService'); -goog.require('blocklyApp.VariableAddModalComponent'); -goog.require('blocklyApp.VariableModalService'); -goog.require('blocklyApp.VariableRenameModalComponent'); -goog.require('blocklyApp.VariableRemoveModalComponent'); -goog.require('blocklyApp.WorkspaceComponent'); - - -blocklyApp.workspace = new Blockly.Workspace(); - -blocklyApp.AppComponent = ng.core.Component({ - selector: 'blockly-app', - template: ` - - - -
- {{getAriaLiveReadout()}} -
- - - - - - - - - - `, - directives: [ - blocklyApp.BlockOptionsModalComponent, - blocklyApp.SidebarComponent, - blocklyApp.ToolboxModalComponent, - blocklyApp.VariableAddModalComponent, - blocklyApp.VariableRenameModalComponent, - blocklyApp.VariableRemoveModalComponent, - blocklyApp.WorkspaceComponent - ], - pipes: [blocklyApp.TranslatePipe], - // All services are declared here, so that all components in the application - // use the same instance of the service. - // https://www.sitepoint.com/angular-2-components-providers-classes-factories-values/ - providers: [ - blocklyApp.AudioService, - blocklyApp.BlockConnectionService, - blocklyApp.BlockOptionsModalService, - blocklyApp.KeyboardInputService, - blocklyApp.NotificationsService, - blocklyApp.ToolboxModalService, - blocklyApp.TreeService, - blocklyApp.UtilsService, - blocklyApp.VariableModalService - ] -}) -.Class({ - constructor: [ - blocklyApp.NotificationsService, function(notificationsService) { - this.notificationsService = notificationsService; - } - ], - getAriaLiveReadout: function() { - return this.notificationsService.getDisplayedMessage(); - } -}); diff --git a/accessible/audio.service.js b/accessible/audio.service.js deleted file mode 100644 index 4f7eb4f0866..00000000000 --- a/accessible/audio.service.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Service for playing audio files. - * @author sll@google.com (Sean Lip) - */ - -goog.provide('blocklyApp.AudioService'); - -goog.require('blocklyApp.NotificationsService'); - - -blocklyApp.AudioService = ng.core.Class({ - constructor: [ - blocklyApp.NotificationsService, function(notificationsService) { - this.notificationsService = notificationsService; - - // We do not play any audio unless a media path prefix is specified. - this.canPlayAudio = false; - - if (ACCESSIBLE_GLOBALS.hasOwnProperty('mediaPathPrefix')) { - this.canPlayAudio = true; - var mediaPathPrefix = ACCESSIBLE_GLOBALS['mediaPathPrefix']; - this.AUDIO_PATHS_ = { - 'connect': mediaPathPrefix + 'click.mp3', - 'delete': mediaPathPrefix + 'delete.mp3', - 'oops': mediaPathPrefix + 'oops.mp3' - }; - } - - this.cachedAudioFiles_ = {}; - // Store callback references here so that they can be removed if a new - // call to this.play_() comes in. - this.onEndedCallbacks_ = { - 'connect': [], - 'delete': [], - 'oops': [] - }; - } - ], - play_: function(audioId, onEndedCallback) { - if (this.canPlayAudio) { - if (!this.cachedAudioFiles_.hasOwnProperty(audioId)) { - this.cachedAudioFiles_[audioId] = new Audio(this.AUDIO_PATHS_[audioId]); - } - - if (onEndedCallback) { - this.onEndedCallbacks_[audioId].push(onEndedCallback); - this.cachedAudioFiles_[audioId].addEventListener( - 'ended', onEndedCallback); - } else { - var that = this; - this.onEndedCallbacks_[audioId].forEach(function(callback) { - that.cachedAudioFiles_[audioId].removeEventListener( - 'ended', callback); - }); - this.onEndedCallbacks_[audioId].length = 0; - } - - this.cachedAudioFiles_[audioId].play(); - } - }, - playConnectSound: function() { - this.play_('connect'); - }, - playDeleteSound: function() { - this.play_('delete'); - }, - playOopsSound: function(optionalStatusMessage) { - if (optionalStatusMessage) { - var that = this; - this.play_('oops', function() { - that.notificationsService.speak(optionalStatusMessage); - }); - } else { - this.play_('oops'); - } - } -}); diff --git a/accessible/block-connection.service.js b/accessible/block-connection.service.js deleted file mode 100644 index c1ee03aaccd..00000000000 --- a/accessible/block-connection.service.js +++ /dev/null @@ -1,135 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Service for handling the mechanics of how blocks - * get connected to each other. - * @author sll@google.com (Sean Lip) - */ - -goog.provide('blocklyApp.BlockConnectionService'); - -goog.require('blocklyApp.AudioService'); -goog.require('blocklyApp.NotificationsService'); - - -blocklyApp.BlockConnectionService = ng.core.Class({ - constructor: [ - blocklyApp.NotificationsService, blocklyApp.AudioService, - function(_notificationsService, _audioService) { - this.notificationsService = _notificationsService; - this.audioService = _audioService; - - // When a user "adds a link" to a block, the connection representing this - // link is stored here. - this.markedConnection_ = null; - }], - findCompatibleConnection_: function(block, targetConnection) { - // Locates and returns a connection on the given block that is compatible - // with the target connection, if one exists. Returns null if no such - // connection exists. - // Note: the targetConnection is assumed to be the markedConnection_, or - // possibly its counterpart (in the case where the marked connection is - // currently attached to another connection). This method therefore ignores - // input connections on the given block, since one doesn't usually mark an - // output connection and attach a block to it. - if (!targetConnection || !targetConnection.getSourceBlock().workspace) { - return null; - } - - var desiredType = Blockly.OPPOSITE_TYPE[targetConnection.type]; - var potentialConnection = ( - desiredType == Blockly.OUTPUT_VALUE ? block.outputConnection : - desiredType == Blockly.PREVIOUS_STATEMENT ? block.previousConnection : - desiredType == Blockly.NEXT_STATEMENT ? block.nextConnection : - null); - - if (potentialConnection && - potentialConnection.checkType_(targetConnection)) { - return potentialConnection; - } else { - return null; - } - }, - isAnyConnectionMarked: function() { - return Boolean(this.markedConnection_); - }, - getMarkedConnectionSourceBlock: function() { - return this.markedConnection_ ? - this.markedConnection_.getSourceBlock() : null; - }, - canBeAttachedToMarkedConnection: function(block) { - return Boolean( - this.findCompatibleConnection_(block, this.markedConnection_)); - }, - canBeMovedToMarkedConnection: function(block) { - if (!this.markedConnection_) { - return false; - } - - // It should not be possible to move any ancestor of the block containing - // the marked connection to the marked connection. - var ancestorBlock = this.getMarkedConnectionSourceBlock(); - while (ancestorBlock) { - if (ancestorBlock.id == block.id) { - return false; - } - ancestorBlock = ancestorBlock.getParent(); - } - - return this.canBeAttachedToMarkedConnection(block); - }, - markConnection: function(connection) { - this.markedConnection_ = connection; - this.notificationsService.speak(Blockly.Msg.ADDED_LINK_MSG); - }, - attachToMarkedConnection: function(block) { - var xml = Blockly.Xml.blockToDom(block); - var reconstitutedBlock = Blockly.Xml.domToBlock(blocklyApp.workspace, xml); - - var targetConnection = null; - if (this.markedConnection_.targetBlock() && - this.markedConnection_.type == Blockly.PREVIOUS_STATEMENT) { - // Is the marked connection a 'previous' connection that is already - // connected? If so, find the block that's currently connected to it, and - // use that block's 'next' connection as the new marked connection. - // Otherwise, splicing does not happen correctly, and inserting a block - // in the middle of a group of two linked blocks will split the group. - targetConnection = this.markedConnection_.targetConnection; - } else { - targetConnection = this.markedConnection_; - } - - var connection = this.findCompatibleConnection_( - reconstitutedBlock, targetConnection); - if (connection) { - targetConnection.connect(connection); - - this.markedConnection_ = null; - this.audioService.playConnectSound(); - return reconstitutedBlock.id; - } else { - // We throw an error here, because we expect any UI controls that would - // result in a non-connection to be disabled or hidden. - throw Error( - 'Unable to connect block to marked connection. This should not ' + - 'happen.'); - } - } -}); diff --git a/accessible/block-options-modal.component.js b/accessible/block-options-modal.component.js deleted file mode 100644 index 6ac7975b5de..00000000000 --- a/accessible/block-options-modal.component.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Component that represents the block options modal. - * - * @author sll@google.com (Sean Lip) - */ - -goog.provide('blocklyApp.BlockOptionsModalComponent'); - -goog.require('blocklyApp.AudioService'); -goog.require('blocklyApp.BlockOptionsModalService'); -goog.require('blocklyApp.KeyboardInputService'); -goog.require('blocklyApp.TranslatePipe'); - -goog.require('Blockly.CommonModal'); - - -blocklyApp.BlockOptionsModalComponent = ng.core.Component({ - selector: 'blockly-block-options-modal', - template: ` -
- -
-

{{'BLOCK_OPTIONS'|translate}}

-
-
- -
-
- -
- -
-
-
- `, - pipes: [blocklyApp.TranslatePipe] -}) -.Class({ - constructor: [ - blocklyApp.BlockOptionsModalService, blocklyApp.KeyboardInputService, - blocklyApp.AudioService, - function(blockOptionsModalService_, keyboardInputService_, audioService_) { - this.blockOptionsModalService = blockOptionsModalService_; - this.keyboardInputService = keyboardInputService_; - this.audioService = audioService_; - - this.modalIsVisible = false; - this.actionButtonsInfo = []; - this.activeButtonIndex = -1; - this.onDismissCallback = null; - - var that = this; - this.blockOptionsModalService.registerPreShowHook( - function(newActionButtonsInfo, onDismissCallback) { - that.modalIsVisible = true; - that.actionButtonsInfo = newActionButtonsInfo; - that.activeActionButtonIndex = -1; - that.onDismissCallback = onDismissCallback; - - Blockly.CommonModal.setupKeyboardOverrides(that); - that.keyboardInputService.addOverride('13', function(evt) { - evt.preventDefault(); - evt.stopPropagation(); - - if (that.activeButtonIndex == -1) { - return; - } - - var button = document.getElementById( - that.getOptionId(that.activeButtonIndex)); - if (that.activeButtonIndex < - that.actionButtonsInfo.length) { - that.actionButtonsInfo[that.activeButtonIndex].action(); - } else { - that.dismissModal(); - } - - that.hideModal(); - }); - - setTimeout(function() { - document.getElementById('blockOptionsModal').focus(); - }, 150); - } - ); - } - ], - focusOnOption: function(index) { - var button = document.getElementById(this.getOptionId(index)); - button.focus(); - }, - // Counts the number of interactive elements for the modal. - numInteractiveElements: function() { - return this.actionButtonsInfo.length + 1; - }, - // Returns the ID for the corresponding option button. - getOptionId: function(index) { - return 'block-options-modal-option-' + index; - }, - // Returns the ID for the "cancel" option button. - getCancelOptionId: function() { - return this.getOptionId(this.actionButtonsInfo.length); - }, - dismissModal: function() { - this.onDismissCallback(); - this.hideModal(); - }, - // Closes the modal. - hideModal: function() { - this.modalIsVisible = false; - this.keyboardInputService.clearOverride(); - this.blockOptionsModalService.hideModal(); - } -}); diff --git a/accessible/block-options-modal.service.js b/accessible/block-options-modal.service.js deleted file mode 100644 index ad775c6d47c..00000000000 --- a/accessible/block-options-modal.service.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Service for the block options modal. - * - * @author sll@google.com (Sean Lip) - */ - -goog.provide('blocklyApp.BlockOptionsModalService'); - - -blocklyApp.BlockOptionsModalService = ng.core.Class({ - constructor: [function() { - this.actionButtonsInfo = []; - // The aim of the pre-show hook is to populate the modal component with the - // information it needs to display the modal (e.g., which action buttons to - // display). - this.preShowHook = function() { - throw Error( - 'A pre-show hook must be defined for the block options modal ' + - 'before it can be shown.'); - }; - this.modalIsShown = false; - this.onDismissCallback = null; - }], - registerPreShowHook: function(preShowHook) { - var that = this; - this.preShowHook = function() { - preShowHook(that.actionButtonsInfo, that.onDismissCallback); - }; - }, - isModalShown: function() { - return this.modalIsShown; - }, - showModal: function(actionButtonsInfo, onDismissCallback) { - this.actionButtonsInfo = actionButtonsInfo; - this.onDismissCallback = onDismissCallback; - - this.preShowHook(); - this.modalIsShown = true; - }, - hideModal: function() { - this.modalIsShown = false; - } -}); diff --git a/accessible/commonModal.js b/accessible/commonModal.js deleted file mode 100644 index 57e88b529ee..00000000000 --- a/accessible/commonModal.js +++ /dev/null @@ -1,77 +0,0 @@ -goog.provide('Blockly.CommonModal'); - - -Blockly.CommonModal = function() {}; - -Blockly.CommonModal.setupKeyboardOverrides = function(component) { - component.keyboardInputService.setOverride({ - // Tab key: navigates to the previous or next item in the list. - '9': function(evt) { - evt.preventDefault(); - evt.stopPropagation(); - - if (evt.shiftKey) { - // Move to the previous item in the list. - if (component.activeButtonIndex <= 0) { - component.activeActionButtonIndex = 0; - component.audioService.playOopsSound(); - } else { - component.activeButtonIndex--; - } - } else { - // Move to the next item in the list. - if (component.activeButtonIndex == component.numInteractiveElements(component) - 1) { - component.audioService.playOopsSound(); - } else { - component.activeButtonIndex++; - } - } - - component.focusOnOption(component.activeButtonIndex, component); - }, - // Escape key: closes the modal. - '27': function() { - component.dismissModal(); - }, - // Up key: no-op. - '38': function(evt) { - evt.preventDefault(); - }, - // Down key: no-op. - '40': function(evt) { - evt.preventDefault(); - } - }); -} - -Blockly.CommonModal.getInteractiveElements = function(component) { - return Array.prototype.filter.call( - component.getInteractiveContainer().elements, function(element) { - if (element.type === 'hidden') { - return false; - } - if (element.disabled) { - return false; - } - if (element.tabIndex < 0) { - return false; - } - return true; - }); -}; - -Blockly.CommonModal.numInteractiveElements = function(component) { - var elements = this.getInteractiveElements(component); - return elements.length; -}; - -Blockly.CommonModal.focusOnOption = function(index, component) { - var elements = this.getInteractiveElements(component); - var button = elements[index]; - button.focus(); -}; - -Blockly.CommonModal.hideModal = function() { - this.modalIsVisible = false; - this.keyboardInputService.clearOverride(); -}; \ No newline at end of file diff --git a/accessible/field-segment.component.js b/accessible/field-segment.component.js deleted file mode 100644 index 8dda20d71d3..00000000000 --- a/accessible/field-segment.component.js +++ /dev/null @@ -1,206 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Component that renders a "field segment" (a group - * of non-editable Blockly.Field followed by 0 or 1 editable Blockly.Field) - * in a block. Also handles any interactions with the field. - * @author madeeha@google.com (Madeeha Ghori) - */ - -goog.provide('blocklyApp.FieldSegmentComponent'); - -goog.require('blocklyApp.NotificationsService'); -goog.require('blocklyApp.TranslatePipe'); -goog.require('blocklyApp.VariableModalService'); - - -blocklyApp.FieldSegmentComponent = ng.core.Component({ - selector: 'blockly-field-segment', - template: ` - - - - `, - inputs: ['prefixFields', 'mainField', 'mainFieldId', 'level'], - pipes: [blocklyApp.TranslatePipe] -}) -.Class({ - constructor: [ - blocklyApp.NotificationsService, - blocklyApp.VariableModalService, - function(notificationsService, variableModalService) { - this.notificationsService = notificationsService; - this.variableModalService = variableModalService; - this.dropdownOptions = []; - this.rawOptions = []; - }], - // Angular2 hook - called after initialization. - ngAfterContentInit: function() { - if (this.mainField) { - this.mainField.initModel(); - } - }, - // Angular2 hook - called to check if the cached component needs an update. - ngDoCheck: function() { - if (this.isDropdown() && this.shouldBreakCache()) { - this.optionValue = this.mainField.getValue(); - this.fieldValue = this.mainField.getValue(); - this.rawOptions = this.mainField.getOptions(); - this.dropdownOptions = this.rawOptions.map(function(valueAndText) { - return { - text: valueAndText[0], - value: valueAndText[1] - }; - }); - - // Set the currently selected value to the variable on the field. - for (var i = 0; i < this.dropdownOptions.length; i++) { - if (this.dropdownOptions[i].text === this.fieldValue) { - this.selectedOption = this.dropdownOptions[i].value; - } - } - } - }, - // Returns whether the mutable, cached information needs to be refreshed. - shouldBreakCache: function() { - var newOptions = this.mainField.getOptions(); - if (newOptions.length != this.rawOptions.length) { - return true; - } - - for (var i = 0; i < this.rawOptions.length; i++) { - // Compare the value of the cached options with the values in the field. - if (newOptions[i][0] != this.rawOptions[i][0]) { - return true; - } - } - - if (this.fieldValue != this.mainField.getValue()) { - return true; - } - - return false; - }, - // Gets the prefix text, to be printed before a field. - getPrefixText: function() { - var prefixTexts = this.prefixFields.map(function(prefixField) { - return prefixField.getText(); - }); - return prefixTexts.join(' '); - }, - // Gets the description, for labeling a field. - getFieldDescription: function() { - var description = this.mainField.getText(); - if (this.prefixFields.length > 0) { - description = this.getPrefixText() + ': ' + description; - } - return description; - }, - // Returns true if the field is text input, false otherwise. - isTextInput: function() { - return this.mainField instanceof Blockly.FieldTextInput && - !(this.mainField instanceof Blockly.FieldNumber); - }, - // Returns true if the field is number input, false otherwise. - isNumberInput: function() { - return this.mainField instanceof Blockly.FieldNumber; - }, - // Returns true if the field is a dropdown, false otherwise. - isDropdown: function() { - return this.mainField instanceof Blockly.FieldDropdown; - }, - // Sets the text value on the underlying field. - setTextValue: function(newValue) { - this.mainField.setValue(newValue); - }, - // Sets the number value on the underlying field. - setNumberValue: function(newValue) { - // Do not permit a residual value of NaN after a backspace event. - this.mainField.setValue(newValue || 0); - }, - // Confirm a selection for dropdown fields. - selectOption: function() { - if (this.optionValue != Blockly.RENAME_VARIABLE_ID && this.optionValue != - Blockly.DELETE_VARIABLE_ID) { - this.mainField.setValue(this.optionValue); - } - - if (this.optionValue == Blockly.RENAME_VARIABLE_ID) { - this.variableModalService.showRenameModal_(this.mainField.getValue()); - } - - if (this.optionValue == Blockly.DELETE_VARIABLE_ID) { - this.variableModalService.showRemoveModal_(this.mainField.getValue()); - } - }, - // Sets the value on a dropdown input. - setDropdownValue: function(optionValue) { - this.optionValue = optionValue - if (this.optionValue == 'NO_ACTION') { - return; - } - - var optionText = undefined; - for (var i = 0; i < this.dropdownOptions.length; i++) { - if (this.dropdownOptions[i].value == optionValue) { - optionText = this.dropdownOptions[i].text; - break; - } - } - - if (!optionText) { - throw Error( - 'There is no option text corresponding to the value: ' + - this.optionValue); - } - - this.notificationsService.speak('Selected option ' + optionText); - } -}); diff --git a/accessible/keyboard-input.service.js b/accessible/keyboard-input.service.js deleted file mode 100644 index 63827563719..00000000000 --- a/accessible/keyboard-input.service.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Service for handling keyboard input. - * - * @author sll@google.com (Sean Lip) - */ - -goog.provide('blocklyApp.KeyboardInputService'); - - -blocklyApp.KeyboardInputService = ng.core.Class({ - constructor: [function() { - // Default custom actions for global keystrokes. The keys of this object - // are string representations of the key codes. - this.keysToActions = {}; - // Override for the default keysToActions mapping (e.g. in a modal - // context). - this.keysToActionsOverride = null; - - // Attach a keydown handler to the entire window. - var that = this; - document.addEventListener('keydown', function(evt) { - var stringifiedKeycode = String(evt.keyCode); - var actionsObject = that.keysToActionsOverride || that.keysToActions; - - if (actionsObject.hasOwnProperty(stringifiedKeycode)) { - actionsObject[stringifiedKeycode](evt); - } - }); - }], - setOverride: function(newKeysToActions) { - this.keysToActionsOverride = newKeysToActions; - }, - addOverride: function(keyCode, action) { - this.keysToActionsOverride[keyCode] = action; - }, - clearOverride: function() { - this.keysToActionsOverride = null; - } -}); diff --git a/accessible/libs/README b/accessible/libs/README deleted file mode 100644 index ebf4d96ba7c..00000000000 --- a/accessible/libs/README +++ /dev/null @@ -1,15 +0,0 @@ -This folder contains the following dependencies for accessible Blockly: - -* Angular2 (angular2-all.umd.min.js, angular2-polyfills.min.js) -* RxJava (Rx.umd.min) - -Used for data binding between the core Blockly workspace and accessible Blockly. -RxJava is required by Angular2. -Fetched from https://code.angularjs.org/ -The current version is 2.0.0-beta.16. - -* ES6 Shim - -Required by Angular2, for Javascript files. -Fetched from https://github.com/paulmillr/es6-shim -The current version is 0.35.1. diff --git a/accessible/libs/Rx.umd.min.js b/accessible/libs/Rx.umd.min.js deleted file mode 100644 index 38c0666b4b3..00000000000 --- a/accessible/libs/Rx.umd.min.js +++ /dev/null @@ -1,748 +0,0 @@ -/** - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2015-2016 Netflix, Inc. - - 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. - -**/ -/** - @license - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2015-2016 Netflix, Inc. - - 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. - - **/ -(function(w){"object"===typeof exports&&"undefined"!==typeof module?module.exports=w():"function"===typeof define&&define.amd?define([],w):("undefined"!==typeof window?window:"undefined"!==typeof global?global:"undefined"!==typeof self?self:this).Rx=w()})(function(){return function a(b,f,g){function k(e,d){if(!f[e]){if(!b[e]){var c="function"==typeof require&&require;if(!d&&c)return c(e,!0);if(l)return l(e,!0);c=Error("Cannot find module '"+e+"'");throw c.code="MODULE_NOT_FOUND",c;}c=f[e]={exports:{}}; -b[e][0].call(c.exports,function(a){var c=b[e][1][a];return k(c?c:a)},c,c.exports,a,b,f,g)}return f[e].exports}for(var l="function"==typeof require&&require,h=0;h=n?h.complete():(c=b?b(c[e],e):c[e],h.next(c),a.index=e+1,this.schedule(a)))};e.prototype._subscribe=function(a){var c=this.arrayLike,m=this.mapFn, -n=this.scheduler,b=c.length;if(n)return n.schedule(e.dispatch,0,{arrayLike:c,index:0,length:b,mapFn:m,subscriber:a});for(n=0;n=a.count?b.complete():(b.next(d[e]),b.isUnsubscribed||(a.index=e+1,this.schedule(a)))};d.prototype._subscribe=function(a){var e=this.array,n=e.length,b=this.scheduler;if(b)return b.schedule(d.dispatch,0,{array:e,index:0,count:n,subscriber:a});for(b=0;bd)this.period=0;c&&"function"===typeof c.schedule||(this.scheduler=l.asap)}g(e,a);e.create=function(a,c){void 0===a&&(a=0);void 0===c&&(c=l.asap);return new e(a,c)};e.dispatch=function(a){var c=a.subscriber,e=a.period;c.next(a.index);c.isUnsubscribed||(a.index+=1,this.schedule(a,e))};e.prototype._subscribe=function(a){var c=this.period;a.add(this.scheduler.schedule(e.dispatch,c,{index:0,subscriber:a,period:c}))};return e}(b.Observable);f.IntervalObservable=a},{"../Observable":3,"../scheduler/asap":224, -"../util/isNumeric":243}],128:[function(a,b,f){var g=this&&this.__extends||function(a,c){function d(){this.constructor=a}for(var e in c)c.hasOwnProperty(e)&&(a[e]=c[e]);a.prototype=null===c?Object.create(c):(d.prototype=c.prototype,new d)},k=a("../util/root"),l=a("../util/isObject"),h=a("../util/tryCatch");b=a("../Observable");var e=a("../util/isFunction"),d=a("../util/SymbolShim"),c=a("../util/errorObject");a=function(a){function b(c,h,f,q){a.call(this);if(null==c)throw Error("iterator cannot be null."); -if(l.isObject(h))this.thisArg=h,this.scheduler=f;else if(e.isFunction(h))this.project=h,this.thisArg=f,this.scheduler=q;else if(null!=h)throw Error("When provided, `project` must be a function.");if((h=c[d.SymbolShim.iterator])||"string"!==typeof c)if(h||void 0===c.length){if(!h)throw new TypeError("Object is not iterable");c=c[d.SymbolShim.iterator]()}else c=new n(c);else c=new m(c);this.iterator=c}g(b,a);b.create=function(a,c,d,e){return new b(a,c,d,e)};b.dispatch=function(a){var d=a.index,e=a.thisArg, -m=a.project,b=a.iterator,n=a.subscriber;a.hasError?n.error(a.error):(b=b.next(),b.done?n.complete():(m?(b=h.tryCatch(m).call(e,b.value,d),b===c.errorObject?(a.error=c.errorObject.e,a.hasError=!0):(n.next(b),a.index=d+1)):(n.next(b.value),a.index=d+1),n.isUnsubscribed||this.schedule(a)))};b.prototype._subscribe=function(a){var d=0,e=this.iterator,m=this.project,n=this.thisArg,f=this.scheduler;if(f)return f.schedule(b.dispatch,0,{index:d,thisArg:n,project:m,iterator:e,subscriber:a});do{f=e.next();if(f.done){a.complete(); -break}else if(m){f=h.tryCatch(m).call(n,f.value,d++);if(f===c.errorObject){a.error(c.errorObject.e);break}a.next(f)}else a.next(f.value);if(a.isUnsubscribed)break}while(1)};return b}(b.Observable);f.IteratorObservable=a;var m=function(){function a(c,d,e){void 0===d&&(d=0);void 0===e&&(e=c.length);this.str=c;this.idx=d;this.len=e}a.prototype[d.SymbolShim.iterator]=function(){return this};a.prototype.next=function(){return this.idxm?-1:1;e=m*Math.floor(Math.abs(e));e=0>=e?0:e>q?q:e}this.arr=c;this.idx=d;this.len=e}a.prototype[d.SymbolShim.iterator]=function(){return this};a.prototype.next=function(){return this.idx=a.end?c.complete():(c.next(e),c.isUnsubscribed||(a.index=d+1,a.start=e+1,this.schedule(a)))};b.prototype._subscribe=function(a){var e=0,d=this.start,c=this.end,m=this.scheduler;if(m)return m.schedule(b.dispatch, -0,{index:e,end:c,start:d,subscriber:a});do{if(e++>=c){a.complete();break}a.next(d++);if(a.isUnsubscribed)break}while(1)};return b}(a("../Observable").Observable);f.RangeObservable=a},{"../Observable":3}],132:[function(a,b,f){var g=this&&this.__extends||function(a,b){function h(){this.constructor=a}for(var e in b)b.hasOwnProperty(e)&&(a[e]=b[e]);a.prototype=null===b?Object.create(b):(h.prototype=b.prototype,new h)};a=function(a){function b(h,e){a.call(this);this.value=h;this.scheduler=e;this._isScalar= -!0}g(b,a);b.create=function(a,e){return new b(a,e)};b.dispatch=function(a){var e=a.value,d=a.subscriber;a.done?d.complete():(d.next(e),d.isUnsubscribed||(a.done=!0,this.schedule(a)))};b.prototype._subscribe=function(a){var e=this.value,d=this.scheduler;if(d)return d.schedule(b.dispatch,0,{done:!1,value:e,subscriber:a});a.next(e);a.isUnsubscribed||a.complete()};return b}(a("../Observable").Observable);f.ScalarObservable=a},{"../Observable":3}],133:[function(a,b,f){var g=this&&this.__extends||function(a, -e){function d(){this.constructor=a}for(var c in e)e.hasOwnProperty(c)&&(a[c]=e[c]);a.prototype=null===e?Object.create(e):(d.prototype=e.prototype,new d)};b=a("../Observable");var k=a("../scheduler/asap"),l=a("../util/isNumeric");a=function(a){function e(d,c,e){void 0===c&&(c=0);void 0===e&&(e=k.asap);a.call(this);this.source=d;this.delayTime=c;this.scheduler=e;if(!l.isNumeric(c)||0>c)this.delayTime=0;e&&"function"===typeof e.schedule||(this.scheduler=k.asap)}g(e,a);e.create=function(a,c,m){void 0=== -c&&(c=0);void 0===m&&(m=k.asap);return new e(a,c,m)};e.dispatch=function(a){return a.source.subscribe(a.subscriber)};e.prototype._subscribe=function(a){return this.scheduler.schedule(e.dispatch,this.delayTime,{source:this.source,subscriber:a})};return e}(b.Observable);f.SubscribeOnObservable=a},{"../Observable":3,"../scheduler/asap":224,"../util/isNumeric":243}],134:[function(a,b,f){var g=this&&this.__extends||function(a,c){function e(){this.constructor=a}for(var b in c)c.hasOwnProperty(b)&&(a[b]= -c[b]);a.prototype=null===c?Object.create(c):(e.prototype=c.prototype,new e)},k=a("../util/isNumeric");b=a("../Observable");var l=a("../scheduler/asap"),h=a("../util/isScheduler"),e=a("../util/isDate");a=function(a){function c(c,b,f){void 0===c&&(c=0);a.call(this);this.period=-1;this.dueTime=0;k.isNumeric(b)?this.period=1>+b&&1||+b:h.isScheduler(b)&&(f=b);h.isScheduler(f)||(f=l.asap);this.scheduler=f;this.dueTime=e.isDate(c)?+c-this.scheduler.now():c}g(c,a);c.create=function(a,d,e){void 0===a&&(a= -0);return new c(a,d,e)};c.dispatch=function(a){var c=a.index,d=a.period,e=a.subscriber;e.next(c);if(!e.isUnsubscribed){if(-1===d)return e.complete();a.index=c+1;this.schedule(a,d)}};c.prototype._subscribe=function(a){return this.scheduler.schedule(c.dispatch,this.dueTime,{index:0,period:this.period,subscriber:a})};return c}(b.Observable);f.TimerObservable=a},{"../Observable":3,"../scheduler/asap":224,"../util/isDate":241,"../util/isNumeric":243,"../util/isScheduler":246}],135:[function(a,b,f){var g= -this&&this.__extends||function(a,d){function c(){this.constructor=a}for(var b in d)d.hasOwnProperty(b)&&(a[b]=d[b]);a.prototype=null===d?Object.create(d):(c.prototype=d.prototype,new c)};b=a("../OuterSubscriber");var k=a("../util/subscribeToResult");f.buffer=function(a){return this.lift(new l(a))};var l=function(){function a(d){this.closingNotifier=d}a.prototype.call=function(a){return new h(a,this.closingNotifier)};return a}(),h=function(a){function d(c,d){a.call(this,c);this.buffer=[];this.add(k.subscribeToResult(this, -d))}g(d,a);d.prototype._next=function(a){this.buffer.push(a)};d.prototype.notifyNext=function(a,d,e,b,h){a=this.buffer;this.buffer=[];this.destination.next(a)};return d}(b.OuterSubscriber)},{"../OuterSubscriber":6,"../util/subscribeToResult":250}],136:[function(a,b,f){var g=this&&this.__extends||function(a,e){function d(){this.constructor=a}for(var c in e)e.hasOwnProperty(c)&&(a[c]=e[c]);a.prototype=null===e?Object.create(e):(d.prototype=e.prototype,new d)};a=a("../Subscriber");f.bufferCount=function(a, -e){void 0===e&&(e=null);return this.lift(new k(a,e))};var k=function(){function a(e,d){this.bufferSize=e;this.startBufferEvery=d}a.prototype.call=function(a){return new l(a,this.bufferSize,this.startBufferEvery)};return a}(),l=function(a){function e(d,c,e){a.call(this,d);this.bufferSize=c;this.startBufferEvery=e;this.buffers=[[]];this.count=0}g(e,a);e.prototype._next=function(a){var c=this.count+=1,e=this.destination,b=this.bufferSize,h=this.buffers,f=h.length,k=-1;0===c%(null==this.startBufferEvery? -b:this.startBufferEvery)&&h.push([]);for(c=0;c=d[0].time-e.now();)d.shift().notification.observe(b);0(d||0)?Number.POSITIVE_INFINITY:d;return this.lift(new e(a,d,b))};var e=function(){function a(c,d,e){this.project=c;this.concurrent=d;this.scheduler=e}a.prototype.call=function(a){return new d(a,this.project,this.concurrent,this.scheduler)};return a}();f.ExpandOperator=e;var d=function(a){function d(e,b,m,h){a.call(this, -e);this.project=b;this.concurrent=m;this.scheduler=h;this.active=this.index=0;this.hasCompleted=!1;ma?this.lift(new l(-1,this)):this.lift(new l(a-1,this))};var l=function(){function a(d,c){this.count=d;this.source=c}a.prototype.call=function(a){return new h(a, -this.count,this.source)};return a}(),h=function(a){function d(c,d,b){a.call(this,c);this.count=d;this.source=b}g(d,a);d.prototype.complete=function(){if(!this.isStopped){var c=this.source,d=this.count;if(0===d)return a.prototype.complete.call(this);-1this.total&&this.destination.next(a)};return b}(a.Subscriber)},{"../Subscriber":9}],194:[function(a,b,f){var g=this&&this.__extends||function(a,d){function c(){this.constructor=a}for(var b in d)d.hasOwnProperty(b)&&(a[b]=d[b]);a.prototype=null===d?Object.create(d):(c.prototype=d.prototype,new c)};b=a("../OuterSubscriber"); -var k=a("../util/subscribeToResult");f.skipUntil=function(a){return this.lift(new l(a))};var l=function(){function a(d){this.notifier=d}a.prototype.call=function(a){return new h(a,this.notifier)};return a}(),h=function(a){function d(c,d){a.call(this,c);this.isInnerStopped=this.hasValue=!1;this.add(k.subscribeToResult(this,d))}g(d,a);d.prototype._next=function(c){this.hasValue&&a.prototype._next.call(this,c)};d.prototype._complete=function(){this.isInnerStopped?a.prototype._complete.call(this):this.unsubscribe()}; -d.prototype.notifyNext=function(a,d,b,e,f){this.hasValue=!0};d.prototype.notifyComplete=function(){this.isInnerStopped=!0;this.isStopped&&a.prototype._complete.call(this)};return d}(b.OuterSubscriber)},{"../OuterSubscriber":6,"../util/subscribeToResult":250}],195:[function(a,b,f){var g=this&&this.__extends||function(a,b){function d(){this.constructor=a}for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);a.prototype=null===b?Object.create(b):(d.prototype=b.prototype,new d)};a=a("../Subscriber");f.skipWhile= -function(a){return this.lift(new k(a))};var k=function(){function a(b){this.predicate=b}a.prototype.call=function(a){return new l(a,this.predicate)};return a}(),l=function(a){function b(d,c){a.call(this,d);this.predicate=c;this.skipping=!0;this.index=0}g(b,a);b.prototype._next=function(a){var c=this.destination;this.skipping&&this.tryCallPredicate(a);this.skipping||c.next(a)};b.prototype.tryCallPredicate=function(a){try{this.skipping=!!this.predicate(a,this.index++)}catch(c){this.destination.error(c)}}; -return b}(a.Subscriber)},{"../Subscriber":9}],196:[function(a,b,f){var g=a("../observable/ArrayObservable"),k=a("../observable/ScalarObservable"),l=a("../observable/EmptyObservable"),h=a("./concat"),e=a("../util/isScheduler");f.startWith=function(){for(var a=[],c=0;cthis.total)throw new k.ArgumentOutOfRangeError;}a.prototype.call= -function(a){return new e(a,this.total)};return a}(),e=function(a){function c(c,b){a.call(this,c);this.total=b;this.count=0}g(c,a);c.prototype._next=function(a){var c=this.total;++this.count<=c&&(this.destination.next(a),this.count===c&&this.destination.complete())};return c}(b.Subscriber)},{"../Subscriber":9,"../observable/EmptyObservable":121,"../util/ArgumentOutOfRangeError":231}],202:[function(a,b,f){var g=this&&this.__extends||function(a,c){function b(){this.constructor=a}for(var e in c)c.hasOwnProperty(e)&& -(a[e]=c[e]);a.prototype=null===c?Object.create(c):(b.prototype=c.prototype,new b)};b=a("../Subscriber");var k=a("../util/ArgumentOutOfRangeError"),l=a("../observable/EmptyObservable");f.takeLast=function(a){return 0===a?new l.EmptyObservable:this.lift(new h(a))};var h=function(){function a(c){this.total=c;if(0>this.total)throw new k.ArgumentOutOfRangeError;}a.prototype.call=function(a){return new e(a,this.total)};return a}(),e=function(a){function c(c,b){a.call(this,c);this.total=b;this.index=this.count= -0;this.ring=Array(b)}g(c,a);c.prototype._next=function(a){var c=this.index,d=this.ring,b=this.total,e=this.count;1this.index};a.prototype.hasCompleted=function(){return this.array.length===this.index};return a}(),u=function(a){function b(c,d,e,f){a.call(this,c);this.parent=d;this.observable=e;this.index=f;this.stillUnsubscribed=!0;this.buffer=[];this.isComplete=!1}k(b,a);b.prototype[c.SymbolShim.iterator]=function(){return this};b.prototype.next= -function(){var a=this.buffer;return 0===a.length&&this.isComplete?{done:!0}:{value:a.shift(),done:!1}};b.prototype.hasValue=function(){return 0=b?this.scheduleNow(a,d):this.scheduleLater(a,b,d)};a.prototype.scheduleNow=function(a,b){return(new g.QueueAction(this,a)).schedule(b)};a.prototype.scheduleLater=function(a,b,d){return(new k.FutureAction(this,a)).schedule(d,b)};return a}(); -f.QueueScheduler=a},{"./FutureAction":221,"./QueueAction":222}],224:[function(a,b,f){a=a("./AsapScheduler");f.asap=new a.AsapScheduler},{"./AsapScheduler":220}],225:[function(a,b,f){a=a("./QueueScheduler");f.queue=new a.QueueScheduler},{"./QueueScheduler":223}],226:[function(a,b,f){var g=this&&this.__extends||function(a,b){function f(){this.constructor=a}for(var e in b)b.hasOwnProperty(e)&&(a[e]=b[e]);a.prototype=null===b?Object.create(b):(f.prototype=b.prototype,new f)};a=function(a){function b(){a.apply(this, -arguments);this.value=null;this.hasNext=!1}g(b,a);b.prototype._subscribe=function(b){this.hasCompleted&&this.hasNext&&b.next(this.value);return a.prototype._subscribe.call(this,b)};b.prototype._next=function(a){this.value=a;this.hasNext=!0};b.prototype._complete=function(){var a=-1,b=this.observers,d=b.length;this.isUnsubscribed=!0;if(this.hasNext)for(;++ac?1:c; -this._windowTime=1>d?1:d}g(b,a);b.prototype._next=function(b){var d=this._getNow();this.events.push(new h(d,b));this._trimBufferThenGetEvents(d);a.prototype._next.call(this,b)};b.prototype._subscribe=function(b){var d=this._trimBufferThenGetEvents(this._getNow()),f=this.scheduler;f&&b.add(b=new l.ObserveOnSubscriber(b,f));for(var f=-1,g=d.length;++fb&&(g=Math.max(g,f-b));0o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(5),a=n(6),u=n(7),c=function(t){function e(e){t.call(this),this.attributeName=e}return r(e,t),Object.defineProperty(e.prototype,"token",{get:function(){return this},enumerable:!0,configurable:!0}),e.prototype.toString=function(){return"@Attribute("+s.stringify(this.attributeName)+")"},e=i([s.CONST(),o("design:paramtypes",[String])],e)}(u.DependencyMetadata);e.AttributeMetadata=c;var p=function(t){function e(e,n){var r=void 0===n?{}:n,i=r.descendants,o=void 0===i?!1:i,s=r.first,a=void 0===s?!1:s,u=r.read,c=void 0===u?null:u;t.call(this),this._selector=e,this.descendants=o,this.first=a,this.read=c}return r(e,t),Object.defineProperty(e.prototype,"isViewQuery",{get:function(){return!1},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"selector",{get:function(){return a.resolveForwardRef(this._selector)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"isVarBindingQuery",{get:function(){return s.isString(this.selector)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"varBindings",{get:function(){return this.selector.split(",")},enumerable:!0,configurable:!0}),e.prototype.toString=function(){return"@Query("+s.stringify(this.selector)+")"},e=i([s.CONST(),o("design:paramtypes",[Object,Object])],e)}(u.DependencyMetadata);e.QueryMetadata=p;var l=function(t){function e(e,n){var r=void 0===n?{}:n,i=r.descendants,o=void 0===i?!1:i,s=r.read,a=void 0===s?null:s;t.call(this,e,{descendants:o,read:a})}return r(e,t),e=i([s.CONST(),o("design:paramtypes",[Object,Object])],e)}(p);e.ContentChildrenMetadata=l;var h=function(t){function e(e,n){var r=(void 0===n?{}:n).read,i=void 0===r?null:r;t.call(this,e,{descendants:!0,first:!0,read:i})}return r(e,t),e=i([s.CONST(),o("design:paramtypes",[Object,Object])],e)}(p);e.ContentChildMetadata=h;var f=function(t){function e(e,n){var r=void 0===n?{}:n,i=r.descendants,o=void 0===i?!1:i,s=r.first,a=void 0===s?!1:s,u=r.read,c=void 0===u?null:u;t.call(this,e,{descendants:o,first:a,read:c})}return r(e,t),Object.defineProperty(e.prototype,"isViewQuery",{get:function(){return!0},enumerable:!0,configurable:!0}),e.prototype.toString=function(){return"@ViewQuery("+s.stringify(this.selector)+")"},e=i([s.CONST(),o("design:paramtypes",[Object,Object])],e)}(p);e.ViewQueryMetadata=f;var d=function(t){function e(e,n){var r=(void 0===n?{}:n).read,i=void 0===r?null:r;t.call(this,e,{descendants:!0,read:i})}return r(e,t),e=i([s.CONST(),o("design:paramtypes",[Object,Object])],e)}(f);e.ViewChildrenMetadata=d;var v=function(t){function e(e,n){var r=(void 0===n?{}:n).read,i=void 0===r?null:r;t.call(this,e,{descendants:!0,first:!0,read:i})}return r(e,t),e=i([s.CONST(),o("design:paramtypes",[Object,Object])],e)}(f);e.ViewChildMetadata=v},function(t,e){(function(t){"use strict";function n(t){Zone.current.scheduleMicroTask("scheduleMicrotask",t)}function r(t){return t.name?t.name:typeof t}function i(){H=!0}function o(){if(H)throw"Cannot enable prod mode after platform setup.";W=!1}function s(){return W}function a(t){return t}function u(){return function(t){return t}}function c(t){return void 0!==t&&null!==t}function p(t){return void 0===t||null===t}function l(t){return"boolean"==typeof t}function h(t){return"number"==typeof t}function f(t){return"string"==typeof t}function d(t){return"function"==typeof t}function v(t){return d(t)}function y(t){return"object"==typeof t&&null!==t}function m(t){return t instanceof U.Promise}function g(t){return Array.isArray(t)}function _(t){return t instanceof e.Date&&!isNaN(t.valueOf())}function b(){}function P(t){if("string"==typeof t)return t;if(void 0===t||null===t)return""+t;if(t.name)return t.name;if(t.overriddenName)return t.overriddenName;var e=t.toString(),n=e.indexOf("\n");return-1===n?e:e.substring(0,n)}function E(t){return t}function w(t,e){return t}function C(t,e){return t[e]}function R(t,e){return t===e||"number"==typeof t&&"number"==typeof e&&isNaN(t)&&isNaN(e)}function S(t){return t}function O(t){return p(t)?null:t}function T(t){return p(t)?!1:t}function x(t){return null!==t&&("function"==typeof t||"object"==typeof t)}function A(t){console.log(t)}function I(t,e,n){for(var r=e.split("."),i=t;r.length>1;){var o=r.shift();i=i.hasOwnProperty(o)&&c(i[o])?i[o]:i[o]={}}(void 0===i||null===i)&&(i={}),i[r.shift()]=n}function M(){if(p(Y))if(c(Symbol)&&c(Symbol.iterator))Y=Symbol.iterator;else for(var t=Object.getOwnPropertyNames(Map.prototype),e=0;e=0&&t[r]==e;r--)n--;t=t.substring(0,n)}return t},t.replace=function(t,e,n){return t.replace(e,n)},t.replaceAll=function(t,e,n){return t.replace(e,n)},t.slice=function(t,e,n){return void 0===e&&(e=0),void 0===n&&(n=null),t.slice(e,null===n?void 0:n)},t.replaceAllMapped=function(t,e,n){return t.replace(e,function(){for(var t=[],e=0;et?-1:t>e?1:0},t}();e.StringWrapper=X;var q=function(){function t(t){void 0===t&&(t=[]),this.parts=t}return t.prototype.add=function(t){this.parts.push(t)},t.prototype.toString=function(){return this.parts.join("")},t}();e.StringJoiner=q;var G=function(t){function e(e){t.call(this),this.message=e}return F(e,t),e.prototype.toString=function(){return this.message},e}(Error);e.NumberParseError=G;var z=function(){function t(){}return t.toFixed=function(t,e){return t.toFixed(e)},t.equal=function(t,e){return t===e},t.parseIntAutoRadix=function(t){var e=parseInt(t);if(isNaN(e))throw new G("Invalid integer literal when parsing "+t);return e},t.parseInt=function(t,e){if(10==e){if(/^(\-|\+)?[0-9]+$/.test(t))return parseInt(t,e)}else if(16==e){if(/^(\-|\+)?[0-9ABCDEFabcdef]+$/.test(t))return parseInt(t,e)}else{var n=parseInt(t,e);if(!isNaN(n))return n}throw new G("Invalid integer literal when parsing "+t+" in base "+e)},t.parseFloat=function(t){return parseFloat(t)},Object.defineProperty(t,"NaN",{get:function(){return NaN},enumerable:!0,configurable:!0}),t.isNaN=function(t){return isNaN(t)},t.isInteger=function(t){return Number.isInteger(t)},t}();e.NumberWrapper=z,e.RegExp=U.RegExp;var K=function(){function t(){}return t.create=function(t,e){return void 0===e&&(e=""),e=e.replace(/g/g,""),new U.RegExp(t,e+"g")},t.firstMatch=function(t,e){return t.lastIndex=0,t.exec(e)},t.test=function(t,e){return t.lastIndex=0,t.test(e)},t.matcher=function(t,e){return t.lastIndex=0,{re:t,input:e}},t.replaceAll=function(t,e,n){var r=t.exec(e),i="";t.lastIndex=0;for(var o=0;r;)i+=e.substring(o,r.index),i+=n(r),o=r.index+r[0].length,t.lastIndex=o,r=t.exec(e);return i+=e.substring(o)},t}();e.RegExpWrapper=K;var $=function(){function t(){}return t.next=function(t){return t.re.exec(t.input)},t}();e.RegExpMatcherWrapper=$;var Q=function(){function t(){}return t.apply=function(t,e){return t.apply(null,e)},t}();e.FunctionWrapper=Q,e.looseIdentical=R,e.getMapKey=S,e.normalizeBlank=O,e.normalizeBool=T,e.isJsObject=x,e.print=A;var J=function(){function t(){}return t.parse=function(t){return U.JSON.parse(t)},t.stringify=function(t){return U.JSON.stringify(t,null,2)},t}();e.Json=J;var Z=function(){function t(){}return t.create=function(t,n,r,i,o,s,a){return void 0===n&&(n=1),void 0===r&&(r=1),void 0===i&&(i=0),void 0===o&&(o=0),void 0===s&&(s=0),void 0===a&&(a=0),new e.Date(t,n-1,r,i,o,s,a)},t.fromISOString=function(t){return new e.Date(t)},t.fromMillis=function(t){return new e.Date(t)},t.toMillis=function(t){return t.getTime()},t.now=function(){return new e.Date},t.toJson=function(t){return t.toJSON()},t}();e.DateWrapper=Z,e.setValueOnPath=I;var Y=null;e.getSymbolIterator=M,e.evalExpression=k,e.isPrimitive=N,e.hasConstructor=D,e.bitWiseOr=V,e.bitWiseAnd=j,e.escape=L}).call(e,function(){return this}())},function(t,e,n){"use strict";function r(t){for(var n in t)e.hasOwnProperty(n)||(e[n]=t[n])}var i=n(7);e.InjectMetadata=i.InjectMetadata,e.OptionalMetadata=i.OptionalMetadata,e.InjectableMetadata=i.InjectableMetadata,e.SelfMetadata=i.SelfMetadata,e.HostMetadata=i.HostMetadata,e.SkipSelfMetadata=i.SkipSelfMetadata,e.DependencyMetadata=i.DependencyMetadata,r(n(8));var o=n(10);e.forwardRef=o.forwardRef,e.resolveForwardRef=o.resolveForwardRef;var s=n(11);e.Injector=s.Injector;var a=n(16);e.ReflectiveInjector=a.ReflectiveInjector;var u=n(24);e.Binding=u.Binding,e.ProviderBuilder=u.ProviderBuilder,e.bind=u.bind,e.Provider=u.Provider,e.provide=u.provide;var c=n(17);e.ResolvedReflectiveFactory=c.ResolvedReflectiveFactory,e.ReflectiveDependency=c.ReflectiveDependency;var p=n(22);e.ReflectiveKey=p.ReflectiveKey;var l=n(23);e.NoProviderError=l.NoProviderError,e.AbstractProviderError=l.AbstractProviderError,e.CyclicDependencyError=l.CyclicDependencyError,e.InstantiationError=l.InstantiationError,e.InvalidProviderError=l.InvalidProviderError,e.NoAnnotationError=l.NoAnnotationError,e.OutOfBoundsError=l.OutOfBoundsError;var h=n(25);e.OpaqueToken=h.OpaqueToken},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=function(){function t(t){this.token=t}return t.prototype.toString=function(){return"@Inject("+o.stringify(this.token)+")"},t=r([o.CONST(),i("design:paramtypes",[Object])],t)}();e.InjectMetadata=s;var a=function(){function t(){}return t.prototype.toString=function(){return"@Optional()"},t=r([o.CONST(),i("design:paramtypes",[])],t)}();e.OptionalMetadata=a;var u=function(){function t(){}return Object.defineProperty(t.prototype,"token",{get:function(){return null},enumerable:!0,configurable:!0}),t=r([o.CONST(),i("design:paramtypes",[])],t)}();e.DependencyMetadata=u;var c=function(){function t(){}return t=r([o.CONST(),i("design:paramtypes",[])],t)}();e.InjectableMetadata=c;var p=function(){function t(){}return t.prototype.toString=function(){return"@Self()"},t=r([o.CONST(),i("design:paramtypes",[])],t)}();e.SelfMetadata=p;var l=function(){function t(){}return t.prototype.toString=function(){return"@SkipSelf()"},t=r([o.CONST(),i("design:paramtypes",[])],t)}();e.SkipSelfMetadata=l;var h=function(){function t(){}return t.prototype.toString=function(){return"@Host()"},t=r([o.CONST(),i("design:paramtypes",[])],t)}();e.HostMetadata=h},function(t,e,n){"use strict";var r=n(7),i=n(9);e.Inject=i.makeParamDecorator(r.InjectMetadata),e.Optional=i.makeParamDecorator(r.OptionalMetadata),e.Injectable=i.makeDecorator(r.InjectableMetadata),e.Self=i.makeParamDecorator(r.SelfMetadata),e.Host=i.makeParamDecorator(r.HostMetadata),e.SkipSelf=i.makeParamDecorator(r.SkipSelfMetadata)},function(t,e,n){"use strict";function r(t){return c.isFunction(t)&&t.hasOwnProperty("annotation")&&(t=t.annotation),t}function i(t,e){if(t===Object||t===String||t===Function||t===Number||t===Array)throw new Error("Can not use native "+c.stringify(t)+" as constructor");if(c.isFunction(t))return t;if(t instanceof Array){var n=t,i=t[t.length-1];if(!c.isFunction(i))throw new Error("Last position of Class method array must be Function in key "+e+" was '"+c.stringify(i)+"'");var o=n.length-1;if(o!=i.length)throw new Error("Number of annotations ("+o+") does not match number of arguments ("+i.length+") in the function: "+c.stringify(i));for(var s=[],a=0,u=n.length-1;u>a;a++){var p=[];s.push(p);var h=n[a];if(h instanceof Array)for(var f=0;f-1?(t.splice(n,1),!0):!1},t.clear=function(t){t.length=0},t.isEmpty=function(t){return 0==t.length},t.fill=function(t,e,n,r){void 0===n&&(n=0),void 0===r&&(r=null),t.fill(e,n,null===r?t.length:r)},t.equals=function(t,e){if(t.length!=e.length)return!1;for(var n=0;nr&&(n=o,r=s)}}return n},t.flatten=function(t){var e=[];return r(t,e),e},t.addAll=function(t,e){for(var n=0;n0&&(this.provider0=e[0],this.keyId0=e[0].key.id),n>1&&(this.provider1=e[1],this.keyId1=e[1].key.id),n>2&&(this.provider2=e[2],this.keyId2=e[2].key.id),n>3&&(this.provider3=e[3],this.keyId3=e[3].key.id),n>4&&(this.provider4=e[4],this.keyId4=e[4].key.id),n>5&&(this.provider5=e[5],this.keyId5=e[5].key.id),n>6&&(this.provider6=e[6],this.keyId6=e[6].key.id),n>7&&(this.provider7=e[7],this.keyId7=e[7].key.id),n>8&&(this.provider8=e[8],this.keyId8=e[8].key.id),n>9&&(this.provider9=e[9],this.keyId9=e[9].key.id)}return t.prototype.getProviderAtIndex=function(t){if(0==t)return this.provider0;if(1==t)return this.provider1;if(2==t)return this.provider2;if(3==t)return this.provider3;if(4==t)return this.provider4;if(5==t)return this.provider5;if(6==t)return this.provider6;if(7==t)return this.provider7;if(8==t)return this.provider8;if(9==t)return this.provider9;throw new s.OutOfBoundsError(t)},t.prototype.createInjectorStrategy=function(t){return new m(t,this)},t}();e.ReflectiveProtoInjectorInlineStrategy=d;var v=function(){function t(t,e){this.providers=e;var n=e.length;this.keyIds=i.ListWrapper.createFixedSize(n);for(var r=0;n>r;r++)this.keyIds[r]=e[r].key.id}return t.prototype.getProviderAtIndex=function(t){if(0>t||t>=this.providers.length)throw new s.OutOfBoundsError(t);return this.providers[t]},t.prototype.createInjectorStrategy=function(t){return new g(this,t)},t}();e.ReflectiveProtoInjectorDynamicStrategy=v;var y=function(){function t(t){this.numberOfProviders=t.length,this._strategy=t.length>h?new v(this,t):new d(this,t)}return t.fromResolvedProviders=function(e){return new t(e)},t.prototype.getProviderAtIndex=function(t){return this._strategy.getProviderAtIndex(t); -},t}();e.ReflectiveProtoInjector=y;var m=function(){function t(t,e){this.injector=t,this.protoStrategy=e,this.obj0=f,this.obj1=f,this.obj2=f,this.obj3=f,this.obj4=f,this.obj5=f,this.obj6=f,this.obj7=f,this.obj8=f,this.obj9=f}return t.prototype.resetConstructionCounter=function(){this.injector._constructionCounter=0},t.prototype.instantiateProvider=function(t){return this.injector._new(t)},t.prototype.getObjByKeyId=function(t){var e=this.protoStrategy,n=this.injector;return e.keyId0===t?(this.obj0===f&&(this.obj0=n._new(e.provider0)),this.obj0):e.keyId1===t?(this.obj1===f&&(this.obj1=n._new(e.provider1)),this.obj1):e.keyId2===t?(this.obj2===f&&(this.obj2=n._new(e.provider2)),this.obj2):e.keyId3===t?(this.obj3===f&&(this.obj3=n._new(e.provider3)),this.obj3):e.keyId4===t?(this.obj4===f&&(this.obj4=n._new(e.provider4)),this.obj4):e.keyId5===t?(this.obj5===f&&(this.obj5=n._new(e.provider5)),this.obj5):e.keyId6===t?(this.obj6===f&&(this.obj6=n._new(e.provider6)),this.obj6):e.keyId7===t?(this.obj7===f&&(this.obj7=n._new(e.provider7)),this.obj7):e.keyId8===t?(this.obj8===f&&(this.obj8=n._new(e.provider8)),this.obj8):e.keyId9===t?(this.obj9===f&&(this.obj9=n._new(e.provider9)),this.obj9):f},t.prototype.getObjAtIndex=function(t){if(0==t)return this.obj0;if(1==t)return this.obj1;if(2==t)return this.obj2;if(3==t)return this.obj3;if(4==t)return this.obj4;if(5==t)return this.obj5;if(6==t)return this.obj6;if(7==t)return this.obj7;if(8==t)return this.obj8;if(9==t)return this.obj9;throw new s.OutOfBoundsError(t)},t.prototype.getMaxNumberOfObjects=function(){return h},t}();e.ReflectiveInjectorInlineStrategy=m;var g=function(){function t(t,e){this.protoStrategy=t,this.injector=e,this.objs=i.ListWrapper.createFixedSize(t.providers.length),i.ListWrapper.fill(this.objs,f)}return t.prototype.resetConstructionCounter=function(){this.injector._constructionCounter=0},t.prototype.instantiateProvider=function(t){return this.injector._new(t)},t.prototype.getObjByKeyId=function(t){for(var e=this.protoStrategy,n=0;nt||t>=this.objs.length)throw new s.OutOfBoundsError(t);return this.objs[t]},t.prototype.getMaxNumberOfObjects=function(){return this.objs.length},t}();e.ReflectiveInjectorDynamicStrategy=g;var _=function(){function t(){}return t.resolve=function(t){return o.resolveReflectiveProviders(t)},t.resolveAndCreate=function(e,n){void 0===n&&(n=null);var r=t.resolve(e);return t.fromResolvedProviders(r,n)},t.fromResolvedProviders=function(t,e){return void 0===e&&(e=null),new b(y.fromResolvedProviders(t),e)},t.fromResolvedBindings=function(e){return t.fromResolvedProviders(e)},Object.defineProperty(t.prototype,"parent",{get:function(){return u.unimplemented()},enumerable:!0,configurable:!0}),t.prototype.debugContext=function(){return null},t.prototype.resolveAndCreateChild=function(t){return u.unimplemented()},t.prototype.createChildFromResolved=function(t){return u.unimplemented()},t.prototype.resolveAndInstantiate=function(t){return u.unimplemented()},t.prototype.instantiateResolved=function(t){return u.unimplemented()},t}();e.ReflectiveInjector=_;var b=function(){function t(t,e,n){void 0===e&&(e=null),void 0===n&&(n=null),this._debugContext=n,this._constructionCounter=0,this._proto=t,this._parent=e,this._strategy=t._strategy.createInjectorStrategy(this)}return t.prototype.debugContext=function(){return this._debugContext()},t.prototype.get=function(t,e){return void 0===e&&(e=l.THROW_IF_NOT_FOUND),this._getByKey(c.ReflectiveKey.get(t),null,null,e)},t.prototype.getAt=function(t){return this._strategy.getObjAtIndex(t)},Object.defineProperty(t.prototype,"parent",{get:function(){return this._parent},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"internalStrategy",{get:function(){return this._strategy},enumerable:!0,configurable:!0}),t.prototype.resolveAndCreateChild=function(t){var e=_.resolve(t);return this.createChildFromResolved(e)},t.prototype.createChildFromResolved=function(e){var n=new y(e),r=new t(n);return r._parent=this,r},t.prototype.resolveAndInstantiate=function(t){return this.instantiateResolved(_.resolve([t])[0])},t.prototype.instantiateResolved=function(t){return this._instantiateProvider(t)},t.prototype._new=function(t){if(this._constructionCounter++>this._strategy.getMaxNumberOfObjects())throw new s.CyclicDependencyError(this,t.key);return this._instantiateProvider(t)},t.prototype._instantiateProvider=function(t){if(t.multiProvider){for(var e=i.ListWrapper.createFixedSize(t.resolvedFactories.length),n=0;n0?this._getByReflectiveDependency(t,R[0]):null,r=S>1?this._getByReflectiveDependency(t,R[1]):null,i=S>2?this._getByReflectiveDependency(t,R[2]):null,o=S>3?this._getByReflectiveDependency(t,R[3]):null,a=S>4?this._getByReflectiveDependency(t,R[4]):null,c=S>5?this._getByReflectiveDependency(t,R[5]):null,p=S>6?this._getByReflectiveDependency(t,R[6]):null,l=S>7?this._getByReflectiveDependency(t,R[7]):null,h=S>8?this._getByReflectiveDependency(t,R[8]):null,f=S>9?this._getByReflectiveDependency(t,R[9]):null,d=S>10?this._getByReflectiveDependency(t,R[10]):null,v=S>11?this._getByReflectiveDependency(t,R[11]):null,y=S>12?this._getByReflectiveDependency(t,R[12]):null,m=S>13?this._getByReflectiveDependency(t,R[13]):null,g=S>14?this._getByReflectiveDependency(t,R[14]):null,_=S>15?this._getByReflectiveDependency(t,R[15]):null,b=S>16?this._getByReflectiveDependency(t,R[16]):null,P=S>17?this._getByReflectiveDependency(t,R[17]):null,E=S>18?this._getByReflectiveDependency(t,R[18]):null,w=S>19?this._getByReflectiveDependency(t,R[19]):null}catch(O){throw(O instanceof s.AbstractProviderError||O instanceof s.InstantiationError)&&O.addKey(this,t.key),O}var T;try{switch(S){case 0:T=C();break;case 1:T=C(n);break;case 2:T=C(n,r);break;case 3:T=C(n,r,i);break;case 4:T=C(n,r,i,o);break;case 5:T=C(n,r,i,o,a);break;case 6:T=C(n,r,i,o,a,c);break;case 7:T=C(n,r,i,o,a,c,p);break;case 8:T=C(n,r,i,o,a,c,p,l);break;case 9:T=C(n,r,i,o,a,c,p,l,h);break;case 10:T=C(n,r,i,o,a,c,p,l,h,f);break;case 11:T=C(n,r,i,o,a,c,p,l,h,f,d);break;case 12:T=C(n,r,i,o,a,c,p,l,h,f,d,v);break;case 13:T=C(n,r,i,o,a,c,p,l,h,f,d,v,y);break;case 14:T=C(n,r,i,o,a,c,p,l,h,f,d,v,y,m);break;case 15:T=C(n,r,i,o,a,c,p,l,h,f,d,v,y,m,g);break;case 16:T=C(n,r,i,o,a,c,p,l,h,f,d,v,y,m,g,_);break;case 17:T=C(n,r,i,o,a,c,p,l,h,f,d,v,y,m,g,_,b);break;case 18:T=C(n,r,i,o,a,c,p,l,h,f,d,v,y,m,g,_,b,P);break;case 19:T=C(n,r,i,o,a,c,p,l,h,f,d,v,y,m,g,_,b,P,E);break;case 20:T=C(n,r,i,o,a,c,p,l,h,f,d,v,y,m,g,_,b,P,E,w);break;default:throw new u.BaseException("Cannot instantiate '"+t.key.displayName+"' because it has more than 20 dependencies")}}catch(O){throw new s.InstantiationError(this,O,O.stack,t.key)}return T},t.prototype._getByReflectiveDependency=function(t,e){return this._getByKey(e.key,e.lowerBoundVisibility,e.upperBoundVisibility,e.optional?null:l.THROW_IF_NOT_FOUND)},t.prototype._getByKey=function(t,e,n,r){return t===P?this:n instanceof p.SelfMetadata?this._getByKeySelf(t,r):this._getByKeyDefault(t,r,e)},t.prototype._throwOrNull=function(t,e){if(e!==l.THROW_IF_NOT_FOUND)return e;throw new s.NoProviderError(this,t)},t.prototype._getByKeySelf=function(t,e){var n=this._strategy.getObjByKeyId(t.id);return n!==f?n:this._throwOrNull(t,e)},t.prototype._getByKeyDefault=function(e,n,r){var i;for(i=r instanceof p.SkipSelfMetadata?this._parent:this;i instanceof t;){var o=i,s=o._strategy.getObjByKeyId(e.id);if(s!==f)return s;i=o._parent}return null!==i?i.get(e.token,n):this._throwOrNull(e,n)},Object.defineProperty(t.prototype,"displayName",{get:function(){return"ReflectiveInjector(providers: ["+r(this,function(t){return' "'+t.key.displayName+'" '}).join(", ")+"])"},enumerable:!0,configurable:!0}),t.prototype.toString=function(){return this.displayName},t}();e.ReflectiveInjector_=b;var P=c.ReflectiveKey.get(l.Injector)},function(t,e,n){"use strict";function r(t){var e,n;if(h.isPresent(t.useClass)){var r=g.resolveForwardRef(t.useClass);e=d.reflector.factory(r),n=c(r)}else h.isPresent(t.useExisting)?(e=function(t){return t},n=[b.fromKey(v.ReflectiveKey.get(t.useExisting))]):h.isPresent(t.useFactory)?(e=t.useFactory,n=u(t.useFactory,t.dependencies)):(e=function(){return t.useValue},n=P);return new w(e,n)}function i(t){return new E(v.ReflectiveKey.get(t.token),[r(t)],t.multi)}function o(t){var e=a(t,[]),n=e.map(i);return f.MapWrapper.values(s(n,new Map))}function s(t,e){for(var n=0;n1){var e=r(s.ListWrapper.reversed(t)),n=e.map(function(t){return a.stringify(t.token)});return" ("+n.join(" -> ")+")"}return""}var o=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},s=n(15),a=n(5),u=n(12),c=function(t){function e(e,n,r){t.call(this,"DI Exception"),this.keys=[n],this.injectors=[e],this.constructResolvingMessage=r,this.message=this.constructResolvingMessage(this.keys)}return o(e,t),e.prototype.addKey=function(t,e){this.injectors.push(t),this.keys.push(e),this.message=this.constructResolvingMessage(this.keys)},Object.defineProperty(e.prototype,"context",{get:function(){return this.injectors[this.injectors.length-1].debugContext()},enumerable:!0,configurable:!0}),e}(u.BaseException);e.AbstractProviderError=c;var p=function(t){function e(e,n){t.call(this,e,n,function(t){var e=a.stringify(s.ListWrapper.first(t).token);return"No provider for "+e+"!"+i(t)})}return o(e,t),e}(c);e.NoProviderError=p;var l=function(t){function e(e,n){t.call(this,e,n,function(t){return"Cannot instantiate cyclic dependency!"+i(t)})}return o(e,t),e}(c);e.CyclicDependencyError=l;var h=function(t){function e(e,n,r,i){t.call(this,"DI Exception",n,r,null),this.keys=[i],this.injectors=[e]}return o(e,t),e.prototype.addKey=function(t,e){this.injectors.push(t),this.keys.push(e)},Object.defineProperty(e.prototype,"wrapperMessage",{get:function(){var t=a.stringify(s.ListWrapper.first(this.keys).token);return"Error during instantiation of "+t+"!"+i(this.keys)+"."},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"causeKey",{get:function(){return this.keys[0]},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"context",{get:function(){return this.injectors[this.injectors.length-1].debugContext()},enumerable:!0,configurable:!0}),e}(u.WrappedException);e.InstantiationError=h;var f=function(t){function e(e){t.call(this,"Invalid provider - only instances of Provider and Type are allowed, got: "+e.toString())}return o(e,t),e}(u.BaseException);e.InvalidProviderError=f;var d=function(t){function e(n,r){t.call(this,e._genMessage(n,r))}return o(e,t),e._genMessage=function(t,e){for(var n=[],r=0,i=e.length;i>r;r++){var o=e[r];a.isBlank(o)||0==o.length?n.push("?"):n.push(o.map(a.stringify).join(" "))}return"Cannot resolve all parameters for '"+a.stringify(t)+"'("+n.join(", ")+"). Make sure that all the parameters are decorated with Inject or have valid type annotations and that '"+a.stringify(t)+"' is decorated with Injectable."},e}(u.BaseException);e.NoAnnotationError=d;var v=function(t){function e(e){t.call(this,"Index "+e+" is out-of-bounds.")}return o(e,t),e}(u.BaseException);e.OutOfBoundsError=v;var y=function(t){function e(e,n){t.call(this,"Cannot mix multi providers and regular providers, got: "+e.toString()+" "+n.toString())}return o(e,t),e}(u.BaseException);e.MixingMultiProvidersWithRegularProvidersError=y},function(t,e,n){"use strict";function r(t){return new h(t)}function i(t,e){var n=e.useClass,r=e.useValue,i=e.useExisting,o=e.useFactory,s=e.deps,a=e.multi;return new p(t,{useClass:n,useValue:r,useExisting:i,useFactory:o,deps:s,multi:a})}var o=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},s=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},a=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},u=n(5),c=n(12),p=function(){function t(t,e){var n=e.useClass,r=e.useValue,i=e.useExisting,o=e.useFactory,s=e.deps,a=e.multi;this.token=t,this.useClass=n,this.useValue=r,this.useExisting=i,this.useFactory=o,this.dependencies=s,this._multi=a}return Object.defineProperty(t.prototype,"multi",{get:function(){return u.normalizeBool(this._multi)},enumerable:!0,configurable:!0}),t=s([u.CONST(),a("design:paramtypes",[Object,Object])],t)}();e.Provider=p;var l=function(t){function e(e,n){var r=n.toClass,i=n.toValue,o=n.toAlias,s=n.toFactory,a=n.deps,u=n.multi;t.call(this,e,{useClass:r,useValue:i,useExisting:o,useFactory:s,deps:a,multi:u})}return o(e,t),Object.defineProperty(e.prototype,"toClass",{get:function(){return this.useClass},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"toAlias",{get:function(){return this.useExisting},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"toFactory",{get:function(){return this.useFactory},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"toValue",{get:function(){return this.useValue},enumerable:!0,configurable:!0}),e=s([u.CONST(),a("design:paramtypes",[Object,Object])],e)}(p);e.Binding=l,e.bind=r;var h=function(){function t(t){this.token=t}return t.prototype.toClass=function(t){if(!u.isType(t))throw new c.BaseException('Trying to create a class provider but "'+u.stringify(t)+'" is not a class!');return new p(this.token,{useClass:t})},t.prototype.toValue=function(t){return new p(this.token,{useValue:t})},t.prototype.toAlias=function(t){if(u.isBlank(t))throw new c.BaseException("Can not alias "+u.stringify(this.token)+" to a blank value!");return new p(this.token,{useExisting:t})},t.prototype.toFactory=function(t,e){if(!u.isFunction(t))throw new c.BaseException('Trying to create a factory provider but "'+u.stringify(t)+'" is not a function!');return new p(this.token,{useFactory:t,deps:e})},t}();e.ProviderBuilder=h,e.provide=i},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=function(){function t(t){this._desc=t}return t.prototype.toString=function(){return"Token "+this._desc},t=r([o.CONST(),i("design:paramtypes",[String])],t)}();e.OpaqueToken=s},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(5),a=n(7),u=n(27),c=function(t){function e(e){var n=void 0===e?{}:e,r=n.selector,i=n.inputs,o=n.outputs,s=n.properties,a=n.events,u=n.host,c=n.bindings,p=n.providers,l=n.exportAs,h=n.queries;t.call(this),this.selector=r,this._inputs=i,this._properties=s,this._outputs=o,this._events=a,this.host=u,this.exportAs=l,this.queries=h,this._providers=p,this._bindings=c}return r(e,t),Object.defineProperty(e.prototype,"inputs",{get:function(){return s.isPresent(this._properties)&&this._properties.length>0?this._properties:this._inputs},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"properties",{get:function(){return this.inputs},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"outputs",{get:function(){return s.isPresent(this._events)&&this._events.length>0?this._events:this._outputs},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"events",{get:function(){return this.outputs},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"providers",{get:function(){return s.isPresent(this._bindings)&&this._bindings.length>0?this._bindings:this._providers},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"bindings",{get:function(){return this.providers},enumerable:!0,configurable:!0}),e=i([s.CONST(),o("design:paramtypes",[Object])],e)}(a.InjectableMetadata);e.DirectiveMetadata=c;var p=function(t){function e(e){var n=void 0===e?{}:e,r=n.selector,i=n.inputs,o=n.outputs,s=n.properties,a=n.events,c=n.host,p=n.exportAs,l=n.moduleId,h=n.bindings,f=n.providers,d=n.viewBindings,v=n.viewProviders,y=n.changeDetection,m=void 0===y?u.ChangeDetectionStrategy.Default:y,g=n.queries,_=n.templateUrl,b=n.template,P=n.styleUrls,E=n.styles,w=n.directives,C=n.pipes,R=n.encapsulation;t.call(this,{selector:r,inputs:i,outputs:o,properties:s,events:a,host:c,exportAs:p,bindings:h,providers:f,queries:g}),this.changeDetection=m,this._viewProviders=v,this._viewBindings=d,this.templateUrl=_,this.template=b,this.styleUrls=P,this.styles=E,this.directives=w,this.pipes=C,this.encapsulation=R,this.moduleId=l}return r(e,t),Object.defineProperty(e.prototype,"viewProviders",{get:function(){return s.isPresent(this._viewBindings)&&this._viewBindings.length>0?this._viewBindings:this._viewProviders},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"viewBindings",{get:function(){return this.viewProviders},enumerable:!0,configurable:!0}),e=i([s.CONST(),o("design:paramtypes",[Object])],e)}(c);e.ComponentMetadata=p;var l=function(t){function e(e){var n=e.name,r=e.pure;t.call(this),this.name=n,this._pure=r}return r(e,t),Object.defineProperty(e.prototype,"pure",{get:function(){return s.isPresent(this._pure)?this._pure:!0},enumerable:!0,configurable:!0}),e=i([s.CONST(),o("design:paramtypes",[Object])],e)}(a.InjectableMetadata);e.PipeMetadata=l;var h=function(){function t(t){this.bindingPropertyName=t}return t=i([s.CONST(),o("design:paramtypes",[String])],t)}();e.InputMetadata=h;var f=function(){function t(t){this.bindingPropertyName=t}return t=i([s.CONST(),o("design:paramtypes",[String])],t)}();e.OutputMetadata=f;var d=function(){function t(t){this.hostPropertyName=t}return t=i([s.CONST(),o("design:paramtypes",[String])],t)}();e.HostBindingMetadata=d;var v=function(){function t(t,e){this.eventName=t,this.args=e}return t=i([s.CONST(),o("design:paramtypes",[String,Array])],t)}();e.HostListenerMetadata=v},function(t,e,n){"use strict";var r=n(28);e.ChangeDetectionStrategy=r.ChangeDetectionStrategy,e.ChangeDetectorRef=r.ChangeDetectorRef,e.WrappedValue=r.WrappedValue,e.SimpleChange=r.SimpleChange,e.IterableDiffers=r.IterableDiffers,e.KeyValueDiffers=r.KeyValueDiffers,e.CollectionChangeRecord=r.CollectionChangeRecord,e.KeyValueChangeRecord=r.KeyValueChangeRecord},function(t,e,n){"use strict";var r=n(29),i=n(30),o=n(31),s=n(32),a=n(5),u=n(32);e.DefaultKeyValueDifferFactory=u.DefaultKeyValueDifferFactory,e.KeyValueChangeRecord=u.KeyValueChangeRecord;var c=n(30);e.DefaultIterableDifferFactory=c.DefaultIterableDifferFactory,e.CollectionChangeRecord=c.CollectionChangeRecord;var p=n(33);e.ChangeDetectionStrategy=p.ChangeDetectionStrategy,e.CHANGE_DETECTION_STRATEGY_VALUES=p.CHANGE_DETECTION_STRATEGY_VALUES,e.ChangeDetectorState=p.ChangeDetectorState,e.CHANGE_DETECTOR_STATE_VALUES=p.CHANGE_DETECTOR_STATE_VALUES,e.isDefaultChangeDetectionStrategy=p.isDefaultChangeDetectionStrategy;var l=n(34);e.ChangeDetectorRef=l.ChangeDetectorRef;var h=n(29);e.IterableDiffers=h.IterableDiffers;var f=n(31);e.KeyValueDiffers=f.KeyValueDiffers;var d=n(35);e.WrappedValue=d.WrappedValue,e.ValueUnwrapper=d.ValueUnwrapper,e.SimpleChange=d.SimpleChange,e.devModeEqual=d.devModeEqual,e.looseIdentical=d.looseIdentical,e.uninitialized=d.uninitialized,e.keyValDiff=a.CONST_EXPR([a.CONST_EXPR(new s.DefaultKeyValueDifferFactory)]),e.iterableDiff=a.CONST_EXPR([a.CONST_EXPR(new i.DefaultIterableDifferFactory)]),e.defaultIterableDiffers=a.CONST_EXPR(new r.IterableDiffers(e.iterableDiff)),e.defaultKeyValueDiffers=a.CONST_EXPR(new o.KeyValueDiffers(e.keyValDiff))},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s); -return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(12),a=n(15),u=n(6),c=function(){function t(t){this.factories=t}return t.create=function(e,n){if(o.isPresent(n)){var r=a.ListWrapper.clone(n.factories);return e=e.concat(r),new t(e)}return new t(e)},t.extend=function(e){return new u.Provider(t,{useFactory:function(n){if(o.isBlank(n))throw new s.BaseException("Cannot extend IterableDiffers without a parent injector");return t.create(e,n)},deps:[[t,new u.SkipSelfMetadata,new u.OptionalMetadata]]})},t.prototype.find=function(t){var e=this.factories.find(function(e){return e.supports(t)});if(o.isPresent(e))return e;throw new s.BaseException("Cannot find a differ supporting object '"+t+"' of type '"+o.getTypeNameForDebugging(t)+"'")},t=r([o.CONST(),i("design:paramtypes",[Array])],t)}();e.IterableDiffers=c},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(12),a=n(15),u=n(5),c=function(){function t(){}return t.prototype.supports=function(t){return a.isListLikeIterable(t)},t.prototype.create=function(t,e){return new l(e)},t=r([o.CONST(),i("design:paramtypes",[])],t)}();e.DefaultIterableDifferFactory=c;var p=function(t,e){return e},l=function(){function t(t){this._trackByFn=t,this._length=null,this._collection=null,this._linkedRecords=null,this._unlinkedRecords=null,this._previousItHead=null,this._itHead=null,this._itTail=null,this._additionsHead=null,this._additionsTail=null,this._movesHead=null,this._movesTail=null,this._removalsHead=null,this._removalsTail=null,this._identityChangesHead=null,this._identityChangesTail=null,this._trackByFn=u.isPresent(this._trackByFn)?this._trackByFn:p}return Object.defineProperty(t.prototype,"collection",{get:function(){return this._collection},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"length",{get:function(){return this._length},enumerable:!0,configurable:!0}),t.prototype.forEachItem=function(t){var e;for(e=this._itHead;null!==e;e=e._next)t(e)},t.prototype.forEachPreviousItem=function(t){var e;for(e=this._previousItHead;null!==e;e=e._nextPrevious)t(e)},t.prototype.forEachAddedItem=function(t){var e;for(e=this._additionsHead;null!==e;e=e._nextAdded)t(e)},t.prototype.forEachMovedItem=function(t){var e;for(e=this._movesHead;null!==e;e=e._nextMoved)t(e)},t.prototype.forEachRemovedItem=function(t){var e;for(e=this._removalsHead;null!==e;e=e._nextRemoved)t(e)},t.prototype.forEachIdentityChange=function(t){var e;for(e=this._identityChangesHead;null!==e;e=e._nextIdentityChange)t(e)},t.prototype.diff=function(t){if(u.isBlank(t)&&(t=[]),!a.isListLikeIterable(t))throw new s.BaseException("Error trying to diff '"+t+"'");return this.check(t)?this:null},t.prototype.onDestroy=function(){},t.prototype.check=function(t){var e=this;this._reset();var n,r,i,o=this._itHead,s=!1;if(u.isArray(t)){var c=t;for(this._length=t.length,n=0;n"+u.stringify(this.currentIndex)+"]"},t}();e.CollectionChangeRecord=h;var f=function(){function t(){this._head=null,this._tail=null}return t.prototype.add=function(t){null===this._head?(this._head=this._tail=t,t._nextDup=null,t._prevDup=null):(this._tail._nextDup=t,t._prevDup=this._tail,t._nextDup=null,this._tail=t)},t.prototype.get=function(t,e){var n;for(n=this._head;null!==n;n=n._nextDup)if((null===e||eo?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(12),a=n(15),u=n(6),c=function(){function t(t){this.factories=t}return t.create=function(e,n){if(o.isPresent(n)){var r=a.ListWrapper.clone(n.factories);return e=e.concat(r),new t(e)}return new t(e)},t.extend=function(e){return new u.Provider(t,{useFactory:function(n){if(o.isBlank(n))throw new s.BaseException("Cannot extend KeyValueDiffers without a parent injector");return t.create(e,n)},deps:[[t,new u.SkipSelfMetadata,new u.OptionalMetadata]]})},t.prototype.find=function(t){var e=this.factories.find(function(e){return e.supports(t)});if(o.isPresent(e))return e;throw new s.BaseException("Cannot find a differ supporting object '"+t+"'")},t=r([o.CONST(),i("design:paramtypes",[Array])],t)}();e.KeyValueDiffers=c},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(15),s=n(5),a=n(12),u=function(){function t(){}return t.prototype.supports=function(t){return t instanceof Map||s.isJsObject(t)},t.prototype.create=function(t){return new c},t=r([s.CONST(),i("design:paramtypes",[])],t)}();e.DefaultKeyValueDifferFactory=u;var c=function(){function t(){this._records=new Map,this._mapHead=null,this._previousMapHead=null,this._changesHead=null,this._changesTail=null,this._additionsHead=null,this._additionsTail=null,this._removalsHead=null,this._removalsTail=null}return Object.defineProperty(t.prototype,"isDirty",{get:function(){return null!==this._additionsHead||null!==this._changesHead||null!==this._removalsHead},enumerable:!0,configurable:!0}),t.prototype.forEachItem=function(t){var e;for(e=this._mapHead;null!==e;e=e._next)t(e)},t.prototype.forEachPreviousItem=function(t){var e;for(e=this._previousMapHead;null!==e;e=e._nextPrevious)t(e)},t.prototype.forEachChangedItem=function(t){var e;for(e=this._changesHead;null!==e;e=e._nextChanged)t(e)},t.prototype.forEachAddedItem=function(t){var e;for(e=this._additionsHead;null!==e;e=e._nextAdded)t(e)},t.prototype.forEachRemovedItem=function(t){var e;for(e=this._removalsHead;null!==e;e=e._nextRemoved)t(e)},t.prototype.diff=function(t){if(s.isBlank(t)&&(t=o.MapWrapper.createFromPairs([])),!(t instanceof Map||s.isJsObject(t)))throw new a.BaseException("Error trying to diff '"+t+"'");return this.check(t)?this:null},t.prototype.onDestroy=function(){},t.prototype.check=function(t){var e=this;this._reset();var n=this._records,r=this._mapHead,i=null,o=null,a=!1;return this._forEach(t,function(t,u){var c;null!==r&&u===r.key?(c=r,s.looseIdentical(t,r.currentValue)||(r.previousValue=r.currentValue,r.currentValue=t,e._addToChanges(r))):(a=!0,null!==r&&(r._next=null,e._removeFromSeq(i,r),e._addToRemovals(r)),n.has(u)?c=n.get(u):(c=new p(u),n.set(u,c),c.currentValue=t,e._addToAdditions(c))),a&&(e._isInRemovals(c)&&e._removeFromRemovals(c),null==o?e._mapHead=c:o._next=c),i=r,o=c,r=null===r?null:r._next}),this._truncate(i,r),this.isDirty},t.prototype._reset=function(){if(this.isDirty){var t;for(t=this._previousMapHead=this._mapHead;null!==t;t=t._next)t._nextPrevious=t._next;for(t=this._changesHead;null!==t;t=t._nextChanged)t.previousValue=t.currentValue;for(t=this._additionsHead;null!=t;t=t._nextAdded)t.previousValue=t.currentValue;this._changesHead=this._changesTail=null,this._additionsHead=this._additionsTail=null,this._removalsHead=this._removalsTail=null}},t.prototype._truncate=function(t,e){for(;null!==e;){null===t?this._mapHead=null:t._next=null;var n=e._next;this._addToRemovals(e),t=e,e=n}for(var r=this._removalsHead;null!==r;r=r._nextRemoved)r.previousValue=r.currentValue,r.currentValue=null,this._records["delete"](r.key)},t.prototype._isInRemovals=function(t){return t===this._removalsHead||null!==t._nextRemoved||null!==t._prevRemoved},t.prototype._addToRemovals=function(t){null===this._removalsHead?this._removalsHead=this._removalsTail=t:(this._removalsTail._nextRemoved=t,t._prevRemoved=this._removalsTail,this._removalsTail=t)},t.prototype._removeFromSeq=function(t,e){var n=e._next;null===t?this._mapHead=n:t._next=n},t.prototype._removeFromRemovals=function(t){var e=t._prevRemoved,n=t._nextRemoved;null===e?this._removalsHead=n:e._nextRemoved=n,null===n?this._removalsTail=e:n._prevRemoved=e,t._prevRemoved=t._nextRemoved=null},t.prototype._addToAdditions=function(t){null===this._additionsHead?this._additionsHead=this._additionsTail=t:(this._additionsTail._nextAdded=t,this._additionsTail=t)},t.prototype._addToChanges=function(t){null===this._changesHead?this._changesHead=this._changesTail=t:(this._changesTail._nextChanged=t,this._changesTail=t)},t.prototype.toString=function(){var t,e=[],n=[],r=[],i=[],o=[];for(t=this._mapHead;null!==t;t=t._next)e.push(s.stringify(t));for(t=this._previousMapHead;null!==t;t=t._nextPrevious)n.push(s.stringify(t));for(t=this._changesHead;null!==t;t=t._nextChanged)r.push(s.stringify(t));for(t=this._additionsHead;null!==t;t=t._nextAdded)i.push(s.stringify(t));for(t=this._removalsHead;null!==t;t=t._nextRemoved)o.push(s.stringify(t));return"map: "+e.join(", ")+"\nprevious: "+n.join(", ")+"\nadditions: "+i.join(", ")+"\nchanges: "+r.join(", ")+"\nremovals: "+o.join(", ")+"\n"},t.prototype._forEach=function(t,e){t instanceof Map?t.forEach(e):o.StringMapWrapper.forEach(t,e)},t}();e.DefaultKeyValueDiffer=c;var p=function(){function t(t){this.key=t,this.previousValue=null,this.currentValue=null,this._nextPrevious=null,this._next=null,this._nextAdded=null,this._nextRemoved=null,this._prevRemoved=null,this._nextChanged=null}return t.prototype.toString=function(){return s.looseIdentical(this.previousValue,this.currentValue)?s.stringify(this.key):s.stringify(this.key)+"["+s.stringify(this.previousValue)+"->"+s.stringify(this.currentValue)+"]"},t}();e.KeyValueChangeRecord=p},function(t,e,n){"use strict";function r(t){return i.isBlank(t)||t===s.Default}var i=n(5);!function(t){t[t.NeverChecked=0]="NeverChecked",t[t.CheckedBefore=1]="CheckedBefore",t[t.Errored=2]="Errored"}(e.ChangeDetectorState||(e.ChangeDetectorState={}));var o=e.ChangeDetectorState;!function(t){t[t.CheckOnce=0]="CheckOnce",t[t.Checked=1]="Checked",t[t.CheckAlways=2]="CheckAlways",t[t.Detached=3]="Detached",t[t.OnPush=4]="OnPush",t[t.Default=5]="Default"}(e.ChangeDetectionStrategy||(e.ChangeDetectionStrategy={}));var s=e.ChangeDetectionStrategy;e.CHANGE_DETECTION_STRATEGY_VALUES=[s.CheckOnce,s.Checked,s.CheckAlways,s.Detached,s.OnPush,s.Default],e.CHANGE_DETECTOR_STATE_VALUES=[o.NeverChecked,o.CheckedBefore,o.Errored],e.isDefaultChangeDetectionStrategy=r},function(t,e){"use strict";var n=function(){function t(){}return t}();e.ChangeDetectorRef=n},function(t,e,n){"use strict";function r(t,e){return o.isListLikeIterable(t)&&o.isListLikeIterable(e)?o.areIterablesEqual(t,e,r):o.isListLikeIterable(t)||i.isPrimitive(t)||o.isListLikeIterable(e)||i.isPrimitive(e)?i.looseIdentical(t,e):!0}var i=n(5),o=n(15),s=n(5);e.looseIdentical=s.looseIdentical,e.uninitialized=i.CONST_EXPR(new Object),e.devModeEqual=r;var a=function(){function t(t){this.wrapped=t}return t.wrap=function(e){return new t(e)},t}();e.WrappedValue=a;var u=function(){function t(){this.hasWrappedValue=!1}return t.prototype.unwrap=function(t){return t instanceof a?(this.hasWrappedValue=!0,t.wrapped):t},t.prototype.reset=function(){this.hasWrappedValue=!1},t}();e.ValueUnwrapper=u;var c=function(){function t(t,e){this.previousValue=t,this.currentValue=e}return t.prototype.isFirstChange=function(){return this.previousValue===e.uninitialized},t}();e.SimpleChange=c},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5);!function(t){t[t.Emulated=0]="Emulated",t[t.Native=1]="Native",t[t.None=2]="None"}(e.ViewEncapsulation||(e.ViewEncapsulation={}));var s=e.ViewEncapsulation;e.VIEW_ENCAPSULATION_VALUES=[s.Emulated,s.Native,s.None];var a=function(){function t(t){var e=void 0===t?{}:t,n=e.templateUrl,r=e.template,i=e.directives,o=e.pipes,s=e.encapsulation,a=e.styles,u=e.styleUrls;this.templateUrl=n,this.template=r,this.styleUrls=u,this.styles=a,this.directives=i,this.pipes=o,this.encapsulation=s}return t=r([o.CONST(),i("design:paramtypes",[Object])],t)}();e.ViewMetadata=a},function(t,e,n){"use strict";var r=n(9);e.Class=r.Class},function(t,e,n){"use strict";var r=n(5);e.enableProdMode=r.enableProdMode},function(t,e,n){"use strict";var r=n(5);e.Type=r.Type;var i=n(40);e.EventEmitter=i.EventEmitter;var o=n(12);e.WrappedException=o.WrappedException;var s=n(14);e.ExceptionHandler=s.ExceptionHandler},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(5),o=n(41);e.PromiseWrapper=o.PromiseWrapper,e.PromiseCompleter=o.PromiseCompleter;var s=n(42),a=n(43),u=n(58),c=n(42);e.Observable=c.Observable;var p=n(42);e.Subject=p.Subject;var l=function(){function t(){}return t.setTimeout=function(t,e){return i.global.setTimeout(t,e)},t.clearTimeout=function(t){i.global.clearTimeout(t)},t.setInterval=function(t,e){return i.global.setInterval(t,e)},t.clearInterval=function(t){i.global.clearInterval(t)},t}();e.TimerWrapper=l;var h=function(){function t(){}return t.subscribe=function(t,e,n,r){return void 0===r&&(r=function(){}),n="function"==typeof n&&n||i.noop,r="function"==typeof r&&r||i.noop,t.subscribe({next:e,error:n,complete:r})},t.isObservable=function(t){return!!t.subscribe},t.hasSubscribers=function(t){return t.observers.length>0},t.dispose=function(t){t.unsubscribe()},t.callNext=function(t,e){t.next(e)},t.callEmit=function(t,e){t.emit(e)},t.callError=function(t,e){t.error(e)},t.callComplete=function(t){t.complete()},t.fromPromise=function(t){return a.PromiseObservable.create(t)},t.toPromise=function(t){return u.toPromise.call(t)},t}();e.ObservableWrapper=h;var f=function(t){function e(e){void 0===e&&(e=!0),t.call(this),this._isAsync=e}return r(e,t),e.prototype.emit=function(e){t.prototype.next.call(this,e)},e.prototype.next=function(e){t.prototype.next.call(this,e)},e.prototype.subscribe=function(e,n,r){var i,o=function(t){return null},s=function(){return null};return e&&"object"==typeof e?(i=this._isAsync?function(t){setTimeout(function(){return e.next(t)})}:function(t){e.next(t)},e.error&&(o=this._isAsync?function(t){setTimeout(function(){return e.error(t)})}:function(t){e.error(t)}),e.complete&&(s=this._isAsync?function(){setTimeout(function(){return e.complete()})}:function(){e.complete()})):(i=this._isAsync?function(t){setTimeout(function(){return e(t)})}:function(t){e(t)},n&&(o=this._isAsync?function(t){setTimeout(function(){return n(t)})}:function(t){n(t)}),r&&(s=this._isAsync?function(){setTimeout(function(){return r()})}:function(){r()})),t.prototype.subscribe.call(this,i,o,s)},e}(s.Subject);e.EventEmitter=f},function(t,e){"use strict";var n=function(){function t(){var t=this;this.promise=new Promise(function(e,n){t.resolve=e,t.reject=n})}return t}();e.PromiseCompleter=n;var r=function(){function t(){}return t.resolve=function(t){return Promise.resolve(t)},t.reject=function(t,e){return Promise.reject(t)},t.catchError=function(t,e){return t["catch"](e)},t.all=function(t){return 0==t.length?Promise.resolve([]):Promise.all(t)},t.then=function(t,e,n){return t.then(e,n)},t.wrap=function(t){return new Promise(function(e,n){try{e(t())}catch(r){n(r)}})},t.scheduleMicrotask=function(e){t.then(t.resolve(null),e,function(t){})},t.isPromise=function(t){return t instanceof Promise},t.completer=function(){return new n},t}();e.PromiseWrapper=r},function(e,n){e.exports=t},function(t,e,n){"use strict";function r(t){var e=t.value,n=t.subscriber;n.isUnsubscribed||(n.next(e),n.complete())}function i(t){var e=t.err,n=t.subscriber;n.isUnsubscribed||n.error(e)}var o=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},s=n(44),a=n(46),u=function(t){function e(e,n){void 0===n&&(n=null),t.call(this),this.promise=e,this.scheduler=n}return o(e,t),e.create=function(t,n){return void 0===n&&(n=null),new e(t,n)},e.prototype._subscribe=function(t){var e=this,n=this.promise,o=this.scheduler;if(null==o)this._isScalar?t.isUnsubscribed||(t.next(this.value),t.complete()):n.then(function(n){e.value=n,e._isScalar=!0,t.isUnsubscribed||(t.next(n),t.complete())},function(e){t.isUnsubscribed||t.error(e)}).then(null,function(t){s.root.setTimeout(function(){throw t})});else if(this._isScalar){if(!t.isUnsubscribed)return o.schedule(r,0,{value:this.value,subscriber:t})}else n.then(function(n){e.value=n,e._isScalar=!0,t.isUnsubscribed||t.add(o.schedule(r,0,{value:n,subscriber:t}))},function(e){t.isUnsubscribed||t.add(o.schedule(i,0,{err:e,subscriber:t}))}).then(null,function(t){s.root.setTimeout(function(){throw t})})},e}(a.Observable);e.PromiseObservable=u},function(t,e,n){(function(t,n){"use strict";var r={"boolean":!1,"function":!0,object:!0,number:!1,string:!1,undefined:!1};e.root=r[typeof self]&&self||r[typeof window]&&window;var i=(r[typeof e]&&e&&!e.nodeType&&e,r[typeof t]&&t&&!t.nodeType&&t,r[typeof n]&&n);!i||i.global!==i&&i.window!==i||(e.root=i)}).call(e,n(45)(t),function(){return this}())},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}},function(t,e,n){"use strict";var r=n(44),i=n(47),o=n(48),s=n(54),a=n(55),u=function(){function t(t){this._isScalar=!1,t&&(this._subscribe=t)}return t.prototype.lift=function(e){var n=new t;return n.source=this,n.operator=e,n},t.prototype.subscribe=function(t,e,n){var r=this.operator,i=o.toSubscriber(t,e,n);if(r?i.add(this._subscribe(r.call(i))):i.add(this._subscribe(i)),i.syncErrorThrowable&&(i.syncErrorThrowable=!1,i.syncErrorThrown))throw i.syncErrorValue;return i},t.prototype.forEach=function(t,e,n){if(n||(r.root.Rx&&r.root.Rx.config&&r.root.Rx.config.Promise?n=r.root.Rx.config.Promise:r.root.Promise&&(n=r.root.Promise)),!n)throw new Error("no Promise impl found");var i=this;return new n(function(n,r){i.subscribe(function(n){var i=s.tryCatch(t).call(e,n);i===a.errorObject&&r(a.errorObject.e)},r,n)})},t.prototype._subscribe=function(t){return this.source.subscribe(t)},t.prototype[i.SymbolShim.observable]=function(){return this},t.create=function(e){return new t(e)},t}();e.Observable=u},function(t,e,n){"use strict";function r(t){var e=o(t);return a(e,t),u(e),i(e),e}function i(t){t["for"]||(t["for"]=s)}function o(t){return t.Symbol||(t.Symbol=function(t){return"@@Symbol("+t+"):"+p++}),t.Symbol}function s(t){return"@@"+t}function a(t,e){if(!t.iterator)if("function"==typeof t["for"])t.iterator=t["for"]("iterator");else if(e.Set&&"function"==typeof(new e.Set)["@@iterator"])t.iterator="@@iterator";else if(e.Map)for(var n=Object.getOwnPropertyNames(e.Map.prototype),r=0;ro?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},h=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},f=n(60),d=n(5),v=n(6),y=n(62),m=n(40),g=n(15),_=n(63),b=n(64),P=n(12),E=n(75),w=n(71);e.createNgZone=r;var C,R=!1;e.createPlatform=i,e.assertPlatform=o,e.disposePlatform=s,e.getPlatform=a,e.coreBootstrap=u,e.coreLoadAndBootstrap=c;var S=function(){function t(){}return Object.defineProperty(t.prototype,"injector",{get:function(){throw P.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"disposed",{get:function(){throw P.unimplemented()},enumerable:!0,configurable:!0}),t}();e.PlatformRef=S;var O=function(t){function e(e){if(t.call(this),this._injector=e,this._applications=[],this._disposeListeners=[],this._disposed=!1,!R)throw new P.BaseException("Platforms have to be created via `createPlatform`!");var n=e.get(y.PLATFORM_INITIALIZER,null);d.isPresent(n)&&n.forEach(function(t){return t()})}return p(e,t),e.prototype.registerDisposeListener=function(t){this._disposeListeners.push(t)},Object.defineProperty(e.prototype,"injector",{get:function(){return this._injector},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"disposed",{get:function(){return this._disposed},enumerable:!0,configurable:!0}),e.prototype.addApplication=function(t){this._applications.push(t)},e.prototype.dispose=function(){g.ListWrapper.clone(this._applications).forEach(function(t){return t.dispose()}),this._disposeListeners.forEach(function(t){return t()}),this._disposed=!0},e.prototype._applicationDisposed=function(t){g.ListWrapper.remove(this._applications,t)},e=l([v.Injectable(),h("design:paramtypes",[v.Injector])],e)}(S);e.PlatformRef_=O;var T=function(){function t(){}return Object.defineProperty(t.prototype,"injector",{get:function(){return P.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"zone",{get:function(){return P.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"componentTypes",{get:function(){return P.unimplemented()},enumerable:!0,configurable:!0}),t}();e.ApplicationRef=T;var x=function(t){function e(e,n,r){var i=this;t.call(this),this._platform=e,this._zone=n,this._injector=r,this._bootstrapListeners=[],this._disposeListeners=[],this._rootComponents=[],this._rootComponentTypes=[],this._changeDetectorRefs=[],this._runningTick=!1,this._enforceNoNewChanges=!1;var o=r.get(f.NgZone);this._enforceNoNewChanges=d.assertionsEnabled(),o.run(function(){i._exceptionHandler=r.get(P.ExceptionHandler)}),this._asyncInitDonePromise=this.run(function(){var t,e=r.get(y.APP_INITIALIZER,null),n=[];if(d.isPresent(e))for(var o=0;o0?(t=m.PromiseWrapper.all(n).then(function(t){return i._asyncInitDone=!0}),i._asyncInitDone=!1):(i._asyncInitDone=!0,t=m.PromiseWrapper.resolve(!0)),t}),m.ObservableWrapper.subscribe(o.onError,function(t){i._exceptionHandler.call(t.error,t.stackTrace)}),m.ObservableWrapper.subscribe(this._zone.onMicrotaskEmpty,function(t){i._zone.run(function(){i.tick()})})}return p(e,t),e.prototype.registerBootstrapListener=function(t){this._bootstrapListeners.push(t)},e.prototype.registerDisposeListener=function(t){this._disposeListeners.push(t)},e.prototype.registerChangeDetector=function(t){this._changeDetectorRefs.push(t)},e.prototype.unregisterChangeDetector=function(t){g.ListWrapper.remove(this._changeDetectorRefs,t)},e.prototype.waitForAsyncInitializers=function(){return this._asyncInitDonePromise},e.prototype.run=function(t){var e,n=this,r=this.injector.get(f.NgZone),i=m.PromiseWrapper.completer();return r.run(function(){try{e=t(),d.isPromise(e)&&m.PromiseWrapper.then(e,function(t){i.resolve(t)},function(t,e){i.reject(t,e),n._exceptionHandler.call(t,e)})}catch(r){throw n._exceptionHandler.call(r,r.stack),r}}),d.isPromise(e)?i.promise:e},e.prototype.bootstrap=function(t){var e=this;if(!this._asyncInitDone)throw new P.BaseException("Cannot bootstrap as there are still asynchronous initializers running. Wait for them using waitForAsyncInitializers().");return this.run(function(){e._rootComponentTypes.push(t.componentType);var n=t.create(e._injector,[],t.selector);n.onDestroy(function(){e._unloadComponent(n)});var r=n.injector.get(_.Testability,null);d.isPresent(r)&&n.injector.get(_.TestabilityRegistry).registerApplication(n.location.nativeElement,r),e._loadComponent(n);var i=e._injector.get(E.Console);return d.assertionsEnabled()&&i.log("Angular 2 is running in the development mode. Call enableProdMode() to enable the production mode."),n})},e.prototype._loadComponent=function(t){this._changeDetectorRefs.push(t.changeDetectorRef),this.tick(),this._rootComponents.push(t),this._bootstrapListeners.forEach(function(e){return e(t)})},e.prototype._unloadComponent=function(t){g.ListWrapper.contains(this._rootComponents,t)&&(this.unregisterChangeDetector(t.changeDetectorRef),g.ListWrapper.remove(this._rootComponents,t))},Object.defineProperty(e.prototype,"injector",{get:function(){return this._injector},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"zone",{get:function(){return this._zone},enumerable:!0,configurable:!0}),e.prototype.tick=function(){if(this._runningTick)throw new P.BaseException("ApplicationRef.tick is called recursively");var t=e._tickScope();try{this._runningTick=!0,this._changeDetectorRefs.forEach(function(t){return t.detectChanges()}),this._enforceNoNewChanges&&this._changeDetectorRefs.forEach(function(t){return t.checkNoChanges()})}finally{this._runningTick=!1,w.wtfLeave(t)}},e.prototype.dispose=function(){g.ListWrapper.clone(this._rootComponents).forEach(function(t){return t.destroy()}),this._disposeListeners.forEach(function(t){return t()}),this._platform._applicationDisposed(this)},Object.defineProperty(e.prototype,"componentTypes",{get:function(){return this._rootComponentTypes},enumerable:!0,configurable:!0}),e._tickScope=w.wtfCreateScope("ApplicationRef#tick()"),e=l([v.Injectable(),h("design:paramtypes",[O,f.NgZone,v.Injector])],e)}(T);e.ApplicationRef_=x,e.PLATFORM_CORE_PROVIDERS=d.CONST_EXPR([O,d.CONST_EXPR(new v.Provider(S,{useExisting:O}))]),e.APPLICATION_CORE_PROVIDERS=d.CONST_EXPR([d.CONST_EXPR(new v.Provider(f.NgZone,{useFactory:r,deps:d.CONST_EXPR([])})),x,d.CONST_EXPR(new v.Provider(T,{useExisting:x}))])},function(t,e,n){"use strict";var r=n(40),i=n(61),o=n(12),s=n(61);e.NgZoneError=s.NgZoneError;var a=function(){function t(t){var e=this,n=t.enableLongStackTrace,o=void 0===n?!1:n;this._hasPendingMicrotasks=!1,this._hasPendingMacrotasks=!1,this._isStable=!0,this._nesting=0,this._onUnstable=new r.EventEmitter(!1),this._onMicrotaskEmpty=new r.EventEmitter(!1),this._onStable=new r.EventEmitter(!1),this._onErrorEvents=new r.EventEmitter(!1),this._zoneImpl=new i.NgZoneImpl({trace:o,onEnter:function(){e._nesting++,e._isStable&&(e._isStable=!1,e._onUnstable.emit(null))},onLeave:function(){e._nesting--,e._checkStable()},setMicrotask:function(t){e._hasPendingMicrotasks=t,e._checkStable()},setMacrotask:function(t){e._hasPendingMacrotasks=t},onError:function(t){return e._onErrorEvents.emit(t)}})}return t.isInAngularZone=function(){return i.NgZoneImpl.isInAngularZone()},t.assertInAngularZone=function(){if(!i.NgZoneImpl.isInAngularZone())throw new o.BaseException("Expected to be in Angular Zone, but it is not!")},t.assertNotInAngularZone=function(){if(i.NgZoneImpl.isInAngularZone())throw new o.BaseException("Expected to not be in Angular Zone, but it is!")},t.prototype._checkStable=function(){var t=this;if(0==this._nesting&&!this._hasPendingMicrotasks&&!this._isStable)try{this._nesting++,this._onMicrotaskEmpty.emit(null)}finally{if(this._nesting--,!this._hasPendingMicrotasks)try{this.runOutsideAngular(function(){return t._onStable.emit(null)})}finally{this._isStable=!0}}},Object.defineProperty(t.prototype,"onUnstable",{get:function(){return this._onUnstable},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"onMicrotaskEmpty",{get:function(){return this._onMicrotaskEmpty},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"onStable",{get:function(){return this._onStable},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"onError",{get:function(){return this._onErrorEvents},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"hasPendingMicrotasks",{get:function(){return this._hasPendingMicrotasks},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"hasPendingMacrotasks",{get:function(){return this._hasPendingMacrotasks},enumerable:!0,configurable:!0}),t.prototype.run=function(t){return this._zoneImpl.runInner(t)},t.prototype.runGuarded=function(t){return this._zoneImpl.runInnerGuarded(t)},t.prototype.runOutsideAngular=function(t){return this._zoneImpl.runOuter(t)},t}();e.NgZone=a},function(t,e){"use strict";var n=function(){function t(t,e){this.error=t,this.stackTrace=e}return t}();e.NgZoneError=n;var r=function(){function t(t){var e=this,r=t.trace,i=t.onEnter,o=t.onLeave,s=t.setMicrotask,a=t.setMacrotask,u=t.onError;if(this.onEnter=i,this.onLeave=o,this.setMicrotask=s,this.setMacrotask=a,this.onError=u,!Zone)throw new Error("Angular2 needs to be run with Zone.js polyfill.");this.outer=this.inner=Zone.current,Zone.wtfZoneSpec&&(this.inner=this.inner.fork(Zone.wtfZoneSpec)),r&&Zone.longStackTraceZoneSpec&&(this.inner=this.inner.fork(Zone.longStackTraceZoneSpec)),this.inner=this.inner.fork({name:"angular",properties:{isAngularZone:!0},onInvokeTask:function(t,n,r,i,o,s){try{return e.onEnter(),t.invokeTask(r,i,o,s)}finally{e.onLeave()}},onInvoke:function(t,n,r,i,o,s,a){try{return e.onEnter(),t.invoke(r,i,o,s,a)}finally{e.onLeave()}},onHasTask:function(t,n,r,i){t.hasTask(r,i),n==r&&("microTask"==i.change?e.setMicrotask(i.microTask):"macroTask"==i.change&&e.setMacrotask(i.macroTask))},onHandleError:function(t,r,i,o){return t.handleError(i,o),e.onError(new n(o,o.stack)),!1}})}return t.isInAngularZone=function(){return Zone.current.get("isAngularZone")===!0},t.prototype.runInner=function(t){return this.inner.run(t)},t.prototype.runInnerGuarded=function(t){return this.inner.runGuarded(t)},t.prototype.runOuter=function(t){return this.outer.run(t)},t}();e.NgZoneImpl=r},function(t,e,n){"use strict";function r(){return""+i()+i()+i()}function i(){return s.StringWrapper.fromCharCode(97+s.Math.floor(25*s.Math.random()))}var o=n(6),s=n(5);e.APP_ID=s.CONST_EXPR(new o.OpaqueToken("AppId")),e.APP_ID_RANDOM_PROVIDER=s.CONST_EXPR(new o.Provider(e.APP_ID,{useFactory:r,deps:[]})),e.PLATFORM_INITIALIZER=s.CONST_EXPR(new o.OpaqueToken("Platform Initializer")),e.APP_INITIALIZER=s.CONST_EXPR(new o.OpaqueToken("Application Initializer")),e.PACKAGE_ROOT_URL=s.CONST_EXPR(new o.OpaqueToken("Application Packages Root URL"))},function(t,e,n){"use strict";function r(t){v=t}var i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(6),a=n(15),u=n(5),c=n(12),p=n(60),l=n(40),h=function(){function t(t){this._ngZone=t,this._pendingCount=0,this._isZoneStable=!0,this._didWork=!1,this._callbacks=[],this._watchAngularEvents()}return t.prototype._watchAngularEvents=function(){var t=this;l.ObservableWrapper.subscribe(this._ngZone.onUnstable,function(e){t._didWork=!0,t._isZoneStable=!1}),this._ngZone.runOutsideAngular(function(){l.ObservableWrapper.subscribe(t._ngZone.onStable,function(e){p.NgZone.assertNotInAngularZone(),u.scheduleMicroTask(function(){t._isZoneStable=!0,t._runCallbacksIfReady()})})})},t.prototype.increasePendingRequestCount=function(){return this._pendingCount+=1,this._didWork=!0,this._pendingCount},t.prototype.decreasePendingRequestCount=function(){if(this._pendingCount-=1,this._pendingCount<0)throw new c.BaseException("pending async requests below zero");return this._runCallbacksIfReady(),this._pendingCount},t.prototype.isStable=function(){return this._isZoneStable&&0==this._pendingCount&&!this._ngZone.hasPendingMacrotasks},t.prototype._runCallbacksIfReady=function(){var t=this;this.isStable()?u.scheduleMicroTask(function(){for(;0!==t._callbacks.length;)t._callbacks.pop()(t._didWork);t._didWork=!1}):this._didWork=!0},t.prototype.whenStable=function(t){this._callbacks.push(t),this._runCallbacksIfReady()},t.prototype.getPendingRequestCount=function(){return this._pendingCount},t.prototype.findBindings=function(t,e,n){return[]},t.prototype.findProviders=function(t,e,n){return[]},t=i([s.Injectable(),o("design:paramtypes",[p.NgZone])],t)}();e.Testability=h;var f=function(){function t(){this._applications=new a.Map,v.addToWindow(this)}return t.prototype.registerApplication=function(t,e){this._applications.set(t,e)},t.prototype.getTestability=function(t){return this._applications.get(t)},t.prototype.getAllTestabilities=function(){return a.MapWrapper.values(this._applications)},t.prototype.getAllRootElements=function(){return a.MapWrapper.keys(this._applications)},t.prototype.findTestabilityInTree=function(t,e){return void 0===e&&(e=!0),v.findTestabilityInTree(this,t,e)},t=i([s.Injectable(),o("design:paramtypes",[])],t)}();e.TestabilityRegistry=f;var d=function(){function t(){}return t.prototype.addToWindow=function(t){},t.prototype.findTestabilityInTree=function(t,e,n){return null},t=i([u.CONST(),o("design:paramtypes",[])],t)}();e.setTestabilityGetter=r;var v=u.CONST_EXPR(new d)},function(t,e,n){"use strict";function r(t){return t instanceof h.ComponentFactory}var i=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},o=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},s=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},a=n(6),u=n(5),c=n(12),p=n(40),l=n(18),h=n(65),f=function(){function t(){}return t}();e.ComponentResolver=f;var d=function(t){function e(){t.apply(this,arguments)}return i(e,t),e.prototype.resolveComponent=function(t){var e=l.reflector.annotations(t),n=e.find(r);if(u.isBlank(n))throw new c.BaseException("No precompiled component "+u.stringify(t)+" found");return p.PromiseWrapper.resolve(n)},e.prototype.clearCache=function(){},e=o([a.Injectable(),s("design:paramtypes",[])],e)}(f);e.ReflectorComponentResolver=d},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(5),a=n(12),u=n(66),c=function(){function t(){}return Object.defineProperty(t.prototype,"location",{get:function(){return a.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"injector",{get:function(){return a.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"instance",{get:function(){return a.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"hostView",{get:function(){return a.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"changeDetectorRef",{get:function(){return a.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"componentType",{get:function(){return a.unimplemented()},enumerable:!0,configurable:!0}),t}();e.ComponentRef=c;var p=function(t){function e(e,n){t.call(this),this._hostElement=e,this._componentType=n}return r(e,t),Object.defineProperty(e.prototype,"location",{get:function(){return this._hostElement.elementRef},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"injector",{get:function(){return this._hostElement.injector},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"instance",{get:function(){return this._hostElement.component},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"hostView",{get:function(){return this._hostElement.parentView.ref},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"changeDetectorRef",{get:function(){return this.hostView},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"componentType",{get:function(){return this._componentType},enumerable:!0,configurable:!0}),e.prototype.destroy=function(){this._hostElement.parentView.destroy()},e.prototype.onDestroy=function(t){this.hostView.onDestroy(t)},e}(c);e.ComponentRef_=p;var l=function(){function t(t,e,n){this.selector=t,this._viewFactory=e,this._componentType=n}return Object.defineProperty(t.prototype,"componentType",{get:function(){return this._componentType},enumerable:!0,configurable:!0}),t.prototype.create=function(t,e,n){void 0===e&&(e=null),void 0===n&&(n=null);var r=t.get(u.ViewUtils);s.isBlank(e)&&(e=[]);var i=this._viewFactory(r,t,null),o=i.create(e,n);return new p(o,this._componentType)},t=i([s.CONST(),o("design:paramtypes",[String,Function,s.Type])],t)}();e.ComponentFactory=l},function(t,e,n){"use strict";function r(t){return i(t,[])}function i(t,e){for(var n=0;ni;i++)n[i]=r>i?t[i]:D}else n=t;return n}function s(t,e,n,r,i,o,s,u,c,p,l,h,f,d,v,y,m,g,_,b){switch(t){case 1:return e+a(n)+r;case 2:return e+a(n)+r+a(i)+o;case 3:return e+a(n)+r+a(i)+o+a(s)+u;case 4:return e+a(n)+r+a(i)+o+a(s)+u+a(c)+p;case 5:return e+a(n)+r+a(i)+o+a(s)+u+a(c)+p+a(l)+h;case 6:return e+a(n)+r+a(i)+o+a(s)+u+a(c)+p+a(l)+h+a(f)+d;case 7:return e+a(n)+r+a(i)+o+a(s)+u+a(c)+p+a(l)+h+a(f)+d+a(v)+y;case 8:return e+a(n)+r+a(i)+o+a(s)+u+a(c)+p+a(l)+h+a(f)+d+a(v)+y+a(m)+g;case 9:return e+a(n)+r+a(i)+o+a(s)+u+a(c)+p+a(l)+h+a(f)+d+a(v)+y+a(m)+g+a(_)+b;default:throw new O.BaseException("Does not support more than 9 expressions")}}function a(t){return null!=t?t.toString():""}function u(t,e,n){if(t){if(!A.devModeEqual(e,n))throw new x.ExpressionChangedAfterItHasBeenCheckedException(e,n,null);return!1}return!R.looseIdentical(e,n)}function c(t,e){if(t.length!=e.length)return!1;for(var n=0;no?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},w=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},C=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},R=n(5),S=n(15),O=n(12),T=n(67),x=n(73),A=n(28),I=n(6),M=n(74),k=n(62),N=function(){function t(t,e){this._renderer=t,this._appId=e,this._nextCompTypeId=0}return t.prototype.createRenderComponentType=function(t,e,n,r){return new M.RenderComponentType(this._appId+"-"+this._nextCompTypeId++,t,e,n,r)},t.prototype.renderComponent=function(t){return this._renderer.renderComponent(t)},t=E([I.Injectable(),C(1,I.Inject(k.APP_ID)),w("design:paramtypes",[M.RootRenderer,String])],t)}();e.ViewUtils=N,e.flattenNestedViewRenderNodes=r;var D=R.CONST_EXPR([]);e.ensureSlotCount=o,e.MAX_INTERPOLATION_VALUES=9,e.interpolate=s,e.checkBinding=u,e.arrayLooseIdentical=c,e.mapLooseIdentical=p,e.castByValue=l,e.pureProxy1=h,e.pureProxy2=f,e.pureProxy3=d,e.pureProxy4=v,e.pureProxy5=y,e.pureProxy6=m,e.pureProxy7=g,e.pureProxy8=_,e.pureProxy9=b,e.pureProxy10=P},function(t,e,n){"use strict";var r=n(5),i=n(15),o=n(12),s=n(68),a=n(69),u=n(70),c=function(){function t(t,e,n,r){this.index=t,this.parentIndex=e,this.parentView=n,this.nativeElement=r,this.nestedViews=null,this.componentView=null}return Object.defineProperty(t.prototype,"elementRef",{get:function(){return new a.ElementRef(this.nativeElement)},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"vcRef",{get:function(){return new u.ViewContainerRef_(this)},enumerable:!0,configurable:!0}),t.prototype.initComponent=function(t,e,n){this.component=t,this.componentConstructorViewQueries=e,this.componentView=n},Object.defineProperty(t.prototype,"parentInjector",{get:function(){return this.parentView.injector(this.parentIndex)},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"injector",{get:function(){return this.parentView.injector(this.index)},enumerable:!0,configurable:!0}),t.prototype.mapNestedViews=function(t,e){var n=[];return r.isPresent(this.nestedViews)&&this.nestedViews.forEach(function(r){r.clazz===t&&n.push(e(r))}),n},t.prototype.attachView=function(t,e){if(t.type===s.ViewType.COMPONENT)throw new o.BaseException("Component views can't be moved!");var n=this.nestedViews;null==n&&(n=[],this.nestedViews=n),i.ListWrapper.insert(n,e,t);var a;if(e>0){var u=n[e-1];a=u.lastRootNode}else a=this.nativeElement;r.isPresent(a)&&t.renderer.attachViewAfter(a,t.flatRootNodes),t.addToContentChildren(this)},t.prototype.detachView=function(t){var e=i.ListWrapper.removeAt(this.nestedViews,t);if(e.type===s.ViewType.COMPONENT)throw new o.BaseException("Component views can't be moved!");return e.renderer.detachView(e.flatRootNodes),e.removeFromContentChildren(this),e},t}();e.AppElement=c},function(t,e){"use strict";!function(t){t[t.HOST=0]="HOST",t[t.COMPONENT=1]="COMPONENT",t[t.EMBEDDED=2]="EMBEDDED"}(e.ViewType||(e.ViewType={}));e.ViewType},function(t,e){"use strict";var n=function(){function t(t){this.nativeElement=t}return t}();e.ElementRef=n},function(t,e,n){"use strict";var r=n(15),i=n(12),o=n(5),s=n(71),a=function(){function t(){}return Object.defineProperty(t.prototype,"element",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"injector",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"parentInjector",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"length",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),t}();e.ViewContainerRef=a;var u=function(){function t(t){this._element=t,this._createComponentInContainerScope=s.wtfCreateScope("ViewContainerRef#createComponent()"),this._insertScope=s.wtfCreateScope("ViewContainerRef#insert()"),this._removeScope=s.wtfCreateScope("ViewContainerRef#remove()"),this._detachScope=s.wtfCreateScope("ViewContainerRef#detach()")}return t.prototype.get=function(t){return this._element.nestedViews[t].ref},Object.defineProperty(t.prototype,"length",{get:function(){var t=this._element.nestedViews;return o.isPresent(t)?t.length:0},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"element",{get:function(){return this._element.elementRef},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"injector",{get:function(){return this._element.injector},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"parentInjector",{get:function(){return this._element.parentInjector},enumerable:!0,configurable:!0}),t.prototype.createEmbeddedView=function(t,e){void 0===e&&(e=-1);var n=t.createEmbeddedView();return this.insert(n,e),n},t.prototype.createComponent=function(t,e,n,r){void 0===e&&(e=-1),void 0===n&&(n=null),void 0===r&&(r=null);var i=this._createComponentInContainerScope(),a=o.isPresent(n)?n:this._element.parentInjector,u=t.create(a,r);return this.insert(u.hostView,e),s.wtfLeave(i,u)},t.prototype.insert=function(t,e){void 0===e&&(e=-1);var n=this._insertScope();-1==e&&(e=this.length);var r=t;return this._element.attachView(r.internalView,e),s.wtfLeave(n,r)},t.prototype.indexOf=function(t){return r.ListWrapper.indexOf(this._element.nestedViews,t.internalView)},t.prototype.remove=function(t){void 0===t&&(t=-1);var e=this._removeScope();-1==t&&(t=this.length-1);var n=this._element.detachView(t);n.destroy(),s.wtfLeave(e)},t.prototype.detach=function(t){void 0===t&&(t=-1);var e=this._detachScope();-1==t&&(t=this.length-1);var n=this._element.detachView(t);return s.wtfLeave(e,n.ref)},t.prototype.clear=function(){for(var t=this.length-1;t>=0;t--)this.remove(t)},t}();e.ViewContainerRef_=u},function(t,e,n){"use strict";function r(t,e){return null}var i=n(72);e.wtfEnabled=i.detectWTF(),e.wtfCreateScope=e.wtfEnabled?i.createScope:function(t,e){return r},e.wtfLeave=e.wtfEnabled?i.leave:function(t,e){return e},e.wtfStartTimeRange=e.wtfEnabled?i.startTimeRange:function(t,e){return null},e.wtfEndTimeRange=e.wtfEnabled?i.endTimeRange:function(t){return null}},function(t,e,n){"use strict";function r(){var t=p.global.wtf;return t&&(u=t.trace)?(c=u.events,!0):!1}function i(t,e){return void 0===e&&(e=null),c.createScope(t,e)}function o(t,e){return u.leaveScope(t,e),e}function s(t,e){return u.beginTimeRange(t,e)}function a(t){u.endTimeRange(t)}var u,c,p=n(5);e.detectWTF=r,e.createScope=i,e.leave=o,e.startTimeRange=s,e.endTimeRange=a},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(12),o=function(t){function e(e,n,r){t.call(this,"Expression has changed after it was checked. "+("Previous value: '"+e+"'. Current value: '"+n+"'"))}return r(e,t),e}(i.BaseException);e.ExpressionChangedAfterItHasBeenCheckedException=o;var s=function(t){function e(e,n,r){t.call(this,"Error in "+r.source,e,n,r)}return r(e,t),e}(i.WrappedException);e.ViewWrappedException=s;var a=function(t){function e(e){t.call(this,"Attempt to use a destroyed view: "+e)}return r(e,t),e}(i.BaseException);e.ViewDestroyedException=a},function(t,e,n){"use strict";var r=n(12),i=function(){function t(t,e,n,r,i){this.id=t,this.templateUrl=e,this.slotCount=n,this.encapsulation=r,this.styles=i}return t}();e.RenderComponentType=i;var o=function(){function t(){}return Object.defineProperty(t.prototype,"injector",{get:function(){return r.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"component",{get:function(){ -return r.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"providerTokens",{get:function(){return r.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"locals",{get:function(){return r.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"source",{get:function(){return r.unimplemented()},enumerable:!0,configurable:!0}),t}();e.RenderDebugInfo=o;var s=function(){function t(){}return t}();e.Renderer=s;var a=function(){function t(){}return t}();e.RootRenderer=a},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(6),s=n(5),a=function(){function t(){}return t.prototype.log=function(t){s.print(t)},t=r([o.Injectable(),i("design:paramtypes",[])],t)}();e.Console=a},function(t,e,n){"use strict";var r=n(60);e.NgZone=r.NgZone,e.NgZoneError=r.NgZoneError},function(t,e,n){"use strict";var r=n(74);e.RootRenderer=r.RootRenderer,e.Renderer=r.Renderer,e.RenderComponentType=r.RenderComponentType},function(t,e,n){"use strict";var r=n(64);e.ComponentResolver=r.ComponentResolver;var i=n(79);e.QueryList=i.QueryList;var o=n(80);e.DynamicComponentLoader=o.DynamicComponentLoader;var s=n(69);e.ElementRef=s.ElementRef;var a=n(81);e.TemplateRef=a.TemplateRef;var u=n(82);e.EmbeddedViewRef=u.EmbeddedViewRef,e.ViewRef=u.ViewRef;var c=n(70);e.ViewContainerRef=c.ViewContainerRef;var p=n(65);e.ComponentRef=p.ComponentRef,e.ComponentFactory=p.ComponentFactory;var l=n(73);e.ExpressionChangedAfterItHasBeenCheckedException=l.ExpressionChangedAfterItHasBeenCheckedException},function(t,e,n){"use strict";var r=n(15),i=n(5),o=n(40),s=function(){function t(){this._dirty=!0,this._results=[],this._emitter=new o.EventEmitter}return Object.defineProperty(t.prototype,"changes",{get:function(){return this._emitter},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"length",{get:function(){return this._results.length},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"first",{get:function(){return r.ListWrapper.first(this._results)},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"last",{get:function(){return r.ListWrapper.last(this._results)},enumerable:!0,configurable:!0}),t.prototype.map=function(t){return this._results.map(t)},t.prototype.filter=function(t){return this._results.filter(t)},t.prototype.reduce=function(t,e){return this._results.reduce(t,e)},t.prototype.forEach=function(t){this._results.forEach(t)},t.prototype.toArray=function(){return r.ListWrapper.clone(this._results)},t.prototype[i.getSymbolIterator()]=function(){return this._results[i.getSymbolIterator()]()},t.prototype.toString=function(){return this._results.toString()},t.prototype.reset=function(t){this._results=r.ListWrapper.flatten(t),this._dirty=!1},t.prototype.notifyOnChanges=function(){this._emitter.emit(this)},t.prototype.setDirty=function(){this._dirty=!0},Object.defineProperty(t.prototype,"dirty",{get:function(){return this._dirty},enumerable:!0,configurable:!0}),t}();e.QueryList=s},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(6),a=n(64),u=n(5),c=function(){function t(){}return t}();e.DynamicComponentLoader=c;var p=function(t){function e(e){t.call(this),this._compiler=e}return r(e,t),e.prototype.loadAsRoot=function(t,e,n,r,i){return this._compiler.resolveComponent(t).then(function(t){var o=t.create(n,i,u.isPresent(e)?e:t.selector);return u.isPresent(r)&&o.onDestroy(r),o})},e.prototype.loadNextToLocation=function(t,e,n,r){return void 0===n&&(n=null),void 0===r&&(r=null),this._compiler.resolveComponent(t).then(function(t){var i=e.parentInjector,o=u.isPresent(n)&&n.length>0?s.ReflectiveInjector.fromResolvedProviders(n,i):i;return e.createComponent(t,e.length,o,r)})},e=i([s.Injectable(),o("design:paramtypes",[a.ComponentResolver])],e)}(c);e.DynamicComponentLoader_=p},function(t,e){"use strict";var n=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},r=function(){function t(){}return Object.defineProperty(t.prototype,"elementRef",{get:function(){return null},enumerable:!0,configurable:!0}),t}();e.TemplateRef=r;var i=function(t){function e(e,n){t.call(this),this._appElement=e,this._viewFactory=n}return n(e,t),e.prototype.createEmbeddedView=function(){var t=this._viewFactory(this._appElement.parentView.viewUtils,this._appElement.parentInjector,this._appElement);return t.create(null,null),t.ref},Object.defineProperty(e.prototype,"elementRef",{get:function(){return this._appElement.elementRef},enumerable:!0,configurable:!0}),e}(r);e.TemplateRef_=i},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(12),o=n(34),s=n(33),a=function(t){function e(){t.apply(this,arguments)}return r(e,t),Object.defineProperty(e.prototype,"changeDetectorRef",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"destroyed",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),e}(o.ChangeDetectorRef);e.ViewRef=a;var u=function(t){function e(){t.apply(this,arguments)}return r(e,t),Object.defineProperty(e.prototype,"rootNodes",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),e}(a);e.EmbeddedViewRef=u;var c=function(){function t(t){this._view=t,this._view=t}return Object.defineProperty(t.prototype,"internalView",{get:function(){return this._view},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"changeDetectorRef",{get:function(){return this},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"rootNodes",{get:function(){return this._view.flatRootNodes},enumerable:!0,configurable:!0}),t.prototype.setLocal=function(t,e){this._view.setLocal(t,e)},t.prototype.hasLocal=function(t){return this._view.hasLocal(t)},Object.defineProperty(t.prototype,"destroyed",{get:function(){return this._view.destroyed},enumerable:!0,configurable:!0}),t.prototype.markForCheck=function(){this._view.markPathToRootAsCheckOnce()},t.prototype.detach=function(){this._view.cdMode=s.ChangeDetectionStrategy.Detached},t.prototype.detectChanges=function(){this._view.detectChanges(!1)},t.prototype.checkNoChanges=function(){this._view.detectChanges(!0)},t.prototype.reattach=function(){this._view.cdMode=s.ChangeDetectionStrategy.CheckAlways,this.markForCheck()},t.prototype.onDestroy=function(t){this._view.disposables.push(t)},t.prototype.destroy=function(){this._view.destroy()},t}();e.ViewRef_=c},function(t,e,n){"use strict";function r(t){return t.map(function(t){return t.nativeElement})}function i(t,e,n){t.childNodes.forEach(function(t){t instanceof v&&(e(t)&&n.push(t),i(t,e,n))})}function o(t,e,n){t instanceof v&&t.childNodes.forEach(function(t){e(t)&&n.push(t),t instanceof v&&o(t,e,n)})}function s(t){return y.get(t)}function a(){return h.MapWrapper.values(y)}function u(t){y.set(t.nativeNode,t)}function c(t){y["delete"](t.nativeNode)}var p=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},l=n(5),h=n(15),f=function(){function t(t,e){this.name=t,this.callback=e}return t}();e.EventListener=f;var d=function(){function t(t,e,n){this._debugInfo=n,this.nativeNode=t,l.isPresent(e)&&e instanceof v?e.addChild(this):this.parent=null,this.listeners=[]}return Object.defineProperty(t.prototype,"injector",{get:function(){return l.isPresent(this._debugInfo)?this._debugInfo.injector:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"componentInstance",{get:function(){return l.isPresent(this._debugInfo)?this._debugInfo.component:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"locals",{get:function(){return l.isPresent(this._debugInfo)?this._debugInfo.locals:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"providerTokens",{get:function(){return l.isPresent(this._debugInfo)?this._debugInfo.providerTokens:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"source",{get:function(){return l.isPresent(this._debugInfo)?this._debugInfo.source:null},enumerable:!0,configurable:!0}),t.prototype.inject=function(t){return this.injector.get(t)},t.prototype.getLocal=function(t){return this.locals[t]},t}();e.DebugNode=d;var v=function(t){function e(e,n,r){t.call(this,e,n,r),this.properties={},this.attributes={},this.childNodes=[],this.nativeElement=e}return p(e,t),e.prototype.addChild=function(t){l.isPresent(t)&&(this.childNodes.push(t),t.parent=this)},e.prototype.removeChild=function(t){var e=this.childNodes.indexOf(t);-1!==e&&(t.parent=null,this.childNodes.splice(e,1))},e.prototype.insertChildrenAfter=function(t,e){var n=this.childNodes.indexOf(t);if(-1!==n){var r=this.childNodes.slice(0,n+1),i=this.childNodes.slice(n+1);this.childNodes=h.ListWrapper.concat(h.ListWrapper.concat(r,e),i);for(var o=0;o0?e[0]:null},e.prototype.queryAll=function(t){var e=[];return i(this,t,e),e},e.prototype.queryAllNodes=function(t){var e=[];return o(this,t,e),e},Object.defineProperty(e.prototype,"children",{get:function(){var t=[];return this.childNodes.forEach(function(n){n instanceof e&&t.push(n)}),t},enumerable:!0,configurable:!0}),e.prototype.triggerEventHandler=function(t,e){this.listeners.forEach(function(n){n.name==t&&n.callback(e)})},e}(d);e.DebugElement=v,e.asNativeElements=r;var y=new Map;e.getDebugNode=s,e.getAllDebugNodes=a,e.indexDebugNode=u,e.removeDebugNodeFromIndex=c},function(t,e,n){"use strict";var r=n(6),i=n(5);e.PLATFORM_DIRECTIVES=i.CONST_EXPR(new r.OpaqueToken("Platform Directives")),e.PLATFORM_PIPES=i.CONST_EXPR(new r.OpaqueToken("Platform Pipes"))},function(t,e,n){"use strict";function r(){return a.reflector}var i=n(5),o=n(6),s=n(75),a=n(18),u=n(20),c=n(63),p=n(59);e.PLATFORM_COMMON_PROVIDERS=i.CONST_EXPR([p.PLATFORM_CORE_PROVIDERS,new o.Provider(a.Reflector,{useFactory:r,deps:[]}),new o.Provider(u.ReflectorReader,{useExisting:a.Reflector}),c.TestabilityRegistry,s.Console])},function(t,e,n){"use strict";var r=n(5),i=n(6),o=n(62),s=n(59),a=n(28),u=n(66),c=n(64),p=n(64),l=n(80),h=n(80);e.APPLICATION_COMMON_PROVIDERS=r.CONST_EXPR([s.APPLICATION_CORE_PROVIDERS,new i.Provider(c.ComponentResolver,{useClass:p.ReflectorComponentResolver}),o.APP_ID_RANDOM_PROVIDER,u.ViewUtils,new i.Provider(a.IterableDiffers,{useValue:a.defaultIterableDiffers}),new i.Provider(a.KeyValueDiffers,{useValue:a.defaultKeyValueDiffers}),new i.Provider(l.DynamicComponentLoader,{useClass:h.DynamicComponentLoader_})])},function(t,e,n){"use strict";function r(t){for(var n in t)e.hasOwnProperty(n)||(e[n]=t[n])}r(n(88)),r(n(102)),r(n(112)),r(n(136))},function(t,e,n){"use strict";var r=n(89);e.AsyncPipe=r.AsyncPipe;var i=n(91);e.DatePipe=i.DatePipe;var o=n(93);e.JsonPipe=o.JsonPipe;var s=n(94);e.SlicePipe=s.SlicePipe;var a=n(95);e.LowerCasePipe=a.LowerCasePipe;var u=n(96);e.NumberPipe=u.NumberPipe,e.DecimalPipe=u.DecimalPipe,e.PercentPipe=u.PercentPipe,e.CurrencyPipe=u.CurrencyPipe;var c=n(97);e.UpperCasePipe=c.UpperCasePipe;var p=n(98);e.ReplacePipe=p.ReplacePipe;var l=n(99);e.I18nPluralPipe=l.I18nPluralPipe;var h=n(100);e.I18nSelectPipe=h.I18nSelectPipe;var f=n(101);e.COMMON_PIPES=f.COMMON_PIPES},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(40),a=n(2),u=n(90),c=function(){function t(){}return t.prototype.createSubscription=function(t,e){return s.ObservableWrapper.subscribe(t,e,function(t){throw t})},t.prototype.dispose=function(t){s.ObservableWrapper.dispose(t)},t.prototype.onDestroy=function(t){s.ObservableWrapper.dispose(t)},t}(),p=function(){function t(){}return t.prototype.createSubscription=function(t,e){return t.then(e)},t.prototype.dispose=function(t){},t.prototype.onDestroy=function(t){},t}(),l=new p,h=new c,f=function(){function t(t){this._latestValue=null,this._latestReturnedValue=null,this._subscription=null,this._obj=null,this._strategy=null,this._ref=t}return t.prototype.ngOnDestroy=function(){o.isPresent(this._subscription)&&this._dispose()},t.prototype.transform=function(t){return o.isBlank(this._obj)?(o.isPresent(t)&&this._subscribe(t),this._latestReturnedValue=this._latestValue,this._latestValue):t!==this._obj?(this._dispose(),this.transform(t)):this._latestValue===this._latestReturnedValue?this._latestReturnedValue:(this._latestReturnedValue=this._latestValue,a.WrappedValue.wrap(this._latestValue))},t.prototype._subscribe=function(t){var e=this;this._obj=t,this._strategy=this._selectStrategy(t),this._subscription=this._strategy.createSubscription(t,function(n){return e._updateLatestValue(t,n)})},t.prototype._selectStrategy=function(e){if(o.isPromise(e))return l;if(s.ObservableWrapper.isObservable(e))return h;throw new u.InvalidPipeArgumentException(t,e)},t.prototype._dispose=function(){this._strategy.dispose(this._subscription),this._latestValue=null,this._latestReturnedValue=null,this._subscription=null,this._obj=null},t.prototype._updateLatestValue=function(t,e){t===this._obj&&(this._latestValue=e,this._ref.markForCheck())},t=r([a.Pipe({name:"async",pure:!1}),a.Injectable(),i("design:paramtypes",[a.ChangeDetectorRef])],t)}();e.AsyncPipe=f},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(5),o=n(12),s=function(t){function e(e,n){t.call(this,"Invalid argument '"+n+"' for pipe '"+i.stringify(e)+"'")}return r(e,t),e}(o.BaseException);e.InvalidPipeArgumentException=s},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(92),a=n(2),u=n(15),c=n(90),p="en-US",l=function(){function t(){}return t.prototype.transform=function(e,n){if(void 0===n&&(n="mediumDate"),o.isBlank(e))return null;if(!this.supports(e))throw new c.InvalidPipeArgumentException(t,e);return o.isNumber(e)&&(e=o.DateWrapper.fromMillis(e)),u.StringMapWrapper.contains(t._ALIASES,n)&&(n=u.StringMapWrapper.get(t._ALIASES,n)),s.DateFormatter.format(e,p,n)},t.prototype.supports=function(t){return o.isDate(t)||o.isNumber(t)},t._ALIASES={medium:"yMMMdjms","short":"yMdjm",fullDate:"yMMMMEEEEd",longDate:"yMMMMd",mediumDate:"yMMMd",shortDate:"yMd",mediumTime:"jms",shortTime:"jm"},t=r([o.CONST(),a.Pipe({name:"date",pure:!0}),a.Injectable(),i("design:paramtypes",[])],t)}();e.DatePipe=l},function(t,e){"use strict";function n(t){return 2==t?"2-digit":"numeric"}function r(t){return 4>t?"short":"long"}function i(t){for(var e,i={},o=0;o=3?i.month=r(s):i.month=n(s);break;case"d":i.day=n(s);break;case"E":i.weekday=r(s);break;case"j":i.hour=n(s);break;case"h":i.hour=n(s),i.hour12=!0;break;case"H":i.hour=n(s),i.hour12=!1;break;case"m":i.minute=n(s);break;case"s":i.second=n(s);break;case"z":i.timeZoneName="long";break;case"Z":i.timeZoneName="short"}o=e}return i}!function(t){t[t.Decimal=0]="Decimal",t[t.Percent=1]="Percent",t[t.Currency=2]="Currency"}(e.NumberFormatStyle||(e.NumberFormatStyle={}));var o=e.NumberFormatStyle,s=function(){function t(){}return t.format=function(t,e,n,r){var i=void 0===r?{}:r,s=i.minimumIntegerDigits,a=void 0===s?1:s,u=i.minimumFractionDigits,c=void 0===u?0:u,p=i.maximumFractionDigits,l=void 0===p?3:p,h=i.currency,f=i.currencyAsSymbol,d=void 0===f?!1:f,v={minimumIntegerDigits:a,minimumFractionDigits:c,maximumFractionDigits:l};return v.style=o[n].toLowerCase(),n==o.Currency&&(v.currency=h,v.currencyDisplay=d?"symbol":"code"),new Intl.NumberFormat(e,v).format(t)},t}();e.NumberFormatter=s;var a=new Map,u=function(){function t(){}return t.format=function(t,e,n){var r=e+n;if(a.has(r))return a.get(r).format(t);var o=new Intl.DateTimeFormat(e,i(n));return a.set(r,o),o.format(t)},t}();e.DateFormatter=u},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(2),a=function(){function t(){}return t.prototype.transform=function(t){return o.Json.stringify(t)},t=r([o.CONST(),s.Pipe({name:"json",pure:!1}),s.Injectable(),i("design:paramtypes",[])],t)}();e.JsonPipe=a},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(15),a=n(2),u=n(90),c=function(){function t(){}return t.prototype.transform=function(e,n,r){if(void 0===r&&(r=null),!this.supports(e))throw new u.InvalidPipeArgumentException(t,e);return o.isBlank(e)?e:o.isString(e)?o.StringWrapper.slice(e,n,r):s.ListWrapper.slice(e,n,r)},t.prototype.supports=function(t){return o.isString(t)||o.isArray(t)},t=r([a.Pipe({name:"slice",pure:!1}),a.Injectable(),i("design:paramtypes",[])],t)}();e.SlicePipe=c},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(2),a=n(90),u=function(){function t(){}return t.prototype.transform=function(e){if(o.isBlank(e))return e;if(!o.isString(e))throw new a.InvalidPipeArgumentException(t,e);return e.toLowerCase()},t=r([o.CONST(),s.Pipe({name:"lowercase"}),s.Injectable(),i("design:paramtypes",[])],t)}();e.LowerCasePipe=u},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(5),a=n(12),u=n(92),c=n(2),p=n(90),l="en-US",h=s.RegExpWrapper.create("^(\\d+)?\\.((\\d+)(\\-(\\d+))?)?$"),f=function(){function t(){}return t._format=function(e,n,r,i,o){if(void 0===i&&(i=null),void 0===o&&(o=!1),s.isBlank(e))return null;if(!s.isNumber(e))throw new p.InvalidPipeArgumentException(t,e);var c=1,f=0,d=3;if(s.isPresent(r)){var v=s.RegExpWrapper.firstMatch(h,r);if(s.isBlank(v))throw new a.BaseException(r+" is not a valid digit info for number pipes");s.isPresent(v[1])&&(c=s.NumberWrapper.parseIntAutoRadix(v[1])),s.isPresent(v[3])&&(f=s.NumberWrapper.parseIntAutoRadix(v[3])),s.isPresent(v[5])&&(d=s.NumberWrapper.parseIntAutoRadix(v[5]))}return u.NumberFormatter.format(e,l,n,{minimumIntegerDigits:c,minimumFractionDigits:f,maximumFractionDigits:d,currency:i,currencyAsSymbol:o})},t=i([s.CONST(),c.Injectable(),o("design:paramtypes",[])],t)}();e.NumberPipe=f;var d=function(t){function e(){t.apply(this,arguments)}return r(e,t),e.prototype.transform=function(t,e){return void 0===e&&(e=null),f._format(t,u.NumberFormatStyle.Decimal,e)},e=i([s.CONST(),c.Pipe({name:"number"}),c.Injectable(),o("design:paramtypes",[])],e)}(f);e.DecimalPipe=d;var v=function(t){function e(){t.apply(this,arguments)}return r(e,t),e.prototype.transform=function(t,e){return void 0===e&&(e=null),f._format(t,u.NumberFormatStyle.Percent,e)},e=i([s.CONST(),c.Pipe({name:"percent"}),c.Injectable(),o("design:paramtypes",[])],e)}(f);e.PercentPipe=v;var y=function(t){function e(){t.apply(this,arguments)}return r(e,t),e.prototype.transform=function(t,e,n,r){return void 0===e&&(e="USD"),void 0===n&&(n=!1),void 0===r&&(r=null),f._format(t,u.NumberFormatStyle.Currency,r,e,n)},e=i([s.CONST(),c.Pipe({name:"currency"}),c.Injectable(),o("design:paramtypes",[])],e)}(f);e.CurrencyPipe=y},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(2),a=n(90),u=function(){function t(){}return t.prototype.transform=function(e){if(o.isBlank(e))return e;if(!o.isString(e))throw new a.InvalidPipeArgumentException(t,e);return e.toUpperCase()},t=r([o.CONST(),s.Pipe({name:"uppercase"}),s.Injectable(),i("design:paramtypes",[])],t)}();e.UpperCasePipe=u},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(2),a=n(90),u=function(){function t(){}return t.prototype.transform=function(e,n,r){if(o.isBlank(e))return e;if(!this._supportedInput(e))throw new a.InvalidPipeArgumentException(t,e);var i=e.toString();if(!this._supportedPattern(n))throw new a.InvalidPipeArgumentException(t,n);if(!this._supportedReplacement(r))throw new a.InvalidPipeArgumentException(t,r);if(o.isFunction(r)){var s=o.isString(n)?o.RegExpWrapper.create(n):n;return o.StringWrapper.replaceAllMapped(i,s,r)}return n instanceof RegExp?o.StringWrapper.replaceAll(i,n,r):o.StringWrapper.replace(i,n,r)},t.prototype._supportedInput=function(t){return o.isString(t)||o.isNumber(t)},t.prototype._supportedPattern=function(t){return o.isString(t)||t instanceof RegExp},t.prototype._supportedReplacement=function(t){return o.isString(t)||o.isFunction(t)},t=r([s.Pipe({name:"replace"}),s.Injectable(),i("design:paramtypes",[])],t)}();e.ReplacePipe=u},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(2),a=n(90),u=o.RegExpWrapper.create("#"),c=function(){function t(){}return t.prototype.transform=function(e,n){var r,i;if(!o.isStringMap(n))throw new a.InvalidPipeArgumentException(t,n);return r=0===e||1===e?"="+e:"other",i=o.isPresent(e)?e.toString():"",o.StringWrapper.replaceAll(n[r],u,i)},t=r([o.CONST(),s.Pipe({name:"i18nPlural",pure:!0}),s.Injectable(),i("design:paramtypes",[])],t)}();e.I18nPluralPipe=c},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(15),a=n(2),u=n(90),c=function(){function t(){}return t.prototype.transform=function(e,n){if(!o.isStringMap(n))throw new u.InvalidPipeArgumentException(t,n);return s.StringMapWrapper.contains(n,e)?n[e]:n.other},t=r([o.CONST(),a.Pipe({name:"i18nSelect",pure:!0}),a.Injectable(),i("design:paramtypes",[])],t)}();e.I18nSelectPipe=c},function(t,e,n){"use strict";var r=n(89),i=n(97),o=n(95),s=n(93),a=n(94),u=n(91),c=n(96),p=n(98),l=n(99),h=n(100),f=n(5);e.COMMON_PIPES=f.CONST_EXPR([r.AsyncPipe,i.UpperCasePipe,o.LowerCasePipe,s.JsonPipe,a.SlicePipe,c.DecimalPipe,c.PercentPipe,c.CurrencyPipe,u.DatePipe,p.ReplacePipe,l.I18nPluralPipe,h.I18nSelectPipe])},function(t,e,n){"use strict";function r(t){for(var n in t)e.hasOwnProperty(n)||(e[n]=t[n])}var i=n(103);e.NgClass=i.NgClass;var o=n(104);e.NgFor=o.NgFor;var s=n(105);e.NgIf=s.NgIf;var a=n(106);e.NgTemplateOutlet=a.NgTemplateOutlet;var u=n(107);e.NgStyle=u.NgStyle;var c=n(108);e.NgSwitch=c.NgSwitch,e.NgSwitchWhen=c.NgSwitchWhen,e.NgSwitchDefault=c.NgSwitchDefault;var p=n(109);e.NgPlural=p.NgPlural,e.NgPluralCase=p.NgPluralCase,e.NgLocalization=p.NgLocalization,r(n(110));var l=n(111);e.CORE_DIRECTIVES=l.CORE_DIRECTIVES},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(2),a=n(15),u=function(){function t(t,e,n,r){this._iterableDiffers=t,this._keyValueDiffers=e,this._ngEl=n,this._renderer=r,this._initialClasses=[]}return Object.defineProperty(t.prototype,"initialClasses",{set:function(t){this._applyInitialClasses(!0),this._initialClasses=o.isPresent(t)&&o.isString(t)?t.split(" "):[],this._applyInitialClasses(!1),this._applyClasses(this._rawClass,!1)},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"rawClass",{set:function(t){this._cleanupClasses(this._rawClass),o.isString(t)&&(t=t.split(" ")),this._rawClass=t,this._iterableDiffer=null,this._keyValueDiffer=null,o.isPresent(t)&&(a.isListLikeIterable(t)?this._iterableDiffer=this._iterableDiffers.find(t).create(null):this._keyValueDiffer=this._keyValueDiffers.find(t).create(null))},enumerable:!0,configurable:!0}),t.prototype.ngDoCheck=function(){if(o.isPresent(this._iterableDiffer)){var t=this._iterableDiffer.diff(this._rawClass);o.isPresent(t)&&this._applyIterableChanges(t)}if(o.isPresent(this._keyValueDiffer)){var t=this._keyValueDiffer.diff(this._rawClass);o.isPresent(t)&&this._applyKeyValueChanges(t)}},t.prototype.ngOnDestroy=function(){this._cleanupClasses(this._rawClass)},t.prototype._cleanupClasses=function(t){this._applyClasses(t,!0),this._applyInitialClasses(!1)},t.prototype._applyKeyValueChanges=function(t){var e=this;t.forEachAddedItem(function(t){e._toggleClass(t.key,t.currentValue)}),t.forEachChangedItem(function(t){e._toggleClass(t.key,t.currentValue)}),t.forEachRemovedItem(function(t){t.previousValue&&e._toggleClass(t.key,!1)})},t.prototype._applyIterableChanges=function(t){var e=this;t.forEachAddedItem(function(t){e._toggleClass(t.item,!0)}),t.forEachRemovedItem(function(t){e._toggleClass(t.item,!1)})},t.prototype._applyInitialClasses=function(t){var e=this;this._initialClasses.forEach(function(n){return e._toggleClass(n,!t)})},t.prototype._applyClasses=function(t,e){var n=this;o.isPresent(t)&&(o.isArray(t)?t.forEach(function(t){return n._toggleClass(t,!e)}):t instanceof Set?t.forEach(function(t){return n._toggleClass(t,!e)}):a.StringMapWrapper.forEach(t,function(t,r){o.isPresent(t)&&n._toggleClass(r,!e)}))},t.prototype._toggleClass=function(t,e){if(t=t.trim(),t.length>0)if(t.indexOf(" ")>-1)for(var n=t.split(/\s+/g),r=0,i=n.length;i>r;r++)this._renderer.setElementClass(this._ngEl.nativeElement,n[r],e);else this._renderer.setElementClass(this._ngEl.nativeElement,t,e)},t=r([s.Directive({selector:"[ngClass]",inputs:["rawClass: ngClass","initialClasses: class"]}),i("design:paramtypes",[s.IterableDiffers,s.KeyValueDiffers,s.ElementRef,s.Renderer])],t)}();e.NgClass=u},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(2),s=n(5),a=n(12),u=function(){function t(t,e,n,r){this._viewContainer=t,this._templateRef=e,this._iterableDiffers=n,this._cdr=r}return Object.defineProperty(t.prototype,"ngForOf",{ -set:function(t){if(this._ngForOf=t,s.isBlank(this._differ)&&s.isPresent(t))try{this._differ=this._iterableDiffers.find(t).create(this._cdr,this._ngForTrackBy)}catch(e){throw new a.BaseException("Cannot find a differ supporting object '"+t+"' of type '"+s.getTypeNameForDebugging(t)+"'. NgFor only supports binding to Iterables such as Arrays.")}},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"ngForTemplate",{set:function(t){s.isPresent(t)&&(this._templateRef=t)},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"ngForTrackBy",{set:function(t){this._ngForTrackBy=t},enumerable:!0,configurable:!0}),t.prototype.ngDoCheck=function(){if(s.isPresent(this._differ)){var t=this._differ.diff(this._ngForOf);s.isPresent(t)&&this._applyChanges(t)}},t.prototype._applyChanges=function(t){var e=this,n=[];t.forEachRemovedItem(function(t){return n.push(new c(t,null))}),t.forEachMovedItem(function(t){return n.push(new c(t,null))});var r=this._bulkRemove(n);t.forEachAddedItem(function(t){return r.push(new c(t,null))}),this._bulkInsert(r);for(var i=0;ii;i++){var s=this._viewContainer.get(i);s.setLocal("first",0===i),s.setLocal("last",i===o-1)}t.forEachIdentityChange(function(t){var n=e._viewContainer.get(t.currentIndex);n.setLocal("$implicit",t.item)})},t.prototype._perViewChange=function(t,e){t.setLocal("$implicit",e.item),t.setLocal("index",e.currentIndex),t.setLocal("even",e.currentIndex%2==0),t.setLocal("odd",e.currentIndex%2==1)},t.prototype._bulkRemove=function(t){t.sort(function(t,e){return t.record.previousIndex-e.record.previousIndex});for(var e=[],n=t.length-1;n>=0;n--){var r=t[n];s.isPresent(r.record.currentIndex)?(r.view=this._viewContainer.detach(r.record.previousIndex),e.push(r)):this._viewContainer.remove(r.record.previousIndex)}return e},t.prototype._bulkInsert=function(t){t.sort(function(t,e){return t.record.currentIndex-e.record.currentIndex});for(var e=0;eo?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(2),s=n(5),a=function(){function t(t,e){this._viewContainer=t,this._templateRef=e,this._prevCondition=null}return Object.defineProperty(t.prototype,"ngIf",{set:function(t){!t||!s.isBlank(this._prevCondition)&&this._prevCondition?t||!s.isBlank(this._prevCondition)&&!this._prevCondition||(this._prevCondition=!1,this._viewContainer.clear()):(this._prevCondition=!0,this._viewContainer.createEmbeddedView(this._templateRef))},enumerable:!0,configurable:!0}),t=r([o.Directive({selector:"[ngIf]",inputs:["ngIf"]}),i("design:paramtypes",[o.ViewContainerRef,o.TemplateRef])],t)}();e.NgIf=a},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(2),s=n(5),a=function(){function t(t){this._viewContainerRef=t}return Object.defineProperty(t.prototype,"ngTemplateOutlet",{set:function(t){s.isPresent(this._insertedViewRef)&&this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._insertedViewRef)),s.isPresent(t)&&(this._insertedViewRef=this._viewContainerRef.createEmbeddedView(t))},enumerable:!0,configurable:!0}),r([o.Input(),i("design:type",o.TemplateRef),i("design:paramtypes",[o.TemplateRef])],t.prototype,"ngTemplateOutlet",null),t=r([o.Directive({selector:"[ngTemplateOutlet]"}),i("design:paramtypes",[o.ViewContainerRef])],t)}();e.NgTemplateOutlet=a},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(2),s=n(5),a=function(){function t(t,e,n){this._differs=t,this._ngEl=e,this._renderer=n}return Object.defineProperty(t.prototype,"rawStyle",{set:function(t){this._rawStyle=t,s.isBlank(this._differ)&&s.isPresent(t)&&(this._differ=this._differs.find(this._rawStyle).create(null))},enumerable:!0,configurable:!0}),t.prototype.ngDoCheck=function(){if(s.isPresent(this._differ)){var t=this._differ.diff(this._rawStyle);s.isPresent(t)&&this._applyChanges(t)}},t.prototype._applyChanges=function(t){var e=this;t.forEachAddedItem(function(t){e._setStyle(t.key,t.currentValue)}),t.forEachChangedItem(function(t){e._setStyle(t.key,t.currentValue)}),t.forEachRemovedItem(function(t){e._setStyle(t.key,null)})},t.prototype._setStyle=function(t,e){this._renderer.setElementStyle(this._ngEl.nativeElement,t,e)},t=r([o.Directive({selector:"[ngStyle]",inputs:["rawStyle: ngStyle"]}),i("design:paramtypes",[o.KeyValueDiffers,o.ElementRef,o.Renderer])],t)}();e.NgStyle=a},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},s=n(2),a=n(5),u=n(15),c=a.CONST_EXPR(new Object),p=function(){function t(t,e){this._viewContainerRef=t,this._templateRef=e}return t.prototype.create=function(){this._viewContainerRef.createEmbeddedView(this._templateRef)},t.prototype.destroy=function(){this._viewContainerRef.clear()},t}();e.SwitchView=p;var l=function(){function t(){this._useDefault=!1,this._valueViews=new u.Map,this._activeViews=[]}return Object.defineProperty(t.prototype,"ngSwitch",{set:function(t){this._emptyAllActiveViews(),this._useDefault=!1;var e=this._valueViews.get(t);a.isBlank(e)&&(this._useDefault=!0,e=a.normalizeBlank(this._valueViews.get(c))),this._activateViews(e),this._switchValue=t},enumerable:!0,configurable:!0}),t.prototype._onWhenValueChanged=function(t,e,n){this._deregisterView(t,n),this._registerView(e,n),t===this._switchValue?(n.destroy(),u.ListWrapper.remove(this._activeViews,n)):e===this._switchValue&&(this._useDefault&&(this._useDefault=!1,this._emptyAllActiveViews()),n.create(),this._activeViews.push(n)),0!==this._activeViews.length||this._useDefault||(this._useDefault=!0,this._activateViews(this._valueViews.get(c)))},t.prototype._emptyAllActiveViews=function(){for(var t=this._activeViews,e=0;eo?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},s=n(2),a=n(5),u=n(15),c=n(108),p="other",l=function(){function t(){}return t}();e.NgLocalization=l;var h=function(){function t(t,e,n){this.value=t,this._view=new c.SwitchView(n,e)}return t=r([s.Directive({selector:"[ngPluralCase]"}),o(0,s.Attribute("ngPluralCase")),i("design:paramtypes",[String,s.TemplateRef,s.ViewContainerRef])],t)}();e.NgPluralCase=h;var f=function(){function t(t){this._localization=t,this._caseViews=new u.Map,this.cases=null}return Object.defineProperty(t.prototype,"ngPlural",{set:function(t){this._switchValue=t,this._updateView()},enumerable:!0,configurable:!0}),t.prototype.ngAfterContentInit=function(){var t=this;this.cases.forEach(function(e){t._caseViews.set(t._formatValue(e),e._view)}),this._updateView()},t.prototype._updateView=function(){this._clearViews();var t=this._caseViews.get(this._switchValue);a.isPresent(t)||(t=this._getCategoryView(this._switchValue)),this._activateView(t)},t.prototype._clearViews=function(){a.isPresent(this._activeView)&&this._activeView.destroy()},t.prototype._activateView=function(t){a.isPresent(t)&&(this._activeView=t,this._activeView.create())},t.prototype._getCategoryView=function(t){var e=this._localization.getPluralCategory(t),n=this._caseViews.get(e);return a.isPresent(n)?n:this._caseViews.get(p)},t.prototype._isValueView=function(t){return"="===t.value[0]},t.prototype._formatValue=function(t){return this._isValueView(t)?this._stripValue(t.value):t.value},t.prototype._stripValue=function(t){return a.NumberWrapper.parseInt(t.substring(1),10)},r([s.ContentChildren(h),i("design:type",s.QueryList)],t.prototype,"cases",void 0),r([s.Input(),i("design:type",Number),i("design:paramtypes",[Number])],t.prototype,"ngPlural",null),t=r([s.Directive({selector:"[ngPlural]"}),i("design:paramtypes",[l])],t)}();e.NgPlural=f},function(t,e){"use strict"},function(t,e,n){"use strict";var r=n(5),i=n(103),o=n(104),s=n(105),a=n(106),u=n(107),c=n(108),p=n(109);e.CORE_DIRECTIVES=r.CONST_EXPR([i.NgClass,o.NgFor,s.NgIf,a.NgTemplateOutlet,u.NgStyle,c.NgSwitch,c.NgSwitchWhen,c.NgSwitchDefault,p.NgPlural,p.NgPluralCase])},function(t,e,n){"use strict";var r=n(113);e.AbstractControl=r.AbstractControl,e.Control=r.Control,e.ControlGroup=r.ControlGroup,e.ControlArray=r.ControlArray;var i=n(114);e.AbstractControlDirective=i.AbstractControlDirective;var o=n(115);e.ControlContainer=o.ControlContainer;var s=n(116);e.NgControlName=s.NgControlName;var a=n(127);e.NgFormControl=a.NgFormControl;var u=n(128);e.NgModel=u.NgModel;var c=n(117);e.NgControl=c.NgControl;var p=n(129);e.NgControlGroup=p.NgControlGroup;var l=n(130);e.NgFormModel=l.NgFormModel;var h=n(131);e.NgForm=h.NgForm;var f=n(118);e.NG_VALUE_ACCESSOR=f.NG_VALUE_ACCESSOR;var d=n(121);e.DefaultValueAccessor=d.DefaultValueAccessor;var v=n(132);e.NgControlStatus=v.NgControlStatus;var y=n(123);e.CheckboxControlValueAccessor=y.CheckboxControlValueAccessor;var m=n(124);e.NgSelectOption=m.NgSelectOption,e.SelectControlValueAccessor=m.SelectControlValueAccessor;var g=n(133);e.FORM_DIRECTIVES=g.FORM_DIRECTIVES,e.RadioButtonState=g.RadioButtonState;var _=n(120);e.NG_VALIDATORS=_.NG_VALIDATORS,e.NG_ASYNC_VALIDATORS=_.NG_ASYNC_VALIDATORS,e.Validators=_.Validators;var b=n(134);e.RequiredValidator=b.RequiredValidator,e.MinLengthValidator=b.MinLengthValidator,e.MaxLengthValidator=b.MaxLengthValidator,e.PatternValidator=b.PatternValidator;var P=n(135);e.FormBuilder=P.FormBuilder;var E=n(135),w=n(125),C=n(5);e.FORM_PROVIDERS=C.CONST_EXPR([E.FormBuilder,w.RadioControlRegistry]),e.FORM_BINDINGS=e.FORM_PROVIDERS},function(t,e,n){"use strict";function r(t){return t instanceof l}function i(t,e){return a.isBlank(e)?null:(e instanceof Array||(e=e.split("/")),e instanceof Array&&p.ListWrapper.isEmpty(e)?null:e.reduce(function(t,e){if(t instanceof f)return a.isPresent(t.controls[e])?t.controls[e]:null;if(t instanceof d){var n=e;return a.isPresent(t.at(n))?t.at(n):null}return null},t))}function o(t){return c.PromiseWrapper.isPromise(t)?u.ObservableWrapper.fromPromise(t):t}var s=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},a=n(5),u=n(40),c=n(41),p=n(15);e.VALID="VALID",e.INVALID="INVALID",e.PENDING="PENDING",e.isControl=r;var l=function(){function t(t,e){this.validator=t,this.asyncValidator=e,this._pristine=!0,this._touched=!1}return Object.defineProperty(t.prototype,"value",{get:function(){return this._value},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"status",{get:function(){return this._status},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"valid",{get:function(){return this._status===e.VALID},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"errors",{get:function(){return this._errors},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"pristine",{get:function(){return this._pristine},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"dirty",{get:function(){return!this.pristine},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"touched",{get:function(){return this._touched},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"untouched",{get:function(){return!this._touched},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"valueChanges",{get:function(){return this._valueChanges},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"statusChanges",{get:function(){return this._statusChanges},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"pending",{get:function(){return this._status==e.PENDING},enumerable:!0,configurable:!0}),t.prototype.markAsTouched=function(){this._touched=!0},t.prototype.markAsDirty=function(t){var e=(void 0===t?{}:t).onlySelf;e=a.normalizeBool(e),this._pristine=!1,a.isPresent(this._parent)&&!e&&this._parent.markAsDirty({onlySelf:e})},t.prototype.markAsPending=function(t){var n=(void 0===t?{}:t).onlySelf;n=a.normalizeBool(n),this._status=e.PENDING,a.isPresent(this._parent)&&!n&&this._parent.markAsPending({onlySelf:n})},t.prototype.setParent=function(t){this._parent=t},t.prototype.updateValueAndValidity=function(t){var n=void 0===t?{}:t,r=n.onlySelf,i=n.emitEvent;r=a.normalizeBool(r),i=a.isPresent(i)?i:!0,this._updateValue(),this._errors=this._runValidator(),this._status=this._calculateStatus(),(this._status==e.VALID||this._status==e.PENDING)&&this._runAsyncValidator(i),i&&(u.ObservableWrapper.callEmit(this._valueChanges,this._value),u.ObservableWrapper.callEmit(this._statusChanges,this._status)),a.isPresent(this._parent)&&!r&&this._parent.updateValueAndValidity({onlySelf:r,emitEvent:i})},t.prototype._runValidator=function(){return a.isPresent(this.validator)?this.validator(this):null},t.prototype._runAsyncValidator=function(t){var n=this;if(a.isPresent(this.asyncValidator)){this._status=e.PENDING,this._cancelExistingSubscription();var r=o(this.asyncValidator(this));this._asyncValidationSubscription=u.ObservableWrapper.subscribe(r,function(e){return n.setErrors(e,{emitEvent:t})})}},t.prototype._cancelExistingSubscription=function(){a.isPresent(this._asyncValidationSubscription)&&u.ObservableWrapper.dispose(this._asyncValidationSubscription)},t.prototype.setErrors=function(t,e){var n=(void 0===e?{}:e).emitEvent;n=a.isPresent(n)?n:!0,this._errors=t,this._status=this._calculateStatus(),n&&u.ObservableWrapper.callEmit(this._statusChanges,this._status),a.isPresent(this._parent)&&this._parent._updateControlsErrors()},t.prototype.find=function(t){return i(this,t)},t.prototype.getError=function(t,e){void 0===e&&(e=null);var n=a.isPresent(e)&&!p.ListWrapper.isEmpty(e)?this.find(e):this;return a.isPresent(n)&&a.isPresent(n._errors)?p.StringMapWrapper.get(n._errors,t):null},t.prototype.hasError=function(t,e){return void 0===e&&(e=null),a.isPresent(this.getError(t,e))},Object.defineProperty(t.prototype,"root",{get:function(){for(var t=this;a.isPresent(t._parent);)t=t._parent;return t},enumerable:!0,configurable:!0}),t.prototype._updateControlsErrors=function(){this._status=this._calculateStatus(),a.isPresent(this._parent)&&this._parent._updateControlsErrors()},t.prototype._initObservables=function(){this._valueChanges=new u.EventEmitter,this._statusChanges=new u.EventEmitter},t.prototype._calculateStatus=function(){return a.isPresent(this._errors)?e.INVALID:this._anyControlsHaveStatus(e.PENDING)?e.PENDING:this._anyControlsHaveStatus(e.INVALID)?e.INVALID:e.VALID},t}();e.AbstractControl=l;var h=function(t){function e(e,n,r){void 0===e&&(e=null),void 0===n&&(n=null),void 0===r&&(r=null),t.call(this,n,r),this._value=e,this.updateValueAndValidity({onlySelf:!0,emitEvent:!1}),this._initObservables()}return s(e,t),e.prototype.updateValue=function(t,e){var n=void 0===e?{}:e,r=n.onlySelf,i=n.emitEvent,o=n.emitModelToViewChange;o=a.isPresent(o)?o:!0,this._value=t,a.isPresent(this._onChange)&&o&&this._onChange(this._value),this.updateValueAndValidity({onlySelf:r,emitEvent:i})},e.prototype._updateValue=function(){},e.prototype._anyControlsHaveStatus=function(t){return!1},e.prototype.registerOnChange=function(t){this._onChange=t},e}(l);e.Control=h;var f=function(t){function e(e,n,r,i){void 0===n&&(n=null),void 0===r&&(r=null),void 0===i&&(i=null),t.call(this,r,i),this.controls=e,this._optionals=a.isPresent(n)?n:{},this._initObservables(),this._setParentForControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!1})}return s(e,t),e.prototype.addControl=function(t,e){this.controls[t]=e,e.setParent(this)},e.prototype.removeControl=function(t){p.StringMapWrapper["delete"](this.controls,t)},e.prototype.include=function(t){p.StringMapWrapper.set(this._optionals,t,!0),this.updateValueAndValidity()},e.prototype.exclude=function(t){p.StringMapWrapper.set(this._optionals,t,!1),this.updateValueAndValidity()},e.prototype.contains=function(t){var e=p.StringMapWrapper.contains(this.controls,t);return e&&this._included(t)},e.prototype._setParentForControls=function(){var t=this;p.StringMapWrapper.forEach(this.controls,function(e,n){e.setParent(t)})},e.prototype._updateValue=function(){this._value=this._reduceValue()},e.prototype._anyControlsHaveStatus=function(t){var e=this,n=!1;return p.StringMapWrapper.forEach(this.controls,function(r,i){n=n||e.contains(i)&&r.status==t}),n},e.prototype._reduceValue=function(){return this._reduceChildren({},function(t,e,n){return t[n]=e.value,t})},e.prototype._reduceChildren=function(t,e){var n=this,r=t;return p.StringMapWrapper.forEach(this.controls,function(t,i){n._included(i)&&(r=e(r,t,i))}),r},e.prototype._included=function(t){var e=p.StringMapWrapper.contains(this._optionals,t);return!e||p.StringMapWrapper.get(this._optionals,t)},e}(l);e.ControlGroup=f;var d=function(t){function e(e,n,r){void 0===n&&(n=null),void 0===r&&(r=null),t.call(this,n,r),this.controls=e,this._initObservables(),this._setParentForControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!1})}return s(e,t),e.prototype.at=function(t){return this.controls[t]},e.prototype.push=function(t){this.controls.push(t),t.setParent(this),this.updateValueAndValidity()},e.prototype.insert=function(t,e){p.ListWrapper.insert(this.controls,t,e),e.setParent(this),this.updateValueAndValidity()},e.prototype.removeAt=function(t){p.ListWrapper.removeAt(this.controls,t),this.updateValueAndValidity()},Object.defineProperty(e.prototype,"length",{get:function(){return this.controls.length},enumerable:!0,configurable:!0}),e.prototype._updateValue=function(){this._value=this.controls.map(function(t){return t.value})},e.prototype._anyControlsHaveStatus=function(t){return this.controls.some(function(e){return e.status==t})},e.prototype._setParentForControls=function(){var t=this;this.controls.forEach(function(e){e.setParent(t)})},e}(l);e.ControlArray=d},function(t,e,n){"use strict";var r=n(5),i=n(12),o=function(){function t(){}return Object.defineProperty(t.prototype,"control",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"value",{get:function(){return r.isPresent(this.control)?this.control.value:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"valid",{get:function(){return r.isPresent(this.control)?this.control.valid:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"errors",{get:function(){return r.isPresent(this.control)?this.control.errors:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"pristine",{get:function(){return r.isPresent(this.control)?this.control.pristine:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"dirty",{get:function(){return r.isPresent(this.control)?this.control.dirty:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"touched",{get:function(){return r.isPresent(this.control)?this.control.touched:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"untouched",{get:function(){return r.isPresent(this.control)?this.control.untouched:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"path",{get:function(){return null},enumerable:!0,configurable:!0}),t}();e.AbstractControlDirective=o},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(114),o=function(t){function e(){t.apply(this,arguments)}return r(e,t),Object.defineProperty(e.prototype,"formDirective",{get:function(){return null},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"path",{get:function(){return null},enumerable:!0,configurable:!0}),e}(i.AbstractControlDirective);e.ControlContainer=o},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},a=n(5),u=n(40),c=n(2),p=n(115),l=n(117),h=n(118),f=n(119),d=n(120),v=a.CONST_EXPR(new c.Provider(l.NgControl,{useExisting:c.forwardRef(function(){return y})})),y=function(t){function e(e,n,r,i){t.call(this),this._parent=e,this._validators=n,this._asyncValidators=r,this.update=new u.EventEmitter,this._added=!1,this.valueAccessor=f.selectValueAccessor(this,i)}return r(e,t),e.prototype.ngOnChanges=function(t){this._added||(this.formDirective.addControl(this),this._added=!0),f.isPropertyUpdated(t,this.viewModel)&&(this.viewModel=this.model,this.formDirective.updateModel(this,this.model))},e.prototype.ngOnDestroy=function(){this.formDirective.removeControl(this)},e.prototype.viewToModelUpdate=function(t){this.viewModel=t,u.ObservableWrapper.callEmit(this.update,t)},Object.defineProperty(e.prototype,"path",{get:function(){return f.controlPath(this.name,this._parent)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"formDirective",{get:function(){return this._parent.formDirective},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"validator",{get:function(){return f.composeValidators(this._validators)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"asyncValidator",{get:function(){return f.composeAsyncValidators(this._asyncValidators)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"control",{get:function(){return this.formDirective.getControl(this)},enumerable:!0,configurable:!0}),e=i([c.Directive({selector:"[ngControl]",bindings:[v],inputs:["name: ngControl","model: ngModel"],outputs:["update: ngModelChange"],exportAs:"ngForm"}),s(0,c.Host()),s(0,c.SkipSelf()),s(1,c.Optional()),s(1,c.Self()),s(1,c.Inject(d.NG_VALIDATORS)),s(2,c.Optional()),s(2,c.Self()),s(2,c.Inject(d.NG_ASYNC_VALIDATORS)),s(3,c.Optional()),s(3,c.Self()),s(3,c.Inject(h.NG_VALUE_ACCESSOR)),o("design:paramtypes",[p.ControlContainer,Array,Array,Array])],e)}(l.NgControl);e.NgControlName=y},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(114),o=n(12),s=function(t){function e(){t.apply(this,arguments),this.name=null,this.valueAccessor=null}return r(e,t),Object.defineProperty(e.prototype,"validator",{get:function(){return o.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"asyncValidator",{get:function(){return o.unimplemented()},enumerable:!0,configurable:!0}),e}(i.AbstractControlDirective);e.NgControl=s},function(t,e,n){"use strict";var r=n(2),i=n(5);e.NG_VALUE_ACCESSOR=i.CONST_EXPR(new r.OpaqueToken("NgValueAccessor"))},function(t,e,n){"use strict";function r(t,e){var n=l.ListWrapper.clone(e.path);return n.push(t),n}function i(t,e){h.isBlank(t)&&s(e,"Cannot find control"),h.isBlank(e.valueAccessor)&&s(e,"No value accessor for"),t.validator=d.Validators.compose([t.validator,e.validator]),t.asyncValidator=d.Validators.composeAsync([t.asyncValidator,e.asyncValidator]),e.valueAccessor.writeValue(t.value),e.valueAccessor.registerOnChange(function(n){e.viewToModelUpdate(n),t.updateValue(n,{emitModelToViewChange:!1}),t.markAsDirty()}),t.registerOnChange(function(t){return e.valueAccessor.writeValue(t)}),e.valueAccessor.registerOnTouched(function(){return t.markAsTouched()})}function o(t,e){h.isBlank(t)&&s(e,"Cannot find control"),t.validator=d.Validators.compose([t.validator,e.validator]),t.asyncValidator=d.Validators.composeAsync([t.asyncValidator,e.asyncValidator])}function s(t,e){var n=t.path.join(" -> ");throw new f.BaseException(e+" '"+n+"'")}function a(t){return h.isPresent(t)?d.Validators.compose(t.map(b.normalizeValidator)):null}function u(t){return h.isPresent(t)?d.Validators.composeAsync(t.map(b.normalizeAsyncValidator)):null}function c(t,e){if(!l.StringMapWrapper.contains(t,"model"))return!1;var n=t.model;return n.isFirstChange()?!0:!h.looseIdentical(e,n.currentValue)}function p(t,e){if(h.isBlank(e))return null;var n,r,i;return e.forEach(function(e){h.hasConstructor(e,v.DefaultValueAccessor)?n=e:h.hasConstructor(e,m.CheckboxControlValueAccessor)||h.hasConstructor(e,y.NumberValueAccessor)||h.hasConstructor(e,g.SelectControlValueAccessor)||h.hasConstructor(e,_.RadioControlValueAccessor)?(h.isPresent(r)&&s(t,"More than one built-in value accessor matches"),r=e):(h.isPresent(i)&&s(t,"More than one custom value accessor matches"),i=e)}),h.isPresent(i)?i:h.isPresent(r)?r:h.isPresent(n)?n:(s(t,"No valid value accessor for"),null)}var l=n(15),h=n(5),f=n(12),d=n(120),v=n(121),y=n(122),m=n(123),g=n(124),_=n(125),b=n(126);e.controlPath=r,e.setUpControl=i,e.setUpControlGroup=o,e.composeValidators=a,e.composeAsyncValidators=u,e.isPropertyUpdated=c,e.selectValueAccessor=p},function(t,e,n){"use strict";function r(t){return u.PromiseWrapper.isPromise(t)?t:c.ObservableWrapper.toPromise(t)}function i(t,e){return e.map(function(e){return e(t)})}function o(t,e){return e.map(function(e){return e(t)})}function s(t){var e=t.reduce(function(t,e){return a.isPresent(e)?p.StringMapWrapper.merge(t,e):t},{});return p.StringMapWrapper.isEmpty(e)?null:e}var a=n(5),u=n(41),c=n(40),p=n(15),l=n(2);e.NG_VALIDATORS=a.CONST_EXPR(new l.OpaqueToken("NgValidators")),e.NG_ASYNC_VALIDATORS=a.CONST_EXPR(new l.OpaqueToken("NgAsyncValidators"));var h=function(){function t(){}return t.required=function(t){return a.isBlank(t.value)||a.isString(t.value)&&""==t.value?{required:!0}:null},t.minLength=function(e){return function(n){if(a.isPresent(t.required(n)))return null;var r=n.value;return r.lengthe?{maxlength:{requiredLength:e,actualLength:r.length}}:null}},t.pattern=function(e){return function(n){if(a.isPresent(t.required(n)))return null;var r=new RegExp("^"+e+"$"),i=n.value;return r.test(i)?null:{pattern:{requiredPattern:"^"+e+"$",actualValue:i}}}},t.nullValidator=function(t){return null},t.compose=function(t){if(a.isBlank(t))return null;var e=t.filter(a.isPresent);return 0==e.length?null:function(t){return s(i(t,e))}},t.composeAsync=function(t){if(a.isBlank(t))return null;var e=t.filter(a.isPresent);return 0==e.length?null:function(t){var n=o(t,e).map(r);return u.PromiseWrapper.all(n).then(s)}},t}();e.Validators=h},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(2),s=n(118),a=n(5),u=a.CONST_EXPR(new o.Provider(s.NG_VALUE_ACCESSOR,{useExisting:o.forwardRef(function(){return c}),multi:!0})),c=function(){function t(t,e){this._renderer=t,this._elementRef=e,this.onChange=function(t){},this.onTouched=function(){}}return t.prototype.writeValue=function(t){var e=a.isBlank(t)?"":t;this._renderer.setElementProperty(this._elementRef.nativeElement,"value",e)},t.prototype.registerOnChange=function(t){this.onChange=t},t.prototype.registerOnTouched=function(t){this.onTouched=t},t=r([o.Directive({selector:"input:not([type=checkbox])[ngControl],textarea[ngControl],input:not([type=checkbox])[ngFormControl],textarea[ngFormControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]", -host:{"(input)":"onChange($event.target.value)","(blur)":"onTouched()"},bindings:[u]}),i("design:paramtypes",[o.Renderer,o.ElementRef])],t)}();e.DefaultValueAccessor=c},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(2),s=n(118),a=n(5),u=a.CONST_EXPR(new o.Provider(s.NG_VALUE_ACCESSOR,{useExisting:o.forwardRef(function(){return c}),multi:!0})),c=function(){function t(t,e){this._renderer=t,this._elementRef=e,this.onChange=function(t){},this.onTouched=function(){}}return t.prototype.writeValue=function(t){this._renderer.setElementProperty(this._elementRef.nativeElement,"value",t)},t.prototype.registerOnChange=function(t){this.onChange=function(e){t(""==e?null:a.NumberWrapper.parseFloat(e))}},t.prototype.registerOnTouched=function(t){this.onTouched=t},t=r([o.Directive({selector:"input[type=number][ngControl],input[type=number][ngFormControl],input[type=number][ngModel]",host:{"(change)":"onChange($event.target.value)","(input)":"onChange($event.target.value)","(blur)":"onTouched()"},bindings:[u]}),i("design:paramtypes",[o.Renderer,o.ElementRef])],t)}();e.NumberValueAccessor=c},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(2),s=n(118),a=n(5),u=a.CONST_EXPR(new o.Provider(s.NG_VALUE_ACCESSOR,{useExisting:o.forwardRef(function(){return c}),multi:!0})),c=function(){function t(t,e){this._renderer=t,this._elementRef=e,this.onChange=function(t){},this.onTouched=function(){}}return t.prototype.writeValue=function(t){this._renderer.setElementProperty(this._elementRef.nativeElement,"checked",t)},t.prototype.registerOnChange=function(t){this.onChange=t},t.prototype.registerOnTouched=function(t){this.onTouched=t},t=r([o.Directive({selector:"input[type=checkbox][ngControl],input[type=checkbox][ngFormControl],input[type=checkbox][ngModel]",host:{"(change)":"onChange($event.target.checked)","(blur)":"onTouched()"},providers:[u]}),i("design:paramtypes",[o.Renderer,o.ElementRef])],t)}();e.CheckboxControlValueAccessor=c},function(t,e,n){"use strict";function r(t,e){return p.isBlank(t)?""+e:(p.isPrimitive(e)||(e="Object"),p.StringWrapper.slice(t+": "+e,0,50))}function i(t){return t.split(":")[0]}var o=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},s=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},a=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},u=n(2),c=n(118),p=n(5),l=n(15),h=p.CONST_EXPR(new u.Provider(c.NG_VALUE_ACCESSOR,{useExisting:u.forwardRef(function(){return f}),multi:!0})),f=function(){function t(t,e){this._renderer=t,this._elementRef=e,this._optionMap=new Map,this._idCounter=0,this.onChange=function(t){},this.onTouched=function(){}}return t.prototype.writeValue=function(t){this.value=t;var e=r(this._getOptionId(t),t);this._renderer.setElementProperty(this._elementRef.nativeElement,"value",e)},t.prototype.registerOnChange=function(t){var e=this;this.onChange=function(n){t(e._getOptionValue(n))}},t.prototype.registerOnTouched=function(t){this.onTouched=t},t.prototype._registerOption=function(){return(this._idCounter++).toString()},t.prototype._getOptionId=function(t){for(var e=0,n=l.MapWrapper.keys(this._optionMap);eo?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(2),s=n(118),a=n(117),u=n(5),c=n(15),p=u.CONST_EXPR(new o.Provider(s.NG_VALUE_ACCESSOR,{useExisting:o.forwardRef(function(){return f}),multi:!0})),l=function(){function t(){this._accessors=[]}return t.prototype.add=function(t,e){this._accessors.push([t,e])},t.prototype.remove=function(t){for(var e=-1,n=0;no?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},a=n(5),u=n(15),c=n(40),p=n(2),l=n(117),h=n(120),f=n(118),d=n(119),v=a.CONST_EXPR(new p.Provider(l.NgControl,{useExisting:p.forwardRef(function(){return y})})),y=function(t){function e(e,n,r){t.call(this),this._validators=e,this._asyncValidators=n,this.update=new c.EventEmitter,this.valueAccessor=d.selectValueAccessor(this,r)}return r(e,t),e.prototype.ngOnChanges=function(t){this._isControlChanged(t)&&(d.setUpControl(this.form,this),this.form.updateValueAndValidity({emitEvent:!1})),d.isPropertyUpdated(t,this.viewModel)&&(this.form.updateValue(this.model),this.viewModel=this.model)},Object.defineProperty(e.prototype,"path",{get:function(){return[]},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"validator",{get:function(){return d.composeValidators(this._validators)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"asyncValidator",{get:function(){return d.composeAsyncValidators(this._asyncValidators)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"control",{get:function(){return this.form},enumerable:!0,configurable:!0}),e.prototype.viewToModelUpdate=function(t){this.viewModel=t,c.ObservableWrapper.callEmit(this.update,t)},e.prototype._isControlChanged=function(t){return u.StringMapWrapper.contains(t,"form")},e=i([p.Directive({selector:"[ngFormControl]",bindings:[v],inputs:["form: ngFormControl","model: ngModel"],outputs:["update: ngModelChange"],exportAs:"ngForm"}),s(0,p.Optional()),s(0,p.Self()),s(0,p.Inject(h.NG_VALIDATORS)),s(1,p.Optional()),s(1,p.Self()),s(1,p.Inject(h.NG_ASYNC_VALIDATORS)),s(2,p.Optional()),s(2,p.Self()),s(2,p.Inject(f.NG_VALUE_ACCESSOR)),o("design:paramtypes",[Array,Array,Array])],e)}(l.NgControl);e.NgFormControl=y},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},a=n(5),u=n(40),c=n(2),p=n(118),l=n(117),h=n(113),f=n(120),d=n(119),v=a.CONST_EXPR(new c.Provider(l.NgControl,{useExisting:c.forwardRef(function(){return y})})),y=function(t){function e(e,n,r){t.call(this),this._validators=e,this._asyncValidators=n,this._control=new h.Control,this._added=!1,this.update=new u.EventEmitter,this.valueAccessor=d.selectValueAccessor(this,r)}return r(e,t),e.prototype.ngOnChanges=function(t){this._added||(d.setUpControl(this._control,this),this._control.updateValueAndValidity({emitEvent:!1}),this._added=!0),d.isPropertyUpdated(t,this.viewModel)&&(this._control.updateValue(this.model),this.viewModel=this.model)},Object.defineProperty(e.prototype,"control",{get:function(){return this._control},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"path",{get:function(){return[]},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"validator",{get:function(){return d.composeValidators(this._validators)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"asyncValidator",{get:function(){return d.composeAsyncValidators(this._asyncValidators)},enumerable:!0,configurable:!0}),e.prototype.viewToModelUpdate=function(t){this.viewModel=t,u.ObservableWrapper.callEmit(this.update,t)},e=i([c.Directive({selector:"[ngModel]:not([ngControl]):not([ngFormControl])",bindings:[v],inputs:["model: ngModel"],outputs:["update: ngModelChange"],exportAs:"ngForm"}),s(0,c.Optional()),s(0,c.Self()),s(0,c.Inject(f.NG_VALIDATORS)),s(1,c.Optional()),s(1,c.Self()),s(1,c.Inject(f.NG_ASYNC_VALIDATORS)),s(2,c.Optional()),s(2,c.Self()),s(2,c.Inject(p.NG_VALUE_ACCESSOR)),o("design:paramtypes",[Array,Array,Array])],e)}(l.NgControl);e.NgModel=y},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},a=n(2),u=n(5),c=n(115),p=n(119),l=n(120),h=u.CONST_EXPR(new a.Provider(c.ControlContainer,{useExisting:a.forwardRef(function(){return f})})),f=function(t){function e(e,n,r){t.call(this),this._validators=n,this._asyncValidators=r,this._parent=e}return r(e,t),e.prototype.ngOnInit=function(){this.formDirective.addControlGroup(this)},e.prototype.ngOnDestroy=function(){this.formDirective.removeControlGroup(this)},Object.defineProperty(e.prototype,"control",{get:function(){return this.formDirective.getControlGroup(this)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"path",{get:function(){return p.controlPath(this.name,this._parent)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"formDirective",{get:function(){return this._parent.formDirective},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"validator",{get:function(){return p.composeValidators(this._validators)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"asyncValidator",{get:function(){return p.composeAsyncValidators(this._asyncValidators)},enumerable:!0,configurable:!0}),e=i([a.Directive({selector:"[ngControlGroup]",providers:[h],inputs:["name: ngControlGroup"],exportAs:"ngForm"}),s(0,a.Host()),s(0,a.SkipSelf()),s(1,a.Optional()),s(1,a.Self()),s(1,a.Inject(l.NG_VALIDATORS)),s(2,a.Optional()),s(2,a.Self()),s(2,a.Inject(l.NG_ASYNC_VALIDATORS)),o("design:paramtypes",[c.ControlContainer,Array,Array])],e)}(c.ControlContainer);e.NgControlGroup=f},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},a=n(5),u=n(15),c=n(12),p=n(40),l=n(2),h=n(115),f=n(119),d=n(120),v=a.CONST_EXPR(new l.Provider(h.ControlContainer,{useExisting:l.forwardRef(function(){return y})})),y=function(t){function e(e,n){t.call(this),this._validators=e,this._asyncValidators=n,this.form=null,this.directives=[],this.ngSubmit=new p.EventEmitter}return r(e,t),e.prototype.ngOnChanges=function(t){if(this._checkFormPresent(),u.StringMapWrapper.contains(t,"form")){var e=f.composeValidators(this._validators);this.form.validator=d.Validators.compose([this.form.validator,e]);var n=f.composeAsyncValidators(this._asyncValidators);this.form.asyncValidator=d.Validators.composeAsync([this.form.asyncValidator,n]),this.form.updateValueAndValidity({onlySelf:!0,emitEvent:!1})}this._updateDomValue()},Object.defineProperty(e.prototype,"formDirective",{get:function(){return this},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"control",{get:function(){return this.form},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"path",{get:function(){return[]},enumerable:!0,configurable:!0}),e.prototype.addControl=function(t){var e=this.form.find(t.path);f.setUpControl(e,t),e.updateValueAndValidity({emitEvent:!1}),this.directives.push(t)},e.prototype.getControl=function(t){return this.form.find(t.path)},e.prototype.removeControl=function(t){u.ListWrapper.remove(this.directives,t)},e.prototype.addControlGroup=function(t){var e=this.form.find(t.path);f.setUpControlGroup(e,t),e.updateValueAndValidity({emitEvent:!1})},e.prototype.removeControlGroup=function(t){},e.prototype.getControlGroup=function(t){return this.form.find(t.path)},e.prototype.updateModel=function(t,e){var n=this.form.find(t.path);n.updateValue(e)},e.prototype.onSubmit=function(){return p.ObservableWrapper.callEmit(this.ngSubmit,null),!1},e.prototype._updateDomValue=function(){var t=this;this.directives.forEach(function(e){var n=t.form.find(e.path);e.valueAccessor.writeValue(n.value)})},e.prototype._checkFormPresent=function(){if(a.isBlank(this.form))throw new c.BaseException('ngFormModel expects a form. Please pass one in. Example:
')},e=i([l.Directive({selector:"[ngFormModel]",bindings:[v],inputs:["form: ngFormModel"],host:{"(submit)":"onSubmit()"},outputs:["ngSubmit"],exportAs:"ngForm"}),s(0,l.Optional()),s(0,l.Self()),s(0,l.Inject(d.NG_VALIDATORS)),s(1,l.Optional()),s(1,l.Self()),s(1,l.Inject(d.NG_ASYNC_VALIDATORS)),o("design:paramtypes",[Array,Array])],e)}(h.ControlContainer);e.NgFormModel=y},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},a=n(40),u=n(15),c=n(5),p=n(2),l=n(115),h=n(113),f=n(119),d=n(120),v=c.CONST_EXPR(new p.Provider(l.ControlContainer,{useExisting:p.forwardRef(function(){return y})})),y=function(t){function e(e,n){t.call(this),this.ngSubmit=new a.EventEmitter,this.form=new h.ControlGroup({},null,f.composeValidators(e),f.composeAsyncValidators(n))}return r(e,t),Object.defineProperty(e.prototype,"formDirective",{get:function(){return this},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"control",{get:function(){return this.form},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"path",{get:function(){return[]},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"controls",{get:function(){return this.form.controls},enumerable:!0,configurable:!0}),e.prototype.addControl=function(t){var e=this;a.PromiseWrapper.scheduleMicrotask(function(){var n=e._findContainer(t.path),r=new h.Control;f.setUpControl(r,t),n.addControl(t.name,r),r.updateValueAndValidity({emitEvent:!1})})},e.prototype.getControl=function(t){return this.form.find(t.path)},e.prototype.removeControl=function(t){var e=this;a.PromiseWrapper.scheduleMicrotask(function(){var n=e._findContainer(t.path);c.isPresent(n)&&(n.removeControl(t.name),n.updateValueAndValidity({emitEvent:!1}))})},e.prototype.addControlGroup=function(t){var e=this;a.PromiseWrapper.scheduleMicrotask(function(){var n=e._findContainer(t.path),r=new h.ControlGroup({});f.setUpControlGroup(r,t),n.addControl(t.name,r),r.updateValueAndValidity({emitEvent:!1})})},e.prototype.removeControlGroup=function(t){var e=this;a.PromiseWrapper.scheduleMicrotask(function(){var n=e._findContainer(t.path);c.isPresent(n)&&(n.removeControl(t.name),n.updateValueAndValidity({emitEvent:!1}))})},e.prototype.getControlGroup=function(t){return this.form.find(t.path)},e.prototype.updateModel=function(t,e){var n=this;a.PromiseWrapper.scheduleMicrotask(function(){var r=n.form.find(t.path);r.updateValue(e)})},e.prototype.onSubmit=function(){return a.ObservableWrapper.callEmit(this.ngSubmit,null),!1},e.prototype._findContainer=function(t){return t.pop(),u.ListWrapper.isEmpty(t)?this.form:this.form.find(t)},e=i([p.Directive({selector:"form:not([ngNoForm]):not([ngFormModel]),ngForm,[ngForm]",bindings:[v],host:{"(submit)":"onSubmit()"},outputs:["ngSubmit"],exportAs:"ngForm"}),s(0,p.Optional()),s(0,p.Self()),s(0,p.Inject(d.NG_VALIDATORS)),s(1,p.Optional()),s(1,p.Self()),s(1,p.Inject(d.NG_ASYNC_VALIDATORS)),o("design:paramtypes",[Array,Array])],e)}(l.ControlContainer);e.NgForm=y},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},s=n(2),a=n(117),u=n(5),c=function(){function t(t){this._cd=t}return Object.defineProperty(t.prototype,"ngClassUntouched",{get:function(){return u.isPresent(this._cd.control)?this._cd.control.untouched:!1},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"ngClassTouched",{get:function(){return u.isPresent(this._cd.control)?this._cd.control.touched:!1},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"ngClassPristine",{get:function(){return u.isPresent(this._cd.control)?this._cd.control.pristine:!1},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"ngClassDirty",{get:function(){return u.isPresent(this._cd.control)?this._cd.control.dirty:!1},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"ngClassValid",{get:function(){return u.isPresent(this._cd.control)?this._cd.control.valid:!1},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"ngClassInvalid",{get:function(){return u.isPresent(this._cd.control)?!this._cd.control.valid:!1},enumerable:!0,configurable:!0}),t=r([s.Directive({selector:"[ngControl],[ngModel],[ngFormControl]",host:{"[class.ng-untouched]":"ngClassUntouched","[class.ng-touched]":"ngClassTouched","[class.ng-pristine]":"ngClassPristine","[class.ng-dirty]":"ngClassDirty","[class.ng-valid]":"ngClassValid","[class.ng-invalid]":"ngClassInvalid"}}),o(0,s.Self()),i("design:paramtypes",[a.NgControl])],t)}();e.NgControlStatus=c},function(t,e,n){"use strict";var r=n(5),i=n(116),o=n(127),s=n(128),a=n(129),u=n(130),c=n(131),p=n(121),l=n(123),h=n(122),f=n(125),d=n(132),v=n(124),y=n(134),m=n(116);e.NgControlName=m.NgControlName;var g=n(127);e.NgFormControl=g.NgFormControl;var _=n(128);e.NgModel=_.NgModel;var b=n(129);e.NgControlGroup=b.NgControlGroup;var P=n(130);e.NgFormModel=P.NgFormModel;var E=n(131);e.NgForm=E.NgForm;var w=n(121);e.DefaultValueAccessor=w.DefaultValueAccessor;var C=n(123);e.CheckboxControlValueAccessor=C.CheckboxControlValueAccessor;var R=n(125);e.RadioControlValueAccessor=R.RadioControlValueAccessor,e.RadioButtonState=R.RadioButtonState;var S=n(122);e.NumberValueAccessor=S.NumberValueAccessor;var O=n(132);e.NgControlStatus=O.NgControlStatus;var T=n(124);e.SelectControlValueAccessor=T.SelectControlValueAccessor,e.NgSelectOption=T.NgSelectOption;var x=n(134);e.RequiredValidator=x.RequiredValidator,e.MinLengthValidator=x.MinLengthValidator,e.MaxLengthValidator=x.MaxLengthValidator,e.PatternValidator=x.PatternValidator;var A=n(117);e.NgControl=A.NgControl,e.FORM_DIRECTIVES=r.CONST_EXPR([i.NgControlName,a.NgControlGroup,o.NgFormControl,s.NgModel,u.NgFormModel,c.NgForm,v.NgSelectOption,p.DefaultValueAccessor,h.NumberValueAccessor,l.CheckboxControlValueAccessor,v.SelectControlValueAccessor,f.RadioControlValueAccessor,d.NgControlStatus,y.RequiredValidator,y.MinLengthValidator,y.MaxLengthValidator,y.PatternValidator])},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},s=n(2),a=n(5),u=n(120),c=n(5),p=u.Validators.required,l=a.CONST_EXPR(new s.Provider(u.NG_VALIDATORS,{useValue:p,multi:!0})),h=function(){function t(){}return t=r([s.Directive({selector:"[required][ngControl],[required][ngFormControl],[required][ngModel]",providers:[l]}),i("design:paramtypes",[])],t)}();e.RequiredValidator=h;var f=a.CONST_EXPR(new s.Provider(u.NG_VALIDATORS,{useExisting:s.forwardRef(function(){return d}),multi:!0})),d=function(){function t(t){this._validator=u.Validators.minLength(c.NumberWrapper.parseInt(t,10))}return t.prototype.validate=function(t){return this._validator(t)},t=r([s.Directive({selector:"[minlength][ngControl],[minlength][ngFormControl],[minlength][ngModel]",providers:[f]}),o(0,s.Attribute("minlength")),i("design:paramtypes",[String])],t)}();e.MinLengthValidator=d;var v=a.CONST_EXPR(new s.Provider(u.NG_VALIDATORS,{useExisting:s.forwardRef(function(){return y}),multi:!0})),y=function(){function t(t){this._validator=u.Validators.maxLength(c.NumberWrapper.parseInt(t,10))}return t.prototype.validate=function(t){return this._validator(t)},t=r([s.Directive({selector:"[maxlength][ngControl],[maxlength][ngFormControl],[maxlength][ngModel]",providers:[v]}),o(0,s.Attribute("maxlength")),i("design:paramtypes",[String])],t)}();e.MaxLengthValidator=y;var m=a.CONST_EXPR(new s.Provider(u.NG_VALIDATORS,{useExisting:s.forwardRef(function(){return g}),multi:!0})),g=function(){function t(t){this._validator=u.Validators.pattern(t)}return t.prototype.validate=function(t){return this._validator(t)},t=r([s.Directive({selector:"[pattern][ngControl],[pattern][ngFormControl],[pattern][ngModel]",providers:[m]}),o(0,s.Attribute("pattern")),i("design:paramtypes",[String])],t)}();e.PatternValidator=g},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(2),s=n(15),a=n(5),u=n(113),c=function(){function t(){}return t.prototype.group=function(t,e){void 0===e&&(e=null);var n=this._reduceControls(t),r=a.isPresent(e)?s.StringMapWrapper.get(e,"optionals"):null,i=a.isPresent(e)?s.StringMapWrapper.get(e,"validator"):null,o=a.isPresent(e)?s.StringMapWrapper.get(e,"asyncValidator"):null;return new u.ControlGroup(n,r,i,o)},t.prototype.control=function(t,e,n){return void 0===e&&(e=null),void 0===n&&(n=null),new u.Control(t,e,n)},t.prototype.array=function(t,e,n){var r=this;void 0===e&&(e=null),void 0===n&&(n=null);var i=t.map(function(t){return r._createControl(t)});return new u.ControlArray(i,e,n)},t.prototype._reduceControls=function(t){var e=this,n={};return s.StringMapWrapper.forEach(t,function(t,r){n[r]=e._createControl(t)}),n},t.prototype._createControl=function(t){if(t instanceof u.Control||t instanceof u.ControlGroup||t instanceof u.ControlArray)return t;if(a.isArray(t)){var e=t[0],n=t.length>1?t[1]:null,r=t.length>2?t[2]:null;return this.control(e,n,r)}return this.control(t)},t=r([o.Injectable(),i("design:paramtypes",[])],t)}();e.FormBuilder=c},function(t,e,n){"use strict";var r=n(5),i=n(112),o=n(102);e.COMMON_DIRECTIVES=r.CONST_EXPR([o.CORE_DIRECTIVES,i.FORM_DIRECTIVES])},function(t,e,n){"use strict";function r(t){for(var n in t)e.hasOwnProperty(n)||(e[n]=t[n])}var i=n(138);e.PLATFORM_DIRECTIVES=i.PLATFORM_DIRECTIVES,e.PLATFORM_PIPES=i.PLATFORM_PIPES,e.COMPILER_PROVIDERS=i.COMPILER_PROVIDERS,e.TEMPLATE_TRANSFORMS=i.TEMPLATE_TRANSFORMS,e.CompilerConfig=i.CompilerConfig,e.RenderTypes=i.RenderTypes,e.UrlResolver=i.UrlResolver,e.DEFAULT_PACKAGE_URL_PROVIDER=i.DEFAULT_PACKAGE_URL_PROVIDER,e.createOfflineCompileUrlResolver=i.createOfflineCompileUrlResolver,e.XHR=i.XHR,e.ViewResolver=i.ViewResolver,e.DirectiveResolver=i.DirectiveResolver,e.PipeResolver=i.PipeResolver,e.SourceModule=i.SourceModule,e.NormalizedComponentWithViewDirectives=i.NormalizedComponentWithViewDirectives,e.OfflineCompiler=i.OfflineCompiler,e.CompileMetadataWithIdentifier=i.CompileMetadataWithIdentifier,e.CompileMetadataWithType=i.CompileMetadataWithType,e.CompileIdentifierMetadata=i.CompileIdentifierMetadata,e.CompileDiDependencyMetadata=i.CompileDiDependencyMetadata,e.CompileProviderMetadata=i.CompileProviderMetadata,e.CompileFactoryMetadata=i.CompileFactoryMetadata,e.CompileTokenMetadata=i.CompileTokenMetadata,e.CompileTypeMetadata=i.CompileTypeMetadata,e.CompileQueryMetadata=i.CompileQueryMetadata,e.CompileTemplateMetadata=i.CompileTemplateMetadata,e.CompileDirectiveMetadata=i.CompileDirectiveMetadata,e.CompilePipeMetadata=i.CompilePipeMetadata,r(n(139))},function(t,e,n){"use strict";function r(t){for(var n in t)e.hasOwnProperty(n)||(e[n]=t[n])}function i(){return new b.CompilerConfig(h.assertionsEnabled(),!1,!0)}var o=n(84);e.PLATFORM_DIRECTIVES=o.PLATFORM_DIRECTIVES,e.PLATFORM_PIPES=o.PLATFORM_PIPES,r(n(139));var s=n(140);e.TEMPLATE_TRANSFORMS=s.TEMPLATE_TRANSFORMS;var a=n(162);e.CompilerConfig=a.CompilerConfig,e.RenderTypes=a.RenderTypes,r(n(155)),r(n(163));var u=n(165);e.RuntimeCompiler=u.RuntimeCompiler,r(n(157)),r(n(184));var c=n(188);e.ViewResolver=c.ViewResolver;var p=n(186); -e.DirectiveResolver=p.DirectiveResolver;var l=n(187);e.PipeResolver=l.PipeResolver;var h=n(5),f=n(6),d=n(140),v=n(144),y=n(183),m=n(185),g=n(166),_=n(168),b=n(162),P=n(64),E=n(165),w=n(150),C=n(199),R=n(157),S=n(142),O=n(143),T=n(188),x=n(186),A=n(187);e.COMPILER_PROVIDERS=h.CONST_EXPR([O.Lexer,S.Parser,v.HtmlParser,d.TemplateParser,y.DirectiveNormalizer,m.RuntimeMetadataResolver,R.DEFAULT_PACKAGE_URL_PROVIDER,g.StyleCompiler,_.ViewCompiler,new f.Provider(b.CompilerConfig,{useFactory:i,deps:[]}),E.RuntimeCompiler,new f.Provider(P.ComponentResolver,{useExisting:E.RuntimeCompiler}),C.DomElementSchemaRegistry,new f.Provider(w.ElementSchemaRegistry,{useExisting:C.DomElementSchemaRegistry}),R.UrlResolver,T.ViewResolver,x.DirectiveResolver,A.PipeResolver])},function(t,e,n){"use strict";function r(t,e,n){void 0===n&&(n=null);var r=[];return e.forEach(function(e){var o=e.visit(t,n);i.isPresent(o)&&r.push(o)}),r}var i=n(5),o=function(){function t(t,e,n){this.value=t,this.ngContentIndex=e,this.sourceSpan=n}return t.prototype.visit=function(t,e){return t.visitText(this,e)},t}();e.TextAst=o;var s=function(){function t(t,e,n){this.value=t,this.ngContentIndex=e,this.sourceSpan=n}return t.prototype.visit=function(t,e){return t.visitBoundText(this,e)},t}();e.BoundTextAst=s;var a=function(){function t(t,e,n){this.name=t,this.value=e,this.sourceSpan=n}return t.prototype.visit=function(t,e){return t.visitAttr(this,e)},t}();e.AttrAst=a;var u=function(){function t(t,e,n,r,i){this.name=t,this.type=e,this.value=n,this.unit=r,this.sourceSpan=i}return t.prototype.visit=function(t,e){return t.visitElementProperty(this,e)},t}();e.BoundElementPropertyAst=u;var c=function(){function t(t,e,n,r){this.name=t,this.target=e,this.handler=n,this.sourceSpan=r}return t.prototype.visit=function(t,e){return t.visitEvent(this,e)},Object.defineProperty(t.prototype,"fullName",{get:function(){return i.isPresent(this.target)?this.target+":"+this.name:this.name},enumerable:!0,configurable:!0}),t}();e.BoundEventAst=c;var p=function(){function t(t,e,n){this.name=t,this.value=e,this.sourceSpan=n}return t.prototype.visit=function(t,e){return t.visitVariable(this,e)},t}();e.VariableAst=p;var l=function(){function t(t,e,n,r,i,o,s,a,u,c,p){this.name=t,this.attrs=e,this.inputs=n,this.outputs=r,this.exportAsVars=i,this.directives=o,this.providers=s,this.hasViewContainer=a,this.children=u,this.ngContentIndex=c,this.sourceSpan=p}return t.prototype.visit=function(t,e){return t.visitElement(this,e)},t.prototype.isBound=function(){return this.inputs.length>0||this.outputs.length>0||this.exportAsVars.length>0||this.directives.length>0},t.prototype.getComponent=function(){for(var t=0;t0;n||e.push(t)}),e}var s=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},a=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},u=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},c=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},p=n(15),l=n(5),h=n(2),f=n(5),d=n(12),v=n(141),y=n(142),m=n(144),g=n(148),_=n(147),b=n(66),P=n(139),E=n(149),w=n(150),C=n(151),R=n(152),S=n(145),O=n(153),T=n(154),x=/^(?:(?:(?:(bind-)|(var-|#)|(on-)|(bindon-))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g,A="template",I="template",M="*",k="class",N=".",D="attr",V="class",j="style",L=E.CssSelector.parse("*")[0];e.TEMPLATE_TRANSFORMS=f.CONST_EXPR(new h.OpaqueToken("TemplateTransforms"));var B=function(t){function e(e,n){t.call(this,n,e)}return s(e,t),e}(_.ParseError);e.TemplateParseError=B;var F=function(){function t(t,e){this.templateAst=t,this.errors=e}return t}();e.TemplateParseResult=F;var U=function(){function t(t,e,n,r){this._exprParser=t,this._schemaRegistry=e,this._htmlParser=n,this.transforms=r}return t.prototype.parse=function(t,e,n,r,i){var o=this.tryParse(t,e,n,r,i);if(l.isPresent(o.errors)){var s=o.errors.join("\n");throw new d.BaseException("Template parse errors:\n"+s)}return o.templateAst},t.prototype.tryParse=function(t,e,n,r,i){var s,a=this._htmlParser.parse(e,i),u=a.errors;if(a.rootNodes.length>0){var c=o(n),p=o(r),h=new T.ProviderViewContext(t,a.rootNodes[0].sourceSpan),f=new W(h,c,p,this._exprParser,this._schemaRegistry);s=S.htmlVisitAll(f,a.rootNodes,G),u=u.concat(f.errors).concat(h.errors)}else s=[];return u.length>0?new F(s,u):(l.isPresent(this.transforms)&&this.transforms.forEach(function(t){s=P.templateVisitAll(t,s)}),new F(s))},t=a([h.Injectable(),c(3,h.Optional()),c(3,h.Inject(e.TEMPLATE_TRANSFORMS)),u("design:paramtypes",[y.Parser,w.ElementSchemaRegistry,m.HtmlParser,Array])],t)}();e.TemplateParser=U;var W=function(){function t(t,e,n,r,i){var o=this;this.providerViewContext=t,this._exprParser=r,this._schemaRegistry=i,this.errors=[],this.directivesIndex=new Map,this.ngContentCount=0,this.selectorMatcher=new E.SelectorMatcher,p.ListWrapper.forEachWithIndex(e,function(t,e){var n=E.CssSelector.parse(t.selector);o.selectorMatcher.addSelectables(n,t),o.directivesIndex.set(t,e)}),this.pipesByName=new Map,n.forEach(function(t){return o.pipesByName.set(t.name,t)})}return t.prototype._reportError=function(t,e){this.errors.push(new B(t,e))},t.prototype._parseInterpolation=function(t,e){var n=e.start.toString();try{var r=this._exprParser.parseInterpolation(t,n);if(this._checkPipes(r,e),l.isPresent(r)&&r.ast.expressions.length>b.MAX_INTERPOLATION_VALUES)throw new d.BaseException("Only support at most "+b.MAX_INTERPOLATION_VALUES+" interpolation values!");return r}catch(i){return this._reportError(""+i,e),this._exprParser.wrapLiteralPrimitive("ERROR",n)}},t.prototype._parseAction=function(t,e){var n=e.start.toString();try{var r=this._exprParser.parseAction(t,n);return this._checkPipes(r,e),r}catch(i){return this._reportError(""+i,e),this._exprParser.wrapLiteralPrimitive("ERROR",n)}},t.prototype._parseBinding=function(t,e){var n=e.start.toString();try{var r=this._exprParser.parseBinding(t,n);return this._checkPipes(r,e),r}catch(i){return this._reportError(""+i,e),this._exprParser.wrapLiteralPrimitive("ERROR",n)}},t.prototype._parseTemplateBindings=function(t,e){var n=this,r=e.start.toString();try{var i=this._exprParser.parseTemplateBindings(t,r);return i.forEach(function(t){l.isPresent(t.expression)&&n._checkPipes(t.expression,e)}),i}catch(o){return this._reportError(""+o,e),[]}},t.prototype._checkPipes=function(t,e){var n=this;if(l.isPresent(t)){var r=new K;t.visit(r),r.pipes.forEach(function(t){n.pipesByName.has(t)||n._reportError("The pipe '"+t+"' could not be found",e)})}},t.prototype.visitExpansion=function(t,e){return null},t.prototype.visitExpansionCase=function(t,e){return null},t.prototype.visitText=function(t,e){var n=e.findNgContentIndex(L),r=this._parseInterpolation(t.value,t.sourceSpan);return l.isPresent(r)?new P.BoundTextAst(r,n,t.sourceSpan):new P.TextAst(t.value,n,t.sourceSpan)},t.prototype.visitAttr=function(t,e){return new P.AttrAst(t.name,t.value,t.sourceSpan)},t.prototype.visitComment=function(t,e){return null},t.prototype.visitElement=function(t,e){var n=this,r=t.name,o=C.preparseElement(t);if(o.type===C.PreparsedElementType.SCRIPT||o.type===C.PreparsedElementType.STYLE)return null;if(o.type===C.PreparsedElementType.STYLESHEET&&R.isStyleUrlResolvable(o.hrefAttr))return null;var s=[],a=[],u=[],c=[],p=[],h=[],f=[],d=!1,v=[];t.attrs.forEach(function(t){var e=n._parseAttr(t,s,a,c,u),r=n._parseInlineTemplateBinding(t,f,p,h);e||r||(v.push(n.visitAttr(t,null)),s.push([t.name,t.value])),r&&(d=!0)});var y=g.splitNsName(r.toLowerCase())[1],m=y==A,_=i(r,s),b=this._parseDirectives(this.selectorMatcher,_),w=this._createDirectiveAsts(t.name,b,a,m?[]:u,t.sourceSpan),O=this._createElementPropertyAsts(t.name,a,w),x=e.isTemplateElement||d,I=new T.ProviderElementContext(this.providerViewContext,e.providerContext,x,w,v,u,t.sourceSpan),M=S.htmlVisitAll(o.nonBindable?z:this,t.children,q.create(m,w,m?e.providerContext:I));I.afterElement();var k,N=l.isPresent(o.projectAs)?E.CssSelector.parse(o.projectAs)[0]:_,D=e.findNgContentIndex(N);if(o.type===C.PreparsedElementType.NG_CONTENT)l.isPresent(t.children)&&t.children.length>0&&this._reportError(" element cannot have content. must be immediately followed by ",t.sourceSpan),k=new P.NgContentAst(this.ngContentCount++,d?null:D,t.sourceSpan);else if(m)this._assertAllEventsPublishedByDirectives(w,c),this._assertNoComponentsNorElementBindingsOnTemplate(w,O,t.sourceSpan),k=new P.EmbeddedTemplateAst(v,c,u,I.transformedDirectiveAsts,I.transformProviders,I.transformedHasViewContainer,M,d?null:D,t.sourceSpan);else{this._assertOnlyOneComponent(w,t.sourceSpan);var V=u.filter(function(t){return 0===t.value.length}),j=d?null:e.findNgContentIndex(N);k=new P.ElementAst(r,v,O,c,V,I.transformedDirectiveAsts,I.transformProviders,I.transformedHasViewContainer,M,d?null:j,t.sourceSpan)}if(d){var L=i(A,f),B=this._parseDirectives(this.selectorMatcher,L),F=this._createDirectiveAsts(t.name,B,p,[],t.sourceSpan),U=this._createElementPropertyAsts(t.name,p,F);this._assertNoComponentsNorElementBindingsOnTemplate(F,U,t.sourceSpan);var W=new T.ProviderElementContext(this.providerViewContext,e.providerContext,e.isTemplateElement,F,[],h,t.sourceSpan);W.afterElement(),k=new P.EmbeddedTemplateAst([],[],h,W.transformedDirectiveAsts,W.transformProviders,W.transformedHasViewContainer,[k],D,t.sourceSpan)}return k},t.prototype._parseInlineTemplateBinding=function(t,e,n,r){var i=null;if(t.name==I)i=t.value;else if(t.name.startsWith(M)){var o=t.name.substring(M.length);i=0==t.value.length?o:o+" "+t.value}if(l.isPresent(i)){for(var s=this._parseTemplateBindings(i,t.sourceSpan),a=0;a-1&&this._reportError('"-" is not allowed in variable names',n),r.push(new P.VariableAst(t,e,n))},t.prototype._parseProperty=function(t,e,n,r,i){this._parsePropertyAst(t,this._parseBinding(e,n),n,r,i)},t.prototype._parsePropertyInterpolation=function(t,e,n,r,i){var o=this._parseInterpolation(e,n);return l.isPresent(o)?(this._parsePropertyAst(t,o,n,r,i),!0):!1},t.prototype._parsePropertyAst=function(t,e,n,r,i){r.push([t,e.source]),i.push(new X(t,e,!1,n))},t.prototype._parseAssignmentEvent=function(t,e,n,r,i){this._parseEvent(t+"Change",e+"=$event",n,r,i)},t.prototype._parseEvent=function(t,e,n,r,i){var o=O.splitAtColon(t,[null,t]),s=o[0],a=o[1],u=this._parseAction(e,n);r.push([t,u.source]),i.push(new P.BoundEventAst(a,s,u,n))},t.prototype._parseLiteralAttr=function(t,e,n,r){r.push(new X(t,this._exprParser.wrapLiteralPrimitive(e,""),!0,n))},t.prototype._parseDirectives=function(t,e){var n=this,r=[];return t.match(e,function(t,e){r.push(e)}),p.ListWrapper.sort(r,function(t,e){var r=t.isComponent,i=e.isComponent;return r&&!i?-1:!r&&i?1:n.directivesIndex.get(t)-n.directivesIndex.get(e)}),r},t.prototype._createDirectiveAsts=function(t,e,n,r,i){var o=this,s=new Set,a=e.map(function(e){var a=[],u=[],c=[];o._createDirectiveHostPropertyAsts(t,e.hostProperties,i,a),o._createDirectiveHostEventAsts(e.hostListeners,i,u),o._createDirectivePropertyAsts(e.inputs,n,c);var p=[];return r.forEach(function(t){(0===t.value.length&&e.isComponent||e.exportAs==t.value)&&(p.push(t),s.add(t.name))}),new P.DirectiveAst(e,c,a,u,p,i)});return r.forEach(function(t){t.value.length>0&&!p.SetWrapper.has(s,t.name)&&o._reportError('There is no directive with "exportAs" set to "'+t.value+'"',t.sourceSpan)}),a},t.prototype._createDirectiveHostPropertyAsts=function(t,e,n,r){var i=this;l.isPresent(e)&&p.StringMapWrapper.forEach(e,function(e,o){var s=i._parseBinding(e,n);r.push(i._createElementPropertyAst(t,o,s,n))})},t.prototype._createDirectiveHostEventAsts=function(t,e,n){var r=this;l.isPresent(t)&&p.StringMapWrapper.forEach(t,function(t,i){r._parseEvent(i,t,e,[],n)})},t.prototype._createDirectivePropertyAsts=function(t,e,n){if(l.isPresent(t)){var r=new Map;e.forEach(function(t){var e=r.get(t.name);(l.isBlank(e)||e.isLiteral)&&r.set(t.name,t)}),p.StringMapWrapper.forEach(t,function(t,e){var i=r.get(t);l.isPresent(i)&&n.push(new P.BoundDirectivePropertyAst(e,i.name,i.expression,i.sourceSpan))})}},t.prototype._createElementPropertyAsts=function(t,e,n){var r=this,i=[],o=new Map;return n.forEach(function(t){t.inputs.forEach(function(t){o.set(t.templateName,t)})}),e.forEach(function(e){!e.isLiteral&&l.isBlank(o.get(e.name))&&i.push(r._createElementPropertyAst(t,e.name,e.expression,e.sourceSpan))}),i},t.prototype._createElementPropertyAst=function(t,e,n,r){var i,o,s=null,a=e.split(N);if(1===a.length)o=this._schemaRegistry.getMappedPropName(a[0]),i=P.PropertyBindingType.Property,this._schemaRegistry.hasProperty(t,o)||this._reportError("Can't bind to '"+o+"' since it isn't a known native property",r);else if(a[0]==D){o=a[1];var u=o.indexOf(":");if(u>-1){var c=o.substring(0,u),p=o.substring(u+1);o=g.mergeNsAndName(c,p)}i=P.PropertyBindingType.Attribute}else a[0]==V?(o=a[1],i=P.PropertyBindingType.Class):a[0]==j?(s=a.length>2?a[2]:null,o=a[1],i=P.PropertyBindingType.Style):(this._reportError("Invalid property name '"+e+"'",r),i=null);return new P.BoundElementPropertyAst(o,i,n,s,r)},t.prototype._findComponentDirectiveNames=function(t){var e=[];return t.forEach(function(t){var n=t.directive.type.name;t.directive.isComponent&&e.push(n)}),e},t.prototype._assertOnlyOneComponent=function(t,e){var n=this._findComponentDirectiveNames(t);n.length>1&&this._reportError("More than one component: "+n.join(","),e)},t.prototype._assertNoComponentsNorElementBindingsOnTemplate=function(t,e,n){var r=this,i=this._findComponentDirectiveNames(t);i.length>0&&this._reportError("Components on an embedded template: "+i.join(","),n),e.forEach(function(t){r._reportError("Property binding "+t.name+" not used by any directive on an embedded template",n)})},t.prototype._assertAllEventsPublishedByDirectives=function(t,e){var n=this,r=new Set;t.forEach(function(t){p.StringMapWrapper.forEach(t.directive.outputs,function(t,e){r.add(t)})}),e.forEach(function(t){(l.isPresent(t.target)||!p.SetWrapper.has(r,t.name))&&n._reportError("Event binding "+t.fullName+" not emitted by any directive on an embedded template",t.sourceSpan)})},t}(),H=function(){function t(){}return t.prototype.visitElement=function(t,e){var n=C.preparseElement(t);if(n.type===C.PreparsedElementType.SCRIPT||n.type===C.PreparsedElementType.STYLE||n.type===C.PreparsedElementType.STYLESHEET)return null;var r=t.attrs.map(function(t){return[t.name,t.value]}),o=i(t.name,r),s=e.findNgContentIndex(o),a=S.htmlVisitAll(this,t.children,G);return new P.ElementAst(t.name,S.htmlVisitAll(this,t.attrs),[],[],[],[],[],!1,a,s,t.sourceSpan)},t.prototype.visitComment=function(t,e){return null},t.prototype.visitAttr=function(t,e){return new P.AttrAst(t.name,t.value,t.sourceSpan)},t.prototype.visitText=function(t,e){var n=e.findNgContentIndex(L);return new P.TextAst(t.value,n,t.sourceSpan)},t.prototype.visitExpansion=function(t,e){return t},t.prototype.visitExpansionCase=function(t,e){return t},t}(),X=function(){function t(t,e,n,r){this.name=t,this.expression=e,this.isLiteral=n,this.sourceSpan=r}return t}();e.splitClasses=r;var q=function(){function t(t,e,n,r){this.isTemplateElement=t,this._ngContentIndexMatcher=e,this._wildcardNgContentIndex=n,this.providerContext=r}return t.create=function(e,n,r){var i=new E.SelectorMatcher,o=null;if(n.length>0&&n[0].directive.isComponent)for(var s=n[0].directive.template.ngContentSelectors,a=0;a0?e[0]:null},t}(),G=new q(!0,new E.SelectorMatcher,null,null),z=new H,K=function(t){function e(){t.apply(this,arguments),this.pipes=new Set}return s(e,t),e.prototype.visitPipe=function(t,e){return this.pipes.add(t.name),t.exp.visit(this),this.visitAll(t.args,e),null},e}(v.RecursiveAstVisitor);e.PipeCollector=K},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(15),o=function(){function t(){}return t.prototype.visit=function(t,e){return void 0===e&&(e=null),null},t.prototype.toString=function(){return"AST"},t}();e.AST=o;var s=function(t){function e(e,n,r){t.call(this),this.prefix=e,this.uninterpretedExpression=n,this.location=r}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitQuote(this,e)},e.prototype.toString=function(){return"Quote"},e}(o);e.Quote=s;var a=function(t){function e(){t.apply(this,arguments)}return r(e,t),e.prototype.visit=function(t,e){void 0===e&&(e=null)},e}(o);e.EmptyExpr=a;var u=function(t){function e(){t.apply(this,arguments)}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitImplicitReceiver(this,e)},e}(o);e.ImplicitReceiver=u;var c=function(t){function e(e){t.call(this),this.expressions=e}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitChain(this,e)},e}(o);e.Chain=c;var p=function(t){function e(e,n,r){t.call(this),this.condition=e,this.trueExp=n,this.falseExp=r}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitConditional(this,e)},e}(o);e.Conditional=p;var l=function(t){function e(e,n){t.call(this),this.receiver=e,this.name=n}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitPropertyRead(this,e)},e}(o);e.PropertyRead=l;var h=function(t){function e(e,n,r){t.call(this),this.receiver=e,this.name=n,this.value=r}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitPropertyWrite(this,e)},e}(o);e.PropertyWrite=h;var f=function(t){function e(e,n){t.call(this),this.receiver=e,this.name=n}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitSafePropertyRead(this,e)},e}(o);e.SafePropertyRead=f;var d=function(t){function e(e,n){t.call(this),this.obj=e,this.key=n}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitKeyedRead(this,e)},e}(o);e.KeyedRead=d;var v=function(t){function e(e,n,r){t.call(this),this.obj=e,this.key=n,this.value=r}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitKeyedWrite(this,e)},e}(o);e.KeyedWrite=v;var y=function(t){function e(e,n,r){t.call(this),this.exp=e,this.name=n,this.args=r}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitPipe(this,e)},e}(o);e.BindingPipe=y;var m=function(t){function e(e){t.call(this),this.value=e}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitLiteralPrimitive(this,e)},e}(o);e.LiteralPrimitive=m;var g=function(t){function e(e){t.call(this),this.expressions=e}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitLiteralArray(this,e)},e}(o);e.LiteralArray=g;var _=function(t){function e(e,n){t.call(this),this.keys=e,this.values=n}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitLiteralMap(this,e)},e}(o);e.LiteralMap=_;var b=function(t){function e(e,n){t.call(this),this.strings=e,this.expressions=n}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitInterpolation(this,e)},e}(o);e.Interpolation=b;var P=function(t){function e(e,n,r){t.call(this),this.operation=e,this.left=n,this.right=r}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitBinary(this,e)},e}(o);e.Binary=P;var E=function(t){function e(e){t.call(this),this.expression=e}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitPrefixNot(this,e)},e}(o);e.PrefixNot=E;var w=function(t){function e(e,n,r){t.call(this),this.receiver=e,this.name=n,this.args=r}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitMethodCall(this,e)},e}(o);e.MethodCall=w;var C=function(t){function e(e,n,r){t.call(this),this.receiver=e,this.name=n,this.args=r}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitSafeMethodCall(this,e)},e}(o);e.SafeMethodCall=C;var R=function(t){function e(e,n){t.call(this),this.target=e,this.args=n}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),t.visitFunctionCall(this,e)},e}(o);e.FunctionCall=R;var S=function(t){function e(e,n,r){t.call(this),this.ast=e,this.source=n,this.location=r}return r(e,t),e.prototype.visit=function(t,e){return void 0===e&&(e=null),this.ast.visit(t,e)},e.prototype.toString=function(){return this.source+" in "+this.location},e}(o);e.ASTWithSource=S;var O=function(){function t(t,e,n,r){this.key=t,this.keyIsVar=e,this.name=n,this.expression=r}return t}();e.TemplateBinding=O;var T=function(){function t(){}return t.prototype.visitBinary=function(t,e){return t.left.visit(this),t.right.visit(this),null},t.prototype.visitChain=function(t,e){return this.visitAll(t.expressions,e)},t.prototype.visitConditional=function(t,e){return t.condition.visit(this),t.trueExp.visit(this),t.falseExp.visit(this),null},t.prototype.visitPipe=function(t,e){return t.exp.visit(this),this.visitAll(t.args,e),null},t.prototype.visitFunctionCall=function(t,e){return t.target.visit(this),this.visitAll(t.args,e),null},t.prototype.visitImplicitReceiver=function(t,e){return null},t.prototype.visitInterpolation=function(t,e){return this.visitAll(t.expressions,e)},t.prototype.visitKeyedRead=function(t,e){return t.obj.visit(this),t.key.visit(this),null},t.prototype.visitKeyedWrite=function(t,e){return t.obj.visit(this),t.key.visit(this),t.value.visit(this),null},t.prototype.visitLiteralArray=function(t,e){return this.visitAll(t.expressions,e)},t.prototype.visitLiteralMap=function(t,e){return this.visitAll(t.values,e)},t.prototype.visitLiteralPrimitive=function(t,e){return null},t.prototype.visitMethodCall=function(t,e){return t.receiver.visit(this),this.visitAll(t.args,e)},t.prototype.visitPrefixNot=function(t,e){return t.expression.visit(this),null},t.prototype.visitPropertyRead=function(t,e){return t.receiver.visit(this),null},t.prototype.visitPropertyWrite=function(t,e){return t.receiver.visit(this),t.value.visit(this),null},t.prototype.visitSafePropertyRead=function(t,e){return t.receiver.visit(this),null},t.prototype.visitSafeMethodCall=function(t,e){return t.receiver.visit(this),this.visitAll(t.args,e)},t.prototype.visitAll=function(t,e){var n=this;return t.forEach(function(t){return t.visit(n,e)}),null},t.prototype.visitQuote=function(t,e){return null},t}();e.RecursiveAstVisitor=T;var x=function(){function t(){}return t.prototype.visitImplicitReceiver=function(t,e){return t},t.prototype.visitInterpolation=function(t,e){return new b(t.strings,this.visitAll(t.expressions))},t.prototype.visitLiteralPrimitive=function(t,e){return new m(t.value)},t.prototype.visitPropertyRead=function(t,e){return new l(t.receiver.visit(this),t.name)},t.prototype.visitPropertyWrite=function(t,e){return new h(t.receiver.visit(this),t.name,t.value)},t.prototype.visitSafePropertyRead=function(t,e){return new f(t.receiver.visit(this),t.name)},t.prototype.visitMethodCall=function(t,e){return new w(t.receiver.visit(this),t.name,this.visitAll(t.args))},t.prototype.visitSafeMethodCall=function(t,e){return new C(t.receiver.visit(this),t.name,this.visitAll(t.args))},t.prototype.visitFunctionCall=function(t,e){return new R(t.target.visit(this),this.visitAll(t.args))},t.prototype.visitLiteralArray=function(t,e){return new g(this.visitAll(t.expressions))},t.prototype.visitLiteralMap=function(t,e){return new _(t.keys,this.visitAll(t.values))},t.prototype.visitBinary=function(t,e){return new P(t.operation,t.left.visit(this),t.right.visit(this))},t.prototype.visitPrefixNot=function(t,e){return new E(t.expression.visit(this))},t.prototype.visitConditional=function(t,e){return new p(t.condition.visit(this),t.trueExp.visit(this),t.falseExp.visit(this))},t.prototype.visitPipe=function(t,e){return new y(t.exp.visit(this),t.name,this.visitAll(t.args))},t.prototype.visitKeyedRead=function(t,e){return new d(t.obj.visit(this),t.key.visit(this))},t.prototype.visitKeyedWrite=function(t,e){return new v(t.obj.visit(this),t.key.visit(this),t.value.visit(this))},t.prototype.visitAll=function(t){for(var e=i.ListWrapper.createFixedSize(t.length),n=0;no?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(8),a=n(5),u=n(12),c=n(15),p=n(143),l=n(141),h=new l.ImplicitReceiver,f=/\{\{([\s\S]*?)\}\}/g,d=function(t){function e(e,n,r,i){t.call(this,"Parser Error: "+e+" "+r+" ["+n+"] in "+i)}return r(e,t),e}(u.BaseException),v=function(){function t(t,e){this.strings=t,this.expressions=e}return t}();e.SplitInterpolation=v;var y=function(){function t(t){this._lexer=t}return t.prototype.parseAction=function(t,e){this._checkNoInterpolation(t,e);var n=this._lexer.tokenize(this._stripComments(t)),r=new m(t,e,n,!0).parseChain();return new l.ASTWithSource(r,t,e)},t.prototype.parseBinding=function(t,e){var n=this._parseBindingAst(t,e);return new l.ASTWithSource(n,t,e)},t.prototype.parseSimpleBinding=function(t,e){var n=this._parseBindingAst(t,e);if(!g.check(n))throw new d("Host binding expression can only contain field access and constants",t,e);return new l.ASTWithSource(n,t,e)},t.prototype._parseBindingAst=function(t,e){var n=this._parseQuote(t,e);if(a.isPresent(n))return n;this._checkNoInterpolation(t,e);var r=this._lexer.tokenize(this._stripComments(t));return new m(t,e,r,!1).parseChain()},t.prototype._parseQuote=function(t,e){if(a.isBlank(t))return null;var n=t.indexOf(":");if(-1==n)return null;var r=t.substring(0,n).trim();if(!p.isIdentifier(r))return null;var i=t.substring(n+1);return new l.Quote(r,i,e)},t.prototype.parseTemplateBindings=function(t,e){var n=this._lexer.tokenize(t);return new m(t,e,n,!1).parseTemplateBindings()},t.prototype.parseInterpolation=function(t,e){var n=this.splitInterpolation(t,e);if(null==n)return null;for(var r=[],i=0;i0))throw new d("Blank expressions are not allowed in interpolated strings",t,"at column "+this._findInterpolationErrorColumn(n,o)+" in",e);i.push(s)}}return new v(r,i)},t.prototype.wrapLiteralPrimitive=function(t,e){return new l.ASTWithSource(new l.LiteralPrimitive(t),t,e)},t.prototype._stripComments=function(t){var e=this._commentStart(t);return a.isPresent(e)?t.substring(0,e).trim():t},t.prototype._commentStart=function(t){for(var e=null,n=0;n1)throw new d("Got interpolation ({{}}) where expression was expected",t,"at column "+this._findInterpolationErrorColumn(n,1)+" in",e)},t.prototype._findInterpolationErrorColumn=function(t,e){for(var n="",r=0;e>r;r++)n+=r%2===0?t[r]:"{{"+t[r]+"}}";return n.length},t=i([s.Injectable(),o("design:paramtypes",[p.Lexer])],t)}();e.Parser=y;var m=function(){function t(t,e,n,r){this.input=t,this.location=e,this.tokens=n,this.parseAction=r,this.index=0}return t.prototype.peek=function(t){var e=this.index+t;return e"))t=new l.Binary(">",t,this.parseAdditive());else if(this.optionalOperator("<="))t=new l.Binary("<=",t,this.parseAdditive());else{if(!this.optionalOperator(">="))return t;t=new l.Binary(">=",t,this.parseAdditive())}},t.prototype.parseAdditive=function(){for(var t=this.parseMultiplicative();;)if(this.optionalOperator("+"))t=new l.Binary("+",t,this.parseMultiplicative());else{if(!this.optionalOperator("-"))return t;t=new l.Binary("-",t,this.parseMultiplicative())}},t.prototype.parseMultiplicative=function(){for(var t=this.parsePrefix();;)if(this.optionalOperator("*"))t=new l.Binary("*",t,this.parsePrefix());else if(this.optionalOperator("%"))t=new l.Binary("%",t,this.parsePrefix());else{if(!this.optionalOperator("/"))return t;t=new l.Binary("/",t,this.parsePrefix())}},t.prototype.parsePrefix=function(){return this.optionalOperator("+")?this.parsePrefix():this.optionalOperator("-")?new l.Binary("-",new l.LiteralPrimitive(0),this.parsePrefix()):this.optionalOperator("!")?new l.PrefixNot(this.parsePrefix()):this.parseCallChain()},t.prototype.parseCallChain=function(){for(var t=this.parsePrimary();;)if(this.optionalCharacter(p.$PERIOD))t=this.parseAccessMemberOrMethodCall(t,!1);else if(this.optionalOperator("?."))t=this.parseAccessMemberOrMethodCall(t,!0);else if(this.optionalCharacter(p.$LBRACKET)){var e=this.parsePipe();if(this.expectCharacter(p.$RBRACKET),this.optionalOperator("=")){var n=this.parseConditional();t=new l.KeyedWrite(t,e,n)}else t=new l.KeyedRead(t,e)}else{if(!this.optionalCharacter(p.$LPAREN))return t;var r=this.parseCallArguments();this.expectCharacter(p.$RPAREN),t=new l.FunctionCall(t,r)}},t.prototype.parsePrimary=function(){if(this.optionalCharacter(p.$LPAREN)){var t=this.parsePipe();return this.expectCharacter(p.$RPAREN),t}if(this.next.isKeywordNull()||this.next.isKeywordUndefined())return this.advance(),new l.LiteralPrimitive(null);if(this.next.isKeywordTrue())return this.advance(),new l.LiteralPrimitive(!0);if(this.next.isKeywordFalse())return this.advance(),new l.LiteralPrimitive(!1);if(this.optionalCharacter(p.$LBRACKET)){var e=this.parseExpressionList(p.$RBRACKET);return this.expectCharacter(p.$RBRACKET),new l.LiteralArray(e)}if(this.next.isCharacter(p.$LBRACE))return this.parseLiteralMap();if(this.next.isIdentifier())return this.parseAccessMemberOrMethodCall(h,!1);if(this.next.isNumber()){var n=this.next.toNumber();return this.advance(),new l.LiteralPrimitive(n)}if(this.next.isString()){var r=this.next.toString();return this.advance(),new l.LiteralPrimitive(r)}throw this.index>=this.tokens.length?this.error("Unexpected end of expression: "+this.input):this.error("Unexpected token "+this.next),new u.BaseException("Fell through all cases in parsePrimary")},t.prototype.parseExpressionList=function(t){var e=[];if(!this.next.isCharacter(t))do e.push(this.parsePipe());while(this.optionalCharacter(p.$COMMA));return e},t.prototype.parseLiteralMap=function(){var t=[],e=[];if(this.expectCharacter(p.$LBRACE),!this.optionalCharacter(p.$RBRACE)){do{var n=this.expectIdentifierOrKeywordOrString();t.push(n),this.expectCharacter(p.$COLON),e.push(this.parsePipe())}while(this.optionalCharacter(p.$COMMA));this.expectCharacter(p.$RBRACE)}return new l.LiteralMap(t,e)},t.prototype.parseAccessMemberOrMethodCall=function(t,e){void 0===e&&(e=!1);var n=this.expectIdentifierOrKeyword();if(this.optionalCharacter(p.$LPAREN)){var r=this.parseCallArguments();return this.expectCharacter(p.$RPAREN),e?new l.SafeMethodCall(t,n,r):new l.MethodCall(t,n,r)}if(!e){if(this.optionalOperator("=")){this.parseAction||this.error("Bindings cannot contain assignments");var i=this.parseConditional();return new l.PropertyWrite(t,n,i)}return new l.PropertyRead(t,n)}return this.optionalOperator("=")?(this.error("The '?.' operator cannot be used in the assignment"),null):new l.SafePropertyRead(t,n)},t.prototype.parseCallArguments=function(){if(this.next.isCharacter(p.$RPAREN))return[];var t=[];do t.push(this.parsePipe());while(this.optionalCharacter(p.$COMMA));return t},t.prototype.parseBlockContent=function(){this.parseAction||this.error("Binding expression cannot contain chained expression");for(var t=[];this.index=e.$TAB&&t<=e.$SPACE||t==X}function p(t){return t>=D&&H>=t||t>=A&&M>=t||t==N||t==e.$$}function l(t){if(0==t.length)return!1;var n=new G(t);if(!p(n.peek))return!1;for(n.advance();n.peek!==e.$EOF;){if(!h(n.peek))return!1;n.advance()}return!0}function h(t){return t>=D&&H>=t||t>=A&&M>=t||t>=T&&x>=t||t==N||t==e.$$}function f(t){return t>=T&&x>=t}function d(t){return t==V||t==I}function v(t){return t==e.$MINUS||t==e.$PLUS}function y(t){return t===e.$SQ||t===e.$DQ||t===e.$BT}function m(t){switch(t){case L:return e.$LF;case j:return e.$FF;case B:return e.$CR;case F:return e.$TAB;case W:return e.$VTAB;default:return t}}var g=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},_=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},b=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},P=n(8),E=n(15),w=n(5),C=n(12);!function(t){t[t.Character=0]="Character",t[t.Identifier=1]="Identifier",t[t.Keyword=2]="Keyword",t[t.String=3]="String",t[t.Operator=4]="Operator",t[t.Number=5]="Number"}(e.TokenType||(e.TokenType={}));var R=e.TokenType,S=function(){function t(){}return t.prototype.tokenize=function(t){for(var e=new G(t),n=[],r=e.scanToken();null!=r;)n.push(r),r=e.scanToken();return n},t=_([P.Injectable(),b("design:paramtypes",[])],t)}();e.Lexer=S;var O=function(){function t(t,e,n,r){this.index=t,this.type=e,this.numValue=n,this.strValue=r}return t.prototype.isCharacter=function(t){return this.type==R.Character&&this.numValue==t},t.prototype.isNumber=function(){return this.type==R.Number},t.prototype.isString=function(){return this.type==R.String},t.prototype.isOperator=function(t){return this.type==R.Operator&&this.strValue==t},t.prototype.isIdentifier=function(){return this.type==R.Identifier},t.prototype.isKeyword=function(){return this.type==R.Keyword},t.prototype.isKeywordVar=function(){return this.type==R.Keyword&&"var"==this.strValue},t.prototype.isKeywordNull=function(){return this.type==R.Keyword&&"null"==this.strValue},t.prototype.isKeywordUndefined=function(){return this.type==R.Keyword&&"undefined"==this.strValue},t.prototype.isKeywordTrue=function(){return this.type==R.Keyword&&"true"==this.strValue},t.prototype.isKeywordFalse=function(){return this.type==R.Keyword&&"false"==this.strValue},t.prototype.toNumber=function(){return this.type==R.Number?this.numValue:-1},t.prototype.toString=function(){switch(this.type){case R.Character:case R.Identifier:case R.Keyword:case R.Operator:case R.String:return this.strValue;case R.Number:return this.numValue.toString();default:return null}},t}();e.Token=O,e.EOF=new O(-1,R.Character,0,""),e.$EOF=0,e.$TAB=9,e.$LF=10,e.$VTAB=11,e.$FF=12,e.$CR=13,e.$SPACE=32,e.$BANG=33,e.$DQ=34,e.$HASH=35,e.$$=36,e.$PERCENT=37,e.$AMPERSAND=38,e.$SQ=39,e.$LPAREN=40,e.$RPAREN=41,e.$STAR=42,e.$PLUS=43,e.$COMMA=44,e.$MINUS=45,e.$PERIOD=46,e.$SLASH=47,e.$COLON=58,e.$SEMICOLON=59,e.$LT=60,e.$EQ=61,e.$GT=62,e.$QUESTION=63;var T=48,x=57,A=65,I=69,M=90;e.$LBRACKET=91,e.$BACKSLASH=92,e.$RBRACKET=93;var k=94,N=95;e.$BT=96;var D=97,V=101,j=102,L=110,B=114,F=116,U=117,W=118,H=122;e.$LBRACE=123,e.$BAR=124,e.$RBRACE=125;var X=160,q=function(t){function e(e){t.call(this),this.message=e}return g(e,t),e.prototype.toString=function(){return this.message},e}(C.BaseException);e.ScannerError=q;var G=function(){function t(t){this.input=t,this.peek=0,this.index=-1,this.length=t.length,this.advance()}return t.prototype.advance=function(){this.peek=++this.index>=this.length?e.$EOF:w.StringWrapper.charCodeAt(this.input,this.index)},t.prototype.scanToken=function(){for(var t=this.input,n=this.length,i=this.peek,o=this.index;i<=e.$SPACE;){if(++o>=n){i=e.$EOF;break}i=w.StringWrapper.charCodeAt(t,o)}if(this.peek=i,this.index=o,o>=n)return null;if(p(i))return this.scanIdentifier();if(f(i))return this.scanNumber(o);var s=o;switch(i){case e.$PERIOD:return this.advance(),f(this.peek)?this.scanNumber(s):r(s,e.$PERIOD);case e.$LPAREN:case e.$RPAREN:case e.$LBRACE:case e.$RBRACE:case e.$LBRACKET:case e.$RBRACKET:case e.$COMMA:case e.$COLON:case e.$SEMICOLON:return this.scanCharacter(s,i);case e.$SQ:case e.$DQ:return this.scanString();case e.$HASH:case e.$PLUS:case e.$MINUS:case e.$STAR:case e.$SLASH:case e.$PERCENT:case k:return this.scanOperator(s,w.StringWrapper.fromCharCode(i));case e.$QUESTION:return this.scanComplexOperator(s,"?",e.$PERIOD,".");case e.$LT:case e.$GT:return this.scanComplexOperator(s,w.StringWrapper.fromCharCode(i),e.$EQ,"=");case e.$BANG:case e.$EQ:return this.scanComplexOperator(s,w.StringWrapper.fromCharCode(i),e.$EQ,"=",e.$EQ,"=");case e.$AMPERSAND:return this.scanComplexOperator(s,"&",e.$AMPERSAND,"&");case e.$BAR:return this.scanComplexOperator(s,"|",e.$BAR,"|");case X:for(;c(this.peek);)this.advance();return this.scanToken()}return this.error("Unexpected character ["+w.StringWrapper.fromCharCode(i)+"]",0),null},t.prototype.scanCharacter=function(t,e){return this.advance(),r(t,e)},t.prototype.scanOperator=function(t,e){return this.advance(),s(t,e)},t.prototype.scanComplexOperator=function(t,e,n,r,i,o){this.advance();var a=e;return this.peek==n&&(this.advance(),a+=r),w.isPresent(i)&&this.peek==i&&(this.advance(),a+=o),s(t,a)},t.prototype.scanIdentifier=function(){var t=this.index;for(this.advance();h(this.peek);)this.advance();var e=this.input.substring(t,this.index);return E.SetWrapper.has(z,e)?o(t,e):i(t,e)},t.prototype.scanNumber=function(t){var n=this.index===t;for(this.advance();;){if(f(this.peek));else if(this.peek==e.$PERIOD)n=!1;else{if(!d(this.peek))break;this.advance(),v(this.peek)&&this.advance(),f(this.peek)||this.error("Invalid exponent",-1),n=!1}this.advance()}var r=this.input.substring(t,this.index),i=n?w.NumberWrapper.parseIntAutoRadix(r):w.NumberWrapper.parseFloat(r);return u(t,i)},t.prototype.scanString=function(){var t=this.index,n=this.peek;this.advance();for(var r,i=this.index,o=this.input;this.peek!=n;)if(this.peek==e.$BACKSLASH){null==r&&(r=new w.StringJoiner),r.add(o.substring(i,this.index)),this.advance();var s;if(this.peek==U){var u=o.substring(this.index+1,this.index+5);try{s=w.NumberWrapper.parseInt(u,16)}catch(c){this.error("Invalid unicode escape [\\u"+u+"]",0)}for(var p=0;5>p;p++)this.advance()}else s=m(this.peek),this.advance();r.add(w.StringWrapper.fromCharCode(s)),i=this.index}else this.peek==e.$EOF?this.error("Unterminated quote",0):this.advance();var l=o.substring(i,this.index);this.advance();var h=l;return null!=r&&(r.add(l),h=r.toString()),a(t,h)},t.prototype.error=function(t,e){var n=this.index+e;throw new q("Lexer Error: "+t+" at column "+n+" in expression ["+this.input+"]")},t}();e.isIdentifier=l,e.isQuote=y;var z=(E.SetWrapper.createFromList(["+","-","*","/","%","^","=","==","!=","===","!==","<",">","<=",">=","&&","||","&","|","!","?","#","?."]),E.SetWrapper.createFromList(["var","null","undefined","true","false","if","else"]))},function(t,e,n){"use strict";function r(t,e,n){return u.isBlank(t)&&(t=d.getHtmlTagDefinition(e).implicitNamespacePrefix,u.isBlank(t)&&u.isPresent(n)&&(t=d.getNsPrefix(n.name))),d.mergeNsAndName(t,e)}function i(t,e){return t.length>0&&t[t.length-1]===e}var o=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},s=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},a=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},u=n(5),c=n(15),p=n(145),l=n(6),h=n(146),f=n(147),d=n(148),v=function(t){function e(e,n,r){t.call(this,n,r),this.elementName=e}return o(e,t),e.create=function(t,n,r){return new e(t,n,r)},e}(f.ParseError);e.HtmlTreeError=v;var y=function(){function t(t,e){this.rootNodes=t,this.errors=e}return t}();e.HtmlParseTreeResult=y;var m=function(){function t(){}return t.prototype.parse=function(t,e,n){void 0===n&&(n=!1);var r=h.tokenizeHtml(t,e,n),i=new g(r.tokens).build();return new y(i.rootNodes,r.errors.concat(i.errors))},t=s([l.Injectable(),a("design:paramtypes",[])],t)}();e.HtmlParser=m;var g=function(){function t(t){this.tokens=t,this.index=-1,this.rootNodes=[],this.errors=[],this.elementStack=[],this._advance()}return t.prototype.build=function(){for(;this.peek.type!==h.HtmlTokenType.EOF;)this.peek.type===h.HtmlTokenType.TAG_OPEN_START?this._consumeStartTag(this._advance()):this.peek.type===h.HtmlTokenType.TAG_CLOSE?this._consumeEndTag(this._advance()):this.peek.type===h.HtmlTokenType.CDATA_START?(this._closeVoidElement(),this._consumeCdata(this._advance())):this.peek.type===h.HtmlTokenType.COMMENT_START?(this._closeVoidElement(),this._consumeComment(this._advance())):this.peek.type===h.HtmlTokenType.TEXT||this.peek.type===h.HtmlTokenType.RAW_TEXT||this.peek.type===h.HtmlTokenType.ESCAPABLE_RAW_TEXT?(this._closeVoidElement(),this._consumeText(this._advance())):this.peek.type===h.HtmlTokenType.EXPANSION_FORM_START?this._consumeExpansion(this._advance()):this._advance();return new y(this.rootNodes,this.errors)},t.prototype._advance=function(){var t=this.peek;return this.index0)return this.errors=this.errors.concat(o.errors),null;var s=new f.ParseSourceSpan(e.sourceSpan.start,i.sourceSpan.end),a=new f.ParseSourceSpan(n.sourceSpan.start,i.sourceSpan.end);return new p.HtmlExpansionCaseAst(e.parts[0],o.rootNodes,s,e.sourceSpan,a)},t.prototype._collectExpansionExpTokens=function(t){for(var e=[],n=[h.HtmlTokenType.EXPANSION_CASE_EXP_START];;){if((this.peek.type===h.HtmlTokenType.EXPANSION_FORM_START||this.peek.type===h.HtmlTokenType.EXPANSION_CASE_EXP_START)&&n.push(this.peek.type),this.peek.type===h.HtmlTokenType.EXPANSION_CASE_EXP_END){if(!i(n,h.HtmlTokenType.EXPANSION_CASE_EXP_START))return this.errors.push(v.create(null,t.sourceSpan,"Invalid expansion form. Missing '}'.")),null;if(n.pop(),0==n.length)return e}if(this.peek.type===h.HtmlTokenType.EXPANSION_FORM_END){if(!i(n,h.HtmlTokenType.EXPANSION_FORM_START))return this.errors.push(v.create(null,t.sourceSpan,"Invalid expansion form. Missing '}'.")),null;n.pop()}if(this.peek.type===h.HtmlTokenType.EOF)return this.errors.push(v.create(null,t.sourceSpan,"Invalid expansion form. Missing '}'.")),null;e.push(this._advance())}},t.prototype._consumeText=function(t){var e=t.parts[0];if(e.length>0&&"\n"==e[0]){var n=this._getParentElement();u.isPresent(n)&&0==n.children.length&&d.getHtmlTagDefinition(n.name).ignoreFirstLf&&(e=e.substring(1))}e.length>0&&this._addToParent(new p.HtmlTextAst(e,t.sourceSpan))},t.prototype._closeVoidElement=function(){if(this.elementStack.length>0){var t=c.ListWrapper.last(this.elementStack);d.getHtmlTagDefinition(t.name).isVoid&&this.elementStack.pop()}},t.prototype._consumeStartTag=function(t){for(var e=t.parts[0],n=t.parts[1],i=[];this.peek.type===h.HtmlTokenType.ATTR_NAME;)i.push(this._consumeAttr(this._advance()));var o=r(e,n,this._getParentElement()),s=!1;this.peek.type===h.HtmlTokenType.TAG_OPEN_END_VOID?(this._advance(),s=!0,null!=d.getNsPrefix(o)||d.getHtmlTagDefinition(o).isVoid||this.errors.push(v.create(o,t.sourceSpan,'Only void and foreign elements can be self closed "'+t.parts[1]+'"'))):this.peek.type===h.HtmlTokenType.TAG_OPEN_END&&(this._advance(),s=!1);var a=this.peek.sourceSpan.start,u=new f.ParseSourceSpan(t.sourceSpan.start,a),c=new p.HtmlElementAst(o,i,[],u,u,null);this._pushElement(c),s&&(this._popElement(o),c.endSourceSpan=u)},t.prototype._pushElement=function(t){if(this.elementStack.length>0){var e=c.ListWrapper.last(this.elementStack);d.getHtmlTagDefinition(e.name).isClosedByChild(t.name)&&this.elementStack.pop()}var n=d.getHtmlTagDefinition(t.name),e=this._getParentElement();if(n.requireExtraParent(u.isPresent(e)?e.name:null)){var r=new p.HtmlElementAst(n.parentToAdd,[],[t],t.sourceSpan,t.startSourceSpan,t.endSourceSpan);this._addToParent(r),this.elementStack.push(r),this.elementStack.push(t)}else this._addToParent(t),this.elementStack.push(t)},t.prototype._consumeEndTag=function(t){var e=r(t.parts[0],t.parts[1],this._getParentElement());this._getParentElement().endSourceSpan=t.sourceSpan,d.getHtmlTagDefinition(e).isVoid?this.errors.push(v.create(e,t.sourceSpan,'Void elements do not have end tags "'+t.parts[1]+'"')):this._popElement(e)||this.errors.push(v.create(e,t.sourceSpan,'Unexpected closing tag "'+t.parts[1]+'"'))},t.prototype._popElement=function(t){for(var e=this.elementStack.length-1;e>=0;e--){var n=this.elementStack[e];if(n.name==t)return c.ListWrapper.splice(this.elementStack,e,this.elementStack.length-e),!0;if(!d.getHtmlTagDefinition(n.name).closedByParent)return!1}return!1},t.prototype._consumeAttr=function(t){var e=d.mergeNsAndName(t.parts[0],t.parts[1]),n=t.sourceSpan.end,r="";if(this.peek.type===h.HtmlTokenType.ATTR_VALUE){var i=this._advance();r=i.parts[0],n=i.sourceSpan.end}return new p.HtmlAttrAst(e,r,new f.ParseSourceSpan(t.sourceSpan.start,n))},t.prototype._getParentElement=function(){return this.elementStack.length>0?c.ListWrapper.last(this.elementStack):null},t.prototype._addToParent=function(t){var e=this._getParentElement();u.isPresent(e)?e.children.push(t):this.rootNodes.push(t)},t}()},function(t,e,n){"use strict";function r(t,e,n){void 0===n&&(n=null);var r=[];return e.forEach(function(e){var o=e.visit(t,n);i.isPresent(o)&&r.push(o)}),r}var i=n(5),o=function(){function t(t,e){this.value=t,this.sourceSpan=e}return t.prototype.visit=function(t,e){return t.visitText(this,e)},t}();e.HtmlTextAst=o;var s=function(){function t(t,e,n,r,i){this.switchValue=t,this.type=e,this.cases=n,this.sourceSpan=r,this.switchValueSourceSpan=i}return t.prototype.visit=function(t,e){return t.visitExpansion(this,e)},t}();e.HtmlExpansionAst=s;var a=function(){function t(t,e,n,r,i){this.value=t,this.expression=e,this.sourceSpan=n,this.valueSourceSpan=r,this.expSourceSpan=i}return t.prototype.visit=function(t,e){return t.visitExpansionCase(this,e)},t}();e.HtmlExpansionCaseAst=a;var u=function(){function t(t,e,n){this.name=t,this.value=e,this.sourceSpan=n}return t.prototype.visit=function(t,e){return t.visitAttr(this,e)},t}();e.HtmlAttrAst=u;var c=function(){function t(t,e,n,r,i,o){this.name=t,this.attrs=e,this.children=n,this.sourceSpan=r,this.startSourceSpan=i,this.endSourceSpan=o}return t.prototype.visit=function(t,e){return t.visitElement(this,e)},t}();e.HtmlElementAst=c;var p=function(){function t(t,e){this.value=t,this.sourceSpan=e}return t.prototype.visit=function(t,e){return t.visitComment(this,e)},t}();e.HtmlCommentAst=p,e.htmlVisitAll=r},function(t,e,n){"use strict";function r(t,e,n){return void 0===n&&(n=!1),new ut(new P.ParseSourceFile(t,e),n).tokenize()}function i(t){var e=t===O?"EOF":_.StringWrapper.fromCharCode(t);return'Unexpected character "'+e+'"'}function o(t){return'Unknown entity "'+t+'" - use the "&#;" or "&#x;" syntax'}function s(t){return!a(t)||t===O}function a(t){return t>=T&&I>=t||t===ot}function u(t){return a(t)||t===q||t===L||t===V||t===k||t===X}function c(t){return(et>t||t>rt)&&(J>t||t>tt)&&(B>t||t>U)}function p(t){return t==F||t==O||!d(t)}function l(t){return t==F||t==O||!f(t)}function h(t,e){return t===K&&e!=K}function f(t){return t>=et&&rt>=t||t>=J&&tt>=t}function d(t){return t>=et&&nt>=t||t>=J&&Z>=t||t>=B&&U>=t}function v(t,e){return y(t)==y(e)}function y(t){return t>=et&&rt>=t?t-et+J:t}function m(t){for(var e,n=[],r=0;r=this.length)throw this._createError(i(O),this._getSpan());this.peek===x?(this.line++,this.column=0):this.peek!==x&&this.peek!==A&&this.column++,this.index++,this.peek=this.index>=this.length?O:_.StringWrapper.charCodeAt(this.input,this.index),this.nextPeek=this.index+1>=this.length?O:_.StringWrapper.charCodeAt(this.input,this.index+1)},t.prototype._attemptCharCode=function(t){return this.peek===t?(this._advance(),!0):!1},t.prototype._attemptCharCodeCaseInsensitive=function(t){return v(this.peek,t)?(this._advance(),!0):!1},t.prototype._requireCharCode=function(t){var e=this._getLocation();if(!this._attemptCharCode(t))throw this._createError(i(this.peek),this._getSpan(e,e))},t.prototype._attemptStr=function(t){for(var e=0;er.offset&&o.push(this.input.substring(r.offset,this.index));this.peek!==e;)o.push(this._readChar(t))}return this._endToken([this._processCarriageReturns(o.join(""))],r)},t.prototype._consumeComment=function(t){var e=this;this._beginToken(w.COMMENT_START,t),this._requireCharCode(j),this._endToken([]);var n=this._consumeRawText(!1,j,function(){return e._attemptStr("->")});this._beginToken(w.COMMENT_END,n.sourceSpan.end),this._endToken([])},t.prototype._consumeCdata=function(t){var e=this;this._beginToken(w.CDATA_START,t),this._requireStr("CDATA["),this._endToken([]);var n=this._consumeRawText(!1,z,function(){return e._attemptStr("]>")});this._beginToken(w.CDATA_END,n.sourceSpan.end),this._endToken([])},t.prototype._consumeDocType=function(t){this._beginToken(w.DOC_TYPE,t),this._attemptUntilChar(q),this._advance(),this._endToken([this.input.substring(t.offset+2,this.index-1)])},t.prototype._consumePrefixAndName=function(){for(var t=this.index,e=null;this.peek!==W&&!c(this.peek);)this._advance();var n;this.peek===W?(this._advance(),e=this.input.substring(t,this.index-1),n=this.index):n=t,this._requireCharCodeUntilFn(u,this.index===n?1:0);var r=this.input.substring(n,this.index);return[e,r]},t.prototype._consumeTagOpen=function(t){var e,n=this._savePosition();try{if(!f(this.peek))throw this._createError(i(this.peek),this._getSpan());var r=this.index;for(this._consumeTagOpenStart(t),e=this.input.substring(r,this.index).toLowerCase(),this._attemptCharCodeUntilFn(s);this.peek!==L&&this.peek!==q;)this._consumeAttributeName(),this._attemptCharCodeUntilFn(s),this._attemptCharCode(X)&&(this._attemptCharCodeUntilFn(s),this._consumeAttributeValue()),this._attemptCharCodeUntilFn(s);this._consumeTagOpenEnd()}catch(o){if(o instanceof at)return this._restorePosition(n),this._beginToken(w.TEXT,t),void this._endToken(["<"]);throw o}var a=E.getHtmlTagDefinition(e).contentType;a===E.HtmlTagContentType.RAW_TEXT?this._consumeRawTextWithTagClose(e,!1):a===E.HtmlTagContentType.ESCAPABLE_RAW_TEXT&&this._consumeRawTextWithTagClose(e,!0)},t.prototype._consumeRawTextWithTagClose=function(t,e){var n=this,r=this._consumeRawText(e,H,function(){return n._attemptCharCode(L)?(n._attemptCharCodeUntilFn(s),n._attemptStrCaseInsensitive(t)?(n._attemptCharCodeUntilFn(s),n._attemptCharCode(q)?!0:!1):!1):!1});this._beginToken(w.TAG_CLOSE,r.sourceSpan.end),this._endToken([null,t])},t.prototype._consumeTagOpenStart=function(t){this._beginToken(w.TAG_OPEN_START,t);var e=this._consumePrefixAndName();this._endToken(e)},t.prototype._consumeAttributeName=function(){this._beginToken(w.ATTR_NAME);var t=this._consumePrefixAndName();this._endToken(t)},t.prototype._consumeAttributeValue=function(){this._beginToken(w.ATTR_VALUE);var t;if(this.peek===V||this.peek===k){var e=this.peek;this._advance();for(var n=[];this.peek!==e;)n.push(this._readChar(!0));t=n.join(""),this._advance()}else{var r=this.index;this._requireCharCodeUntilFn(u,1),t=this.input.substring(r,this.index)}this._endToken([this._processCarriageReturns(t)])},t.prototype._consumeTagOpenEnd=function(){var t=this._attemptCharCode(L)?w.TAG_OPEN_END_VOID:w.TAG_OPEN_END;this._beginToken(t),this._requireCharCode(q),this._endToken([])},t.prototype._consumeTagClose=function(t){this._beginToken(w.TAG_CLOSE,t),this._attemptCharCodeUntilFn(s);var e;e=this._consumePrefixAndName(),this._attemptCharCodeUntilFn(s),this._requireCharCode(q),this._endToken(e)},t.prototype._consumeExpansionFormStart=function(){this._beginToken(w.EXPANSION_FORM_START,this._getLocation()),this._requireCharCode(K),this._endToken([]),this._beginToken(w.RAW_TEXT,this._getLocation());var t=this._readUntil(Q);this._endToken([t],this._getLocation()),this._requireCharCode(Q),this._attemptCharCodeUntilFn(s),this._beginToken(w.RAW_TEXT,this._getLocation());var e=this._readUntil(Q);this._endToken([e],this._getLocation()),this._requireCharCode(Q),this._attemptCharCodeUntilFn(s),this.expansionCaseStack.push(w.EXPANSION_FORM_START)},t.prototype._consumeExpansionCaseStart=function(){this._requireCharCode(X),this._beginToken(w.EXPANSION_CASE_VALUE,this._getLocation());var t=this._readUntil(K).trim();this._endToken([t],this._getLocation()),this._attemptCharCodeUntilFn(s),this._beginToken(w.EXPANSION_CASE_EXP_START,this._getLocation()),this._requireCharCode(K),this._endToken([],this._getLocation()),this._attemptCharCodeUntilFn(s),this.expansionCaseStack.push(w.EXPANSION_CASE_EXP_START)},t.prototype._consumeExpansionCaseEnd=function(){this._beginToken(w.EXPANSION_CASE_EXP_END,this._getLocation()),this._requireCharCode($),this._endToken([],this._getLocation()),this._attemptCharCodeUntilFn(s),this.expansionCaseStack.pop()},t.prototype._consumeExpansionFormEnd=function(){this._beginToken(w.EXPANSION_FORM_END,this._getLocation()),this._requireCharCode($),this._endToken([]),this.expansionCaseStack.pop()},t.prototype._consumeText=function(){var t=this._getLocation();this._beginToken(w.TEXT,t);var e=[],n=!1;for(this.peek===K&&this.nextPeek===K?(e.push(this._readChar(!0)),e.push(this._readChar(!0)),n=!0):e.push(this._readChar(!0));!this.isTextEnd(n);)this.peek===K&&this.nextPeek===K?(e.push(this._readChar(!0)),e.push(this._readChar(!0)),n=!0):this.peek===$&&this.nextPeek===$&&n?(e.push(this._readChar(!0)),e.push(this._readChar(!0)),n=!1):e.push(this._readChar(!0));this._endToken([this._processCarriageReturns(e.join(""))])},t.prototype.isTextEnd=function(t){if(this.peek===H||this.peek===O)return!0;if(this.tokenizeExpansionForms){if(h(this.peek,this.nextPeek))return!0;if(this.peek===$&&!t&&(this.isInExpansionCase()||this.isInExpansionForm()))return!0}return!1},t.prototype._savePosition=function(){return[this.peek,this.index,this.column,this.line,this.tokens.length]},t.prototype._readUntil=function(t){var e=this.index;return this._attemptUntilChar(t),this.input.substring(e,this.index)},t.prototype._restorePosition=function(t){this.peek=t[0],this.index=t[1],this.column=t[2],this.line=t[3];var e=t[4];e0&&this.expansionCaseStack[this.expansionCaseStack.length-1]===w.EXPANSION_CASE_EXP_START},t.prototype.isInExpansionForm=function(){return this.expansionCaseStack.length>0&&this.expansionCaseStack[this.expansionCaseStack.length-1]===w.EXPANSION_FORM_START},t}()},function(t,e){"use strict";var n=function(){function t(t,e,n,r){this.file=t,this.offset=e,this.line=n,this.col=r}return t.prototype.toString=function(){return this.file.url+"@"+this.line+":"+this.col},t}();e.ParseLocation=n;var r=function(){function t(t,e){this.content=t,this.url=e}return t}();e.ParseSourceFile=r;var i=function(){function t(t,e){this.start=t,this.end=e}return t.prototype.toString=function(){return this.start.file.content.substring(this.start.offset,this.end.offset)},t}();e.ParseSourceSpan=i;var o=function(){function t(t,e){this.span=t,this.msg=e}return t.prototype.toString=function(){var t=this.span.start.file.content,e=this.span.start.offset;e>t.length-1&&(e=t.length-1);for(var n=e,r=0,i=0;100>r&&e>0&&(e--,r++,"\n"!=t[e]||3!=++i););for(r=0,i=0;100>r&&n]"+t.substring(this.span.start.offset,n+1);return this.msg+' ("'+o+'"): '+this.span.start},t}();e.ParseError=o},function(t,e,n){"use strict";function r(t){var e=p[t.toLowerCase()];return a.isPresent(e)?e:l}function i(t){if("@"!=t[0])return[null,t];var e=a.RegExpWrapper.firstMatch(h,t);return[e[1],e[2]]}function o(t){return i(t)[0]}function s(t,e){return a.isPresent(t)?"@"+t+":"+e:e}var a=n(5);e.NAMED_ENTITIES=a.CONST_EXPR({Aacute:"Á",aacute:"á",Acirc:"Â",acirc:"â",acute:"´",AElig:"Æ",aelig:"æ",Agrave:"À",agrave:"à",alefsym:"ℵ",Alpha:"Α",alpha:"α",amp:"&",and:"∧",ang:"∠",apos:"'",Aring:"Å",aring:"å",asymp:"≈",Atilde:"Ã",atilde:"ã",Auml:"Ä",auml:"ä",bdquo:"„",Beta:"Β",beta:"β",brvbar:"¦",bull:"•",cap:"∩",Ccedil:"Ç",ccedil:"ç",cedil:"¸",cent:"¢",Chi:"Χ",chi:"χ",circ:"ˆ",clubs:"♣",cong:"≅",copy:"©",crarr:"↵",cup:"∪",curren:"¤",dagger:"†",Dagger:"‡",darr:"↓",dArr:"⇓",deg:"°",Delta:"Δ",delta:"δ",diams:"♦",divide:"÷",Eacute:"É",eacute:"é",Ecirc:"Ê",ecirc:"ê",Egrave:"È",egrave:"è",empty:"∅",emsp:" ",ensp:" ",Epsilon:"Ε",epsilon:"ε",equiv:"≡",Eta:"Η",eta:"η",ETH:"Ð",eth:"ð",Euml:"Ë",euml:"ë",euro:"€",exist:"∃",fnof:"ƒ",forall:"∀",frac12:"½",frac14:"¼",frac34:"¾",frasl:"⁄",Gamma:"Γ",gamma:"γ",ge:"≥",gt:">",harr:"↔",hArr:"⇔",hearts:"♥",hellip:"…",Iacute:"Í",iacute:"í",Icirc:"Î",icirc:"î",iexcl:"¡",Igrave:"Ì",igrave:"ì",image:"ℑ",infin:"∞","int":"∫",Iota:"Ι",iota:"ι",iquest:"¿",isin:"∈",Iuml:"Ï",iuml:"ï",Kappa:"Κ",kappa:"κ",Lambda:"Λ",lambda:"λ",lang:"⟨",laquo:"«",larr:"←",lArr:"⇐",lceil:"⌈",ldquo:"“",le:"≤",lfloor:"⌊",lowast:"∗",loz:"◊",lrm:"‎",lsaquo:"‹",lsquo:"‘",lt:"<",macr:"¯",mdash:"—",micro:"µ",middot:"·",minus:"−",Mu:"Μ",mu:"μ",nabla:"∇",nbsp:" ",ndash:"–",ne:"≠",ni:"∋",not:"¬",notin:"∉",nsub:"⊄",Ntilde:"Ñ",ntilde:"ñ",Nu:"Ν",nu:"ν",Oacute:"Ó",oacute:"ó",Ocirc:"Ô",ocirc:"ô",OElig:"Œ",oelig:"œ",Ograve:"Ò",ograve:"ò",oline:"‾",Omega:"Ω",omega:"ω",Omicron:"Ο",omicron:"ο",oplus:"⊕",or:"∨",ordf:"ª",ordm:"º",Oslash:"Ø",oslash:"ø",Otilde:"Õ",otilde:"õ",otimes:"⊗",Ouml:"Ö",ouml:"ö",para:"¶",permil:"‰",perp:"⊥",Phi:"Φ",phi:"φ",Pi:"Π",pi:"π",piv:"ϖ",plusmn:"±",pound:"£",prime:"′",Prime:"″",prod:"∏",prop:"∝",Psi:"Ψ",psi:"ψ",quot:'"',radic:"√",rang:"⟩",raquo:"»",rarr:"→",rArr:"⇒",rceil:"⌉",rdquo:"”",real:"ℜ",reg:"®",rfloor:"⌋",Rho:"Ρ",rho:"ρ",rlm:"‏",rsaquo:"›",rsquo:"’",sbquo:"‚",Scaron:"Š",scaron:"š",sdot:"⋅",sect:"§",shy:"­",Sigma:"Σ",sigma:"σ",sigmaf:"ς",sim:"∼",spades:"♠",sub:"⊂",sube:"⊆",sum:"∑",sup:"⊃",sup1:"¹",sup2:"²",sup3:"³",supe:"⊇",szlig:"ß",Tau:"Τ",tau:"τ",there4:"∴",Theta:"Θ",theta:"θ",thetasym:"ϑ",thinsp:" ",THORN:"Þ",thorn:"þ",tilde:"˜",times:"×",trade:"™",Uacute:"Ú",uacute:"ú",uarr:"↑",uArr:"⇑",Ucirc:"Û",ucirc:"û",Ugrave:"Ù",ugrave:"ù",uml:"¨",upsih:"ϒ",Upsilon:"Υ",upsilon:"υ",Uuml:"Ü",uuml:"ü",weierp:"℘",Xi:"Ξ",xi:"ξ",Yacute:"Ý",yacute:"ý",yen:"¥",yuml:"ÿ",Yuml:"Ÿ",Zeta:"Ζ",zeta:"ζ",zwj:"‍",zwnj:"‌"}),function(t){t[t.RAW_TEXT=0]="RAW_TEXT",t[t.ESCAPABLE_RAW_TEXT=1]="ESCAPABLE_RAW_TEXT",t[t.PARSABLE_DATA=2]="PARSABLE_DATA"}(e.HtmlTagContentType||(e.HtmlTagContentType={}));var u=e.HtmlTagContentType,c=function(){function t(t){var e=this,n=void 0===t?{}:t,r=n.closedByChildren,i=n.requiredParents,o=n.implicitNamespacePrefix,s=n.contentType,c=n.closedByParent,p=n.isVoid,l=n.ignoreFirstLf;this.closedByChildren={},this.closedByParent=!1,a.isPresent(r)&&r.length>0&&r.forEach(function(t){return e.closedByChildren[t]=!0}),this.isVoid=a.normalizeBool(p),this.closedByParent=a.normalizeBool(c)||this.isVoid,a.isPresent(i)&&i.length>0&&(this.requiredParents={},this.parentToAdd=i[0],i.forEach(function(t){return e.requiredParents[t]=!0})),this.implicitNamespacePrefix=o,this.contentType=a.isPresent(s)?s:u.PARSABLE_DATA,this.ignoreFirstLf=a.normalizeBool(l)}return t.prototype.requireExtraParent=function(t){if(a.isBlank(this.requiredParents))return!1;if(a.isBlank(t))return!0;var e=t.toLowerCase();return 1!=this.requiredParents[e]&&"template"!=e},t.prototype.isClosedByChild=function(t){return this.isVoid||a.normalizeBool(this.closedByChildren[t.toLowerCase()])},t}();e.HtmlTagDefinition=c;var p={base:new c({isVoid:!0}),meta:new c({isVoid:!0}),area:new c({isVoid:!0}),embed:new c({isVoid:!0}),link:new c({isVoid:!0}),img:new c({isVoid:!0}),input:new c({isVoid:!0}),param:new c({isVoid:!0}),hr:new c({isVoid:!0}),br:new c({isVoid:!0}),source:new c({isVoid:!0}),track:new c({isVoid:!0}),wbr:new c({isVoid:!0}),p:new c({closedByChildren:["address","article","aside","blockquote","div","dl","fieldset","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","hr","main","nav","ol","p","pre","section","table","ul"],closedByParent:!0}),thead:new c({closedByChildren:["tbody","tfoot"]}),tbody:new c({closedByChildren:["tbody","tfoot"],closedByParent:!0}),tfoot:new c({closedByChildren:["tbody"],closedByParent:!0}),tr:new c({closedByChildren:["tr"],requiredParents:["tbody","tfoot","thead"],closedByParent:!0}),td:new c({closedByChildren:["td","th"],closedByParent:!0}),th:new c({closedByChildren:["td","th"],closedByParent:!0}),col:new c({requiredParents:["colgroup"],isVoid:!0}),svg:new c({implicitNamespacePrefix:"svg"}),math:new c({implicitNamespacePrefix:"math"}),li:new c({closedByChildren:["li"],closedByParent:!0}),dt:new c({closedByChildren:["dt","dd"]}),dd:new c({closedByChildren:["dt","dd"],closedByParent:!0}),rb:new c({closedByChildren:["rb","rt","rtc","rp"],closedByParent:!0}),rt:new c({closedByChildren:["rb","rt","rtc","rp"],closedByParent:!0}),rtc:new c({closedByChildren:["rb","rtc","rp"],closedByParent:!0}),rp:new c({closedByChildren:["rb","rt","rtc","rp"],closedByParent:!0}),optgroup:new c({closedByChildren:["optgroup"],closedByParent:!0}),option:new c({closedByChildren:["option","optgroup"],closedByParent:!0}),pre:new c({ignoreFirstLf:!0}),listing:new c({ignoreFirstLf:!0}),style:new c({contentType:u.RAW_TEXT}),script:new c({contentType:u.RAW_TEXT}),title:new c({contentType:u.ESCAPABLE_RAW_TEXT}),textarea:new c({contentType:u.ESCAPABLE_RAW_TEXT,ignoreFirstLf:!0})},l=new c;e.getHtmlTagDefinition=r;var h=/^@([^:]+):(.+)/g;e.splitNsName=i,e.getNsPrefix=o,e.mergeNsAndName=s},function(t,e,n){"use strict";var r=n(15),i=n(5),o=n(12),s="",a=i.RegExpWrapper.create("(\\:not\\()|([-\\w]+)|(?:\\.([-\\w]+))|(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])|(\\))|(\\s*,\\s*)"),u=function(){function t(){this.element=null,this.classNames=[],this.attrs=[],this.notSelectors=[]}return t.parse=function(e){for(var n,s=[],u=function(t,e){e.notSelectors.length>0&&i.isBlank(e.element)&&r.ListWrapper.isEmpty(e.classNames)&&r.ListWrapper.isEmpty(e.attrs)&&(e.element="*"),t.push(e)},c=new t,p=i.RegExpWrapper.matcher(a,e),l=c,h=!1;i.isPresent(n=i.RegExpMatcherWrapper.next(p));){if(i.isPresent(n[1])){if(h)throw new o.BaseException("Nesting :not is not allowed in a selector");h=!0,l=new t,c.notSelectors.push(l)}if(i.isPresent(n[2])&&l.setElement(n[2]),i.isPresent(n[3])&&l.addClassName(n[3]),i.isPresent(n[4])&&l.addAttribute(n[4],n[5]),i.isPresent(n[6])&&(h=!1,l=c),i.isPresent(n[7])){if(h)throw new o.BaseException("Multiple selectors in :not are not supported");u(s,c),c=l=new t}}return u(s,c),s},t.prototype.isElementSelector=function(){return i.isPresent(this.element)&&r.ListWrapper.isEmpty(this.classNames)&&r.ListWrapper.isEmpty(this.attrs)&&0===this.notSelectors.length},t.prototype.setElement=function(t){void 0===t&&(t=null),this.element=t},t.prototype.getMatchingElementTemplate=function(){for(var t=i.isPresent(this.element)?this.element:"div",e=this.classNames.length>0?' class="'+this.classNames.join(" ")+'"':"",n="",r=0;r"},t.prototype.addAttribute=function(t,e){void 0===e&&(e=s),this.attrs.push(t),e=i.isPresent(e)?e.toLowerCase():s,this.attrs.push(e)},t.prototype.addClassName=function(t){this.classNames.push(t.toLowerCase())},t.prototype.toString=function(){var t="";if(i.isPresent(this.element)&&(t+=this.element),i.isPresent(this.classNames))for(var e=0;e0&&(t+="="+r),t+="]"}return this.notSelectors.forEach(function(e){return t+=":not("+e+")"}),t},t}();e.CssSelector=u;var c=function(){function t(){this._elementMap=new r.Map,this._elementPartialMap=new r.Map,this._classMap=new r.Map,this._classPartialMap=new r.Map,this._attrValueMap=new r.Map,this._attrValuePartialMap=new r.Map,this._listContexts=[]}return t.createNotMatcher=function(e){var n=new t;return n.addSelectables(e,null),n},t.prototype.addSelectables=function(t,e){var n=null;t.length>1&&(n=new p(t),this._listContexts.push(n));for(var r=0;r0&&(i.isBlank(this.listContext)||!this.listContext.alreadyMatched)){var r=c.createNotMatcher(this.notSelectors);n=!r.match(t,null)}return n&&i.isPresent(e)&&(i.isBlank(this.listContext)||!this.listContext.alreadyMatched)&&(i.isPresent(this.listContext)&&(this.listContext.alreadyMatched=!0),e(this.selector,this.cbContext)),n},t}();e.SelectorContext=l},function(t,e){"use strict";var n=function(){function t(){}return t.prototype.hasProperty=function(t,e){return!0},t.prototype.getMappedPropName=function(t){return t},t}();e.ElementSchemaRegistry=n},function(t,e,n){"use strict";function r(t){var e=null,n=null,r=null,o=!1,_=null;t.attrs.forEach(function(t){var i=t.name.toLowerCase();i==a?e=t.value:i==l?n=t.value:i==p?r=t.value:t.name==v?o=!0:t.name==y&&t.value.length>0&&(_=t.value)}),e=i(e);var b=t.name.toLowerCase(),P=m.OTHER;return s.splitNsName(b)[1]==u?P=m.NG_CONTENT:b==f?P=m.STYLE:b==d?P=m.SCRIPT:b==c&&r==h&&(P=m.STYLESHEET),new g(P,e,n,o,_)}function i(t){return o.isBlank(t)||0===t.length?"*":t}var o=n(5),s=n(148),a="select",u="ng-content",c="link",p="rel",l="href",h="stylesheet",f="style",d="script",v="ngNonBindable",y="ngProjectAs";e.preparseElement=r,function(t){t[t.NG_CONTENT=0]="NG_CONTENT",t[t.STYLE=1]="STYLE",t[t.STYLESHEET=2]="STYLESHEET",t[t.SCRIPT=3]="SCRIPT",t[t.OTHER=4]="OTHER"}(e.PreparsedElementType||(e.PreparsedElementType={}));var m=e.PreparsedElementType,g=function(){function t(t,e,n,r,i){this.type=t,this.selectAttr=e,this.hrefAttr=n,this.nonBindable=r,this.projectAs=i}return t}();e.PreparsedElement=g},function(t,e,n){"use strict";function r(t){if(o.isBlank(t)||0===t.length||"/"==t[0])return!1;var e=o.RegExpWrapper.firstMatch(u,t);return o.isBlank(e)||"package"==e[1]||"asset"==e[1]}function i(t,e,n){var i=[],u=o.StringWrapper.replaceAllMapped(n,a,function(n){var s=o.isPresent(n[1])?n[1]:n[2];return r(s)?(i.push(t.resolve(e,s)),""):n[0]});return new s(u,i)}var o=n(5),s=function(){function t(t,e){this.style=t,this.styleUrls=e}return t}();e.StyleWithImports=s,e.isStyleUrlResolvable=r,e.extractStyleUrls=i;var a=/@import\s+(?:url\()?\s*(?:(?:['"]([^'"]*))|([^;\)\s]*))[^;]*;?/g,u=/^([a-zA-Z\-\+\.]+):/g},function(t,e,n){"use strict";function r(t){return a.StringWrapper.replaceAllMapped(t,u,function(t){return"-"+t[1].toLowerCase()})}function i(t){return a.StringWrapper.replaceAllMapped(t,c,function(t){return t[1].toUpperCase()})}function o(t,e){var n=a.StringWrapper.split(t.trim(),/\s*:\s*/g);return n.length>1?n:e}function s(t){return a.StringWrapper.replaceAll(t,/\W/g,"_")}var a=n(5);e.MODULE_SUFFIX=a.IS_DART?".dart":"";var u=/([A-Z])/g,c=/-([a-z])/g;e.camelCaseToDashCase=r,e.dashCaseToCamelCase=i,e.splitAtColon=o,e.sanitizeIdentifier=s},function(t,e,n){"use strict";function r(t,e){var n=e.useExisting,r=e.useValue,i=e.deps;return new v.CompileProviderMetadata({token:t.token,useClass:t.useClass,useExisting:n,useFactory:t.useFactory,useValue:r,deps:i,multi:t.multi})}function i(t,e){var n=e.eager,r=e.providers;return new d.ProviderAst(t.token,t.multiProvider,t.eager||n,r,t.providerType,t.sourceSpan)}function o(t,e,n,r){return void 0===r&&(r=null),h.isBlank(r)&&(r=[]),h.isPresent(t)&&t.forEach(function(t){if(h.isArray(t))o(t,e,n,r);else{var i;t instanceof v.CompileProviderMetadata?i=t:t instanceof v.CompileTypeMetadata?i=new v.CompileProviderMetadata({token:new v.CompileTokenMetadata({identifier:t}),useClass:t}):n.push(new g("Unknown provider type "+t,e)),h.isPresent(i)&&r.push(i)}}),r}function s(t,e,n){var r=new v.CompileTokenMap;t.forEach(function(t){var i=new v.CompileProviderMetadata({token:new v.CompileTokenMetadata({identifier:t.type}),useClass:t.type});a([i],t.isComponent?d.ProviderAstType.Component:d.ProviderAstType.Directive,!0,e,n,r)});var i=t.filter(function(t){return t.isComponent}).concat(t.filter(function(t){return!t.isComponent}));return i.forEach(function(t){a(o(t.providers,e,n),d.ProviderAstType.PublicService,!1,e,n,r),a(o(t.viewProviders,e,n),d.ProviderAstType.PrivateService,!1,e,n,r)}),r}function a(t,e,n,r,i,o){t.forEach(function(t){var s=o.get(t.token);h.isPresent(s)&&s.multiProvider!==t.multi&&i.push(new g("Mixing multi and non multi provider is not possible for token "+s.token.name,r)),h.isBlank(s)?(s=new d.ProviderAst(t.token,t.multi,n,[t],e,r),o.add(t.token,s)):(t.multi||f.ListWrapper.clear(s.providers),s.providers.push(t))})}function u(t){var e=new v.CompileTokenMap;return h.isPresent(t.viewQueries)&&t.viewQueries.forEach(function(t){return p(e,t)}),t.type.diDeps.forEach(function(t){h.isPresent(t.viewQuery)&&p(e,t.viewQuery)}),e}function c(t){var e=new v.CompileTokenMap;return t.forEach(function(t){h.isPresent(t.queries)&&t.queries.forEach(function(t){return p(e,t)}),t.type.diDeps.forEach(function(t){h.isPresent(t.query)&&p(e,t.query)})}),e}function p(t,e){e.selectors.forEach(function(n){var r=t.get(n);h.isBlank(r)&&(r=[],t.add(n,r)),r.push(e)})}var l=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},h=n(5),f=n(15),d=n(139),v=n(155),y=n(158),m=n(147),g=function(t){function e(e,n){t.call(this,n,e)}return l(e,t),e}(m.ParseError);e.ProviderError=g;var _=function(){function t(t,e){var n=this;this.component=t,this.sourceSpan=e,this.errors=[],this.viewQueries=u(t),this.viewProviders=new v.CompileTokenMap,o(t.viewProviders,e,this.errors).forEach(function(t){h.isBlank(n.viewProviders.get(t.token))&&n.viewProviders.add(t.token,!0)})}return t}();e.ProviderViewContext=_;var b=function(){function t(t,e,n,r,i,o,a){var u=this;this._viewContext=t,this._parent=e,this._isViewRoot=n,this._directiveAsts=r,this._sourceSpan=a,this._transformedProviders=new v.CompileTokenMap,this._seenProviders=new v.CompileTokenMap,this._hasViewContainer=!1,this._attrs={},i.forEach(function(t){return u._attrs[t.name]=t.value});var p=r.map(function(t){return t.directive});this._allProviders=s(p,a,t.errors),this._contentQueries=c(p);var l=new v.CompileTokenMap;this._allProviders.values().forEach(function(t){u._addQueryReadsTo(t.token,l)}),o.forEach(function(t){var e=new v.CompileTokenMetadata({value:t.name});u._addQueryReadsTo(e,l)}),h.isPresent(l.get(y.identifierToken(y.Identifiers.ViewContainerRef)))&&(this._hasViewContainer=!0),this._allProviders.values().forEach(function(t){var e=t.eager||h.isPresent(l.get(t.token));e&&u._getOrCreateLocalProvider(t.providerType,t.token,!0)})}return t.prototype.afterElement=function(){var t=this;this._allProviders.values().forEach(function(e){t._getOrCreateLocalProvider(e.providerType,e.token,!1)})},Object.defineProperty(t.prototype,"transformProviders",{get:function(){return this._transformedProviders.values()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"transformedDirectiveAsts",{get:function(){var t=this._transformedProviders.values().map(function(t){return t.token.identifier}),e=f.ListWrapper.clone(this._directiveAsts);return f.ListWrapper.sort(e,function(e,n){return t.indexOf(e.directive.type)-t.indexOf(n.directive.type)}),e},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"transformedHasViewContainer",{get:function(){return this._hasViewContainer},enumerable:!0,configurable:!0}),t.prototype._addQueryReadsTo=function(t,e){this._getQueriesFor(t).forEach(function(n){var r=h.isPresent(n.read)?n.read:t;h.isBlank(e.get(r))&&e.add(r,!0)})},t.prototype._getQueriesFor=function(t){for(var e,n=[],r=this,i=0;null!==r;)e=r._contentQueries.get(t),h.isPresent(e)&&f.ListWrapper.addAll(n,e.filter(function(t){return t.descendants||1>=i})),r._directiveAsts.length>0&&i++,r=r._parent;return e=this._viewContext.viewQueries.get(t),h.isPresent(e)&&f.ListWrapper.addAll(n,e),n},t.prototype._getOrCreateLocalProvider=function(t,e,n){var o=this,s=this._allProviders.get(e);if(h.isBlank(s)||(t===d.ProviderAstType.Directive||t===d.ProviderAstType.PublicService)&&s.providerType===d.ProviderAstType.PrivateService||(t===d.ProviderAstType.PrivateService||t===d.ProviderAstType.PublicService)&&s.providerType===d.ProviderAstType.Builtin)return null;var a=this._transformedProviders.get(e);if(h.isPresent(a))return a;if(h.isPresent(this._seenProviders.get(e)))return this._viewContext.errors.push(new g("Cannot instantiate cyclic dependency! "+e.name,this._sourceSpan)),null;this._seenProviders.add(e,!0);var u=s.providers.map(function(t){var e,i=t.useValue,a=t.useExisting;if(h.isPresent(t.useExisting)){var u=o._getDependency(s.providerType,new v.CompileDiDependencyMetadata({token:t.useExisting}),n);h.isPresent(u.token)?a=u.token:(a=null,i=u.value)}else if(h.isPresent(t.useFactory)){var c=h.isPresent(t.deps)?t.deps:t.useFactory.diDeps;e=c.map(function(t){return o._getDependency(s.providerType,t,n)})}else if(h.isPresent(t.useClass)){var c=h.isPresent(t.deps)?t.deps:t.useClass.diDeps;e=c.map(function(t){return o._getDependency(s.providerType,t,n)})}return r(t,{useExisting:a,useValue:i,deps:e})});return a=i(s,{eager:n,providers:u}),this._transformedProviders.add(e,a),a},t.prototype._getLocalDependency=function(t,e,n){if(void 0===n&&(n=null),e.isAttribute){var r=this._attrs[e.token.value];return new v.CompileDiDependencyMetadata({isValue:!0,value:h.normalizeBlank(r)})}if(h.isPresent(e.query)||h.isPresent(e.viewQuery))return e;if(h.isPresent(e.token)){if(t===d.ProviderAstType.Directive||t===d.ProviderAstType.Component){if(e.token.equalsTo(y.identifierToken(y.Identifiers.Renderer))||e.token.equalsTo(y.identifierToken(y.Identifiers.ElementRef))||e.token.equalsTo(y.identifierToken(y.Identifiers.ChangeDetectorRef))||e.token.equalsTo(y.identifierToken(y.Identifiers.TemplateRef)))return e;e.token.equalsTo(y.identifierToken(y.Identifiers.ViewContainerRef))&&(this._hasViewContainer=!0)}if(e.token.equalsTo(y.identifierToken(y.Identifiers.Injector)))return e;if(h.isPresent(this._getOrCreateLocalProvider(t,e.token,n)))return e}return null},t.prototype._getDependency=function(t,e,n){void 0===n&&(n=null);var r=this,i=n,o=null;if(e.isSkipSelf||(o=this._getLocalDependency(t,e,n)),e.isSelf)h.isBlank(o)&&e.isOptional&&(o=new v.CompileDiDependencyMetadata({isValue:!0,value:null}));else{for(;h.isBlank(o)&&h.isPresent(r._parent);){var s=r;r=r._parent,s._isViewRoot&&(i=!1),o=r._getLocalDependency(d.ProviderAstType.PublicService,e,i)}h.isBlank(o)&&(o=!e.isHost||this._viewContext.component.type.isHost||y.identifierToken(this._viewContext.component.type).equalsTo(e.token)||h.isPresent(this._viewContext.viewProviders.get(e.token))?e:e.isOptional?o=new v.CompileDiDependencyMetadata({ -isValue:!0,value:null}):null)}return h.isBlank(o)&&this._viewContext.errors.push(new g("No provider for "+e.token.name,this._sourceSpan)),o},t}();e.ProviderElementContext=b},function(t,e,n){"use strict";function r(t){return N[t["class"]](t)}function i(t,e){var n=y.CssSelector.parse(e)[0].getMatchingElementTemplate();return M.create({type:new x({runtime:Object,name:t.name+"_Host",moduleUrl:t.moduleUrl,isHost:!0}),template:new I({template:n,templateUrl:"",styles:[],styleUrls:[],ngContentSelectors:[]}),changeDetection:d.ChangeDetectionStrategy.Default,inputs:[],outputs:[],host:{},lifecycleHooks:[],isComponent:!0,selector:"*",providers:[],viewProviders:[],queries:[],viewQueries:[]})}function o(t,e){return l.isBlank(t)?null:t.map(function(t){return a(t,e)})}function s(t){return l.isBlank(t)?null:t.map(u)}function a(t,e){return l.isArray(t)?o(t,e):l.isString(t)||l.isBlank(t)||l.isBoolean(t)||l.isNumber(t)?t:e(t)}function u(t){return l.isArray(t)?s(t):l.isString(t)||l.isBlank(t)||l.isBoolean(t)||l.isNumber(t)?t:t.toJson()}function c(t){return l.isPresent(t)?t:[]}var p=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},l=n(5),h=n(12),f=n(15),d=n(28),v=n(36),y=n(149),m=n(153),g=n(156),_=n(157),b=/^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))$/g,P=function(){function t(){}return Object.defineProperty(t.prototype,"identifier",{get:function(){return h.unimplemented()},enumerable:!0,configurable:!0}),t}();e.CompileMetadataWithIdentifier=P;var E=function(t){function e(){t.apply(this,arguments)}return p(e,t),Object.defineProperty(e.prototype,"type",{get:function(){return h.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"identifier",{get:function(){return h.unimplemented()},enumerable:!0,configurable:!0}),e}(P);e.CompileMetadataWithType=E,e.metadataFromJson=r;var w=function(){function t(t){var e=void 0===t?{}:t,n=e.runtime,r=e.name,i=e.moduleUrl,o=e.prefix,s=e.value;this.runtime=n,this.name=r,this.prefix=o,this.moduleUrl=i,this.value=s}return t.fromJson=function(e){var n=l.isArray(e.value)?o(e.value,r):a(e.value,r);return new t({name:e.name,prefix:e.prefix,moduleUrl:e.moduleUrl,value:n})},t.prototype.toJson=function(){var t=l.isArray(this.value)?s(this.value):u(this.value);return{"class":"Identifier",name:this.name,moduleUrl:this.moduleUrl,prefix:this.prefix,value:t}},Object.defineProperty(t.prototype,"identifier",{get:function(){return this},enumerable:!0,configurable:!0}),t}();e.CompileIdentifierMetadata=w;var C=function(){function t(t){var e=void 0===t?{}:t,n=e.isAttribute,r=e.isSelf,i=e.isHost,o=e.isSkipSelf,s=e.isOptional,a=e.isValue,u=e.query,c=e.viewQuery,p=e.token,h=e.value;this.isAttribute=l.normalizeBool(n),this.isSelf=l.normalizeBool(r),this.isHost=l.normalizeBool(i),this.isSkipSelf=l.normalizeBool(o),this.isOptional=l.normalizeBool(s),this.isValue=l.normalizeBool(a),this.query=u,this.viewQuery=c,this.token=p,this.value=h}return t.fromJson=function(e){return new t({token:a(e.token,O.fromJson),query:a(e.query,A.fromJson),viewQuery:a(e.viewQuery,A.fromJson),value:e.value,isAttribute:e.isAttribute,isSelf:e.isSelf,isHost:e.isHost,isSkipSelf:e.isSkipSelf,isOptional:e.isOptional,isValue:e.isValue})},t.prototype.toJson=function(){return{token:u(this.token),query:u(this.query),viewQuery:u(this.viewQuery),value:this.value,isAttribute:this.isAttribute,isSelf:this.isSelf,isHost:this.isHost,isSkipSelf:this.isSkipSelf,isOptional:this.isOptional,isValue:this.isValue}},t}();e.CompileDiDependencyMetadata=C;var R=function(){function t(t){var e=t.token,n=t.useClass,r=t.useValue,i=t.useExisting,o=t.useFactory,s=t.deps,a=t.multi;this.token=e,this.useClass=n,this.useValue=r,this.useExisting=i,this.useFactory=o,this.deps=l.normalizeBlank(s),this.multi=l.normalizeBool(a)}return t.fromJson=function(e){return new t({token:a(e.token,O.fromJson),useClass:a(e.useClass,x.fromJson),useExisting:a(e.useExisting,O.fromJson),useValue:a(e.useValue,w.fromJson),useFactory:a(e.useFactory,S.fromJson),multi:e.multi,deps:o(e.deps,C.fromJson)})},t.prototype.toJson=function(){return{"class":"Provider",token:u(this.token),useClass:u(this.useClass),useExisting:u(this.useExisting),useValue:u(this.useValue),useFactory:u(this.useFactory),multi:this.multi,deps:s(this.deps)}},t}();e.CompileProviderMetadata=R;var S=function(){function t(t){var e=t.runtime,n=t.name,r=t.moduleUrl,i=t.prefix,o=t.diDeps,s=t.value;this.runtime=e,this.name=n,this.prefix=i,this.moduleUrl=r,this.diDeps=c(o),this.value=s}return Object.defineProperty(t.prototype,"identifier",{get:function(){return this},enumerable:!0,configurable:!0}),t.fromJson=function(e){return new t({name:e.name,prefix:e.prefix,moduleUrl:e.moduleUrl,value:e.value,diDeps:o(e.diDeps,C.fromJson)})},t.prototype.toJson=function(){return{"class":"Factory",name:this.name,prefix:this.prefix,moduleUrl:this.moduleUrl,value:this.value,diDeps:s(this.diDeps)}},t}();e.CompileFactoryMetadata=S;var O=function(){function t(t){var e=t.value,n=t.identifier,r=t.identifierIsInstance;this.value=e,this.identifier=n,this.identifierIsInstance=l.normalizeBool(r)}return t.fromJson=function(e){return new t({value:e.value,identifier:a(e.identifier,w.fromJson),identifierIsInstance:e.identifierIsInstance})},t.prototype.toJson=function(){return{value:this.value,identifier:u(this.identifier),identifierIsInstance:this.identifierIsInstance}},Object.defineProperty(t.prototype,"runtimeCacheKey",{get:function(){return l.isPresent(this.identifier)?this.identifier.runtime:this.value},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"assetCacheKey",{get:function(){return l.isPresent(this.identifier)?l.isPresent(this.identifier.moduleUrl)&&l.isPresent(_.getUrlScheme(this.identifier.moduleUrl))?this.identifier.name+"|"+this.identifier.moduleUrl+"|"+this.identifierIsInstance:null:this.value},enumerable:!0,configurable:!0}),t.prototype.equalsTo=function(t){var e=this.runtimeCacheKey,n=this.assetCacheKey;return l.isPresent(e)&&e==t.runtimeCacheKey||l.isPresent(n)&&n==t.assetCacheKey},Object.defineProperty(t.prototype,"name",{get:function(){return l.isPresent(this.value)?m.sanitizeIdentifier(this.value):this.identifier.name},enumerable:!0,configurable:!0}),t}();e.CompileTokenMetadata=O;var T=function(){function t(){this._valueMap=new Map,this._values=[]}return t.prototype.add=function(t,e){var n=this.get(t);if(l.isPresent(n))throw new h.BaseException("Can only add to a TokenMap! Token: "+t.name);this._values.push(e);var r=t.runtimeCacheKey;l.isPresent(r)&&this._valueMap.set(r,e);var i=t.assetCacheKey;l.isPresent(i)&&this._valueMap.set(i,e)},t.prototype.get=function(t){var e,n=t.runtimeCacheKey,r=t.assetCacheKey;return l.isPresent(n)&&(e=this._valueMap.get(n)),l.isBlank(e)&&l.isPresent(r)&&(e=this._valueMap.get(r)),e},t.prototype.values=function(){return this._values},Object.defineProperty(t.prototype,"size",{get:function(){return this._values.length},enumerable:!0,configurable:!0}),t}();e.CompileTokenMap=T;var x=function(){function t(t){var e=void 0===t?{}:t,n=e.runtime,r=e.name,i=e.moduleUrl,o=e.prefix,s=e.isHost,a=e.value,u=e.diDeps;this.runtime=n,this.name=r,this.moduleUrl=i,this.prefix=o,this.isHost=l.normalizeBool(s),this.value=a,this.diDeps=c(u)}return t.fromJson=function(e){return new t({name:e.name,moduleUrl:e.moduleUrl,prefix:e.prefix,isHost:e.isHost,value:e.value,diDeps:o(e.diDeps,C.fromJson)})},Object.defineProperty(t.prototype,"identifier",{get:function(){return this},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"type",{get:function(){return this},enumerable:!0,configurable:!0}),t.prototype.toJson=function(){return{"class":"Type",name:this.name,moduleUrl:this.moduleUrl,prefix:this.prefix,isHost:this.isHost,value:this.value,diDeps:s(this.diDeps)}},t}();e.CompileTypeMetadata=x;var A=function(){function t(t){var e=void 0===t?{}:t,n=e.selectors,r=e.descendants,i=e.first,o=e.propertyName,s=e.read;this.selectors=n,this.descendants=l.normalizeBool(r),this.first=l.normalizeBool(i),this.propertyName=o,this.read=s}return t.fromJson=function(e){return new t({selectors:o(e.selectors,O.fromJson),descendants:e.descendants,first:e.first,propertyName:e.propertyName,read:a(e.read,O.fromJson)})},t.prototype.toJson=function(){return{selectors:s(this.selectors),descendants:this.descendants,first:this.first,propertyName:this.propertyName,read:u(this.read)}},t}();e.CompileQueryMetadata=A;var I=function(){function t(t){var e=void 0===t?{}:t,n=e.encapsulation,r=e.template,i=e.templateUrl,o=e.styles,s=e.styleUrls,a=e.ngContentSelectors;this.encapsulation=l.isPresent(n)?n:v.ViewEncapsulation.Emulated,this.template=r,this.templateUrl=i,this.styles=l.isPresent(o)?o:[],this.styleUrls=l.isPresent(s)?s:[],this.ngContentSelectors=l.isPresent(a)?a:[]}return t.fromJson=function(e){return new t({encapsulation:l.isPresent(e.encapsulation)?v.VIEW_ENCAPSULATION_VALUES[e.encapsulation]:e.encapsulation,template:e.template,templateUrl:e.templateUrl,styles:e.styles,styleUrls:e.styleUrls,ngContentSelectors:e.ngContentSelectors})},t.prototype.toJson=function(){return{encapsulation:l.isPresent(this.encapsulation)?l.serializeEnum(this.encapsulation):this.encapsulation,template:this.template,templateUrl:this.templateUrl,styles:this.styles,styleUrls:this.styleUrls,ngContentSelectors:this.ngContentSelectors}},t}();e.CompileTemplateMetadata=I;var M=function(){function t(t){var e=void 0===t?{}:t,n=e.type,r=e.isComponent,i=e.selector,o=e.exportAs,s=e.changeDetection,a=e.inputs,u=e.outputs,p=e.hostListeners,l=e.hostProperties,h=e.hostAttributes,f=e.lifecycleHooks,d=e.providers,v=e.viewProviders,y=e.queries,m=e.viewQueries,g=e.template;this.type=n,this.isComponent=r,this.selector=i,this.exportAs=o,this.changeDetection=s,this.inputs=a,this.outputs=u,this.hostListeners=p,this.hostProperties=l,this.hostAttributes=h,this.lifecycleHooks=c(f),this.providers=c(d),this.viewProviders=c(v),this.queries=c(y),this.viewQueries=c(m),this.template=g}return t.create=function(e){var n=void 0===e?{}:e,r=n.type,i=n.isComponent,o=n.selector,s=n.exportAs,a=n.changeDetection,u=n.inputs,c=n.outputs,p=n.host,h=n.lifecycleHooks,d=n.providers,v=n.viewProviders,y=n.queries,g=n.viewQueries,_=n.template,P={},E={},w={};l.isPresent(p)&&f.StringMapWrapper.forEach(p,function(t,e){var n=l.RegExpWrapper.firstMatch(b,e);l.isBlank(n)?w[e]=t:l.isPresent(n[1])?E[n[1]]=t:l.isPresent(n[2])&&(P[n[2]]=t)});var C={};l.isPresent(u)&&u.forEach(function(t){var e=m.splitAtColon(t,[t,t]);C[e[0]]=e[1]});var R={};return l.isPresent(c)&&c.forEach(function(t){var e=m.splitAtColon(t,[t,t]);R[e[0]]=e[1]}),new t({type:r,isComponent:l.normalizeBool(i),selector:o,exportAs:s,changeDetection:a,inputs:C,outputs:R,hostListeners:P,hostProperties:E,hostAttributes:w,lifecycleHooks:l.isPresent(h)?h:[],providers:d,viewProviders:v,queries:y,viewQueries:g,template:_})},Object.defineProperty(t.prototype,"identifier",{get:function(){return this.type},enumerable:!0,configurable:!0}),t.fromJson=function(e){return new t({isComponent:e.isComponent,selector:e.selector,exportAs:e.exportAs,type:l.isPresent(e.type)?x.fromJson(e.type):e.type,changeDetection:l.isPresent(e.changeDetection)?d.CHANGE_DETECTION_STRATEGY_VALUES[e.changeDetection]:e.changeDetection,inputs:e.inputs,outputs:e.outputs,hostListeners:e.hostListeners,hostProperties:e.hostProperties,hostAttributes:e.hostAttributes,lifecycleHooks:e.lifecycleHooks.map(function(t){return g.LIFECYCLE_HOOKS_VALUES[t]}),template:l.isPresent(e.template)?I.fromJson(e.template):e.template,providers:o(e.providers,r),viewProviders:o(e.viewProviders,r),queries:o(e.queries,A.fromJson),viewQueries:o(e.viewQueries,A.fromJson)})},t.prototype.toJson=function(){return{"class":"Directive",isComponent:this.isComponent,selector:this.selector,exportAs:this.exportAs,type:l.isPresent(this.type)?this.type.toJson():this.type,changeDetection:l.isPresent(this.changeDetection)?l.serializeEnum(this.changeDetection):this.changeDetection,inputs:this.inputs,outputs:this.outputs,hostListeners:this.hostListeners,hostProperties:this.hostProperties,hostAttributes:this.hostAttributes,lifecycleHooks:this.lifecycleHooks.map(function(t){return l.serializeEnum(t)}),template:l.isPresent(this.template)?this.template.toJson():this.template,providers:s(this.providers),viewProviders:s(this.viewProviders),queries:s(this.queries),viewQueries:s(this.viewQueries)}},t}();e.CompileDirectiveMetadata=M,e.createHostComponentMeta=i;var k=function(){function t(t){var e=void 0===t?{}:t,n=e.type,r=e.name,i=e.pure,o=e.lifecycleHooks;this.type=n,this.name=r,this.pure=l.normalizeBool(i),this.lifecycleHooks=c(o)}return Object.defineProperty(t.prototype,"identifier",{get:function(){return this.type},enumerable:!0,configurable:!0}),t.fromJson=function(e){return new t({type:l.isPresent(e.type)?x.fromJson(e.type):e.type,name:e.name,pure:e.pure})},t.prototype.toJson=function(){return{"class":"Pipe",type:l.isPresent(this.type)?this.type.toJson():null,name:this.name,pure:this.pure}},t}();e.CompilePipeMetadata=k;var N={Directive:M.fromJson,Pipe:k.fromJson,Type:x.fromJson,Provider:R.fromJson,Identifier:w.fromJson,Factory:S.fromJson}},function(t,e){"use strict";!function(t){t[t.OnInit=0]="OnInit",t[t.OnDestroy=1]="OnDestroy",t[t.DoCheck=2]="DoCheck",t[t.OnChanges=3]="OnChanges",t[t.AfterContentInit=4]="AfterContentInit",t[t.AfterContentChecked=5]="AfterContentChecked",t[t.AfterViewInit=6]="AfterViewInit",t[t.AfterViewChecked=7]="AfterViewChecked"}(e.LifecycleHooks||(e.LifecycleHooks={}));var n=e.LifecycleHooks;e.LIFECYCLE_HOOKS_VALUES=[n.OnInit,n.OnDestroy,n.DoCheck,n.OnChanges,n.AfterContentInit,n.AfterContentChecked,n.AfterViewInit,n.AfterViewChecked]},function(t,e,n){"use strict";function r(){return new _}function i(){return new _(g)}function o(t){var e=a(t);return e&&e[b.Scheme]||""}function s(t,e,n,r,i,o,s){var a=[];return v.isPresent(t)&&a.push(t+":"),v.isPresent(n)&&(a.push("//"),v.isPresent(e)&&a.push(e+"@"),a.push(n),v.isPresent(r)&&a.push(":"+r)),v.isPresent(i)&&a.push(i),v.isPresent(o)&&a.push("?"+o),v.isPresent(s)&&a.push("#"+s),a.join("")}function a(t){return v.RegExpWrapper.firstMatch(P,t)}function u(t){if("/"==t)return"/";for(var e="/"==t[0]?"/":"",n="/"===t[t.length-1]?"/":"",r=t.split("/"),i=[],o=0,s=0;s0?i.pop():o++;break;default:i.push(a)}}if(""==e){for(;o-- >0;)i.unshift("..");0===i.length&&i.push(".")}return e+i.join("/")+n}function c(t){var e=t[b.Path];return e=v.isBlank(e)?"":u(e),t[b.Path]=e,s(t[b.Scheme],t[b.UserInfo],t[b.Domain],t[b.Port],e,t[b.QueryData],t[b.Fragment])}function p(t,e){var n=a(encodeURI(e)),r=a(t);if(v.isPresent(n[b.Scheme]))return c(n);n[b.Scheme]=r[b.Scheme];for(var i=b.Scheme;i<=b.Port;i++)v.isBlank(n[i])&&(n[i]=r[i]);if("/"==n[b.Path][0])return c(n);var o=r[b.Path];v.isBlank(o)&&(o="/");var s=o.lastIndexOf("/");return o=o.substring(0,s+1)+n[b.Path],n[b.Path]=o,c(n)}var l=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},h=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},f=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},d=n(6),v=n(5),y=n(62),m=n(6),g="asset:";e.createUrlResolverWithoutPackagePrefix=r,e.createOfflineCompileUrlResolver=i,e.DEFAULT_PACKAGE_URL_PROVIDER=new m.Provider(y.PACKAGE_ROOT_URL,{useValue:"/"});var _=function(){function t(t){void 0===t&&(t=null),this._packagePrefix=t}return t.prototype.resolve=function(t,e){var n=e;v.isPresent(t)&&t.length>0&&(n=p(t,n));var r=a(n),i=this._packagePrefix;if(v.isPresent(i)&&v.isPresent(r)&&"package"==r[b.Scheme]){var o=r[b.Path];if(this._packagePrefix!==g)return i=v.StringWrapper.stripRight(i,"/"),o=v.StringWrapper.stripLeft(o,"/"),i+"/"+o;var s=o.split(/\//);n="asset:"+s[0]+"/lib/"+s.slice(1).join("/")}return n},t=l([d.Injectable(),f(0,d.Inject(y.PACKAGE_ROOT_URL)),h("design:paramtypes",[String])],t)}();e.UrlResolver=_,e.getUrlScheme=o;var b,P=v.RegExpWrapper.create("^(?:([^:/?#.]+):)?(?://(?:([^/?#]*)@)?([\\w\\d\\-\\u0100-\\uffff.%]*)(?::([0-9]+))?)?([^?#]+)?(?:\\?([^#]*))?(?:#(.*))?$");!function(t){t[t.Scheme=1]="Scheme",t[t.UserInfo=2]="UserInfo",t[t.Domain=3]="Domain",t[t.Port=4]="Port",t[t.Path=5]="Path",t[t.QueryData=6]="QueryData",t[t.Fragment=7]="Fragment"}(b||(b={}))},function(t,e,n){"use strict";function r(t){return new i.CompileTokenMetadata({identifier:t})}var i=n(155),o=n(159),s=n(160),a=n(66),u=n(28),c=n(67),p=n(69),l=n(70),h=n(74),f=n(36),d=n(68),v=n(78),y=n(11),m=n(81),g=n(153),_="asset:angular2/lib/src/core/linker/view"+g.MODULE_SUFFIX,b="asset:angular2/lib/src/core/linker/view_utils"+g.MODULE_SUFFIX,P="asset:angular2/lib/src/core/change_detection/change_detection"+g.MODULE_SUFFIX,E=a.ViewUtils,w=o.AppView,C=s.DebugContext,R=c.AppElement,S=p.ElementRef,O=l.ViewContainerRef,T=u.ChangeDetectorRef,x=h.RenderComponentType,A=v.QueryList,I=m.TemplateRef,M=m.TemplateRef_,k=u.ValueUnwrapper,N=y.Injector,D=f.ViewEncapsulation,V=d.ViewType,j=u.ChangeDetectionStrategy,L=s.StaticNodeDebugInfo,B=h.Renderer,F=u.SimpleChange,U=u.uninitialized,W=u.ChangeDetectorState,H=a.flattenNestedViewRenderNodes,X=u.devModeEqual,q=a.interpolate,G=a.checkBinding,z=a.castByValue,K=function(){function t(){}return t.ViewUtils=new i.CompileIdentifierMetadata({name:"ViewUtils",moduleUrl:"asset:angular2/lib/src/core/linker/view_utils"+g.MODULE_SUFFIX,runtime:E}),t.AppView=new i.CompileIdentifierMetadata({name:"AppView",moduleUrl:_,runtime:w}),t.AppElement=new i.CompileIdentifierMetadata({name:"AppElement",moduleUrl:"asset:angular2/lib/src/core/linker/element"+g.MODULE_SUFFIX,runtime:R}),t.ElementRef=new i.CompileIdentifierMetadata({name:"ElementRef",moduleUrl:"asset:angular2/lib/src/core/linker/element_ref"+g.MODULE_SUFFIX,runtime:S}),t.ViewContainerRef=new i.CompileIdentifierMetadata({name:"ViewContainerRef",moduleUrl:"asset:angular2/lib/src/core/linker/view_container_ref"+g.MODULE_SUFFIX,runtime:O}),t.ChangeDetectorRef=new i.CompileIdentifierMetadata({name:"ChangeDetectorRef",moduleUrl:"asset:angular2/lib/src/core/change_detection/change_detector_ref"+g.MODULE_SUFFIX,runtime:T}),t.RenderComponentType=new i.CompileIdentifierMetadata({name:"RenderComponentType",moduleUrl:"asset:angular2/lib/src/core/render/api"+g.MODULE_SUFFIX,runtime:x}),t.QueryList=new i.CompileIdentifierMetadata({name:"QueryList",moduleUrl:"asset:angular2/lib/src/core/linker/query_list"+g.MODULE_SUFFIX,runtime:A}),t.TemplateRef=new i.CompileIdentifierMetadata({name:"TemplateRef",moduleUrl:"asset:angular2/lib/src/core/linker/template_ref"+g.MODULE_SUFFIX,runtime:I}),t.TemplateRef_=new i.CompileIdentifierMetadata({name:"TemplateRef_",moduleUrl:"asset:angular2/lib/src/core/linker/template_ref"+g.MODULE_SUFFIX,runtime:M}),t.ValueUnwrapper=new i.CompileIdentifierMetadata({name:"ValueUnwrapper",moduleUrl:P,runtime:k}),t.Injector=new i.CompileIdentifierMetadata({name:"Injector",moduleUrl:"asset:angular2/lib/src/core/di/injector"+g.MODULE_SUFFIX,runtime:N}),t.ViewEncapsulation=new i.CompileIdentifierMetadata({name:"ViewEncapsulation",moduleUrl:"asset:angular2/lib/src/core/metadata/view"+g.MODULE_SUFFIX,runtime:D}),t.ViewType=new i.CompileIdentifierMetadata({name:"ViewType",moduleUrl:"asset:angular2/lib/src/core/linker/view_type"+g.MODULE_SUFFIX,runtime:V}),t.ChangeDetectionStrategy=new i.CompileIdentifierMetadata({name:"ChangeDetectionStrategy",moduleUrl:P,runtime:j}),t.StaticNodeDebugInfo=new i.CompileIdentifierMetadata({name:"StaticNodeDebugInfo",moduleUrl:"asset:angular2/lib/src/core/linker/debug_context"+g.MODULE_SUFFIX,runtime:L}),t.DebugContext=new i.CompileIdentifierMetadata({name:"DebugContext",moduleUrl:"asset:angular2/lib/src/core/linker/debug_context"+g.MODULE_SUFFIX,runtime:C}),t.Renderer=new i.CompileIdentifierMetadata({name:"Renderer",moduleUrl:"asset:angular2/lib/src/core/render/api"+g.MODULE_SUFFIX,runtime:B}),t.SimpleChange=new i.CompileIdentifierMetadata({name:"SimpleChange",moduleUrl:P,runtime:F}),t.uninitialized=new i.CompileIdentifierMetadata({name:"uninitialized",moduleUrl:P,runtime:U}),t.ChangeDetectorState=new i.CompileIdentifierMetadata({name:"ChangeDetectorState",moduleUrl:P,runtime:W}),t.checkBinding=new i.CompileIdentifierMetadata({name:"checkBinding",moduleUrl:b,runtime:G}),t.flattenNestedViewRenderNodes=new i.CompileIdentifierMetadata({name:"flattenNestedViewRenderNodes",moduleUrl:b,runtime:H}),t.devModeEqual=new i.CompileIdentifierMetadata({name:"devModeEqual",moduleUrl:P,runtime:X}),t.interpolate=new i.CompileIdentifierMetadata({name:"interpolate",moduleUrl:b,runtime:q}),t.castByValue=new i.CompileIdentifierMetadata({name:"castByValue",moduleUrl:b,runtime:z}),t.pureProxies=[null,new i.CompileIdentifierMetadata({name:"pureProxy1",moduleUrl:b,runtime:a.pureProxy1}),new i.CompileIdentifierMetadata({name:"pureProxy2",moduleUrl:b,runtime:a.pureProxy2}),new i.CompileIdentifierMetadata({name:"pureProxy3",moduleUrl:b,runtime:a.pureProxy3}),new i.CompileIdentifierMetadata({name:"pureProxy4",moduleUrl:b,runtime:a.pureProxy4}),new i.CompileIdentifierMetadata({name:"pureProxy5",moduleUrl:b,runtime:a.pureProxy5}),new i.CompileIdentifierMetadata({name:"pureProxy6",moduleUrl:b,runtime:a.pureProxy6}),new i.CompileIdentifierMetadata({name:"pureProxy7",moduleUrl:b,runtime:a.pureProxy7}),new i.CompileIdentifierMetadata({name:"pureProxy8",moduleUrl:b,runtime:a.pureProxy8}),new i.CompileIdentifierMetadata({name:"pureProxy9",moduleUrl:b,runtime:a.pureProxy9}),new i.CompileIdentifierMetadata({name:"pureProxy10",moduleUrl:b,runtime:a.pureProxy10})],t}();e.Identifiers=K,e.identifierToken=r},function(t,e,n){"use strict";function r(t){var e;if(t instanceof o.AppElement){var n=t;if(e=n.nativeElement,s.isPresent(n.nestedViews))for(var i=n.nestedViews.length-1;i>=0;i--){var a=n.nestedViews[i];a.rootNodesOrAppElements.length>0&&(e=r(a.rootNodesOrAppElements[a.rootNodesOrAppElements.length-1]))}}else e=t;return e}var i=n(15),o=n(67),s=n(5),a=n(40),u=n(82),c=n(68),p=n(66),l=n(28),h=n(71),f=n(73),d=n(160),v=n(161),y=s.CONST_EXPR(new Object),m=h.wtfCreateScope("AppView#check(ascii id)"),g=function(){function t(t,e,n,r,i,o,s,a,p){this.clazz=t,this.componentType=e,this.type=n,this.locals=r,this.viewUtils=i,this.parentInjector=o,this.declarationAppElement=s,this.cdMode=a,this.staticNodeDebugInfos=p,this.contentChildren=[],this.viewChildren=[],this.viewContainerElement=null,this.cdState=l.ChangeDetectorState.NeverChecked,this.context=null,this.destroyed=!1,this._currentDebugContext=null,this.ref=new u.ViewRef_(this),n===c.ViewType.COMPONENT||n===c.ViewType.HOST?this.renderer=i.renderComponent(e):this.renderer=s.parentView.renderer}return t.prototype.create=function(t,e){var n,r;switch(this.type){case c.ViewType.COMPONENT:n=this.declarationAppElement.component,r=p.ensureSlotCount(t,this.componentType.slotCount);break;case c.ViewType.EMBEDDED:n=this.declarationAppElement.parentView.context,r=this.declarationAppElement.parentView.projectableNodes;break;case c.ViewType.HOST:n=y,r=t}if(this._hasExternalHostElement=s.isPresent(e),this.context=n,this.projectableNodes=r,!this.debugMode)return this.createInternal(e);this._resetDebug();try{return this.createInternal(e)}catch(i){throw this._rethrowWithContext(i,i.stack),i}},t.prototype.createInternal=function(t){return null},t.prototype.init=function(t,e,n,r){this.rootNodesOrAppElements=t,this.allNodes=e,this.disposables=n,this.subscriptions=r,this.type===c.ViewType.COMPONENT&&(this.declarationAppElement.parentView.viewChildren.push(this),this.renderParent=this.declarationAppElement.parentView,this.dirtyParentQueriesInternal())},t.prototype.selectOrCreateHostElement=function(t,e,n){var r;return r=s.isPresent(e)?this.renderer.selectRootElement(e,n):this.renderer.createElement(null,t,n)},t.prototype.injectorGet=function(t,e,n){if(!this.debugMode)return this.injectorGetInternal(t,e,n);this._resetDebug();try{return this.injectorGetInternal(t,e,n)}catch(r){throw this._rethrowWithContext(r,r.stack),r}},t.prototype.injectorGetInternal=function(t,e,n){return n},t.prototype.injector=function(t){return s.isPresent(t)?new v.ElementInjector(this,t):this.parentInjector},t.prototype.destroy=function(){this._hasExternalHostElement?this.renderer.detachView(this.flatRootNodes):s.isPresent(this.viewContainerElement)&&this.viewContainerElement.detachView(this.viewContainerElement.nestedViews.indexOf(this)),this._destroyRecurse()},t.prototype._destroyRecurse=function(){if(!this.destroyed){for(var t=this.contentChildren,e=0;e0?this.rootNodesOrAppElements[this.rootNodesOrAppElements.length-1]:null;return r(t)},enumerable:!0,configurable:!0}),t.prototype.hasLocal=function(t){return i.StringMapWrapper.contains(this.locals,t)},t.prototype.setLocal=function(t,e){this.locals[t]=e},t.prototype.dirtyParentQueriesInternal=function(){},t.prototype.addRenderContentChild=function(t){this.contentChildren.push(t),t.renderParent=this,t.dirtyParentQueriesInternal()},t.prototype.removeContentChild=function(t){i.ListWrapper.remove(this.contentChildren,t),t.dirtyParentQueriesInternal(),t.renderParent=null},t.prototype.detectChanges=function(t){var e=m(this.clazz);if(this.cdMode!==l.ChangeDetectionStrategy.Detached&&this.cdMode!==l.ChangeDetectionStrategy.Checked&&this.cdState!==l.ChangeDetectorState.Errored){if(this.destroyed&&this.throwDestroyedError("detectChanges"),this.debugMode){this._resetDebug();try{this.detectChangesInternal(t)}catch(n){throw this._rethrowWithContext(n,n.stack),n}}else this.detectChangesInternal(t);this.cdMode===l.ChangeDetectionStrategy.CheckOnce&&(this.cdMode=l.ChangeDetectionStrategy.Checked),this.cdState=l.ChangeDetectorState.CheckedBefore,h.wtfLeave(e)}},t.prototype.detectChangesInternal=function(t){this.detectContentChildrenChanges(t),this.detectViewChildrenChanges(t)},t.prototype.detectContentChildrenChanges=function(t){for(var e=0;eo?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=n(15),a=n(68),u=function(){function t(t,e,n){this.providerTokens=t,this.componentToken=e,this.varTokens=n}return t=r([o.CONST(),i("design:paramtypes",[Array,Object,Object])],t)}();e.StaticNodeDebugInfo=u;var c=function(){function t(t,e,n,r){this._view=t,this._nodeIndex=e,this._tplRow=n,this._tplCol=r}return Object.defineProperty(t.prototype,"_staticNodeInfo",{get:function(){return o.isPresent(this._nodeIndex)?this._view.staticNodeDebugInfos[this._nodeIndex]:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"context",{get:function(){return this._view.context},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"component",{get:function(){var t=this._staticNodeInfo;return o.isPresent(t)&&o.isPresent(t.componentToken)?this.injector.get(t.componentToken):null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"componentRenderElement",{get:function(){for(var t=this._view;o.isPresent(t.declarationAppElement)&&t.type!==a.ViewType.COMPONENT;)t=t.declarationAppElement.parentView;return o.isPresent(t.declarationAppElement)?t.declarationAppElement.nativeElement:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"injector",{get:function(){return this._view.injector(this._nodeIndex)},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"renderNode",{get:function(){return o.isPresent(this._nodeIndex)&&o.isPresent(this._view.allNodes)?this._view.allNodes[this._nodeIndex]:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"providerTokens",{get:function(){var t=this._staticNodeInfo;return o.isPresent(t)?t.providerTokens:null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"source",{get:function(){return this._view.componentType.templateUrl+":"+this._tplRow+":"+this._tplCol},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"locals",{get:function(){var t=this,e={};return s.ListWrapper.forEachWithIndex(this._view.staticNodeDebugInfos,function(n,r){var i=n.varTokens;s.StringMapWrapper.forEach(i,function(n,i){var s;s=o.isBlank(n)?o.isPresent(t._view.allNodes)?t._view.allNodes[r]:null:t._view.injectorGet(n,r,null), -e[i]=s})}),s.StringMapWrapper.forEach(this._view.locals,function(t,n){e[n]=t}),e},enumerable:!0,configurable:!0}),t}();e.DebugContext=c},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(5),o=n(11),s=i.CONST_EXPR(new Object),a=function(t){function e(e,n){t.call(this),this._view=e,this._nodeIndex=n}return r(e,t),e.prototype.get=function(t,e){void 0===e&&(e=o.THROW_IF_NOT_FOUND);var n=s;return n===s&&(n=this._view.injectorGet(t,this._nodeIndex,s)),n===s&&(n=this._view.parentInjector.get(t,e)),n},e}(o.Injector);e.ElementInjector=a},function(t,e,n){"use strict";var r=n(5),i=n(12),o=n(158),s=function(){function t(t,e,n,i){void 0===i&&(i=null),this.genDebugInfo=t,this.logBindingUpdate=e,this.useJit=n,r.isBlank(i)&&(i=new u),this.renderTypes=i}return t}();e.CompilerConfig=s;var a=function(){function t(){}return Object.defineProperty(t.prototype,"renderer",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"renderText",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"renderElement",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"renderComment",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"renderNode",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"renderEvent",{get:function(){return i.unimplemented()},enumerable:!0,configurable:!0}),t}();e.RenderTypes=a;var u=function(){function t(){this.renderer=o.Identifiers.Renderer,this.renderText=null,this.renderElement=null,this.renderComment=null,this.renderNode=null,this.renderEvent=null}return t}();e.DefaultRenderTypes=u},function(t,e,n){"use strict";function r(t){return t.dependencies.forEach(function(t){t.factoryPlaceholder.moduleUrl=o(t.comp)}),t.statements}function i(t){return t.dependencies.forEach(function(t){t.valuePlaceholder.moduleUrl=s(t.sourceUrl,t.isShimmed)}),t.statements}function o(t){var e=t.type.moduleUrl,n=e.substring(0,e.length-f.MODULE_SUFFIX.length);return n+".ngfactory"+f.MODULE_SUFFIX}function s(t,e){return e?t+".shim"+f.MODULE_SUFFIX:""+t+f.MODULE_SUFFIX}function a(t){if(!t.isComponent)throw new c.BaseException("Could not compile '"+t.type.name+"' because it is not a component.")}var u=n(155),c=n(12),p=n(15),l=n(164),h=n(65),f=n(153),d=new u.CompileIdentifierMetadata({name:"ComponentFactory",runtime:h.ComponentFactory,moduleUrl:"asset:angular2/lib/src/core/linker/component_factory"+f.MODULE_SUFFIX}),v=function(){function t(t,e){this.moduleUrl=t,this.source=e}return t}();e.SourceModule=v;var y=function(){function t(t,e,n){this.component=t,this.directives=e,this.pipes=n}return t}();e.NormalizedComponentWithViewDirectives=y;var m=function(){function t(t,e,n,r,i){this._directiveNormalizer=t,this._templateParser=e,this._styleCompiler=n,this._viewCompiler=r,this._outputEmitter=i}return t.prototype.normalizeDirectiveMetadata=function(t){return this._directiveNormalizer.normalizeDirective(t)},t.prototype.compileTemplates=function(t){var e=this;if(0===t.length)throw new c.BaseException("No components given");var n=[],r=[],i=o(t[0].component);return t.forEach(function(t){var i=t.component;a(i);var o=e._compileComponent(i,t.directives,t.pipes,n);r.push(o);var s=u.createHostComponentMeta(i.type,i.selector),c=e._compileComponent(s,[i],[],n),p=i.type.name+"NgFactory";n.push(l.variable(p).set(l.importExpr(d).instantiate([l.literal(i.selector),l.variable(c),l.importExpr(i.type)],l.importType(d,null,[l.TypeModifier.Const]))).toDeclStmt(null,[l.StmtModifier.Final])),r.push(p)}),this._codegenSourceModule(i,n,r)},t.prototype.compileStylesheet=function(t,e){var n=this._styleCompiler.compileStylesheet(t,e,!1),r=this._styleCompiler.compileStylesheet(t,e,!0);return[this._codegenSourceModule(s(t,!1),i(n),[n.stylesVar]),this._codegenSourceModule(s(t,!0),i(r),[r.stylesVar])]},t.prototype._compileComponent=function(t,e,n,o){var s=this._styleCompiler.compileComponent(t),a=this._templateParser.parse(t,t.template.template,e,n,t.type.name),u=this._viewCompiler.compileComponent(t,a,l.variable(s.stylesVar),n);return p.ListWrapper.addAll(o,i(s)),p.ListWrapper.addAll(o,r(u)),u.viewFactoryVar},t.prototype._codegenSourceModule=function(t,e,n){return new v(t,this._outputEmitter.emitStatements(t,e,n))},t}();e.OfflineCompiler=m},function(t,e,n){"use strict";function r(t,e,n){var r=new ot(t,e);return n.visitExpression(r,null)}function i(t){var e=new st;return e.visitAllStatements(t,null),e.varNames}function o(t,e){return void 0===e&&(e=null),new C(t,e)}function s(t,e){return void 0===e&&(e=null),new M(t,null,e)}function a(t,e,n){return void 0===e&&(e=null),void 0===n&&(n=null),d.isPresent(t)?new g(t,e,n):null}function u(t,e){return void 0===e&&(e=null),new I(t,e)}function c(t,e){return void 0===e&&(e=null),new U(t,e)}function p(t,e){return void 0===e&&(e=null),new W(t,e)}function l(t){return new N(t)}function h(t,e,n){return void 0===n&&(n=null),new j(t,e,n)}var f=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},d=n(5);!function(t){t[t.Const=0]="Const"}(e.TypeModifier||(e.TypeModifier={}));var v=(e.TypeModifier,function(){function t(t){void 0===t&&(t=null),this.modifiers=t,d.isBlank(t)&&(this.modifiers=[])}return t.prototype.hasModifier=function(t){return-1!==this.modifiers.indexOf(t)},t}());e.Type=v,function(t){t[t.Dynamic=0]="Dynamic",t[t.Bool=1]="Bool",t[t.String=2]="String",t[t.Int=3]="Int",t[t.Number=4]="Number",t[t.Function=5]="Function"}(e.BuiltinTypeName||(e.BuiltinTypeName={}));var y=e.BuiltinTypeName,m=function(t){function e(e,n){void 0===n&&(n=null),t.call(this,n),this.name=e}return f(e,t),e.prototype.visitType=function(t,e){return t.visitBuiltintType(this,e)},e}(v);e.BuiltinType=m;var g=function(t){function e(e,n,r){void 0===n&&(n=null),void 0===r&&(r=null),t.call(this,r),this.value=e,this.typeParams=n}return f(e,t),e.prototype.visitType=function(t,e){return t.visitExternalType(this,e)},e}(v);e.ExternalType=g;var _=function(t){function e(e,n){void 0===n&&(n=null),t.call(this,n),this.of=e}return f(e,t),e.prototype.visitType=function(t,e){return t.visitArrayType(this,e)},e}(v);e.ArrayType=_;var b=function(t){function e(e,n){void 0===n&&(n=null),t.call(this,n),this.valueType=e}return f(e,t),e.prototype.visitType=function(t,e){return t.visitMapType(this,e)},e}(v);e.MapType=b,e.DYNAMIC_TYPE=new m(y.Dynamic),e.BOOL_TYPE=new m(y.Bool),e.INT_TYPE=new m(y.Int),e.NUMBER_TYPE=new m(y.Number),e.STRING_TYPE=new m(y.String),e.FUNCTION_TYPE=new m(y.Function),function(t){t[t.Equals=0]="Equals",t[t.NotEquals=1]="NotEquals",t[t.Identical=2]="Identical",t[t.NotIdentical=3]="NotIdentical",t[t.Minus=4]="Minus",t[t.Plus=5]="Plus",t[t.Divide=6]="Divide",t[t.Multiply=7]="Multiply",t[t.Modulo=8]="Modulo",t[t.And=9]="And",t[t.Or=10]="Or",t[t.Lower=11]="Lower",t[t.LowerEquals=12]="LowerEquals",t[t.Bigger=13]="Bigger",t[t.BiggerEquals=14]="BiggerEquals"}(e.BinaryOperator||(e.BinaryOperator={}));var P=e.BinaryOperator,E=function(){function t(t){this.type=t}return t.prototype.prop=function(t){return new B(this,t)},t.prototype.key=function(t,e){return void 0===e&&(e=null),new F(this,t,e)},t.prototype.callMethod=function(t,e){return new T(this,t,e)},t.prototype.callFn=function(t){return new x(this,t)},t.prototype.instantiate=function(t,e){return void 0===e&&(e=null),new A(this,t,e)},t.prototype.conditional=function(t,e){return void 0===e&&(e=null),new k(this,t,e)},t.prototype.equals=function(t){return new L(P.Equals,this,t)},t.prototype.notEquals=function(t){return new L(P.NotEquals,this,t)},t.prototype.identical=function(t){return new L(P.Identical,this,t)},t.prototype.notIdentical=function(t){return new L(P.NotIdentical,this,t)},t.prototype.minus=function(t){return new L(P.Minus,this,t)},t.prototype.plus=function(t){return new L(P.Plus,this,t)},t.prototype.divide=function(t){return new L(P.Divide,this,t)},t.prototype.multiply=function(t){return new L(P.Multiply,this,t)},t.prototype.modulo=function(t){return new L(P.Modulo,this,t)},t.prototype.and=function(t){return new L(P.And,this,t)},t.prototype.or=function(t){return new L(P.Or,this,t)},t.prototype.lower=function(t){return new L(P.Lower,this,t)},t.prototype.lowerEquals=function(t){return new L(P.LowerEquals,this,t)},t.prototype.bigger=function(t){return new L(P.Bigger,this,t)},t.prototype.biggerEquals=function(t){return new L(P.BiggerEquals,this,t)},t.prototype.isBlank=function(){return this.equals(e.NULL_EXPR)},t.prototype.cast=function(t){return new D(this,t)},t.prototype.toStmt=function(){return new G(this)},t}();e.Expression=E,function(t){t[t.This=0]="This",t[t.Super=1]="Super",t[t.CatchError=2]="CatchError",t[t.CatchStack=3]="CatchStack"}(e.BuiltinVar||(e.BuiltinVar={}));var w=e.BuiltinVar,C=function(t){function e(e,n){void 0===n&&(n=null),t.call(this,n),d.isString(e)?(this.name=e,this.builtin=null):(this.name=null,this.builtin=e)}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitReadVarExpr(this,e)},e.prototype.set=function(t){return new R(this.name,t)},e}(E);e.ReadVarExpr=C;var R=function(t){function e(e,n,r){void 0===r&&(r=null),t.call(this,d.isPresent(r)?r:n.type),this.name=e,this.value=n}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitWriteVarExpr(this,e)},e.prototype.toDeclStmt=function(t,e){return void 0===t&&(t=null),void 0===e&&(e=null),new X(this.name,this.value,t,e)},e}(E);e.WriteVarExpr=R;var S=function(t){function e(e,n,r,i){void 0===i&&(i=null),t.call(this,d.isPresent(i)?i:r.type),this.receiver=e,this.index=n,this.value=r}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitWriteKeyExpr(this,e)},e}(E);e.WriteKeyExpr=S;var O=function(t){function e(e,n,r,i){void 0===i&&(i=null),t.call(this,d.isPresent(i)?i:r.type),this.receiver=e,this.name=n,this.value=r}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitWritePropExpr(this,e)},e}(E);e.WritePropExpr=O,function(t){t[t.ConcatArray=0]="ConcatArray",t[t.SubscribeObservable=1]="SubscribeObservable",t[t.bind=2]="bind"}(e.BuiltinMethod||(e.BuiltinMethod={}));var T=(e.BuiltinMethod,function(t){function e(e,n,r,i){void 0===i&&(i=null),t.call(this,i),this.receiver=e,this.args=r,d.isString(n)?(this.name=n,this.builtin=null):(this.name=null,this.builtin=n)}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitInvokeMethodExpr(this,e)},e}(E));e.InvokeMethodExpr=T;var x=function(t){function e(e,n,r){void 0===r&&(r=null),t.call(this,r),this.fn=e,this.args=n}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitInvokeFunctionExpr(this,e)},e}(E);e.InvokeFunctionExpr=x;var A=function(t){function e(e,n,r){t.call(this,r),this.classExpr=e,this.args=n}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitInstantiateExpr(this,e)},e}(E);e.InstantiateExpr=A;var I=function(t){function e(e,n){void 0===n&&(n=null),t.call(this,n),this.value=e}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitLiteralExpr(this,e)},e}(E);e.LiteralExpr=I;var M=function(t){function e(e,n,r){void 0===n&&(n=null),void 0===r&&(r=null),t.call(this,n),this.value=e,this.typeParams=r}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitExternalExpr(this,e)},e}(E);e.ExternalExpr=M;var k=function(t){function e(e,n,r,i){void 0===r&&(r=null),void 0===i&&(i=null),t.call(this,d.isPresent(i)?i:n.type),this.condition=e,this.falseCase=r,this.trueCase=n}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitConditionalExpr(this,e)},e}(E);e.ConditionalExpr=k;var N=function(t){function n(n){t.call(this,e.BOOL_TYPE),this.condition=n}return f(n,t),n.prototype.visitExpression=function(t,e){return t.visitNotExpr(this,e)},n}(E);e.NotExpr=N;var D=function(t){function e(e,n){t.call(this,n),this.value=e}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitCastExpr(this,e)},e}(E);e.CastExpr=D;var V=function(){function t(t,e){void 0===e&&(e=null),this.name=t,this.type=e}return t}();e.FnParam=V;var j=function(t){function e(e,n,r){void 0===r&&(r=null),t.call(this,r),this.params=e,this.statements=n}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitFunctionExpr(this,e)},e.prototype.toDeclStmt=function(t,e){return void 0===e&&(e=null),new q(t,this.params,this.statements,this.type,e)},e}(E);e.FunctionExpr=j;var L=function(t){function e(e,n,r,i){void 0===i&&(i=null),t.call(this,d.isPresent(i)?i:n.type),this.operator=e,this.rhs=r,this.lhs=n}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitBinaryOperatorExpr(this,e)},e}(E);e.BinaryOperatorExpr=L;var B=function(t){function e(e,n,r){void 0===r&&(r=null),t.call(this,r),this.receiver=e,this.name=n}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitReadPropExpr(this,e)},e.prototype.set=function(t){return new O(this.receiver,this.name,t)},e}(E);e.ReadPropExpr=B;var F=function(t){function e(e,n,r){void 0===r&&(r=null),t.call(this,r),this.receiver=e,this.index=n}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitReadKeyExpr(this,e)},e.prototype.set=function(t){return new S(this.receiver,this.index,t)},e}(E);e.ReadKeyExpr=F;var U=function(t){function e(e,n){void 0===n&&(n=null),t.call(this,n),this.entries=e}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitLiteralArrayExpr(this,e)},e}(E);e.LiteralArrayExpr=U;var W=function(t){function e(e,n){void 0===n&&(n=null),t.call(this,n),this.entries=e,this.valueType=null,d.isPresent(n)&&(this.valueType=n.valueType)}return f(e,t),e.prototype.visitExpression=function(t,e){return t.visitLiteralMapExpr(this,e)},e}(E);e.LiteralMapExpr=W,e.THIS_EXPR=new C(w.This),e.SUPER_EXPR=new C(w.Super),e.CATCH_ERROR_VAR=new C(w.CatchError),e.CATCH_STACK_VAR=new C(w.CatchStack),e.NULL_EXPR=new I(null,null),function(t){t[t.Final=0]="Final",t[t.Private=1]="Private"}(e.StmtModifier||(e.StmtModifier={}));var H=(e.StmtModifier,function(){function t(t){void 0===t&&(t=null),this.modifiers=t,d.isBlank(t)&&(this.modifiers=[])}return t.prototype.hasModifier=function(t){return-1!==this.modifiers.indexOf(t)},t}());e.Statement=H;var X=function(t){function e(e,n,r,i){void 0===r&&(r=null),void 0===i&&(i=null),t.call(this,i),this.name=e,this.value=n,this.type=d.isPresent(r)?r:n.type}return f(e,t),e.prototype.visitStatement=function(t,e){return t.visitDeclareVarStmt(this,e)},e}(H);e.DeclareVarStmt=X;var q=function(t){function e(e,n,r,i,o){void 0===i&&(i=null),void 0===o&&(o=null),t.call(this,o),this.name=e,this.params=n,this.statements=r,this.type=i}return f(e,t),e.prototype.visitStatement=function(t,e){return t.visitDeclareFunctionStmt(this,e)},e}(H);e.DeclareFunctionStmt=q;var G=function(t){function e(e){t.call(this),this.expr=e}return f(e,t),e.prototype.visitStatement=function(t,e){return t.visitExpressionStmt(this,e)},e}(H);e.ExpressionStatement=G;var z=function(t){function e(e){t.call(this),this.value=e}return f(e,t),e.prototype.visitStatement=function(t,e){return t.visitReturnStmt(this,e)},e}(H);e.ReturnStatement=z;var K=function(){function t(t,e){void 0===t&&(t=null),this.type=t,this.modifiers=e,d.isBlank(e)&&(this.modifiers=[])}return t.prototype.hasModifier=function(t){return-1!==this.modifiers.indexOf(t)},t}();e.AbstractClassPart=K;var $=function(t){function e(e,n,r){void 0===n&&(n=null),void 0===r&&(r=null),t.call(this,n,r),this.name=e}return f(e,t),e}(K);e.ClassField=$;var Q=function(t){function e(e,n,r,i,o){void 0===i&&(i=null),void 0===o&&(o=null),t.call(this,i,o),this.name=e,this.params=n,this.body=r}return f(e,t),e}(K);e.ClassMethod=Q;var J=function(t){function e(e,n,r,i){void 0===r&&(r=null),void 0===i&&(i=null),t.call(this,r,i),this.name=e,this.body=n}return f(e,t),e}(K);e.ClassGetter=J;var Z=function(t){function e(e,n,r,i,o,s,a){void 0===a&&(a=null),t.call(this,a),this.name=e,this.parent=n,this.fields=r,this.getters=i,this.constructorMethod=o,this.methods=s}return f(e,t),e.prototype.visitStatement=function(t,e){return t.visitDeclareClassStmt(this,e)},e}(H);e.ClassStmt=Z;var Y=function(t){function e(e,n,r){void 0===r&&(r=d.CONST_EXPR([])),t.call(this),this.condition=e,this.trueCase=n,this.falseCase=r}return f(e,t),e.prototype.visitStatement=function(t,e){return t.visitIfStmt(this,e)},e}(H);e.IfStmt=Y;var tt=function(t){function e(e){t.call(this),this.comment=e}return f(e,t),e.prototype.visitStatement=function(t,e){return t.visitCommentStmt(this,e)},e}(H);e.CommentStmt=tt;var et=function(t){function e(e,n){t.call(this),this.bodyStmts=e,this.catchStmts=n}return f(e,t),e.prototype.visitStatement=function(t,e){return t.visitTryCatchStmt(this,e)},e}(H);e.TryCatchStmt=et;var nt=function(t){function e(e){t.call(this),this.error=e}return f(e,t),e.prototype.visitStatement=function(t,e){return t.visitThrowStmt(this,e)},e}(H);e.ThrowStmt=nt;var rt=function(){function t(){}return t.prototype.visitReadVarExpr=function(t,e){return t},t.prototype.visitWriteVarExpr=function(t,e){return new R(t.name,t.value.visitExpression(this,e))},t.prototype.visitWriteKeyExpr=function(t,e){return new S(t.receiver.visitExpression(this,e),t.index.visitExpression(this,e),t.value.visitExpression(this,e))},t.prototype.visitWritePropExpr=function(t,e){return new O(t.receiver.visitExpression(this,e),t.name,t.value.visitExpression(this,e))},t.prototype.visitInvokeMethodExpr=function(t,e){var n=d.isPresent(t.builtin)?t.builtin:t.name;return new T(t.receiver.visitExpression(this,e),n,this.visitAllExpressions(t.args,e),t.type)},t.prototype.visitInvokeFunctionExpr=function(t,e){return new x(t.fn.visitExpression(this,e),this.visitAllExpressions(t.args,e),t.type)},t.prototype.visitInstantiateExpr=function(t,e){return new A(t.classExpr.visitExpression(this,e),this.visitAllExpressions(t.args,e),t.type)},t.prototype.visitLiteralExpr=function(t,e){return t},t.prototype.visitExternalExpr=function(t,e){return t},t.prototype.visitConditionalExpr=function(t,e){return new k(t.condition.visitExpression(this,e),t.trueCase.visitExpression(this,e),t.falseCase.visitExpression(this,e))},t.prototype.visitNotExpr=function(t,e){return new N(t.condition.visitExpression(this,e))},t.prototype.visitCastExpr=function(t,e){return new D(t.value.visitExpression(this,e),e)},t.prototype.visitFunctionExpr=function(t,e){return t},t.prototype.visitBinaryOperatorExpr=function(t,e){return new L(t.operator,t.lhs.visitExpression(this,e),t.rhs.visitExpression(this,e),t.type)},t.prototype.visitReadPropExpr=function(t,e){return new B(t.receiver.visitExpression(this,e),t.name,t.type)},t.prototype.visitReadKeyExpr=function(t,e){return new F(t.receiver.visitExpression(this,e),t.index.visitExpression(this,e),t.type)},t.prototype.visitLiteralArrayExpr=function(t,e){return new U(this.visitAllExpressions(t.entries,e))},t.prototype.visitLiteralMapExpr=function(t,e){var n=this;return new W(t.entries.map(function(t){return[t[0],t[1].visitExpression(n,e)]}))},t.prototype.visitAllExpressions=function(t,e){var n=this;return t.map(function(t){return t.visitExpression(n,e)})},t.prototype.visitDeclareVarStmt=function(t,e){return new X(t.name,t.value.visitExpression(this,e),t.type,t.modifiers)},t.prototype.visitDeclareFunctionStmt=function(t,e){return t},t.prototype.visitExpressionStmt=function(t,e){return new G(t.expr.visitExpression(this,e))},t.prototype.visitReturnStmt=function(t,e){return new z(t.value.visitExpression(this,e))},t.prototype.visitDeclareClassStmt=function(t,e){return t},t.prototype.visitIfStmt=function(t,e){return new Y(t.condition.visitExpression(this,e),this.visitAllStatements(t.trueCase,e),this.visitAllStatements(t.falseCase,e))},t.prototype.visitTryCatchStmt=function(t,e){return new et(this.visitAllStatements(t.bodyStmts,e),this.visitAllStatements(t.catchStmts,e))},t.prototype.visitThrowStmt=function(t,e){return new nt(t.error.visitExpression(this,e))},t.prototype.visitCommentStmt=function(t,e){return t},t.prototype.visitAllStatements=function(t,e){var n=this;return t.map(function(t){return t.visitStatement(n,e)})},t}();e.ExpressionTransformer=rt;var it=function(){function t(){}return t.prototype.visitReadVarExpr=function(t,e){return t},t.prototype.visitWriteVarExpr=function(t,e){return t.value.visitExpression(this,e),t},t.prototype.visitWriteKeyExpr=function(t,e){return t.receiver.visitExpression(this,e),t.index.visitExpression(this,e),t.value.visitExpression(this,e),t},t.prototype.visitWritePropExpr=function(t,e){return t.receiver.visitExpression(this,e),t.value.visitExpression(this,e),t},t.prototype.visitInvokeMethodExpr=function(t,e){return t.receiver.visitExpression(this,e),this.visitAllExpressions(t.args,e),t},t.prototype.visitInvokeFunctionExpr=function(t,e){return t.fn.visitExpression(this,e),this.visitAllExpressions(t.args,e),t},t.prototype.visitInstantiateExpr=function(t,e){return t.classExpr.visitExpression(this,e),this.visitAllExpressions(t.args,e),t},t.prototype.visitLiteralExpr=function(t,e){return t},t.prototype.visitExternalExpr=function(t,e){return t},t.prototype.visitConditionalExpr=function(t,e){return t.condition.visitExpression(this,e),t.trueCase.visitExpression(this,e),t.falseCase.visitExpression(this,e),t},t.prototype.visitNotExpr=function(t,e){return t.condition.visitExpression(this,e),t},t.prototype.visitCastExpr=function(t,e){return t.value.visitExpression(this,e),t},t.prototype.visitFunctionExpr=function(t,e){return t},t.prototype.visitBinaryOperatorExpr=function(t,e){return t.lhs.visitExpression(this,e),t.rhs.visitExpression(this,e),t},t.prototype.visitReadPropExpr=function(t,e){return t.receiver.visitExpression(this,e),t},t.prototype.visitReadKeyExpr=function(t,e){return t.receiver.visitExpression(this,e),t.index.visitExpression(this,e),t},t.prototype.visitLiteralArrayExpr=function(t,e){return this.visitAllExpressions(t.entries,e),t},t.prototype.visitLiteralMapExpr=function(t,e){var n=this;return t.entries.forEach(function(t){return t[1].visitExpression(n,e)}),t},t.prototype.visitAllExpressions=function(t,e){var n=this;t.forEach(function(t){return t.visitExpression(n,e)})},t.prototype.visitDeclareVarStmt=function(t,e){return t.value.visitExpression(this,e),t},t.prototype.visitDeclareFunctionStmt=function(t,e){return t},t.prototype.visitExpressionStmt=function(t,e){return t.expr.visitExpression(this,e),t},t.prototype.visitReturnStmt=function(t,e){return t.value.visitExpression(this,e),t},t.prototype.visitDeclareClassStmt=function(t,e){return t},t.prototype.visitIfStmt=function(t,e){return t.condition.visitExpression(this,e),this.visitAllStatements(t.trueCase,e),this.visitAllStatements(t.falseCase,e),t},t.prototype.visitTryCatchStmt=function(t,e){return this.visitAllStatements(t.bodyStmts,e),this.visitAllStatements(t.catchStmts,e),t},t.prototype.visitThrowStmt=function(t,e){return t.error.visitExpression(this,e),t},t.prototype.visitCommentStmt=function(t,e){return t},t.prototype.visitAllStatements=function(t,e){var n=this;t.forEach(function(t){return t.visitStatement(n,e)})},t}();e.RecursiveExpressionVisitor=it,e.replaceVarInExpression=r;var ot=function(t){function e(e,n){t.call(this),this._varName=e,this._newValue=n}return f(e,t),e.prototype.visitReadVarExpr=function(t,e){return t.name==this._varName?this._newValue:t},e}(rt);e.findReadVarNames=i;var st=function(t){function e(){t.apply(this,arguments),this.varNames=new Set}return f(e,t),e.prototype.visitReadVarExpr=function(t,e){return this.varNames.add(t.name),null},e}(it);e.variable=o,e.importExpr=s,e.importType=a,e.literal=u,e.literalArr=c,e.literalMap=p,e.not=l,e.fn=h},function(t,e,n){"use strict";function r(t){if(!t.isComponent)throw new a.BaseException("Could not compile '"+t.type.name+"' because it is not a component.")}var i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(5),a=n(12),u=n(15),c=n(40),p=n(155),l=n(6),h=n(166),f=n(168),d=n(140),v=n(183),y=n(185),m=n(65),g=n(162),_=n(164),b=n(191),P=n(194),E=n(198),w=n(184),C=function(){function t(t,e,n,r,i,o,s){this._runtimeMetadataResolver=t,this._templateNormalizer=e,this._templateParser=n,this._styleCompiler=r,this._viewCompiler=i,this._xhr=o,this._genConfig=s,this._styleCache=new Map,this._hostCacheKeys=new Map,this._compiledTemplateCache=new Map,this._compiledTemplateDone=new Map}return t.prototype.resolveComponent=function(t){var e=this._runtimeMetadataResolver.getDirectiveMetadata(t),n=this._hostCacheKeys.get(t);if(s.isBlank(n)){n=new Object,this._hostCacheKeys.set(t,n),r(e);var i=p.createHostComponentMeta(e.type,e.selector);this._loadAndCompileComponent(n,i,[e],[],[])}return this._compiledTemplateDone.get(n).then(function(n){return new m.ComponentFactory(e.selector,n.viewFactory,t)})},t.prototype.clearCache=function(){this._styleCache.clear(),this._compiledTemplateCache.clear(),this._compiledTemplateDone.clear(),this._hostCacheKeys.clear()},t.prototype._loadAndCompileComponent=function(t,e,n,r,i){var o=this,a=this._compiledTemplateCache.get(t),u=this._compiledTemplateDone.get(t);return s.isBlank(a)&&(a=new R,this._compiledTemplateCache.set(t,a),u=c.PromiseWrapper.all([this._compileComponentStyles(e)].concat(n.map(function(t){return o._templateNormalizer.normalizeDirective(t)}))).then(function(t){var n=t.slice(1),s=t[0],u=o._templateParser.parse(e,e.template.template,n,r,e.type.name),p=[];return a.init(o._compileComponent(e,u,s,r,i,p)),c.PromiseWrapper.all(p).then(function(t){return a})}),this._compiledTemplateDone.set(t,u)),a},t.prototype._compileComponent=function(t,e,n,r,i,o){var a=this,c=this._viewCompiler.compileComponent(t,e,new _.ExternalExpr(new p.CompileIdentifierMetadata({runtime:n})),r);c.dependencies.forEach(function(t){var e=u.ListWrapper.clone(i),n=t.comp.type.runtime,r=a._runtimeMetadataResolver.getViewDirectivesMetadata(t.comp.type.runtime),s=a._runtimeMetadataResolver.getViewPipesMetadata(t.comp.type.runtime),c=u.ListWrapper.contains(e,n);e.push(n);var p=a._loadAndCompileComponent(t.comp.type.runtime,t.comp,r,s,e);t.factoryPlaceholder.runtime=p.proxyViewFactory,t.factoryPlaceholder.name="viewFactory_"+t.comp.type.name,c||o.push(a._compiledTemplateDone.get(n))});var l;return l=s.IS_DART||!this._genConfig.useJit?P.interpretStatements(c.statements,c.viewFactoryVar,new E.InterpretiveAppViewInstanceFactory):b.jitStatements(t.type.name+".template.js",c.statements,c.viewFactoryVar)},t.prototype._compileComponentStyles=function(t){var e=this._styleCompiler.compileComponent(t);return this._resolveStylesCompileResult(t.type.name,e)},t.prototype._resolveStylesCompileResult=function(t,e){var n=this,r=e.dependencies.map(function(t){return n._loadStylesheetDep(t)});return c.PromiseWrapper.all(r).then(function(t){for(var r=[],i=0;io?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(155),a=n(164),u=n(36),c=n(167),p=n(157),l=n(152),h=n(6),f=n(5),d="%COMP%",v="_nghost-"+d,y="_ngcontent-"+d,m=function(){function t(t,e,n){this.sourceUrl=t,this.isShimmed=e,this.valuePlaceholder=n}return t}();e.StylesCompileDependency=m;var g=function(){function t(t,e,n){this.statements=t,this.stylesVar=e,this.dependencies=n}return t}();e.StylesCompileResult=g;var _=function(){function t(t){this._urlResolver=t,this._shadowCss=new c.ShadowCss}return t.prototype.compileComponent=function(t){var e=t.template.encapsulation===u.ViewEncapsulation.Emulated;return this._compileStyles(r(t),t.template.styles,t.template.styleUrls,e)},t.prototype.compileStylesheet=function(t,e,n){var i=l.extractStyleUrls(this._urlResolver,t,e);return this._compileStyles(r(null),[i.style],i.styleUrls,n)},t.prototype._compileStyles=function(t,e,n,i){for(var o=this,u=e.map(function(t){return a.literal(o._shimIfNeeded(t,i))}),c=[],p=0;p0?o.push(u):(o.length>0&&(r.push(o.join("")),n.push(x),o=[]),n.push(u)),u==O&&i++}return o.length>0&&(r.push(o.join("")),n.push(x)),new I(n.join(""),r)}var s=n(15),a=n(5),u=function(){function t(){this.strictStyling=!0}return t.prototype.shimCssText=function(t,e,n){return void 0===n&&(n=""),t=r(t),t=this._insertDirectives(t),this._scopeCssText(t,e,n)},t.prototype._insertDirectives=function(t){return t=this._insertPolyfillDirectivesInCssText(t),this._insertPolyfillRulesInCssText(t)},t.prototype._insertPolyfillDirectivesInCssText=function(t){return a.StringWrapper.replaceAllMapped(t,c,function(t){return t[1]+"{"})},t.prototype._insertPolyfillRulesInCssText=function(t){return a.StringWrapper.replaceAllMapped(t,p,function(t){var e=t[0];return e=a.StringWrapper.replace(e,t[1],""),e=a.StringWrapper.replace(e,t[2],""),t[3]+e})},t.prototype._scopeCssText=function(t,e,n){var r=this._extractUnscopedRulesFromCssText(t);return t=this._insertPolyfillHostInCssText(t),t=this._convertColonHost(t),t=this._convertColonHostContext(t),t=this._convertShadowDOMSelectors(t), -a.isPresent(e)&&(t=this._scopeSelectors(t,e,n)),t=t+"\n"+r,t.trim()},t.prototype._extractUnscopedRulesFromCssText=function(t){for(var e,n="",r=a.RegExpWrapper.matcher(l,t);a.isPresent(e=a.RegExpMatcherWrapper.next(r));){var i=e[0];i=a.StringWrapper.replace(i,e[2],""),i=a.StringWrapper.replace(i,e[1],e[3]),n+=i+"\n\n"}return n},t.prototype._convertColonHost=function(t){return this._convertColonRule(t,v,this._colonHostPartReplacer)},t.prototype._convertColonHostContext=function(t){return this._convertColonRule(t,y,this._colonHostContextPartReplacer)},t.prototype._convertColonRule=function(t,e,n){return a.StringWrapper.replaceAllMapped(t,e,function(t){if(a.isPresent(t[2])){for(var e=t[2].split(","),r=[],i=0;i","+","~"],i=t,o="["+e+"]",u=0;u0&&!s.ListWrapper.contains(r,e)&&!a.StringWrapper.contains(e,o)){var n=/([^:]*)(:*)(.*)/g,i=a.RegExpWrapper.firstMatch(n,e);a.isPresent(i)&&(t=i[1]+o+i[2]+i[3])}return t}).join(c)}return i},t.prototype._insertPolyfillHostInCssText=function(t){return t=a.StringWrapper.replaceAll(t,w,f),t=a.StringWrapper.replaceAll(t,E,h)},t}();e.ShadowCss=u;var c=/polyfill-next-selector[^}]*content:[\s]*?['"](.*?)['"][;\s]*}([^{]*?){/gim,p=/(polyfill-rule)[^}]*(content:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim,l=/(polyfill-unscoped-rule)[^}]*(content:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim,h="-shadowcsshost",f="-shadowcsscontext",d=")(?:\\(((?:\\([^)(]*\\)|[^)(]*)+?)\\))?([^,{]*)",v=a.RegExpWrapper.create("("+h+d,"im"),y=a.RegExpWrapper.create("("+f+d,"im"),m=h+"-no-combinator",g=[/::shadow/g,/::content/g,/\/shadow-deep\//g,/\/shadow\//g],_=/(?:>>>)|(?:\/deep\/)/g,b="([>\\s~+[.,{:][\\s\\S]*)?$",P=a.RegExpWrapper.create(h,"im"),E=/:host/gim,w=/:host-context/gim,C=/\/\*[\s\S]*?\*\//g,R=/(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g,S=/([{}])/g,O="{",T="}",x="%BLOCK%",A=function(){function t(t,e){this.selector=t,this.content=e}return t}();e.CssRule=A,e.processRules=i;var I=function(){function t(t,e){this.escapedString=t,this.blocks=e}return t}()},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(6),s=n(169),a=n(174),u=n(176),c=n(162),p=function(){function t(t,e,n){this.statements=t,this.viewFactoryVar=e,this.dependencies=n}return t}();e.ViewCompileResult=p;var l=function(){function t(t){this._genConfig=t}return t.prototype.compileComponent=function(t,e,n,r){var i=[],o=[],c=new a.CompileView(t,this._genConfig,r,n,0,s.CompileElement.createNull(),[]);return u.buildView(c,e,o,i),new p(i,c.viewFactory.name,o)},t=r([o.Injectable(),i("design:paramtypes",[c.CompilerConfig])],t)}();e.ViewCompiler=l},function(t,e,n){"use strict";function r(t,e,n,r){var i;return i=e>0?s.literal(t).lowerEquals(u.InjectMethodVars.requestNodeIndex).and(u.InjectMethodVars.requestNodeIndex.lowerEquals(s.literal(t+e))):s.literal(t).identical(u.InjectMethodVars.requestNodeIndex),new s.IfStmt(u.InjectMethodVars.token.identical(f.createDiTokenExpression(n.token)).and(i),[new s.ReturnStatement(r)])}function i(t,e,n,r,i,o){var a,u,p=o.view;if(r?(a=s.literalArr(n),u=new s.ArrayType(s.DYNAMIC_TYPE)):(a=n[0],u=n[0].type),c.isBlank(u)&&(u=s.DYNAMIC_TYPE),i)p.fields.push(new s.ClassField(t,u,[s.StmtModifier.Private])),p.createMethod.addStmt(s.THIS_EXPR.prop(t).set(a).toStmt());else{var l="_"+t;p.fields.push(new s.ClassField(l,u,[s.StmtModifier.Private]));var h=new v.CompileMethod(p);h.resetDebugInfo(o.nodeIndex,o.sourceAst),h.addStmt(new s.IfStmt(s.THIS_EXPR.prop(l).isBlank(),[s.THIS_EXPR.prop(l).set(a).toStmt()])),h.addStmt(new s.ReturnStatement(s.THIS_EXPR.prop(l))),p.getters.push(new s.ClassGetter(t,h.finish(),u))}return s.THIS_EXPR.prop(t)}var o=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},s=n(164),a=n(158),u=n(170),c=n(5),p=n(15),l=n(139),h=n(155),f=n(171),d=n(172),v=n(173),y=function(){function t(t,e,n,r,i){this.parent=t,this.view=e,this.nodeIndex=n,this.renderNode=r,this.sourceAst=i}return t.prototype.isNull=function(){return c.isBlank(this.renderNode)},t.prototype.isRootElement=function(){return this.view!=this.parent.view},t}();e.CompileNode=y;var m=function(t){function e(e,n,r,i,o,u,p,l,f,d,v){t.call(this,e,n,r,i,o),this.component=u,this._directives=p,this._resolvedProvidersArray=l,this.hasViewContainer=f,this.hasEmbeddedView=d,this.variableTokens=v,this._compViewExpr=null,this._instances=new h.CompileTokenMap,this._queryCount=0,this._queries=new h.CompileTokenMap,this._componentConstructorViewQueryLists=[],this.contentNodesByNgContentIndex=null,this.elementRef=s.importExpr(a.Identifiers.ElementRef).instantiate([this.renderNode]),this._instances.add(a.identifierToken(a.Identifiers.ElementRef),this.elementRef),this.injector=s.THIS_EXPR.callMethod("injector",[s.literal(this.nodeIndex)]),this._instances.add(a.identifierToken(a.Identifiers.Injector),this.injector),this._instances.add(a.identifierToken(a.Identifiers.Renderer),s.THIS_EXPR.prop("renderer")),(this.hasViewContainer||this.hasEmbeddedView||c.isPresent(this.component))&&this._createAppElement()}return o(e,t),e.createNull=function(){return new e(null,null,null,null,null,null,[],[],!1,!1,{})},e.prototype._createAppElement=function(){var t="_appEl_"+this.nodeIndex,e=this.isRootElement()?null:this.parent.nodeIndex;this.view.fields.push(new s.ClassField(t,s.importType(a.Identifiers.AppElement),[s.StmtModifier.Private]));var n=s.THIS_EXPR.prop(t).set(s.importExpr(a.Identifiers.AppElement).instantiate([s.literal(this.nodeIndex),s.literal(e),s.THIS_EXPR,this.renderNode])).toStmt();this.view.createMethod.addStmt(n),this.appElement=s.THIS_EXPR.prop(t),this._instances.add(a.identifierToken(a.Identifiers.AppElement),this.appElement)},e.prototype.setComponentView=function(t){this._compViewExpr=t,this.contentNodesByNgContentIndex=p.ListWrapper.createFixedSize(this.component.template.ngContentSelectors.length);for(var e=0;e=i})),r._directives.length>0&&i++,r=r.parent;return e=this.view.componentView.viewQueries.get(t),c.isPresent(e)&&p.ListWrapper.addAll(n,e),n},e.prototype._addQuery=function(t,e){var n="_query_"+t.selectors[0].name+"_"+this.nodeIndex+"_"+this._queryCount++,r=d.createQueryList(t,e,n,this.view),i=new d.CompileQuery(t,r,e,this.view);return d.addQueryToTokenMap(this._queries,i),i},e.prototype._getLocalDependency=function(t,e){var n=null;if(c.isBlank(n)&&c.isPresent(e.query)&&(n=this._addQuery(e.query,null).queryList),c.isBlank(n)&&c.isPresent(e.viewQuery)&&(n=d.createQueryList(e.viewQuery,null,"_viewQuery_"+e.viewQuery.selectors[0].name+"_"+this.nodeIndex+"_"+this._componentConstructorViewQueryLists.length,this.view),this._componentConstructorViewQueryLists.push(n)),c.isPresent(e.token)){if(c.isBlank(n)&&e.token.equalsTo(a.identifierToken(a.Identifiers.ChangeDetectorRef)))return t===l.ProviderAstType.Component?this._compViewExpr.prop("ref"):s.THIS_EXPR.prop("ref");c.isBlank(n)&&(n=this._instances.get(e.token))}return n},e.prototype._getDependency=function(t,e){var n=this,r=null;for(e.isValue&&(r=s.literal(e.value)),c.isBlank(r)&&!e.isSkipSelf&&(r=this._getLocalDependency(t,e));c.isBlank(r)&&!n.parent.isNull();)n=n.parent,r=n._getLocalDependency(l.ProviderAstType.PublicService,new h.CompileDiDependencyMetadata({token:e.token}));return c.isBlank(r)&&(r=f.injectFromViewParentInjector(e.token,e.isOptional)),c.isBlank(r)&&(r=s.NULL_EXPR),f.getPropertyInView(r,this.view,n.view)},e}(y);e.CompileElement=m;var g=function(){function t(t,e){this.query=t,this.read=c.isPresent(t.meta.read)?t.meta.read:e}return t}()},function(t,e,n){"use strict";function r(t,e){if(i.isBlank(e))return c.NULL_EXPR;var n=i.resolveEnumToken(t.runtime,e);return c.importExpr(new o.CompileIdentifierMetadata({name:t.name+"."+n,moduleUrl:t.moduleUrl,runtime:e}))}var i=n(5),o=n(155),s=n(28),a=n(36),u=n(68),c=n(164),p=n(158),l=function(){function t(){}return t.fromValue=function(t){return r(p.Identifiers.ViewType,t)},t.HOST=t.fromValue(u.ViewType.HOST),t.COMPONENT=t.fromValue(u.ViewType.COMPONENT),t.EMBEDDED=t.fromValue(u.ViewType.EMBEDDED),t}();e.ViewTypeEnum=l;var h=function(){function t(){}return t.fromValue=function(t){return r(p.Identifiers.ViewEncapsulation,t)},t.Emulated=t.fromValue(a.ViewEncapsulation.Emulated),t.Native=t.fromValue(a.ViewEncapsulation.Native),t.None=t.fromValue(a.ViewEncapsulation.None),t}();e.ViewEncapsulationEnum=h;var f=function(){function t(){}return t.fromValue=function(t){return r(p.Identifiers.ChangeDetectorState,t)},t.NeverChecked=t.fromValue(s.ChangeDetectorState.NeverChecked),t.CheckedBefore=t.fromValue(s.ChangeDetectorState.CheckedBefore),t.Errored=t.fromValue(s.ChangeDetectorState.Errored),t}();e.ChangeDetectorStateEnum=f;var d=function(){function t(){}return t.fromValue=function(t){return r(p.Identifiers.ChangeDetectionStrategy,t)},t.CheckOnce=t.fromValue(s.ChangeDetectionStrategy.CheckOnce),t.Checked=t.fromValue(s.ChangeDetectionStrategy.Checked),t.CheckAlways=t.fromValue(s.ChangeDetectionStrategy.CheckAlways),t.Detached=t.fromValue(s.ChangeDetectionStrategy.Detached),t.OnPush=t.fromValue(s.ChangeDetectionStrategy.OnPush),t.Default=t.fromValue(s.ChangeDetectionStrategy.Default),t}();e.ChangeDetectionStrategyEnum=d;var v=function(){function t(){}return t.viewUtils=c.variable("viewUtils"),t.parentInjector=c.variable("parentInjector"),t.declarationEl=c.variable("declarationEl"),t}();e.ViewConstructorVars=v;var y=function(){function t(){}return t.renderer=c.THIS_EXPR.prop("renderer"),t.projectableNodes=c.THIS_EXPR.prop("projectableNodes"),t.viewUtils=c.THIS_EXPR.prop("viewUtils"),t}();e.ViewProperties=y;var m=function(){function t(){}return t.event=c.variable("$event"),t}();e.EventHandlerVars=m;var g=function(){function t(){}return t.token=c.variable("token"),t.requestNodeIndex=c.variable("requestNodeIndex"),t.notFoundResult=c.variable("notFoundResult"),t}();e.InjectMethodVars=g;var _=function(){function t(){}return t.throwOnChange=c.variable("throwOnChange"),t.changes=c.variable("changes"),t.changed=c.variable("changed"),t.valUnwrapper=c.variable("valUnwrapper"),t}();e.DetectChangesVars=_},function(t,e,n){"use strict";function r(t,e,n){if(e===n)return t;for(var r=l.THIS_EXPR,i=e;i!==n&&c.isPresent(i.declarationElement.view);)i=i.declarationElement.view,r=r.prop("parent");if(i!==n)throw new p.BaseException("Internal error: Could not calculate a property in a parent view: "+t);if(t instanceof l.ReadPropExpr){var o=t;(n.fields.some(function(t){return t.name==o.name})||n.getters.some(function(t){return t.name==o.name}))&&(r=r.cast(n.classType))}return l.replaceVarInExpression(l.THIS_EXPR.name,r,t)}function i(t,e){var n=[s(t)];return e&&n.push(l.NULL_EXPR),l.THIS_EXPR.prop("parentInjector").callMethod("get",n)}function o(t,e){return"viewFactory_"+t.type.name+e}function s(t){return c.isPresent(t.value)?l.literal(t.value):t.identifierIsInstance?l.importExpr(t.identifier).instantiate([],l.importType(t.identifier,[],[l.TypeModifier.Const])):l.importExpr(t.identifier)}function a(t){for(var e=[],n=l.literalArr([]),r=0;r0&&(n=n.callMethod(l.BuiltinMethod.ConcatArray,[l.literalArr(e)]),e=[]),n=n.callMethod(l.BuiltinMethod.ConcatArray,[i])):e.push(i)}return e.length>0&&(n=n.callMethod(l.BuiltinMethod.ConcatArray,[l.literalArr(e)])),n}function u(t,e,n,r){r.fields.push(new l.ClassField(n.name,null,[l.StmtModifier.Private]));var i=e0?s.values[s.values.length-1]:null;if(e instanceof h&&e.view===t.embeddedView)s=e;else{var n=new h(t.embeddedView,[]);s.values.push(n),s=n}}),s.values.push(t),r.length>0&&e.dirtyParentQueriesMethod.addStmt(o.callMethod("setDirty",[]).toStmt())},t.prototype.afterChildren=function(t){var e=r(this._values),n=[this.queryList.callMethod("reset",[c.literalArr(e)]).toStmt()];if(a.isPresent(this.ownerDirectiveExpression)){var i=this.meta.first?this.queryList.prop("first"):this.queryList;n.push(this.ownerDirectiveExpression.prop(this.meta.propertyName).set(i).toStmt())}this.meta.first||n.push(this.queryList.callMethod("notifyOnChanges",[]).toStmt()),t.addStmt(new c.IfStmt(this.queryList.prop("dirty"),n))},t}();e.CompileQuery=f,e.createQueryList=o,e.addQueryToTokenMap=s},function(t,e,n){"use strict";var r=n(5),i=n(15),o=n(164),s=function(){function t(t,e){this.nodeIndex=t,this.sourceAst=e}return t}(),a=new s(null,null),u=function(){function t(t){this._view=t,this._newState=a,this._currState=a,this._bodyStatements=[],this._debugEnabled=this._view.genConfig.genDebugInfo}return t.prototype._updateDebugContextIfNeeded=function(){if(this._newState.nodeIndex!==this._currState.nodeIndex||this._newState.sourceAst!==this._currState.sourceAst){var t=this._updateDebugContext(this._newState);r.isPresent(t)&&this._bodyStatements.push(t.toStmt())}},t.prototype._updateDebugContext=function(t){if(this._currState=this._newState=t,this._debugEnabled){var e=r.isPresent(t.sourceAst)?t.sourceAst.sourceSpan.start:null;return o.THIS_EXPR.callMethod("debug",[o.literal(t.nodeIndex),r.isPresent(e)?o.literal(e.line):o.NULL_EXPR,r.isPresent(e)?o.literal(e.col):o.NULL_EXPR])}return null},t.prototype.resetDebugInfoExpr=function(t,e){var n=this._updateDebugContext(new s(t,e));return r.isPresent(n)?n:o.NULL_EXPR},t.prototype.resetDebugInfo=function(t,e){this._newState=new s(t,e)},t.prototype.addStmt=function(t){this._updateDebugContextIfNeeded(),this._bodyStatements.push(t)},t.prototype.addStmts=function(t){this._updateDebugContextIfNeeded(),i.ListWrapper.addAll(this._bodyStatements,t)},t.prototype.finish=function(){return this._bodyStatements},t.prototype.isEmpty=function(){return 0===this._bodyStatements.length},t}();e.CompileMethod=u},function(t,e,n){"use strict";function r(t,e){return e>0?l.ViewType.EMBEDDED:t.type.isHost?l.ViewType.HOST:l.ViewType.COMPONENT}var i=n(5),o=n(15),s=n(164),a=n(170),u=n(172),c=n(173),p=n(175),l=n(68),h=n(155),f=n(171),d=function(){function t(t,e,n,a,p,d,v){var y=this;this.component=t,this.genConfig=e,this.pipeMetas=n,this.styles=a,this.viewIndex=p,this.declarationElement=d,this.templateVariableBindings=v,this.nodes=[],this.rootNodesOrAppElements=[],this.bindings=[],this.classStatements=[],this.eventHandlerMethods=[],this.fields=[],this.getters=[],this.disposables=[],this.subscriptions=[],this.purePipes=new Map,this.pipes=[],this.variables=new Map,this.literalArrayCount=0,this.literalMapCount=0,this.pipeCount=0,this.createMethod=new c.CompileMethod(this),this.injectorGetMethod=new c.CompileMethod(this),this.updateContentQueriesMethod=new c.CompileMethod(this),this.dirtyParentQueriesMethod=new c.CompileMethod(this),this.updateViewQueriesMethod=new c.CompileMethod(this),this.detectChangesInInputsMethod=new c.CompileMethod(this),this.detectChangesRenderPropertiesMethod=new c.CompileMethod(this),this.afterContentLifecycleCallbacksMethod=new c.CompileMethod(this),this.afterViewLifecycleCallbacksMethod=new c.CompileMethod(this),this.destroyMethod=new c.CompileMethod(this),this.viewType=r(t,p),this.className="_View_"+t.type.name+p,this.classType=s.importType(new h.CompileIdentifierMetadata({name:this.className})),this.viewFactory=s.variable(f.getViewFactoryName(t,p)),this.viewType===l.ViewType.COMPONENT||this.viewType===l.ViewType.HOST?this.componentView=this:this.componentView=this.declarationElement.view.componentView;var m=new h.CompileTokenMap;if(this.viewType===l.ViewType.COMPONENT){var g=s.THIS_EXPR.prop("context");o.ListWrapper.forEachWithIndex(this.component.viewQueries,function(t,e){var n="_viewQuery_"+t.selectors[0].name+"_"+e,r=u.createQueryList(t,g,n,y),i=new u.CompileQuery(t,r,g,y);u.addQueryToTokenMap(m,i)});var _=0;this.component.type.diDeps.forEach(function(t){if(i.isPresent(t.viewQuery)){var e=s.THIS_EXPR.prop("declarationAppElement").prop("componentConstructorViewQueries").key(s.literal(_++)),n=new u.CompileQuery(t.viewQuery,e,null,y);u.addQueryToTokenMap(m,n)}})}this.viewQueries=m,v.forEach(function(t){y.variables.set(t[1],s.THIS_EXPR.prop("locals").key(s.literal(t[0])))}),this.declarationElement.isNull()||this.declarationElement.setEmbeddedView(this)}return t.prototype.callPipe=function(t,e,n){var r=this.componentView,o=r.purePipes.get(t);return i.isBlank(o)&&(o=new p.CompilePipe(r,t),o.pure&&r.purePipes.set(t,o),r.pipes.push(o)),o.call(this,[e].concat(n))},t.prototype.getVariable=function(t){if(t==a.EventHandlerVars.event.name)return a.EventHandlerVars.event;for(var e=this,n=e.variables.get(t);i.isBlank(n)&&i.isPresent(e.declarationElement.view);)e=e.declarationElement.view,n=e.variables.get(t);return i.isPresent(n)?f.getPropertyInView(n,this,e):null},t.prototype.createLiteralArray=function(t){for(var e=s.THIS_EXPR.prop("_arr_"+this.literalArrayCount++),n=[],r=[],i=0;i=0;r--){var s=t.pipeMetas[r];if(s.name==e){n=s;break}}if(i.isBlank(n))throw new o.BaseException("Illegal state: Could not find pipe "+e+" although the parser should have detected this error!");return n}var i=n(5),o=n(12),s=n(164),a=n(158),u=n(171),c=function(){function t(t,e){this.instance=t,this.argCount=e}return t}(),p=function(){function t(t,e){this.view=t,this._purePipeProxies=[],this.meta=r(t,e),this.instance=s.THIS_EXPR.prop("_pipe_"+e+"_"+t.pipeCount++)}return Object.defineProperty(t.prototype,"pure",{get:function(){return this.meta.pure},enumerable:!0,configurable:!0}),t.prototype.create=function(){var t=this,e=this.meta.type.diDeps.map(function(t){return t.token.equalsTo(a.identifierToken(a.Identifiers.ChangeDetectorRef))?s.THIS_EXPR.prop("ref"):u.injectFromViewParentInjector(t.token,!1)});this.view.fields.push(new s.ClassField(this.instance.name,s.importType(this.meta.type),[s.StmtModifier.Private])),this.view.createMethod.resetDebugInfo(null,null),this.view.createMethod.addStmt(s.THIS_EXPR.prop(this.instance.name).set(s.importExpr(this.meta.type).instantiate(e)).toStmt()),this._purePipeProxies.forEach(function(e){u.createPureProxy(t.instance.prop("transform").callMethod(s.BuiltinMethod.bind,[t.instance]),e.argCount,e.instance,t.view)})},t.prototype.call=function(t,e){if(this.meta.pure){var n=new c(s.THIS_EXPR.prop(this.instance.name+"_"+this._purePipeProxies.length),e.length);return this._purePipeProxies.push(n),u.getPropertyInView(s.importExpr(a.Identifiers.castByValue).callFn([n.instance,this.instance.prop("transform")]),t,this.view).callFn(e)}return u.getPropertyInView(this.instance,t,this.view).callMethod("transform",e)},t}();e.CompilePipe=p},function(t,e,n){"use strict";function r(t,e,n,r){var i=new L(t,n,r);return S.templateVisitAll(i,e,t.declarationElement.isNull()?t.declarationElement:t.declarationElement.parent),I.bindView(t,e),t.afterNodes(),c(t,r),i.nestedViewCount}function i(t,e){var n={};return _.StringMapWrapper.forEach(t,function(t,e){n[e]=t}),e.forEach(function(t){_.StringMapWrapper.forEach(t.hostAttributes,function(t,e){var r=n[e];n[e]=g.isPresent(r)?a(e,r,t):t})}),u(n)}function o(t){var e={};return t.forEach(function(t){e[t.name]=t.value}),e}function s(t,e,n){var r={},i=null;return e.forEach(function(t){t.directive.isComponent&&(i=t.directive),t.exportAsVars.forEach(function(e){r[e.name]=P.identifierToken(t.directive.type)})}),t.forEach(function(t){r[t.name]=g.isPresent(i)?P.identifierToken(i.type):null}),r}function a(t,e,n){return t==k||t==N?e+" "+n:n}function u(t){var e=[];_.StringMapWrapper.forEach(t,function(t,n){e.push([n,t])}),_.ListWrapper.sort(e,function(t,e){return g.StringWrapper.compare(t[0],e[0])});var n=[];return e.forEach(function(t){n.push([t[0],t[1]])}),n}function c(t,e){var n=b.NULL_EXPR;t.genConfig.genDebugInfo&&(n=b.variable("nodeDebugInfos_"+t.component.type.name+t.viewIndex),e.push(n.set(b.literalArr(t.nodes.map(p),new b.ArrayType(new b.ExternalType(P.Identifiers.StaticNodeDebugInfo),[b.TypeModifier.Const]))).toDeclStmt(null,[b.StmtModifier.Final])));var r=b.variable("renderType_"+t.component.type.name);0===t.viewIndex&&e.push(r.set(b.NULL_EXPR).toDeclStmt(b.importType(P.Identifiers.RenderComponentType)));var i=l(t,r,n);e.push(i),e.push(h(t,i,r))}function p(t){var e=t instanceof R.CompileElement?t:null,n=[],r=b.NULL_EXPR,i=[];return g.isPresent(e)&&(n=e.getProviderTokens(),g.isPresent(e.component)&&(r=O.createDiTokenExpression(P.identifierToken(e.component.type))),_.StringMapWrapper.forEach(e.variableTokens,function(t,e){i.push([e,g.isPresent(t)?O.createDiTokenExpression(t):b.NULL_EXPR])})),b.importExpr(P.Identifiers.StaticNodeDebugInfo).instantiate([b.literalArr(n,new b.ArrayType(b.DYNAMIC_TYPE,[b.TypeModifier.Const])),r,b.literalMap(i,new b.MapType(b.DYNAMIC_TYPE,[b.TypeModifier.Const]))],b.importType(P.Identifiers.StaticNodeDebugInfo,null,[b.TypeModifier.Const]))}function l(t,e,n){var r=t.templateVariableBindings.map(function(t){return[t[0],b.NULL_EXPR]}),i=[new b.FnParam(E.ViewConstructorVars.viewUtils.name,b.importType(P.Identifiers.ViewUtils)),new b.FnParam(E.ViewConstructorVars.parentInjector.name,b.importType(P.Identifiers.Injector)),new b.FnParam(E.ViewConstructorVars.declarationEl.name,b.importType(P.Identifiers.AppElement))],o=new b.ClassMethod(null,i,[b.SUPER_EXPR.callFn([b.variable(t.className),e,E.ViewTypeEnum.fromValue(t.viewType),b.literalMap(r),E.ViewConstructorVars.viewUtils,E.ViewConstructorVars.parentInjector,E.ViewConstructorVars.declarationEl,E.ChangeDetectionStrategyEnum.fromValue(m(t)),n]).toStmt()]),s=[new b.ClassMethod("createInternal",[new b.FnParam(V.name,b.STRING_TYPE)],f(t),b.importType(P.Identifiers.AppElement)),new b.ClassMethod("injectorGetInternal",[new b.FnParam(E.InjectMethodVars.token.name,b.DYNAMIC_TYPE),new b.FnParam(E.InjectMethodVars.requestNodeIndex.name,b.NUMBER_TYPE),new b.FnParam(E.InjectMethodVars.notFoundResult.name,b.DYNAMIC_TYPE)],v(t.injectorGetMethod.finish(),E.InjectMethodVars.notFoundResult),b.DYNAMIC_TYPE),new b.ClassMethod("detectChangesInternal",[new b.FnParam(E.DetectChangesVars.throwOnChange.name,b.BOOL_TYPE)],d(t)),new b.ClassMethod("dirtyParentQueriesInternal",[],t.dirtyParentQueriesMethod.finish()),new b.ClassMethod("destroyInternal",[],t.destroyMethod.finish())].concat(t.eventHandlerMethods),a=new b.ClassStmt(t.className,b.importExpr(P.Identifiers.AppView,[y(t)]),t.fields,t.getters,o,s.filter(function(t){return t.body.length>0}));return a}function h(t,e,n){var r,i=[new b.FnParam(E.ViewConstructorVars.viewUtils.name,b.importType(P.Identifiers.ViewUtils)),new b.FnParam(E.ViewConstructorVars.parentInjector.name,b.importType(P.Identifiers.Injector)),new b.FnParam(E.ViewConstructorVars.declarationEl.name,b.importType(P.Identifiers.AppElement))],o=[];return r=t.component.template.templateUrl==t.component.type.moduleUrl?t.component.type.moduleUrl+" class "+t.component.type.name+" - inline template":t.component.template.templateUrl,0===t.viewIndex&&(o=[new b.IfStmt(n.identical(b.NULL_EXPR),[n.set(E.ViewConstructorVars.viewUtils.callMethod("createRenderComponentType",[b.literal(r),b.literal(t.component.template.ngContentSelectors.length),E.ViewEncapsulationEnum.fromValue(t.component.template.encapsulation),t.styles])).toStmt()])]), -b.fn(i,o.concat([new b.ReturnStatement(b.variable(e.name).instantiate(e.constructorMethod.params.map(function(t){return b.variable(t.name)})))]),b.importType(P.Identifiers.AppView,[y(t)])).toDeclStmt(t.viewFactory.name,[b.StmtModifier.Final])}function f(t){var e=b.NULL_EXPR,n=[];t.viewType===T.ViewType.COMPONENT&&(e=E.ViewProperties.renderer.callMethod("createViewRoot",[b.THIS_EXPR.prop("declarationAppElement").prop("nativeElement")]),n=[D.set(e).toDeclStmt(b.importType(t.genConfig.renderTypes.renderNode),[b.StmtModifier.Final])]);var r;return r=t.viewType===T.ViewType.HOST?t.nodes[0].appElement:b.NULL_EXPR,n.concat(t.createMethod.finish()).concat([b.THIS_EXPR.callMethod("init",[O.createFlatArray(t.rootNodesOrAppElements),b.literalArr(t.nodes.map(function(t){return t.renderNode})),b.literalArr(t.disposables),b.literalArr(t.subscriptions)]).toStmt(),new b.ReturnStatement(r)])}function d(t){var e=[];if(t.detectChangesInInputsMethod.isEmpty()&&t.updateContentQueriesMethod.isEmpty()&&t.afterContentLifecycleCallbacksMethod.isEmpty()&&t.detectChangesRenderPropertiesMethod.isEmpty()&&t.updateViewQueriesMethod.isEmpty()&&t.afterViewLifecycleCallbacksMethod.isEmpty())return e;_.ListWrapper.addAll(e,t.detectChangesInInputsMethod.finish()),e.push(b.THIS_EXPR.callMethod("detectContentChildrenChanges",[E.DetectChangesVars.throwOnChange]).toStmt());var n=t.updateContentQueriesMethod.finish().concat(t.afterContentLifecycleCallbacksMethod.finish());n.length>0&&e.push(new b.IfStmt(b.not(E.DetectChangesVars.throwOnChange),n)),_.ListWrapper.addAll(e,t.detectChangesRenderPropertiesMethod.finish()),e.push(b.THIS_EXPR.callMethod("detectViewChildrenChanges",[E.DetectChangesVars.throwOnChange]).toStmt());var r=t.updateViewQueriesMethod.finish().concat(t.afterViewLifecycleCallbacksMethod.finish());r.length>0&&e.push(new b.IfStmt(b.not(E.DetectChangesVars.throwOnChange),r));var i=[],o=b.findReadVarNames(e);return _.SetWrapper.has(o,E.DetectChangesVars.changed.name)&&i.push(E.DetectChangesVars.changed.set(b.literal(!0)).toDeclStmt(b.BOOL_TYPE)),_.SetWrapper.has(o,E.DetectChangesVars.changes.name)&&i.push(E.DetectChangesVars.changes.set(b.NULL_EXPR).toDeclStmt(new b.MapType(b.importType(P.Identifiers.SimpleChange)))),_.SetWrapper.has(o,E.DetectChangesVars.valUnwrapper.name)&&i.push(E.DetectChangesVars.valUnwrapper.set(b.importExpr(P.Identifiers.ValueUnwrapper).instantiate([])).toDeclStmt(null,[b.StmtModifier.Final])),i.concat(e)}function v(t,e){return t.length>0?t.concat([new b.ReturnStatement(e)]):t}function y(t){var e=t.component.type;return e.isHost?b.DYNAMIC_TYPE:b.importType(e)}function m(t){var e;return e=t.viewType===T.ViewType.COMPONENT?w.isDefaultChangeDetectionStrategy(t.component.changeDetection)?w.ChangeDetectionStrategy.CheckAlways:w.ChangeDetectionStrategy.CheckOnce:w.ChangeDetectionStrategy.CheckAlways}var g=n(5),_=n(15),b=n(164),P=n(158),E=n(170),w=n(28),C=n(174),R=n(169),S=n(139),O=n(171),T=n(68),x=n(36),A=n(155),I=n(177),M="$implicit",k="class",N="style",D=b.variable("parentRenderNode"),V=b.variable("rootSelector"),j=function(){function t(t,e){this.comp=t,this.factoryPlaceholder=e}return t}();e.ViewCompileDependency=j,e.buildView=r;var L=function(){function t(t,e,n){this.view=t,this.targetDependencies=e,this.targetStatements=n,this.nestedViewCount=0}return t.prototype._isRootNode=function(t){return t.view!==this.view},t.prototype._addRootNodeAndProject=function(t,e,n){var r=t instanceof R.CompileElement&&t.hasViewContainer?t.appElement:null;this._isRootNode(n)?this.view.viewType!==T.ViewType.COMPONENT&&this.view.rootNodesOrAppElements.push(g.isPresent(r)?r:t.renderNode):g.isPresent(n.component)&&g.isPresent(e)&&n.addContentNode(e,g.isPresent(r)?r:t.renderNode)},t.prototype._getParentRenderNode=function(t){return this._isRootNode(t)?this.view.viewType===T.ViewType.COMPONENT?D:b.NULL_EXPR:g.isPresent(t.component)&&t.component.template.encapsulation!==x.ViewEncapsulation.Native?b.NULL_EXPR:t.renderNode},t.prototype.visitBoundText=function(t,e){return this._visitText(t,"",t.ngContentIndex,e)},t.prototype.visitText=function(t,e){return this._visitText(t,t.value,t.ngContentIndex,e)},t.prototype._visitText=function(t,e,n,r){var i="_text_"+this.view.nodes.length;this.view.fields.push(new b.ClassField(i,b.importType(this.view.genConfig.renderTypes.renderText),[b.StmtModifier.Private]));var o=b.THIS_EXPR.prop(i),s=new R.CompileNode(r,this.view,this.view.nodes.length,o,t),a=b.THIS_EXPR.prop(i).set(E.ViewProperties.renderer.callMethod("createText",[this._getParentRenderNode(r),b.literal(e),this.view.createMethod.resetDebugInfoExpr(this.view.nodes.length,t)])).toStmt();return this.view.nodes.push(s),this.view.createMethod.addStmt(a),this._addRootNodeAndProject(s,n,r),o},t.prototype.visitNgContent=function(t,e){this.view.createMethod.resetDebugInfo(null,t);var n=this._getParentRenderNode(e),r=E.ViewProperties.projectableNodes.key(b.literal(t.index),new b.ArrayType(b.importType(this.view.genConfig.renderTypes.renderNode)));return n!==b.NULL_EXPR?this.view.createMethod.addStmt(E.ViewProperties.renderer.callMethod("projectNodes",[n,b.importExpr(P.Identifiers.flattenNestedViewRenderNodes).callFn([r])]).toStmt()):this._isRootNode(e)?this.view.viewType!==T.ViewType.COMPONENT&&this.view.rootNodesOrAppElements.push(r):g.isPresent(e.component)&&g.isPresent(t.ngContentIndex)&&e.addContentNode(t.ngContentIndex,r),null},t.prototype.visitElement=function(t,e){var n,r=this.view.nodes.length,a=this.view.createMethod.resetDebugInfoExpr(r,t);n=0===r&&this.view.viewType===T.ViewType.HOST?b.THIS_EXPR.callMethod("selectOrCreateHostElement",[b.literal(t.name),V,a]):E.ViewProperties.renderer.callMethod("createElement",[this._getParentRenderNode(e),b.literal(t.name),a]);var u="_el_"+r;this.view.fields.push(new b.ClassField(u,b.importType(this.view.genConfig.renderTypes.renderElement),[b.StmtModifier.Private])),this.view.createMethod.addStmt(b.THIS_EXPR.prop(u).set(n).toStmt());for(var c=b.THIS_EXPR.prop(u),p=t.getComponent(),l=t.directives.map(function(t){return t.directive}),h=s(t.exportAsVars,t.directives,this.view.viewType),f=o(t.attrs),d=i(f,l),v=0;v0?t.value:M,t.name]}),a=t.directives.map(function(t){return t.directive}),u=new R.CompileElement(e,this.view,n,o,t,null,a,t.providers,t.hasViewContainer,!0,{});this.view.nodes.push(u),this.nestedViewCount++;var c=new C.CompileView(this.view.component,this.view.genConfig,this.view.pipeMetas,b.NULL_EXPR,this.view.viewIndex+this.nestedViewCount,u,s);return this.nestedViewCount+=r(c,t.children,this.targetDependencies,this.targetStatements),u.beforeChildren(),this._addRootNodeAndProject(u,t.ngContentIndex,e),u.afterChildren(0),null},t.prototype.visitAttr=function(t,e){return null},t.prototype.visitDirective=function(t,e){return null},t.prototype.visitEvent=function(t,e){return null},t.prototype.visitVariable=function(t,e){return null},t.prototype.visitDirectiveProperty=function(t,e){return null},t.prototype.visitElementProperty=function(t,e){return null},t}()},function(t,e,n){"use strict";function r(t,e){var n=new c(t);o.templateVisitAll(n,e),t.pipes.forEach(function(t){u.bindPipeDestroyLifecycleCallbacks(t.meta,t.instance,t.view)})}var i=n(15),o=n(139),s=n(178),a=n(181),u=n(182);e.bindView=r;var c=function(){function t(t){this.view=t,this._nodeIndex=0}return t.prototype.visitBoundText=function(t,e){var n=this.view.nodes[this._nodeIndex++];return s.bindRenderText(t,n,this.view),null},t.prototype.visitText=function(t,e){return this._nodeIndex++,null},t.prototype.visitNgContent=function(t,e){return null},t.prototype.visitElement=function(t,e){var n=this.view.nodes[this._nodeIndex++],r=a.collectEventListeners(t.outputs,t.directives,n);return s.bindRenderInputs(t.inputs,n),a.bindRenderOutputs(r),i.ListWrapper.forEachWithIndex(t.directives,function(t,e){var i=n.directiveInstances[e];s.bindDirectiveInputs(t,i,n),u.bindDirectiveDetectChangesLifecycleCallbacks(t,i,n),s.bindDirectiveHostProps(t,i,n),a.bindDirectiveOutputs(t,i,r)}),o.templateVisitAll(this,t.children,n),i.ListWrapper.forEachWithIndex(t.directives,function(t,e){var r=n.directiveInstances[e];u.bindDirectiveAfterContentLifecycleCallbacks(t.directive,r,n),u.bindDirectiveAfterViewLifecycleCallbacks(t.directive,r,n),u.bindDirectiveDestroyLifecycleCallbacks(t.directive,r,n)}),null},t.prototype.visitEmbeddedTemplate=function(t,e){var n=this.view.nodes[this._nodeIndex++],r=a.collectEventListeners(t.outputs,t.directives,n);return i.ListWrapper.forEachWithIndex(t.directives,function(t,e){var i=n.directiveInstances[e];s.bindDirectiveInputs(t,i,n),u.bindDirectiveDetectChangesLifecycleCallbacks(t,i,n),a.bindDirectiveOutputs(t,i,r),u.bindDirectiveAfterContentLifecycleCallbacks(t.directive,i,n),u.bindDirectiveAfterViewLifecycleCallbacks(t.directive,i,n),u.bindDirectiveDestroyLifecycleCallbacks(t.directive,i,n)}),null},t.prototype.visitAttr=function(t,e){return null},t.prototype.visitDirective=function(t,e){return null},t.prototype.visitEvent=function(t,e){return null},t.prototype.visitVariable=function(t,e){return null},t.prototype.visitDirectiveProperty=function(t,e){return null},t.prototype.visitElementProperty=function(t,e){return null},t}()},function(t,e,n){"use strict";function r(t){return h.THIS_EXPR.prop("_expr_"+t)}function i(t){return h.variable("currVal_"+t)}function o(t,e,n,r,i,o,s){var a=b.convertCdExpressionToIr(t,i,r,d.DetectChangesVars.valUnwrapper);if(!y.isBlank(a.expression)){if(t.fields.push(new h.ClassField(n.name,null,[h.StmtModifier.Private])),t.createMethod.addStmt(h.THIS_EXPR.prop(n.name).set(h.importExpr(f.Identifiers.uninitialized)).toStmt()),a.needsValueUnwrapper){var u=d.DetectChangesVars.valUnwrapper.callMethod("reset",[]).toStmt();s.addStmt(u)}s.addStmt(e.set(a.expression).toDeclStmt(null,[h.StmtModifier.Final]));var c=h.importExpr(f.Identifiers.checkBinding).callFn([d.DetectChangesVars.throwOnChange,n,e]);a.needsValueUnwrapper&&(c=d.DetectChangesVars.valUnwrapper.prop("hasWrappedValue").or(c)),s.addStmt(new h.IfStmt(c,o.concat([h.THIS_EXPR.prop(n.name).set(e).toStmt()])))}}function s(t,e,n){var s=n.bindings.length;n.bindings.push(new P.CompileBinding(e,t));var a=i(s),u=r(s);n.detectChangesRenderPropertiesMethod.resetDebugInfo(e.nodeIndex,t),o(n,a,u,t.value,h.THIS_EXPR.prop("context"),[h.THIS_EXPR.prop("renderer").callMethod("setText",[e.renderNode,a]).toStmt()],n.detectChangesRenderPropertiesMethod)}function a(t,e,n){var s=n.view,a=n.renderNode;t.forEach(function(t){var u=s.bindings.length;s.bindings.push(new P.CompileBinding(n,t)),s.detectChangesRenderPropertiesMethod.resetDebugInfo(n.nodeIndex,t);var c,p=r(u),f=i(u),d=f,m=[];switch(t.type){case v.PropertyBindingType.Property:c="setElementProperty",s.genConfig.logBindingUpdate&&m.push(l(a,t.name,f));break;case v.PropertyBindingType.Attribute:c="setElementAttribute",d=d.isBlank().conditional(h.NULL_EXPR,d.callMethod("toString",[]));break;case v.PropertyBindingType.Class:c="setElementClass";break;case v.PropertyBindingType.Style:c="setElementStyle";var g=d.callMethod("toString",[]);y.isPresent(t.unit)&&(g=g.plus(h.literal(t.unit))),d=d.isBlank().conditional(h.NULL_EXPR,g)}m.push(h.THIS_EXPR.prop("renderer").callMethod(c,[a,h.literal(t.name),d]).toStmt()),o(s,f,p,t.value,e,m,s.detectChangesRenderPropertiesMethod)})}function u(t,e){a(t,h.THIS_EXPR.prop("context"),e)}function c(t,e,n){a(t.hostProperties,e,n)}function p(t,e,n){if(0!==t.inputs.length){var s=n.view,a=s.detectChangesInInputsMethod;a.resetDebugInfo(n.nodeIndex,n.sourceAst);var u=t.directive.lifecycleHooks,c=-1!==u.indexOf(m.LifecycleHooks.OnChanges),p=t.directive.isComponent&&!g.isDefaultChangeDetectionStrategy(t.directive.changeDetection);c&&a.addStmt(d.DetectChangesVars.changes.set(h.NULL_EXPR).toStmt()),p&&a.addStmt(d.DetectChangesVars.changed.set(h.literal(!1)).toStmt()),t.inputs.forEach(function(t){var u=s.bindings.length;s.bindings.push(new P.CompileBinding(n,t)),a.resetDebugInfo(n.nodeIndex,t);var v=r(u),y=i(u),m=[e.prop(t.directiveName).set(y).toStmt()];c&&(m.push(new h.IfStmt(d.DetectChangesVars.changes.identical(h.NULL_EXPR),[d.DetectChangesVars.changes.set(h.literalMap([],new h.MapType(h.importType(f.Identifiers.SimpleChange)))).toStmt()])),m.push(d.DetectChangesVars.changes.key(h.literal(t.directiveName)).set(h.importExpr(f.Identifiers.SimpleChange).instantiate([v,y])).toStmt())),p&&m.push(d.DetectChangesVars.changed.set(h.literal(!0)).toStmt()),s.genConfig.logBindingUpdate&&m.push(l(n.renderNode,t.directiveName,y)),o(s,y,v,t.value,h.THIS_EXPR.prop("context"),m,a)}),p&&a.addStmt(new h.IfStmt(d.DetectChangesVars.changed,[n.appElement.prop("componentView").callMethod("markAsCheckOnce",[]).toStmt()]))}}function l(t,e,n){return h.THIS_EXPR.prop("renderer").callMethod("setBindingDebugInfo",[t,h.literal("ng-reflect-"+_.camelCaseToDashCase(e)),n.isBlank().conditional(h.NULL_EXPR,n.callMethod("toString",[]))]).toStmt()}var h=n(164),f=n(158),d=n(170),v=n(139),y=n(5),m=n(156),g=n(33),_=n(153),b=n(179),P=n(180);e.bindRenderText=s,e.bindRenderInputs=u,e.bindDirectiveHostProps=c,e.bindDirectiveInputs=p},function(t,e,n){"use strict";function r(t,e,n,r){var i=new y(t,e,r),o=n.visit(i,v.Expression);return new d(o,i.needsValueUnwrapper)}function i(t,e,n){var r=new y(t,e,null),i=[];return u(n.visit(r,v.Statement),i),i}function o(t,e){if(t!==v.Statement)throw new l.BaseException("Expected a statement, but saw "+e)}function s(t,e){if(t!==v.Expression)throw new l.BaseException("Expected an expression, but saw "+e)}function a(t,e){return t===v.Statement?e.toStmt():e}function u(t,e){h.isArray(t)?t.forEach(function(t){return u(t,e)}):e.push(t)}var c=n(164),p=n(158),l=n(12),h=n(5),f=c.variable("#implicit"),d=function(){function t(t,e){this.expression=t,this.needsValueUnwrapper=e}return t}();e.ExpressionWithWrappedValueInfo=d,e.convertCdExpressionToIr=r,e.convertCdStatementToIr=i;var v;!function(t){t[t.Statement=0]="Statement",t[t.Expression=1]="Expression"}(v||(v={}));var y=function(){function t(t,e,n){this._nameResolver=t,this._implicitReceiver=e,this._valueUnwrapper=n,this.needsValueUnwrapper=!1}return t.prototype.visitBinary=function(t,e){var n;switch(t.operation){case"+":n=c.BinaryOperator.Plus;break;case"-":n=c.BinaryOperator.Minus;break;case"*":n=c.BinaryOperator.Multiply;break;case"/":n=c.BinaryOperator.Divide;break;case"%":n=c.BinaryOperator.Modulo;break;case"&&":n=c.BinaryOperator.And;break;case"||":n=c.BinaryOperator.Or;break;case"==":n=c.BinaryOperator.Equals;break;case"!=":n=c.BinaryOperator.NotEquals;break;case"===":n=c.BinaryOperator.Identical;break;case"!==":n=c.BinaryOperator.NotIdentical;break;case"<":n=c.BinaryOperator.Lower;break;case">":n=c.BinaryOperator.Bigger;break;case"<=":n=c.BinaryOperator.LowerEquals;break;case">=":n=c.BinaryOperator.BiggerEquals;break;default:throw new l.BaseException("Unsupported operation "+t.operation)}return a(e,new c.BinaryOperatorExpr(n,t.left.visit(this,v.Expression),t.right.visit(this,v.Expression)))},t.prototype.visitChain=function(t,e){return o(e,t),this.visitAll(t.expressions,e)},t.prototype.visitConditional=function(t,e){var n=t.condition.visit(this,v.Expression);return a(e,n.conditional(t.trueExp.visit(this,v.Expression),t.falseExp.visit(this,v.Expression)))},t.prototype.visitPipe=function(t,e){var n=t.exp.visit(this,v.Expression),r=this.visitAll(t.args,v.Expression),i=this._nameResolver.callPipe(t.name,n,r);return this.needsValueUnwrapper=!0,a(e,this._valueUnwrapper.callMethod("unwrap",[i]))},t.prototype.visitFunctionCall=function(t,e){return a(e,t.target.visit(this,v.Expression).callFn(this.visitAll(t.args,v.Expression)))},t.prototype.visitImplicitReceiver=function(t,e){return s(e,t),f},t.prototype.visitInterpolation=function(t,e){s(e,t);for(var n=[c.literal(t.expressions.length)],r=0;r=0){var a=i[o],c=s(a),p=l.variable("pd_"+this._actionResultExprs.length);this._actionResultExprs.push(p),u.isPresent(c)&&(i[o]=p.set(c.cast(l.DYNAMIC_TYPE).notIdentical(l.literal(!1))).toDeclStmt(null,[l.StmtModifier.Final]))}this._method.addStmts(i)},t.prototype.finishMethod=function(){var t=this._hasComponentHostListener?this.compileElement.appElement.prop("componentView"):l.THIS_EXPR,e=l.literal(!0);this._actionResultExprs.forEach(function(t){e=e.and(t)});var n=[t.callMethod("markPathToRootAsCheckOnce",[]).toStmt()].concat(this._method.finish()).concat([new l.ReturnStatement(e)]);this.compileElement.view.eventHandlerMethods.push(new l.ClassMethod(this._methodName,[this._eventParam],n,l.BOOL_TYPE,[l.StmtModifier.Private]))},t.prototype.listenToRenderer=function(){var t,e=l.THIS_EXPR.callMethod("eventHandler",[l.fn([this._eventParam],[new l.ReturnStatement(l.THIS_EXPR.callMethod(this._methodName,[p.EventHandlerVars.event]))])]);t=u.isPresent(this.eventTarget)?p.ViewProperties.renderer.callMethod("listenGlobal",[l.literal(this.eventTarget),l.literal(this.eventName),e]):p.ViewProperties.renderer.callMethod("listen",[this.compileElement.renderNode,l.literal(this.eventName),e]);var n=l.variable("disposable_"+this.compileElement.view.disposables.length);this.compileElement.view.disposables.push(n),this.compileElement.view.createMethod.addStmt(n.set(t).toDeclStmt(l.FUNCTION_TYPE,[l.StmtModifier.Private]))},t.prototype.listenToDirective=function(t,e){var n=l.variable("subscription_"+this.compileElement.view.subscriptions.length);this.compileElement.view.subscriptions.push(n);var r=l.THIS_EXPR.callMethod("eventHandler",[l.fn([this._eventParam],[l.THIS_EXPR.callMethod(this._methodName,[p.EventHandlerVars.event]).toStmt()])]);this.compileElement.view.createMethod.addStmt(n.set(t.prop(e).callMethod(l.BuiltinMethod.SubscribeObservable,[r])).toDeclStmt(null,[l.StmtModifier.Final]))},t}();e.CompileEventListener=v,e.collectEventListeners=r,e.bindDirectiveOutputs=i,e.bindRenderOutputs=o},function(t,e,n){"use strict";function r(t,e,n){var r=n.view,i=r.detectChangesInInputsMethod,o=t.directive.lifecycleHooks;-1!==o.indexOf(p.LifecycleHooks.OnChanges)&&t.inputs.length>0&&i.addStmt(new u.IfStmt(c.DetectChangesVars.changes.notIdentical(u.NULL_EXPR),[e.callMethod("ngOnChanges",[c.DetectChangesVars.changes]).toStmt()])),-1!==o.indexOf(p.LifecycleHooks.OnInit)&&i.addStmt(new u.IfStmt(l.and(h),[e.callMethod("ngOnInit",[]).toStmt()])),-1!==o.indexOf(p.LifecycleHooks.DoCheck)&&i.addStmt(new u.IfStmt(h,[e.callMethod("ngDoCheck",[]).toStmt()]))}function i(t,e,n){var r=n.view,i=t.lifecycleHooks,o=r.afterContentLifecycleCallbacksMethod;o.resetDebugInfo(n.nodeIndex,n.sourceAst),-1!==i.indexOf(p.LifecycleHooks.AfterContentInit)&&o.addStmt(new u.IfStmt(l,[e.callMethod("ngAfterContentInit",[]).toStmt()])),-1!==i.indexOf(p.LifecycleHooks.AfterContentChecked)&&o.addStmt(e.callMethod("ngAfterContentChecked",[]).toStmt())}function o(t,e,n){var r=n.view,i=t.lifecycleHooks,o=r.afterViewLifecycleCallbacksMethod;o.resetDebugInfo(n.nodeIndex,n.sourceAst),-1!==i.indexOf(p.LifecycleHooks.AfterViewInit)&&o.addStmt(new u.IfStmt(l,[e.callMethod("ngAfterViewInit",[]).toStmt()])),-1!==i.indexOf(p.LifecycleHooks.AfterViewChecked)&&o.addStmt(e.callMethod("ngAfterViewChecked",[]).toStmt())}function s(t,e,n){var r=n.view.destroyMethod;r.resetDebugInfo(n.nodeIndex,n.sourceAst),-1!==t.lifecycleHooks.indexOf(p.LifecycleHooks.OnDestroy)&&r.addStmt(e.callMethod("ngOnDestroy",[]).toStmt())}function a(t,e,n){var r=n.destroyMethod;-1!==t.lifecycleHooks.indexOf(p.LifecycleHooks.OnDestroy)&&r.addStmt(e.callMethod("ngOnDestroy",[]).toStmt())}var u=n(164),c=n(170),p=n(156),l=u.THIS_EXPR.prop("cdState").identical(c.ChangeDetectorStateEnum.NeverChecked),h=u.not(c.DetectChangesVars.throwOnChange);e.bindDirectiveDetectChangesLifecycleCallbacks=r,e.bindDirectiveAfterContentLifecycleCallbacks=i,e.bindDirectiveAfterViewLifecycleCallbacks=o,e.bindDirectiveDestroyLifecycleCallbacks=s,e.bindPipeDestroyLifecycleCallbacks=a},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(155),s=n(5),a=n(12),u=n(40),c=n(184),p=n(157),l=n(152),h=n(6),f=n(36),d=n(145),v=n(144),y=n(151),m=function(){function t(t,e,n){this._xhr=t,this._urlResolver=e,this._htmlParser=n}return t.prototype.normalizeDirective=function(t){return t.isComponent?this.normalizeTemplate(t.type,t.template).then(function(e){return new o.CompileDirectiveMetadata({type:t.type,isComponent:t.isComponent,selector:t.selector,exportAs:t.exportAs,changeDetection:t.changeDetection,inputs:t.inputs,outputs:t.outputs,hostListeners:t.hostListeners,hostProperties:t.hostProperties,hostAttributes:t.hostAttributes,lifecycleHooks:t.lifecycleHooks,providers:t.providers,viewProviders:t.viewProviders,queries:t.queries,viewQueries:t.viewQueries,template:e})}):u.PromiseWrapper.resolve(t)},t.prototype.normalizeTemplate=function(t,e){var n=this;if(s.isPresent(e.template))return u.PromiseWrapper.resolve(this.normalizeLoadedTemplate(t,e,e.template,t.moduleUrl));if(s.isPresent(e.templateUrl)){var r=this._urlResolver.resolve(t.moduleUrl,e.templateUrl);return this._xhr.get(r).then(function(i){return n.normalizeLoadedTemplate(t,e,i,r)})}throw new a.BaseException("No template specified for component "+t.name)},t.prototype.normalizeLoadedTemplate=function(t,e,n,r){var i=this,s=this._htmlParser.parse(n,t.name);if(s.errors.length>0){var u=s.errors.join("\n");throw new a.BaseException("Template parse errors:\n"+u)}var c=new g;d.htmlVisitAll(c,s.rootNodes);var p=e.styles.concat(c.styles),h=c.styleUrls.filter(l.isStyleUrlResolvable).map(function(t){return i._urlResolver.resolve(r,t)}).concat(e.styleUrls.filter(l.isStyleUrlResolvable).map(function(e){return i._urlResolver.resolve(t.moduleUrl,e)})),v=p.map(function(t){var e=l.extractStyleUrls(i._urlResolver,r,t);return e.styleUrls.forEach(function(t){return h.push(t)}),e.style}),y=e.encapsulation;return y===f.ViewEncapsulation.Emulated&&0===v.length&&0===h.length&&(y=f.ViewEncapsulation.None),new o.CompileTemplateMetadata({encapsulation:y,template:n,templateUrl:r,styles:v,styleUrls:h,ngContentSelectors:c.ngContentSelectors})},t=r([h.Injectable(),i("design:paramtypes",[c.XHR,p.UrlResolver,v.HtmlParser])],t)}();e.DirectiveNormalizer=m;var g=function(){function t(){this.ngContentSelectors=[],this.styles=[],this.styleUrls=[],this.ngNonBindableStackCount=0}return t.prototype.visitElement=function(t,e){var n=y.preparseElement(t);switch(n.type){case y.PreparsedElementType.NG_CONTENT:0===this.ngNonBindableStackCount&&this.ngContentSelectors.push(n.selectAttr);break;case y.PreparsedElementType.STYLE:var r="";t.children.forEach(function(t){t instanceof d.HtmlTextAst&&(r+=t.value)}),this.styles.push(r);break;case y.PreparsedElementType.STYLESHEET:this.styleUrls.push(n.hrefAttr)}return n.nonBindable&&this.ngNonBindableStackCount++,d.htmlVisitAll(this,t.children),n.nonBindable&&this.ngNonBindableStackCount--,null},t.prototype.visitComment=function(t,e){return null},t.prototype.visitAttr=function(t,e){return null},t.prototype.visitText=function(t,e){return null},t.prototype.visitExpansion=function(t,e){return null},t.prototype.visitExpansionCase=function(t,e){return null},t}()},function(t,e){"use strict";var n=function(){function t(){}return t.prototype.get=function(t){return null},t}();e.XHR=n},function(t,e,n){"use strict";function r(t,e){var n=[];return h.isPresent(e)&&o(e,n),h.isPresent(t.directives)&&o(t.directives,n),n}function i(t,e){var n=[];return h.isPresent(e)&&o(e,n),h.isPresent(t.pipes)&&o(t.pipes,n),n}function o(t,e){for(var n=0;n0?r:"package:"+r+O.MODULE_SUFFIX}return t.importUri(e)}var u=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},c=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},p=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},l=n(6),h=n(5),f=n(15),d=n(12),v=n(23),y=n(155),m=n(26),g=n(4),_=n(186),b=n(187),P=n(188),E=n(189),w=n(156),C=n(18),R=n(6),S=n(84),O=n(153),T=n(190),x=n(157),A=n(24),I=n(17),M=n(7),k=n(4),N=n(20),D=function(){function t(t,e,n,r,i,o){this._directiveResolver=t,this._pipeResolver=e,this._viewResolver=n,this._platformDirectives=r,this._platformPipes=i,this._directiveCache=new Map,this._pipeCache=new Map,this._anonymousTypes=new Map,this._anonymousTypeIndex=0,h.isPresent(o)?this._reflector=o:this._reflector=C.reflector}return t.prototype.sanitizeTokenName=function(t){var e=h.stringify(t);if(e.indexOf("(")>=0){var n=this._anonymousTypes.get(t);h.isBlank(n)&&(this._anonymousTypes.set(t,this._anonymousTypeIndex++),n=this._anonymousTypes.get(t)),e="anonymous_token_"+n+"_"}return O.sanitizeIdentifier(e)},t.prototype.getDirectiveMetadata=function(t){var e=this._directiveCache.get(t);if(h.isBlank(e)){var n=this._directiveResolver.resolve(t),r=null,i=null,o=null,s=[];if(n instanceof m.ComponentMetadata){T.assertArrayOfStrings("styles",n.styles);var u=n;r=a(this._reflector,t,u);var c=this._viewResolver.resolve(t);T.assertArrayOfStrings("styles",c.styles),i=new y.CompileTemplateMetadata({encapsulation:c.encapsulation,template:c.template,templateUrl:c.templateUrl,styles:c.styles,styleUrls:c.styleUrls}),o=u.changeDetection,h.isPresent(n.viewProviders)&&(s=this.getProvidersMetadata(n.viewProviders))}var p=[];h.isPresent(n.providers)&&(p=this.getProvidersMetadata(n.providers));var l=[],f=[];h.isPresent(n.queries)&&(l=this.getQueriesMetadata(n.queries,!1), -f=this.getQueriesMetadata(n.queries,!0)),e=y.CompileDirectiveMetadata.create({selector:n.selector,exportAs:n.exportAs,isComponent:h.isPresent(i),type:this.getTypeMetadata(t,r),template:i,changeDetection:o,inputs:n.inputs,outputs:n.outputs,host:n.host,lifecycleHooks:w.LIFECYCLE_HOOKS_VALUES.filter(function(e){return E.hasLifecycleHook(e,t)}),providers:p,viewProviders:s,queries:l,viewQueries:f}),this._directiveCache.set(t,e)}return e},t.prototype.getTypeMetadata=function(t,e){return new y.CompileTypeMetadata({name:this.sanitizeTokenName(t),moduleUrl:e,runtime:t,diDeps:this.getDependenciesMetadata(t,null)})},t.prototype.getFactoryMetadata=function(t,e){return new y.CompileFactoryMetadata({name:this.sanitizeTokenName(t),moduleUrl:e,runtime:t,diDeps:this.getDependenciesMetadata(t,null)})},t.prototype.getPipeMetadata=function(t){var e=this._pipeCache.get(t);if(h.isBlank(e)){var n=this._pipeResolver.resolve(t),r=this._reflector.importUri(t);e=new y.CompilePipeMetadata({type:this.getTypeMetadata(t,r),name:n.name,pure:n.pure,lifecycleHooks:w.LIFECYCLE_HOOKS_VALUES.filter(function(e){return E.hasLifecycleHook(e,t)})}),this._pipeCache.set(t,e)}return e},t.prototype.getViewDirectivesMetadata=function(t){for(var e=this,n=this._viewResolver.resolve(t),i=r(n,this._platformDirectives),o=0;oo?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(6),a=n(5),u=n(12),c=n(15),p=n(3),l=n(18),h=n(20),f=function(){function t(t){a.isPresent(t)?this._reflector=t:this._reflector=l.reflector}return t.prototype.resolve=function(t){var e=this._reflector.annotations(s.resolveForwardRef(t));if(a.isPresent(e)){var n=e.find(r);if(a.isPresent(n)){var i=this._reflector.propMetadata(t);return this._mergeWithPropertyMetadata(n,i,t)}}throw new u.BaseException("No Directive annotation found on "+a.stringify(t))},t.prototype._mergeWithPropertyMetadata=function(t,e,n){var r=[],i=[],o={},s={};return c.StringMapWrapper.forEach(e,function(t,e){t.forEach(function(t){if(t instanceof p.InputMetadata&&(a.isPresent(t.bindingPropertyName)?r.push(e+": "+t.bindingPropertyName):r.push(e)),t instanceof p.OutputMetadata&&(a.isPresent(t.bindingPropertyName)?i.push(e+": "+t.bindingPropertyName):i.push(e)),t instanceof p.HostBindingMetadata&&(a.isPresent(t.hostPropertyName)?o["["+t.hostPropertyName+"]"]=e:o["["+e+"]"]=e),t instanceof p.HostListenerMetadata){var n=a.isPresent(t.args)?t.args.join(", "):"";o["("+t.eventName+")"]=e+"("+n+")"}t instanceof p.ContentChildrenMetadata&&(s[e]=t),t instanceof p.ViewChildrenMetadata&&(s[e]=t),t instanceof p.ContentChildMetadata&&(s[e]=t),t instanceof p.ViewChildMetadata&&(s[e]=t)})}),this._merge(t,r,i,o,s,n)},t.prototype._merge=function(t,e,n,r,i,o){var s,l=a.isPresent(t.inputs)?c.ListWrapper.concat(t.inputs,e):e;a.isPresent(t.outputs)?(t.outputs.forEach(function(t){if(c.ListWrapper.contains(n,t))throw new u.BaseException("Output event '"+t+"' defined multiple times in '"+a.stringify(o)+"'")}),s=c.ListWrapper.concat(t.outputs,n)):s=n;var h=a.isPresent(t.host)?c.StringMapWrapper.merge(t.host,r):r,f=a.isPresent(t.queries)?c.StringMapWrapper.merge(t.queries,i):i;return t instanceof p.ComponentMetadata?new p.ComponentMetadata({selector:t.selector,inputs:l,outputs:s,host:h,exportAs:t.exportAs,moduleId:t.moduleId,queries:f,changeDetection:t.changeDetection,providers:t.providers,viewProviders:t.viewProviders}):new p.DirectiveMetadata({selector:t.selector,inputs:l,outputs:s,host:h,exportAs:t.exportAs,queries:f,providers:t.providers})},t=i([s.Injectable(),o("design:paramtypes",[h.ReflectorReader])],t)}();e.DirectiveResolver=f,e.CODEGEN_DIRECTIVE_RESOLVER=new f(l.reflector)},function(t,e,n){"use strict";function r(t){return t instanceof c.PipeMetadata}var i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(6),a=n(5),u=n(12),c=n(3),p=n(20),l=n(18),h=function(){function t(t){a.isPresent(t)?this._reflector=t:this._reflector=l.reflector}return t.prototype.resolve=function(t){var e=this._reflector.annotations(s.resolveForwardRef(t));if(a.isPresent(e)){var n=e.find(r);if(a.isPresent(n))return n}throw new u.BaseException("No Pipe decorator found on "+a.stringify(t))},t=i([s.Injectable(),o("design:paramtypes",[p.ReflectorReader])],t)}();e.PipeResolver=h,e.CODEGEN_PIPE_RESOLVER=new h(l.reflector)},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(6),s=n(36),a=n(26),u=n(5),c=n(12),p=n(15),l=n(20),h=n(18),f=function(){function t(t){this._cache=new p.Map,u.isPresent(t)?this._reflector=t:this._reflector=h.reflector}return t.prototype.resolve=function(t){var e=this._cache.get(t);return u.isBlank(e)&&(e=this._resolve(t),this._cache.set(t,e)),e},t.prototype._resolve=function(t){var e,n;if(this._reflector.annotations(t).forEach(function(t){t instanceof s.ViewMetadata&&(n=t),t instanceof a.ComponentMetadata&&(e=t)}),!u.isPresent(e)){if(u.isBlank(n))throw new c.BaseException("Could not compile '"+u.stringify(t)+"' because it is not a component.");return n}if(u.isBlank(e.template)&&u.isBlank(e.templateUrl)&&u.isBlank(n))throw new c.BaseException("Component '"+u.stringify(t)+"' must have either 'template' or 'templateUrl' set.");if(u.isPresent(e.template)&&u.isPresent(n))this._throwMixingViewAndComponent("template",t);else if(u.isPresent(e.templateUrl)&&u.isPresent(n))this._throwMixingViewAndComponent("templateUrl",t);else if(u.isPresent(e.directives)&&u.isPresent(n))this._throwMixingViewAndComponent("directives",t);else if(u.isPresent(e.pipes)&&u.isPresent(n))this._throwMixingViewAndComponent("pipes",t);else if(u.isPresent(e.encapsulation)&&u.isPresent(n))this._throwMixingViewAndComponent("encapsulation",t);else if(u.isPresent(e.styles)&&u.isPresent(n))this._throwMixingViewAndComponent("styles",t);else{if(!u.isPresent(e.styleUrls)||!u.isPresent(n))return u.isPresent(n)?n:new s.ViewMetadata({templateUrl:e.templateUrl,template:e.template,directives:e.directives,pipes:e.pipes,encapsulation:e.encapsulation,styles:e.styles,styleUrls:e.styleUrls});this._throwMixingViewAndComponent("styleUrls",t)}return null},t.prototype._throwMixingViewAndComponent=function(t,e){throw new c.BaseException("Component '"+u.stringify(e)+"' cannot have both '"+t+"' and '@View' set at the same time\"")},t=r([o.Injectable(),i("design:paramtypes",[l.ReflectorReader])],t)}();e.ViewResolver=f},function(t,e,n){"use strict";function r(t,e){if(!(e instanceof i.Type))return!1;var n=e.prototype;switch(t){case o.LifecycleHooks.AfterContentInit:return!!n.ngAfterContentInit;case o.LifecycleHooks.AfterContentChecked:return!!n.ngAfterContentChecked;case o.LifecycleHooks.AfterViewInit:return!!n.ngAfterViewInit;case o.LifecycleHooks.AfterViewChecked:return!!n.ngAfterViewChecked;case o.LifecycleHooks.OnChanges:return!!n.ngOnChanges;case o.LifecycleHooks.DoCheck:return!!n.ngDoCheck;case o.LifecycleHooks.OnDestroy:return!!n.ngOnDestroy;case o.LifecycleHooks.OnInit:return!!n.ngOnInit;default:return!1}}var i=n(5),o=n(156);e.hasLifecycleHook=r},function(t,e,n){"use strict";function r(t,e){if(i.assertionsEnabled()&&!i.isBlank(e)){if(!i.isArray(e))throw new o.BaseException("Expected '"+t+"' to be an array of strings.");for(var n=0;nn;n++)e+=" ";return e}var o=n(5),s=n(12),a=n(164),u=/'|\\|\n|\r|\$/g;e.CATCH_ERROR_VAR=a.variable("error"),e.CATCH_STACK_VAR=a.variable("stack");var c=function(){function t(){}return t}();e.OutputEmitter=c;var p=function(){function t(t){this.indent=t,this.parts=[]}return t}(),l=function(){function t(t,e){this._exportedVars=t,this._indent=e,this._classes=[],this._lines=[new p(e)]}return t.createRoot=function(e){return new t(e,0)},Object.defineProperty(t.prototype,"_currentLine",{get:function(){return this._lines[this._lines.length-1]},enumerable:!0,configurable:!0}),t.prototype.isExportedVar=function(t){return-1!==this._exportedVars.indexOf(t)},t.prototype.println=function(t){void 0===t&&(t=""),this.print(t,!0)},t.prototype.lineIsEmpty=function(){return 0===this._currentLine.parts.length},t.prototype.print=function(t,e){void 0===e&&(e=!1),t.length>0&&this._currentLine.parts.push(t),e&&this._lines.push(new p(this._indent))},t.prototype.removeEmptyLastLine=function(){this.lineIsEmpty()&&this._lines.pop()},t.prototype.incIndent=function(){this._indent++,this._currentLine.indent=this._indent},t.prototype.decIndent=function(){this._indent--,this._currentLine.indent=this._indent},t.prototype.pushClass=function(t){this._classes.push(t)},t.prototype.popClass=function(){return this._classes.pop()},Object.defineProperty(t.prototype,"currentClass",{get:function(){return this._classes.length>0?this._classes[this._classes.length-1]:null},enumerable:!0,configurable:!0}),t.prototype.toSource=function(){var t=this._lines;return 0===t[t.length-1].parts.length&&(t=t.slice(0,t.length-1)),t.map(function(t){return t.parts.length>0?i(t.indent)+t.parts.join(""):""}).join("\n")},t}();e.EmitterVisitorContext=l;var h=function(){function t(t){this._escapeDollarInStrings=t}return t.prototype.visitExpressionStmt=function(t,e){return t.expr.visitExpression(this,e),e.println(";"),null},t.prototype.visitReturnStmt=function(t,e){return e.print("return "),t.value.visitExpression(this,e),e.println(";"),null},t.prototype.visitIfStmt=function(t,e){e.print("if ("),t.condition.visitExpression(this,e),e.print(") {");var n=o.isPresent(t.falseCase)&&t.falseCase.length>0;return t.trueCase.length<=1&&!n?(e.print(" "),this.visitAllStatements(t.trueCase,e),e.removeEmptyLastLine(),e.print(" ")):(e.println(),e.incIndent(),this.visitAllStatements(t.trueCase,e),e.decIndent(),n&&(e.println("} else {"),e.incIndent(),this.visitAllStatements(t.falseCase,e),e.decIndent())),e.println("}"),null},t.prototype.visitThrowStmt=function(t,e){return e.print("throw "),t.error.visitExpression(this,e),e.println(";"),null},t.prototype.visitCommentStmt=function(t,e){var n=t.comment.split("\n");return n.forEach(function(t){e.println("// "+t)}),null},t.prototype.visitWriteVarExpr=function(t,e){var n=e.lineIsEmpty();return n||e.print("("),e.print(t.name+" = "),t.value.visitExpression(this,e),n||e.print(")"),null},t.prototype.visitWriteKeyExpr=function(t,e){var n=e.lineIsEmpty();return n||e.print("("),t.receiver.visitExpression(this,e),e.print("["),t.index.visitExpression(this,e),e.print("] = "),t.value.visitExpression(this,e),n||e.print(")"),null},t.prototype.visitWritePropExpr=function(t,e){var n=e.lineIsEmpty();return n||e.print("("),t.receiver.visitExpression(this,e),e.print("."+t.name+" = "),t.value.visitExpression(this,e),n||e.print(")"),null},t.prototype.visitInvokeMethodExpr=function(t,e){t.receiver.visitExpression(this,e);var n=t.name;return o.isPresent(t.builtin)&&(n=this.getBuiltinMethodName(t.builtin),o.isBlank(n))?null:(e.print("."+n+"("),this.visitAllExpressions(t.args,e,","),e.print(")"),null)},t.prototype.visitInvokeFunctionExpr=function(t,e){return t.fn.visitExpression(this,e),e.print("("),this.visitAllExpressions(t.args,e,","),e.print(")"),null},t.prototype.visitReadVarExpr=function(t,n){var r=t.name;if(o.isPresent(t.builtin))switch(t.builtin){case a.BuiltinVar.Super:r="super";break;case a.BuiltinVar.This:r="this";break;case a.BuiltinVar.CatchError:r=e.CATCH_ERROR_VAR.name;break;case a.BuiltinVar.CatchStack:r=e.CATCH_STACK_VAR.name;break;default:throw new s.BaseException("Unknown builtin variable "+t.builtin)}return n.print(r),null},t.prototype.visitInstantiateExpr=function(t,e){return e.print("new "),t.classExpr.visitExpression(this,e),e.print("("),this.visitAllExpressions(t.args,e,","),e.print(")"),null},t.prototype.visitLiteralExpr=function(t,e){var n=t.value;return o.isString(n)?e.print(r(n,this._escapeDollarInStrings)):o.isBlank(n)?e.print("null"):e.print(""+n),null},t.prototype.visitConditionalExpr=function(t,e){return t.condition.visitExpression(this,e),e.print("? "),t.trueCase.visitExpression(this,e),e.print(": "),t.falseCase.visitExpression(this,e),null},t.prototype.visitNotExpr=function(t,e){return e.print("!"),t.condition.visitExpression(this,e),null},t.prototype.visitBinaryOperatorExpr=function(t,e){var n;switch(t.operator){case a.BinaryOperator.Equals:n="==";break;case a.BinaryOperator.Identical:n="===";break;case a.BinaryOperator.NotEquals:n="!=";break;case a.BinaryOperator.NotIdentical:n="!==";break;case a.BinaryOperator.And:n="&&";break;case a.BinaryOperator.Or:n="||";break;case a.BinaryOperator.Plus:n="+";break;case a.BinaryOperator.Minus:n="-";break;case a.BinaryOperator.Divide:n="/";break;case a.BinaryOperator.Multiply:n="*";break;case a.BinaryOperator.Modulo:n="%";break;case a.BinaryOperator.Lower:n="<";break;case a.BinaryOperator.LowerEquals:n="<=";break;case a.BinaryOperator.Bigger:n=">";break;case a.BinaryOperator.BiggerEquals:n=">=";break;default:throw new s.BaseException("Unknown operator "+t.operator)}return e.print("("),t.lhs.visitExpression(this,e),e.print(" "+n+" "),t.rhs.visitExpression(this,e),e.print(")"),null},t.prototype.visitReadPropExpr=function(t,e){return t.receiver.visitExpression(this,e),e.print("."),e.print(t.name),null},t.prototype.visitReadKeyExpr=function(t,e){return t.receiver.visitExpression(this,e),e.print("["),t.index.visitExpression(this,e),e.print("]"),null},t.prototype.visitLiteralArrayExpr=function(t,e){var n=t.entries.length>1;return e.print("[",n),e.incIndent(),this.visitAllExpressions(t.entries,e,",",n),e.decIndent(),e.print("]",n),null},t.prototype.visitLiteralMapExpr=function(t,e){var n=this,i=t.entries.length>1;return e.print("{",i),e.incIndent(),this.visitAllObjects(function(t){e.print(r(t[0],n._escapeDollarInStrings)+": "),t[1].visitExpression(n,e)},t.entries,e,",",i),e.decIndent(),e.print("}",i),null},t.prototype.visitAllExpressions=function(t,e,n,r){var i=this;void 0===r&&(r=!1),this.visitAllObjects(function(t){return t.visitExpression(i,e)},t,e,n,r)},t.prototype.visitAllObjects=function(t,e,n,r,i){void 0===i&&(i=!1);for(var o=0;o0&&n.print(r,i),t(e[o]);i&&n.println()},t.prototype.visitAllStatements=function(t,e){var n=this;t.forEach(function(t){return t.visitStatement(n,e)})},t}();e.AbstractEmitterVisitor=h,e.escapeSingleQuoteString=r},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(5),o=n(12),s=n(164),a=n(192),u=function(t){function e(){t.call(this,!1)}return r(e,t),e.prototype.visitDeclareClassStmt=function(t,e){var n=this;return e.pushClass(t),this._visitClassConstructor(t,e),i.isPresent(t.parent)&&(e.print(t.name+".prototype = Object.create("),t.parent.visitExpression(this,e),e.println(".prototype);")),t.getters.forEach(function(r){return n._visitClassGetter(t,r,e)}),t.methods.forEach(function(r){return n._visitClassMethod(t,r,e)}),e.popClass(),null},e.prototype._visitClassConstructor=function(t,e){e.print("function "+t.name+"("),i.isPresent(t.constructorMethod)&&this._visitParams(t.constructorMethod.params,e),e.println(") {"),e.incIndent(),i.isPresent(t.constructorMethod)&&t.constructorMethod.body.length>0&&(e.println("var self = this;"),this.visitAllStatements(t.constructorMethod.body,e)),e.decIndent(),e.println("}")},e.prototype._visitClassGetter=function(t,e,n){n.println("Object.defineProperty("+t.name+".prototype, '"+e.name+"', { get: function() {"),n.incIndent(),e.body.length>0&&(n.println("var self = this;"),this.visitAllStatements(e.body,n)),n.decIndent(),n.println("}});")},e.prototype._visitClassMethod=function(t,e,n){n.print(t.name+".prototype."+e.name+" = function("),this._visitParams(e.params,n),n.println(") {"),n.incIndent(),e.body.length>0&&(n.println("var self = this;"),this.visitAllStatements(e.body,n)),n.decIndent(),n.println("};")},e.prototype.visitReadVarExpr=function(e,n){if(e.builtin===s.BuiltinVar.This)n.print("self");else{if(e.builtin===s.BuiltinVar.Super)throw new o.BaseException("'super' needs to be handled at a parent ast node, not at the variable level!");t.prototype.visitReadVarExpr.call(this,e,n)}return null},e.prototype.visitDeclareVarStmt=function(t,e){return e.print("var "+t.name+" = "),t.value.visitExpression(this,e),e.println(";"),null},e.prototype.visitCastExpr=function(t,e){return t.value.visitExpression(this,e),null},e.prototype.visitInvokeFunctionExpr=function(e,n){var r=e.fn;return r instanceof s.ReadVarExpr&&r.builtin===s.BuiltinVar.Super?(n.currentClass.parent.visitExpression(this,n),n.print(".call(this"),e.args.length>0&&(n.print(", "),this.visitAllExpressions(e.args,n,",")),n.print(")")):t.prototype.visitInvokeFunctionExpr.call(this,e,n),null},e.prototype.visitFunctionExpr=function(t,e){return e.print("function("),this._visitParams(t.params,e),e.println(") {"),e.incIndent(),this.visitAllStatements(t.statements,e),e.decIndent(),e.print("}"),null},e.prototype.visitDeclareFunctionStmt=function(t,e){return e.print("function "+t.name+"("),this._visitParams(t.params,e),e.println(") {"),e.incIndent(),this.visitAllStatements(t.statements,e),e.decIndent(),e.println("}"),null},e.prototype.visitTryCatchStmt=function(t,e){e.println("try {"),e.incIndent(),this.visitAllStatements(t.bodyStmts,e),e.decIndent(),e.println("} catch ("+a.CATCH_ERROR_VAR.name+") {"),e.incIndent();var n=[a.CATCH_STACK_VAR.set(a.CATCH_ERROR_VAR.prop("stack")).toDeclStmt(null,[s.StmtModifier.Final])].concat(t.catchStmts);return this.visitAllStatements(n,e),e.decIndent(),e.println("}"),null},e.prototype._visitParams=function(t,e){this.visitAllObjects(function(t){return e.print(t.name)},t,e,",")},e.prototype.getBuiltinMethodName=function(t){var e;switch(t){case s.BuiltinMethod.ConcatArray:e="concat";break;case s.BuiltinMethod.SubscribeObservable:e="subscribe";break;case s.BuiltinMethod.bind:e="bind";break;default:throw new o.BaseException("Unknown builtin method: "+t)}return e},e}(a.AbstractEmitterVisitor);e.AbstractJsEmitterVisitor=u},function(t,e,n){"use strict";function r(t,e,n){var r=t.concat([new c.ReturnStatement(c.variable(e))]),i=new y(null,null,null,null,new Map,new Map,new Map,new Map,n),o=new _,s=o.visitAllStatements(r,i);return a.isPresent(s)?s.value:null}function i(t){return a.IS_DART?t instanceof v:a.isPresent(t)&&a.isPresent(t.props)&&a.isPresent(t.getters)&&a.isPresent(t.methods)}function o(t,e,n,r,i){for(var o=r.createChildWihtLocalVars(),s=0;si();case c.BinaryOperator.BiggerEquals:return r()>=i();default:throw new l.BaseException("Unknown operator "+t.operator)}},t.prototype.visitReadPropExpr=function(t,e){var n,r=t.receiver.visitExpression(this,e);if(i(r)){var o=r;n=o.props.has(t.name)?o.props.get(t.name):o.getters.has(t.name)?o.getters.get(t.name)():o.methods.has(t.name)?o.methods.get(t.name):p.reflector.getter(t.name)(r)}else n=p.reflector.getter(t.name)(r);return n},t.prototype.visitReadKeyExpr=function(t,e){var n=t.receiver.visitExpression(this,e),r=t.index.visitExpression(this,e);return n[r]},t.prototype.visitLiteralArrayExpr=function(t,e){return this.visitAllExpressions(t.entries,e)},t.prototype.visitLiteralMapExpr=function(t,e){var n=this,r={};return t.entries.forEach(function(t){return r[t[0]]=t[1].visitExpression(n,e)}),r},t.prototype.visitAllExpressions=function(t,e){var n=this;return t.map(function(t){return t.visitExpression(n,e)})},t.prototype.visitAllStatements=function(t,e){for(var n=0;n0?i(n[0]):null;a.isPresent(r)&&(e.print(": "),r.visitExpression(this,e),n=n.slice(1)),e.println(" {"),e.incIndent(),this.visitAllStatements(n,e),e.decIndent(),e.println("}")},e.prototype._visitClassMethod=function(t,e){a.isPresent(t.type)?t.type.visitType(this,e):e.print("void"),e.print(" "+t.name+"("),this._visitParams(t.params,e),e.println(") {"),e.incIndent(),this.visitAllStatements(t.body,e),e.decIndent(),e.println("}")},e.prototype.visitFunctionExpr=function(t,e){return e.print("("),this._visitParams(t.params,e),e.println(") {"),e.incIndent(),this.visitAllStatements(t.statements,e),e.decIndent(),e.print("}"),null},e.prototype.visitDeclareFunctionStmt=function(t,e){return a.isPresent(t.type)?t.type.visitType(this,e):e.print("void"),e.print(" "+t.name+"("),this._visitParams(t.params,e),e.println(") {"),e.incIndent(),this.visitAllStatements(t.statements,e),e.decIndent(),e.println("}"),null},e.prototype.getBuiltinMethodName=function(t){var e;switch(t){case c.BuiltinMethod.ConcatArray:e=".addAll";break;case c.BuiltinMethod.SubscribeObservable:e="listen";break;case c.BuiltinMethod.bind:e=null;break;default:throw new u.BaseException("Unknown builtin method: "+t)}return e},e.prototype.visitTryCatchStmt=function(t,e){return e.println("try {"),e.incIndent(),this.visitAllStatements(t.bodyStmts,e),e.decIndent(),e.println("} catch ("+p.CATCH_ERROR_VAR.name+", "+p.CATCH_STACK_VAR.name+") {"),e.incIndent(),this.visitAllStatements(t.catchStmts,e),e.decIndent(),e.println("}"),null},e.prototype.visitBinaryOperatorExpr=function(e,n){switch(e.operator){case c.BinaryOperator.Identical:n.print("identical("),e.lhs.visitExpression(this,n),n.print(", "),e.rhs.visitExpression(this,n),n.print(")");break;case c.BinaryOperator.NotIdentical:n.print("!identical("),e.lhs.visitExpression(this,n),n.print(", "),e.rhs.visitExpression(this,n),n.print(")");break;default:t.prototype.visitBinaryOperatorExpr.call(this,e,n)}return null},e.prototype.visitLiteralArrayExpr=function(e,n){return o(e.type)&&n.print("const "),t.prototype.visitLiteralArrayExpr.call(this,e,n)},e.prototype.visitLiteralMapExpr=function(e,n){return o(e.type)&&n.print("const "),a.isPresent(e.valueType)&&(n.print("")),t.prototype.visitLiteralMapExpr.call(this,e,n)},e.prototype.visitInstantiateExpr=function(t,e){return e.print(o(t.type)?"const":"new"),e.print(" "),t.classExpr.visitExpression(this,e),e.print("("),this.visitAllExpressions(t.args,e,","),e.print(")"),null},e.prototype.visitBuiltintType=function(t,e){var n;switch(t.name){case c.BuiltinTypeName.Bool:n="bool";break;case c.BuiltinTypeName.Dynamic:n="dynamic";break;case c.BuiltinTypeName.Function:n="Function";break;case c.BuiltinTypeName.Number:n="num";break;case c.BuiltinTypeName.Int:n="int";break;case c.BuiltinTypeName.String:n="String";break;default:throw new u.BaseException("Unsupported builtin type "+t.name)}return e.print(n),null},e.prototype.visitExternalType=function(t,e){return this._visitIdentifier(t.value,t.typeParams,e),null},e.prototype.visitArrayType=function(t,e){return e.print("List<"),a.isPresent(t.of)?t.of.visitType(this,e):e.print("dynamic"),e.print(">"),null},e.prototype.visitMapType=function(t,e){return e.print("Map"),null},e.prototype._visitParams=function(t,e){var n=this;this.visitAllObjects(function(t){a.isPresent(t.type)&&(t.type.visitType(n,e),e.print(" ")),e.print(t.name)},t,e,",")},e.prototype._visitIdentifier=function(t,e,n){var r=this;if(a.isPresent(t.moduleUrl)&&t.moduleUrl!=this._moduleUrl){var i=this.importsWithPrefixes.get(t.moduleUrl);a.isBlank(i)&&(i="import"+this.importsWithPrefixes.size,this.importsWithPrefixes.set(t.moduleUrl,i)),n.print(i+".")}n.print(t.name),a.isPresent(e)&&e.length>0&&(n.print("<"),this.visitAllObjects(function(t){return t.visitType(r,n)},e,n,","),n.print(">"))},e}(p.AbstractEmitterVisitor)},function(t,e,n){"use strict";function r(t,e,n){var r=n===l.Dart?"package:":"",o=h.parse(t,!1),u=h.parse(e,!0);if(a.isBlank(u))return e;if(o.firstLevelDir==u.firstLevelDir&&o.packageName==u.packageName)return i(o.modulePath,u.modulePath,n);if("lib"==u.firstLevelDir)return""+r+u.packageName+"/"+u.modulePath;throw new s.BaseException("Can't import url "+e+" from "+t)}function i(t,e,n){for(var r=t.split(p),i=e.split(p),s=o(r,i),a=[],u=r.length-1-s,h=0;u>h;h++)a.push("..");0>=u&&n===l.JS&&a.push(".");for(var h=s;hn&&t[n]==e[n];)n++;return n}var s=n(12),a=n(5),u=/asset:([^\/]+)\/([^\/]+)\/(.+)/g,c="/",p=/\//g;!function(t){t[t.Dart=0]="Dart",t[t.JS=1]="JS"}(e.ImportEnv||(e.ImportEnv={}));var l=e.ImportEnv;e.getImportModulePath=r;var h=function(){function t(t,e,n){this.packageName=t,this.firstLevelDir=e,this.modulePath=n}return t.parse=function(e,n){var r=a.RegExpWrapper.firstMatch(u,e);if(a.isPresent(r))return new t(r[1],r[2],r[3]);if(n)return null;throw new s.BaseException("Url "+e+" is not a valid asset: url")},t}();e.getRelativePath=i,e.getLongestPathSegmentPrefix=o},function(t,e,n){"use strict";function r(t){var e,n=new h(p),r=u.EmitterVisitorContext.createRoot([]);return e=s.isArray(t)?t:[t],e.forEach(function(t){if(t instanceof o.Statement)t.visitStatement(n,r);else if(t instanceof o.Expression)t.visitExpression(n,r);else{if(!(t instanceof o.Type))throw new a.BaseException("Don't know how to print debug info for "+t);t.visitType(n,r)}}),r.toSource()}var i=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},o=n(164),s=n(5),a=n(12),u=n(192),c=n(196),p="asset://debug/lib";e.debugOutputAstAsTypeScript=r;var l=function(){function t(){}return t.prototype.emitStatements=function(t,e,n){var r=new h(t),i=u.EmitterVisitorContext.createRoot(n);r.visitAllStatements(e,i);var o=[];return r.importsWithPrefixes.forEach(function(e,n){o.push("imp"+("ort * as "+e+" from '"+c.getImportModulePath(t,n,c.ImportEnv.JS)+"';"))}),o.push(i.toSource()),o.join("\n")},t}();e.TypeScriptEmitter=l;var h=function(t){function e(e){t.call(this,!1),this._moduleUrl=e,this.importsWithPrefixes=new Map}return i(e,t),e.prototype.visitExternalExpr=function(t,e){return this._visitIdentifier(t.value,t.typeParams,e),null},e.prototype.visitDeclareVarStmt=function(t,e){return e.isExportedVar(t.name)&&e.print("export "),t.hasModifier(o.StmtModifier.Final)?e.print("const"):e.print("var"),e.print(" "+t.name),s.isPresent(t.type)&&(e.print(":"),t.type.visitType(this,e)),e.print(" = "),t.value.visitExpression(this,e),e.println(";"),null},e.prototype.visitCastExpr=function(t,e){return e.print("(<"),t.type.visitType(this,e),e.print(">"),t.value.visitExpression(this,e),e.print(")"),null},e.prototype.visitDeclareClassStmt=function(t,e){var n=this;return e.pushClass(t),e.isExportedVar(t.name)&&e.print("export "),e.print("class "+t.name),s.isPresent(t.parent)&&(e.print(" extends "),t.parent.visitExpression(this,e)),e.println(" {"),e.incIndent(),t.fields.forEach(function(t){return n._visitClassField(t,e)}),s.isPresent(t.constructorMethod)&&this._visitClassConstructor(t,e),t.getters.forEach(function(t){return n._visitClassGetter(t,e)}),t.methods.forEach(function(t){return n._visitClassMethod(t,e)}),e.decIndent(),e.println("}"),e.popClass(),null},e.prototype._visitClassField=function(t,e){t.hasModifier(o.StmtModifier.Private)&&e.print("private "),e.print(t.name),s.isPresent(t.type)?(e.print(":"),t.type.visitType(this,e)):e.print(": any"),e.println(";")},e.prototype._visitClassGetter=function(t,e){t.hasModifier(o.StmtModifier.Private)&&e.print("private "),e.print("get "+t.name+"()"),s.isPresent(t.type)&&(e.print(":"),t.type.visitType(this,e)),e.println(" {"),e.incIndent(),this.visitAllStatements(t.body,e),e.decIndent(),e.println("}")},e.prototype._visitClassConstructor=function(t,e){e.print("constructor("),this._visitParams(t.constructorMethod.params,e),e.println(") {"),e.incIndent(),this.visitAllStatements(t.constructorMethod.body,e),e.decIndent(),e.println("}")},e.prototype._visitClassMethod=function(t,e){t.hasModifier(o.StmtModifier.Private)&&e.print("private "),e.print(t.name+"("),this._visitParams(t.params,e),e.print("):"),s.isPresent(t.type)?t.type.visitType(this,e):e.print("void"),e.println(" {"),e.incIndent(),this.visitAllStatements(t.body,e),e.decIndent(),e.println("}")},e.prototype.visitFunctionExpr=function(t,e){return e.print("("),this._visitParams(t.params,e),e.print("):"),s.isPresent(t.type)?t.type.visitType(this,e):e.print("void"),e.println(" => {"),e.incIndent(),this.visitAllStatements(t.statements,e),e.decIndent(),e.print("}"),null},e.prototype.visitDeclareFunctionStmt=function(t,e){return e.isExportedVar(t.name)&&e.print("export "),e.print("function "+t.name+"("),this._visitParams(t.params,e),e.print("):"),s.isPresent(t.type)?t.type.visitType(this,e):e.print("void"),e.println(" {"),e.incIndent(),this.visitAllStatements(t.statements,e),e.decIndent(),e.println("}"),null},e.prototype.visitTryCatchStmt=function(t,e){e.println("try {"),e.incIndent(),this.visitAllStatements(t.bodyStmts,e),e.decIndent(),e.println("} catch ("+u.CATCH_ERROR_VAR.name+") {"),e.incIndent();var n=[u.CATCH_STACK_VAR.set(u.CATCH_ERROR_VAR.prop("stack")).toDeclStmt(null,[o.StmtModifier.Final])].concat(t.catchStmts);return this.visitAllStatements(n,e),e.decIndent(),e.println("}"),null},e.prototype.visitBuiltintType=function(t,e){var n;switch(t.name){case o.BuiltinTypeName.Bool:n="boolean";break;case o.BuiltinTypeName.Dynamic:n="any";break;case o.BuiltinTypeName.Function:n="Function";break;case o.BuiltinTypeName.Number:n="number";break;case o.BuiltinTypeName.Int:n="number";break;case o.BuiltinTypeName.String:n="string";break;default:throw new a.BaseException("Unsupported builtin type "+t.name)}return e.print(n),null},e.prototype.visitExternalType=function(t,e){return this._visitIdentifier(t.value,t.typeParams,e),null},e.prototype.visitArrayType=function(t,e){return s.isPresent(t.of)?t.of.visitType(this,e):e.print("any"),e.print("[]"),null},e.prototype.visitMapType=function(t,e){return e.print("{[key: string]:"),s.isPresent(t.valueType)?t.valueType.visitType(this,e):e.print("any"),e.print("}"),null},e.prototype.getBuiltinMethodName=function(t){var e;switch(t){case o.BuiltinMethod.ConcatArray:e="concat";break;case o.BuiltinMethod.SubscribeObservable:e="subscribe";break;case o.BuiltinMethod.bind:e="bind";break;default:throw new a.BaseException("Unknown builtin method: "+t)}return e},e.prototype._visitParams=function(t,e){var n=this;this.visitAllObjects(function(t){e.print(t.name),s.isPresent(t.type)&&(e.print(":"),t.type.visitType(n,e))},t,e,",")},e.prototype._visitIdentifier=function(t,e,n){var r=this;if(s.isPresent(t.moduleUrl)&&t.moduleUrl!=this._moduleUrl){var i=this.importsWithPrefixes.get(t.moduleUrl);s.isBlank(i)&&(i="import"+this.importsWithPrefixes.size,this.importsWithPrefixes.set(t.moduleUrl,i)),n.print(i+".")}n.print(t.name),s.isPresent(e)&&e.length>0&&(n.print("<"),this.visitAllObjects(function(t){return t.visitType(r,n)},e,n,","),n.print(">"))},e}(u.AbstractEmitterVisitor)},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(5),o=n(159),s=n(12),a=function(){function t(){}return t.prototype.createInstance=function(t,e,n,r,i,a){if(t===o.AppView)return new u(n,r,i,a);throw new s.BaseException("Can't instantiate class "+t+" in interpretative mode")},t}();e.InterpretiveAppViewInstanceFactory=a;var u=function(t){function e(e,n,r,i){t.call(this,e[0],e[1],e[2],e[3],e[4],e[5],e[6],e[7],e[8]),this.props=n,this.getters=r,this.methods=i}return r(e,t),e.prototype.createInternal=function(e){var n=this.methods.get("createInternal");return i.isPresent(n)?n(e):t.prototype.createInternal.call(this,e)},e.prototype.injectorGetInternal=function(e,n,r){var o=this.methods.get("injectorGetInternal");return i.isPresent(o)?o(e,n,r):t.prototype.injectorGet.call(this,e,n,r)},e.prototype.destroyInternal=function(){var e=this.methods.get("destroyInternal");return i.isPresent(e)?e():t.prototype.destroyInternal.call(this)},e.prototype.dirtyParentQueriesInternal=function(){var e=this.methods.get("dirtyParentQueriesInternal");return i.isPresent(e)?e():t.prototype.dirtyParentQueriesInternal.call(this)},e.prototype.detectChangesInternal=function(e){var n=this.methods.get("detectChangesInternal");return i.isPresent(n)?n(e):t.prototype.detectChangesInternal.call(this,e)},e}(o.AppView)},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(6),a=n(5),u=n(15),c=n(200),p=n(148),l=n(150),h=a.CONST_EXPR({xlink:"http://www.w3.org/1999/xlink",svg:"http://www.w3.org/2000/svg"}),f=function(t){function e(){t.apply(this,arguments),this._protoElements=new Map}return r(e,t),e.prototype._getProtoElement=function(t){var e=this._protoElements.get(t);if(a.isBlank(e)){var n=p.splitNsName(t);e=a.isPresent(n[0])?c.DOM.createElementNS(h[n[0]],n[1]):c.DOM.createElement(n[1]),this._protoElements.set(t,e)}return e},e.prototype.hasProperty=function(t,e){if(-1!==t.indexOf("-"))return!0;var n=this._getProtoElement(t);return c.DOM.hasProperty(n,e)},e.prototype.getMappedPropName=function(t){var e=u.StringMapWrapper.get(c.DOM.attrToPropMap,t);return a.isPresent(e)?e:t},e=i([s.Injectable(),o("design:paramtypes",[])],e)}(l.ElementSchemaRegistry);e.DomElementSchemaRegistry=f},function(t,e,n){"use strict";function r(t){i.isBlank(e.DOM)&&(e.DOM=t)}var i=n(5);e.DOM=null,e.setRootDomAdapter=r;var o=function(){function t(){}return Object.defineProperty(t.prototype,"attrToPropMap",{get:function(){return this._attrToPropMap},set:function(t){this._attrToPropMap=t},enumerable:!0,configurable:!0}),t}();e.DomAdapter=o},function(t,e,n){"use strict";function r(){return s.isBlank(c.getPlatform())&&c.createPlatform(c.ReflectiveInjector.resolveAndCreate(a.BROWSER_PROVIDERS)),c.assertPlatform(a.BROWSER_PLATFORM_MARKER)}function i(t,n){c.reflector.reflectionCapabilities=new p.ReflectionCapabilities;var i=c.ReflectiveInjector.resolveAndCreate([e.BROWSER_APP_PROVIDERS,s.isPresent(n)?n:[]],r().injector);return c.coreLoadAndBootstrap(i,t)}var o=n(202);e.BROWSER_PROVIDERS=o.BROWSER_PROVIDERS,e.CACHED_TEMPLATE_PROVIDER=o.CACHED_TEMPLATE_PROVIDER,e.ELEMENT_PROBE_PROVIDERS=o.ELEMENT_PROBE_PROVIDERS,e.ELEMENT_PROBE_PROVIDERS_PROD_MODE=o.ELEMENT_PROBE_PROVIDERS_PROD_MODE,e.inspectNativeElement=o.inspectNativeElement,e.BrowserDomAdapter=o.BrowserDomAdapter,e.By=o.By,e.Title=o.Title,e.DOCUMENT=o.DOCUMENT,e.enableDebugTools=o.enableDebugTools,e.disableDebugTools=o.disableDebugTools;var s=n(5),a=n(202),u=n(137),c=n(2),p=n(21),l=n(220),h=n(137),f=n(6);e.BROWSER_APP_PROVIDERS=s.CONST_EXPR([a.BROWSER_APP_COMMON_PROVIDERS,u.COMPILER_PROVIDERS,new f.Provider(h.XHR,{useClass:l.XHRImpl})]),e.browserPlatform=r,e.bootstrap=i},function(t,e,n){"use strict";function r(){return new c.ExceptionHandler(h.DOM,!s.IS_DART)}function i(){return h.DOM.defaultDoc()}function o(){E.BrowserDomAdapter.makeCurrent(),R.wtfInit(),w.BrowserGetTestability.init()}var s=n(5),a=n(6),u=n(184),c=n(2),p=n(87),l=n(63),h=n(200),f=n(203),d=n(205),v=n(206),y=n(208),m=n(209),g=n(217),_=n(217),b=n(216),P=n(210),E=n(218),w=n(221),C=n(222),R=n(223),S=n(204),O=n(206),T=n(224),x=n(208);e.DOCUMENT=x.DOCUMENT;var A=n(228);e.Title=A.Title;var I=n(224);e.ELEMENT_PROBE_PROVIDERS=I.ELEMENT_PROBE_PROVIDERS,e.ELEMENT_PROBE_PROVIDERS_PROD_MODE=I.ELEMENT_PROBE_PROVIDERS_PROD_MODE,e.inspectNativeElement=I.inspectNativeElement,e.By=I.By;var M=n(218);e.BrowserDomAdapter=M.BrowserDomAdapter;var k=n(229);e.enableDebugTools=k.enableDebugTools,e.disableDebugTools=k.disableDebugTools;var N=n(206);e.HAMMER_GESTURE_CONFIG=N.HAMMER_GESTURE_CONFIG,e.HammerGestureConfig=N.HammerGestureConfig,e.BROWSER_PLATFORM_MARKER=s.CONST_EXPR(new a.OpaqueToken("BrowserPlatformMarker")),e.BROWSER_PROVIDERS=s.CONST_EXPR([new a.Provider(e.BROWSER_PLATFORM_MARKER,{useValue:!0}),c.PLATFORM_COMMON_PROVIDERS,new a.Provider(c.PLATFORM_INITIALIZER,{useValue:o,multi:!0})]),e.BROWSER_APP_COMMON_PROVIDERS=s.CONST_EXPR([c.APPLICATION_COMMON_PROVIDERS,p.FORM_PROVIDERS,new a.Provider(c.PLATFORM_PIPES,{useValue:p.COMMON_PIPES,multi:!0}),new a.Provider(c.PLATFORM_DIRECTIVES,{useValue:p.COMMON_DIRECTIVES,multi:!0}),new a.Provider(c.ExceptionHandler,{useFactory:r,deps:[]}),new a.Provider(y.DOCUMENT,{useFactory:i,deps:[]}),new a.Provider(S.EVENT_MANAGER_PLUGINS,{useClass:f.DomEventsPlugin,multi:!0}),new a.Provider(S.EVENT_MANAGER_PLUGINS,{useClass:d.KeyEventsPlugin,multi:!0}),new a.Provider(S.EVENT_MANAGER_PLUGINS,{useClass:v.HammerGesturesPlugin,multi:!0}),new a.Provider(O.HAMMER_GESTURE_CONFIG,{useClass:O.HammerGestureConfig}),new a.Provider(m.DomRootRenderer,{useClass:m.DomRootRenderer_}),new a.Provider(c.RootRenderer,{useExisting:m.DomRootRenderer}),new a.Provider(_.SharedStylesHost,{useExisting:g.DomSharedStylesHost}),g.DomSharedStylesHost,l.Testability,b.BrowserDetails,P.AnimationBuilder,S.EventManager,T.ELEMENT_PROBE_PROVIDERS]),e.CACHED_TEMPLATE_PROVIDER=s.CONST_EXPR([new a.Provider(u.XHR,{useClass:C.CachedXHR})]),e.initDomAdapter=o},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(200),a=n(2),u=n(204),c=function(t){function e(){t.apply(this,arguments)}return r(e,t),e.prototype.supports=function(t){return!0},e.prototype.addEventListener=function(t,e,n){var r=this.manager.getZone(),i=function(t){return r.runGuarded(function(){return n(t)})};return this.manager.getZone().runOutsideAngular(function(){return s.DOM.onAndCancel(t,e,i)})},e.prototype.addGlobalEventListener=function(t,e,n){var r=s.DOM.getGlobalEventTarget(t),i=this.manager.getZone(),o=function(t){return i.runGuarded(function(){return n(t)})};return this.manager.getZone().runOutsideAngular(function(){return s.DOM.onAndCancel(r,e,o)})},e=i([a.Injectable(),o("design:paramtypes",[])],e)}(u.EventManagerPlugin);e.DomEventsPlugin=c},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},s=n(5),a=n(12),u=n(6),c=n(60),p=n(15);e.EVENT_MANAGER_PLUGINS=s.CONST_EXPR(new u.OpaqueToken("EventManagerPlugins"));var l=function(){function t(t,e){var n=this;this._zone=e,t.forEach(function(t){return t.manager=n}),this._plugins=p.ListWrapper.reversed(t)}return t.prototype.addEventListener=function(t,e,n){var r=this._findPluginFor(e);return r.addEventListener(t,e,n)},t.prototype.addGlobalEventListener=function(t,e,n){var r=this._findPluginFor(e);return r.addGlobalEventListener(t,e,n)},t.prototype.getZone=function(){return this._zone},t.prototype._findPluginFor=function(t){for(var e=this._plugins,n=0;no?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(200),a=n(5),u=n(15),c=n(204),p=n(6),l=["alt","control","meta","shift"],h={alt:function(t){return t.altKey},control:function(t){return t.ctrlKey},meta:function(t){return t.metaKey},shift:function(t){return t.shiftKey}},f=function(t){function e(){t.call(this)}return r(e,t),e.prototype.supports=function(t){return a.isPresent(e.parseEventName(t))},e.prototype.addEventListener=function(t,n,r){var i=e.parseEventName(n),o=e.eventCallback(t,u.StringMapWrapper.get(i,"fullKey"),r,this.manager.getZone());return this.manager.getZone().runOutsideAngular(function(){return s.DOM.onAndCancel(t,u.StringMapWrapper.get(i,"domEventName"),o)})},e.parseEventName=function(t){var n=t.toLowerCase().split("."),r=n.shift();if(0===n.length||!a.StringWrapper.equals(r,"keydown")&&!a.StringWrapper.equals(r,"keyup"))return null;var i=e._normalizeKey(n.pop()),o="";if(l.forEach(function(t){u.ListWrapper.contains(n,t)&&(u.ListWrapper.remove(n,t),o+=t+".")}),o+=i,0!=n.length||0===i.length)return null;var s=u.StringMapWrapper.create();return u.StringMapWrapper.set(s,"domEventName",r),u.StringMapWrapper.set(s,"fullKey",o),s},e.getEventFullKey=function(t){var e="",n=s.DOM.getEventKey(t);return n=n.toLowerCase(),a.StringWrapper.equals(n," ")?n="space":a.StringWrapper.equals(n,".")&&(n="dot"),l.forEach(function(r){if(r!=n){var i=u.StringMapWrapper.get(h,r);i(t)&&(e+=r+".")}}),e+=n},e.eventCallback=function(t,n,r,i){return function(t){a.StringWrapper.equals(e.getEventFullKey(t),n)&&i.runGuarded(function(){return r(t)})}},e._normalizeKey=function(t){switch(t){case"esc":return"escape";default:return t}},e=i([p.Injectable(),o("design:paramtypes",[])],e)}(c.EventManagerPlugin);e.KeyEventsPlugin=f},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},a=n(207),u=n(5),c=n(12),p=n(2);e.HAMMER_GESTURE_CONFIG=u.CONST_EXPR(new p.OpaqueToken("HammerGestureConfig"));var l=function(){function t(){this.events=[],this.overrides={}}return t.prototype.buildHammer=function(t){var e=new Hammer(t);e.get("pinch").set({enable:!0}),e.get("rotate").set({enable:!0});for(var n in this.overrides)e.get(n).set(this.overrides[n]);return e},t=i([p.Injectable(),o("design:paramtypes",[])],t)}();e.HammerGestureConfig=l;var h=function(t){function n(e){t.call(this),this._config=e}return r(n,t),n.prototype.supports=function(e){if(!t.prototype.supports.call(this,e)&&!this.isCustomEvent(e))return!1;if(!u.isPresent(window.Hammer))throw new c.BaseException("Hammer.js is not loaded, can not bind "+e+" event");return!0},n.prototype.addEventListener=function(t,e,n){var r=this,i=this.manager.getZone();return e=e.toLowerCase(),i.runOutsideAngular(function(){var o=r._config.buildHammer(t),s=function(t){i.runGuarded(function(){n(t)})};return o.on(e,s),function(){o.off(e,s)}})},n.prototype.isCustomEvent=function(t){return this._config.events.indexOf(t)>-1},n=i([p.Injectable(),s(0,p.Inject(e.HAMMER_GESTURE_CONFIG)),o("design:paramtypes",[l])],n)}(a.HammerGesturesPluginCommon);e.HammerGesturesPlugin=h},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(204),o=n(15),s={pan:!0,panstart:!0,panmove:!0,panend:!0,pancancel:!0,panleft:!0,panright:!0,panup:!0,pandown:!0,pinch:!0,pinchstart:!0,pinchmove:!0,pinchend:!0,pinchcancel:!0,pinchin:!0,pinchout:!0,press:!0,pressup:!0,rotate:!0,rotatestart:!0,rotatemove:!0,rotateend:!0,rotatecancel:!0,swipe:!0,swipeleft:!0,swiperight:!0,swipeup:!0,swipedown:!0,tap:!0},a=function(t){function e(){t.call(this)}return r(e,t),e.prototype.supports=function(t){return t=t.toLowerCase(),o.StringMapWrapper.contains(s,t)},e}(i.EventManagerPlugin);e.HammerGesturesPluginCommon=a},function(t,e,n){"use strict";var r=n(6),i=n(5);e.DOCUMENT=i.CONST_EXPR(new r.OpaqueToken("DocumentToken"))},function(t,e,n){"use strict";function r(t,e){var n=E.DOM.parentElement(t);if(e.length>0&&y.isPresent(n)){var r=E.DOM.nextSibling(t);if(y.isPresent(r))for(var i=0;io?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s); -return o>3&&s&&Object.defineProperty(e,n,s),s},h=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},f=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},d=n(6),v=n(210),y=n(5),m=n(12),g=n(217),_=n(204),b=n(208),P=n(3),E=n(200),w=n(215),C=y.CONST_EXPR({xlink:"http://www.w3.org/1999/xlink",svg:"http://www.w3.org/2000/svg"}),R="template bindings={}",S=/^template bindings=(.*)$/g,O=function(){function t(t,e,n,r){this.document=t,this.eventManager=e,this.sharedStylesHost=n,this.animate=r,this._registeredComponents=new Map}return t.prototype.renderComponent=function(t){var e=this._registeredComponents.get(t.id);return y.isBlank(e)&&(e=new x(this,t),this._registeredComponents.set(t.id,e)),e},t}();e.DomRootRenderer=O;var T=function(t){function e(e,n,r,i){t.call(this,e,n,r,i)}return p(e,t),e=l([d.Injectable(),f(0,d.Inject(b.DOCUMENT)),h("design:paramtypes",[Object,_.EventManager,g.DomSharedStylesHost,v.AnimationBuilder])],e)}(O);e.DomRootRenderer_=T;var x=function(){function t(t,e){this._rootRenderer=t,this.componentProto=e,this._styles=u(e.id,e.styles,[]),e.encapsulation!==P.ViewEncapsulation.Native&&this._rootRenderer.sharedStylesHost.addStyles(this._styles),this.componentProto.encapsulation===P.ViewEncapsulation.Emulated?(this._contentAttr=s(e.id),this._hostAttr=a(e.id)):(this._contentAttr=null,this._hostAttr=null)}return t.prototype.selectRootElement=function(t,e){var n;if(y.isString(t)){if(n=E.DOM.querySelector(this._rootRenderer.document,t),y.isBlank(n))throw new m.BaseException('The selector "'+t+'" did not match any elements')}else n=t;return E.DOM.clearNodes(n),n},t.prototype.createElement=function(t,e,n){var r=c(e),i=y.isPresent(r[0])?E.DOM.createElementNS(C[r[0]],r[1]):E.DOM.createElement(r[1]);return y.isPresent(this._contentAttr)&&E.DOM.setAttribute(i,this._contentAttr,""),y.isPresent(t)&&E.DOM.appendChild(t,i),i},t.prototype.createViewRoot=function(t){var e;if(this.componentProto.encapsulation===P.ViewEncapsulation.Native){e=E.DOM.createShadowRoot(t),this._rootRenderer.sharedStylesHost.addHost(e);for(var n=0;no?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(6),s=n(211),a=n(216),u=function(){function t(t){this.browserDetails=t}return t.prototype.css=function(){return new s.CssAnimationBuilder(this.browserDetails)},t=r([o.Injectable(),i("design:paramtypes",[a.BrowserDetails])],t)}();e.AnimationBuilder=u},function(t,e,n){"use strict";var r=n(212),i=n(213),o=function(){function t(t){this.browserDetails=t,this.data=new r.CssAnimationOptions}return t.prototype.addAnimationClass=function(t){return this.data.animationClasses.push(t),this},t.prototype.addClass=function(t){return this.data.classesToAdd.push(t),this},t.prototype.removeClass=function(t){return this.data.classesToRemove.push(t),this},t.prototype.setDuration=function(t){return this.data.duration=t,this},t.prototype.setDelay=function(t){return this.data.delay=t,this},t.prototype.setStyles=function(t,e){return this.setFromStyles(t).setToStyles(e)},t.prototype.setFromStyles=function(t){return this.data.fromStyles=t,this},t.prototype.setToStyles=function(t){return this.data.toStyles=t,this},t.prototype.start=function(t){return new i.Animation(t,this.data,this.browserDetails)},t}();e.CssAnimationBuilder=o},function(t,e){"use strict";var n=function(){function t(){this.classesToAdd=[],this.classesToRemove=[],this.animationClasses=[]}return t}();e.CssAnimationOptions=n},function(t,e,n){"use strict";var r=n(5),i=n(214),o=n(215),s=n(15),a=n(200),u=function(){function t(t,e,n){var i=this;this.element=t,this.data=e,this.browserDetails=n,this.callbacks=[],this.eventClearFunctions=[],this.completed=!1,this._stringPrefix="",this.startTime=r.DateWrapper.toMillis(r.DateWrapper.now()),this._stringPrefix=a.DOM.getAnimationPrefix(),this.setup(),this.wait(function(t){return i.start()})}return Object.defineProperty(t.prototype,"totalTime",{get:function(){var t=null!=this.computedDelay?this.computedDelay:0,e=null!=this.computedDuration?this.computedDuration:0;return t+e},enumerable:!0,configurable:!0}),t.prototype.wait=function(t){this.browserDetails.raf(t,2)},t.prototype.setup=function(){null!=this.data.fromStyles&&this.applyStyles(this.data.fromStyles),null!=this.data.duration&&this.applyStyles({transitionDuration:this.data.duration.toString()+"ms"}),null!=this.data.delay&&this.applyStyles({transitionDelay:this.data.delay.toString()+"ms"})},t.prototype.start=function(){this.addClasses(this.data.classesToAdd),this.addClasses(this.data.animationClasses),this.removeClasses(this.data.classesToRemove),null!=this.data.toStyles&&this.applyStyles(this.data.toStyles);var t=a.DOM.getComputedStyle(this.element);this.computedDelay=i.Math.max(this.parseDurationString(t.getPropertyValue(this._stringPrefix+"transition-delay")),this.parseDurationString(this.element.style.getPropertyValue(this._stringPrefix+"transition-delay"))),this.computedDuration=i.Math.max(this.parseDurationString(t.getPropertyValue(this._stringPrefix+"transition-duration")),this.parseDurationString(this.element.style.getPropertyValue(this._stringPrefix+"transition-duration"))),this.addEvents()},t.prototype.applyStyles=function(t){var e=this;s.StringMapWrapper.forEach(t,function(t,n){var i=o.camelCaseToDashCase(n);r.isPresent(a.DOM.getStyle(e.element,i))?a.DOM.setStyle(e.element,i,t.toString()):a.DOM.setStyle(e.element,e._stringPrefix+i,t.toString())})},t.prototype.addClasses=function(t){for(var e=0,n=t.length;n>e;e++)a.DOM.addClass(this.element,t[e])},t.prototype.removeClasses=function(t){for(var e=0,n=t.length;n>e;e++)a.DOM.removeClass(this.element,t[e])},t.prototype.addEvents=function(){var t=this;this.totalTime>0?this.eventClearFunctions.push(a.DOM.onAndCancel(this.element,a.DOM.getTransitionEnd(),function(e){return t.handleAnimationEvent(e)})):this.handleAnimationCompleted()},t.prototype.handleAnimationEvent=function(t){var e=i.Math.round(1e3*t.elapsedTime);this.browserDetails.elapsedTimeIncludesDelay||(e+=this.computedDelay),t.stopPropagation(),e>=this.totalTime&&this.handleAnimationCompleted()},t.prototype.handleAnimationCompleted=function(){this.removeClasses(this.data.animationClasses),this.callbacks.forEach(function(t){return t()}),this.callbacks=[],this.eventClearFunctions.forEach(function(t){return t()}),this.eventClearFunctions=[],this.completed=!0},t.prototype.onComplete=function(t){return this.completed?t():this.callbacks.push(t),this},t.prototype.parseDurationString=function(t){var e=0;if(null==t||t.length<2)return e;if("ms"==t.substring(t.length-2)){var n=r.NumberWrapper.parseInt(this.stripLetters(t),10);n>e&&(e=n)}else if("s"==t.substring(t.length-1)){var o=1e3*r.NumberWrapper.parseFloat(this.stripLetters(t)),n=i.Math.floor(o);n>e&&(e=n)}return e},t.prototype.stripLetters=function(t){return r.StringWrapper.replaceAll(t,r.RegExpWrapper.create("[^0-9]+$",""),"")},t}();e.Animation=u},function(t,e,n){"use strict";var r=n(5);e.Math=r.global.Math,e.NaN=typeof e.NaN},function(t,e,n){"use strict";function r(t){return o.StringWrapper.replaceAllMapped(t,s,function(t){return"-"+t[1].toLowerCase()})}function i(t){return o.StringWrapper.replaceAllMapped(t,a,function(t){return t[1].toUpperCase()})}var o=n(5),s=/([A-Z])/g,a=/-([a-z])/g;e.camelCaseToDashCase=r,e.dashCaseToCamelCase=i},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(6),s=n(214),a=n(200),u=function(){function t(){this.elapsedTimeIncludesDelay=!1,this.doesElapsedTimeIncludesDelay()}return t.prototype.doesElapsedTimeIncludesDelay=function(){var t=this,e=a.DOM.createElement("div");a.DOM.setAttribute(e,"style","position: absolute; top: -9999px; left: -9999px; width: 1px;\n height: 1px; transition: all 1ms linear 1ms;"),this.raf(function(n){a.DOM.on(e,"transitionend",function(n){var r=s.Math.round(1e3*n.elapsedTime);t.elapsedTimeIncludesDelay=2==r,a.DOM.remove(e)}),a.DOM.setStyle(e,"width","2px")},2)},t.prototype.raf=function(t,e){void 0===e&&(e=1);var n=new c(t,e);return function(){return n.cancel()}},t=r([o.Injectable(),i("design:paramtypes",[])],t)}();e.BrowserDetails=u;var c=function(){function t(t,e){this.callback=t,this.frames=e,this._raf()}return t.prototype._raf=function(){var t=this;this.currentFrameId=a.DOM.requestAnimationFrame(function(e){return t._nextFrame(e)})},t.prototype._nextFrame=function(t){this.frames--,this.frames>0?this._raf():this.callback(t)},t.prototype.cancel=function(){a.DOM.cancelAnimationFrame(this.currentFrameId),this.currentFrameId=null},t}()},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},a=n(200),u=n(6),c=n(15),p=n(208),l=function(){function t(){this._styles=[],this._stylesSet=new Set}return t.prototype.addStyles=function(t){var e=this,n=[];t.forEach(function(t){c.SetWrapper.has(e._stylesSet,t)||(e._stylesSet.add(t),e._styles.push(t),n.push(t))}),this.onStylesAdded(n)},t.prototype.onStylesAdded=function(t){},t.prototype.getAllStyles=function(){return this._styles},t=i([u.Injectable(),o("design:paramtypes",[])],t)}();e.SharedStylesHost=l;var h=function(t){function e(e){t.call(this),this._hostNodes=new Set,this._hostNodes.add(e.head)}return r(e,t),e.prototype._addStylesToHost=function(t,e){for(var n=0;n0},e.prototype.tagName=function(t){return t.tagName},e.prototype.attributeMap=function(t){for(var e=new Map,n=t.attributes,r=0;r=200&&300>=i?e.resolve(r):e.reject("Failed to load "+t,null)},n.onerror=function(){e.reject("Failed to load "+t,null)},n.send(),e.promise},e}(s.XHR);e.XHRImpl=a},function(t,e,n){"use strict";var r=n(15),i=n(5),o=n(200),s=n(2),a=function(){function t(t){this._testability=t}return t.prototype.isStable=function(){return this._testability.isStable()},t.prototype.whenStable=function(t){this._testability.whenStable(t)},t.prototype.findBindings=function(t,e,n){return this.findProviders(t,e,n)},t.prototype.findProviders=function(t,e,n){return this._testability.findBindings(t,e,n)},t}(),u=function(){function t(){}return t.init=function(){s.setTestabilityGetter(new t)},t.prototype.addToWindow=function(t){i.global.getAngularTestability=function(e,n){void 0===n&&(n=!0);var r=t.findTestabilityInTree(e,n);if(null==r)throw new Error("Could not find testability for element.");return new a(r)},i.global.getAllAngularTestabilities=function(){var e=t.getAllTestabilities();return e.map(function(t){return new a(t)})},i.global.getAllAngularRootElements=function(){return t.getAllRootElements()};var e=function(t){var e=i.global.getAllAngularTestabilities(),n=e.length,r=!1,o=function(e){r=r||e,n--,0==n&&t(r)};e.forEach(function(t){t.whenStable(o)})};i.global.frameworkStabilizers||(i.global.frameworkStabilizers=r.ListWrapper.createGrowableSize(0)),i.global.frameworkStabilizers.push(e)},t.prototype.findTestabilityInTree=function(t,e,n){if(null==e)return null;var r=t.getTestability(e);return i.isPresent(r)?r:n?o.DOM.isShadowRoot(e)?this.findTestabilityInTree(t,o.DOM.getHost(e),!0):this.findTestabilityInTree(t,o.DOM.parentElement(e),!0):null},t}();e.BrowserGetTestability=u},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(184),o=n(12),s=n(5),a=n(41),u=function(t){function e(){if(t.call(this),this._cache=s.global.$templateCache,null==this._cache)throw new o.BaseException("CachedXHR: Template cache was not found in $templateCache.")}return r(e,t),e.prototype.get=function(t){return this._cache.hasOwnProperty(t)?a.PromiseWrapper.resolve(this._cache[t]):a.PromiseWrapper.reject("CachedXHR: Did not find cached template for "+t,null)},e}(i.XHR);e.CachedXHR=u},function(t,e){"use strict";function n(){}e.wtfInit=n},function(t,e,n){"use strict";function r(t){for(var n in t)e.hasOwnProperty(n)||(e[n]=t[n])}var i=n(200);e.DOM=i.DOM,e.setRootDomAdapter=i.setRootDomAdapter,e.DomAdapter=i.DomAdapter;var o=n(209);e.DomRenderer=o.DomRenderer;var s=n(208);e.DOCUMENT=s.DOCUMENT;var a=n(217);e.SharedStylesHost=a.SharedStylesHost,e.DomSharedStylesHost=a.DomSharedStylesHost;var u=n(203);e.DomEventsPlugin=u.DomEventsPlugin;var c=n(204);e.EVENT_MANAGER_PLUGINS=c.EVENT_MANAGER_PLUGINS,e.EventManager=c.EventManager,e.EventManagerPlugin=c.EventManagerPlugin,r(n(225)),r(n(226))},function(t,e,n){"use strict";var r=n(5),i=n(200),o=function(){function t(){}return t.all=function(){return function(t){return!0}},t.css=function(t){return function(e){return r.isPresent(e.nativeElement)?i.DOM.elementMatches(e.nativeElement,t):!1}},t.directive=function(t){return function(e){return-1!==e.providerTokens.indexOf(t)}},t}();e.By=o},function(t,e,n){"use strict";function r(t){return c.getDebugNode(t)}function i(t){return s.assertionsEnabled()?o(t):t}function o(t){return u.DOM.setGlobalVar(d,r),u.DOM.setGlobalVar(v,f),new h.DebugDomRootRenderer(t)}var s=n(5),a=n(6),u=n(200),c=n(83),p=n(209),l=n(2),h=n(227),f=s.CONST_EXPR({ApplicationRef:l.ApplicationRef,NgZone:l.NgZone}),d="ng.probe",v="ng.coreTokens";e.inspectNativeElement=r,e.ELEMENT_PROBE_PROVIDERS=s.CONST_EXPR([new a.Provider(l.RootRenderer,{useFactory:i,deps:[p.DomRootRenderer]})]),e.ELEMENT_PROBE_PROVIDERS_PROD_MODE=s.CONST_EXPR([new a.Provider(l.RootRenderer,{useFactory:o,deps:[p.DomRootRenderer]})])},function(t,e,n){"use strict";var r=n(5),i=n(83),o=function(){function t(t){this._delegate=t}return t.prototype.renderComponent=function(t){return new s(this._delegate.renderComponent(t))},t}();e.DebugDomRootRenderer=o;var s=function(){function t(t){this._delegate=t}return t.prototype.selectRootElement=function(t,e){var n=this._delegate.selectRootElement(t,e),r=new i.DebugElement(n,null,e);return i.indexDebugNode(r),n},t.prototype.createElement=function(t,e,n){var r=this._delegate.createElement(t,e,n),o=new i.DebugElement(r,i.getDebugNode(t),n);return o.name=e,i.indexDebugNode(o),r},t.prototype.createViewRoot=function(t){return this._delegate.createViewRoot(t)},t.prototype.createTemplateAnchor=function(t,e){var n=this._delegate.createTemplateAnchor(t,e),r=new i.DebugNode(n,i.getDebugNode(t),e);return i.indexDebugNode(r),n},t.prototype.createText=function(t,e,n){var r=this._delegate.createText(t,e,n),o=new i.DebugNode(r,i.getDebugNode(t),n);return i.indexDebugNode(o),r},t.prototype.projectNodes=function(t,e){var n=i.getDebugNode(t);if(r.isPresent(n)&&n instanceof i.DebugElement){var o=n;e.forEach(function(t){o.addChild(i.getDebugNode(t))})}this._delegate.projectNodes(t,e)},t.prototype.attachViewAfter=function(t,e){var n=i.getDebugNode(t);if(r.isPresent(n)){var o=n.parent;if(e.length>0&&r.isPresent(o)){var s=[];e.forEach(function(t){return s.push(i.getDebugNode(t))}),o.insertChildrenAfter(n,s)}}this._delegate.attachViewAfter(t,e)},t.prototype.detachView=function(t){t.forEach(function(t){var e=i.getDebugNode(t);r.isPresent(e)&&r.isPresent(e.parent)&&e.parent.removeChild(e)}),this._delegate.detachView(t)},t.prototype.destroyView=function(t,e){e.forEach(function(t){i.removeDebugNodeFromIndex(i.getDebugNode(t))}),this._delegate.destroyView(t,e)},t.prototype.listen=function(t,e,n){var o=i.getDebugNode(t);return r.isPresent(o)&&o.listeners.push(new i.EventListener(e,n)),this._delegate.listen(t,e,n)},t.prototype.listenGlobal=function(t,e,n){return this._delegate.listenGlobal(t,e,n)},t.prototype.setElementProperty=function(t,e,n){ -var o=i.getDebugNode(t);r.isPresent(o)&&o instanceof i.DebugElement&&(o.properties[e]=n),this._delegate.setElementProperty(t,e,n)},t.prototype.setElementAttribute=function(t,e,n){var o=i.getDebugNode(t);r.isPresent(o)&&o instanceof i.DebugElement&&(o.attributes[e]=n),this._delegate.setElementAttribute(t,e,n)},t.prototype.setBindingDebugInfo=function(t,e,n){this._delegate.setBindingDebugInfo(t,e,n)},t.prototype.setElementClass=function(t,e,n){this._delegate.setElementClass(t,e,n)},t.prototype.setElementStyle=function(t,e,n){this._delegate.setElementStyle(t,e,n)},t.prototype.invokeElementMethod=function(t,e,n){this._delegate.invokeElementMethod(t,e,n)},t.prototype.setText=function(t,e){this._delegate.setText(t,e)},t}();e.DebugDomRenderer=s},function(t,e,n){"use strict";var r=n(200),i=function(){function t(){}return t.prototype.getTitle=function(){return r.DOM.getTitle()},t.prototype.setTitle=function(t){r.DOM.setTitle(t)},t}();e.Title=i},function(t,e,n){"use strict";function r(t){a.ng=new s.AngularTools(t)}function i(){delete a.ng}var o=n(5),s=n(230),a=o.global;e.enableDebugTools=r,e.disableDebugTools=i},function(t,e,n){"use strict";var r=n(59),i=n(5),o=n(231),s=n(200),a=function(){function t(t,e){this.msPerTick=t,this.numTicks=e}return t}();e.ChangeDetectionPerfRecord=a;var u=function(){function t(t){this.profiler=new c(t)}return t}();e.AngularTools=u;var c=function(){function t(t){this.appRef=t.injector.get(r.ApplicationRef)}return t.prototype.timeChangeDetection=function(t){var e=i.isPresent(t)&&t.record,n="Change Detection",r=i.isPresent(o.window.console.profile);e&&r&&o.window.console.profile(n);for(var u=s.DOM.performanceNow(),c=0;5>c||s.DOM.performanceNow()-u<500;)this.appRef.tick(),c++;var p=s.DOM.performanceNow();e&&r&&o.window.console.profileEnd(n);var l=(p-u)/c;return o.window.console.log("ran "+c+" change detection cycles"),o.window.console.log(i.NumberWrapper.toFixed(l,2)+" ms per check"),new a(l,c)},t}();e.AngularProfiler=c},function(t,e){"use strict";var n=window;e.window=n,e.document=window.document,e.location=window.location,e.gc=window.gc?function(){return window.gc()}:function(){return null},e.performance=window.performance?window.performance:null,e.Event=window.Event,e.MouseEvent=window.MouseEvent,e.KeyboardEvent=window.KeyboardEvent,e.EventTarget=window.EventTarget,e.History=window.History,e.Location=window.Location,e.EventListener=window.EventListener},function(t,e,n){"use strict";var r=n(2),i=n(233),o=n(241),s=n(245),a=n(244),u=n(246),c=n(239),p=n(243),l=n(235);e.Request=l.Request;var h=n(242);e.Response=h.Response;var f=n(234);e.Connection=f.Connection,e.ConnectionBackend=f.ConnectionBackend;var d=n(244);e.BrowserXhr=d.BrowserXhr;var v=n(239);e.BaseRequestOptions=v.BaseRequestOptions,e.RequestOptions=v.RequestOptions;var y=n(243);e.BaseResponseOptions=y.BaseResponseOptions,e.ResponseOptions=y.ResponseOptions;var m=n(241);e.XHRBackend=m.XHRBackend,e.XHRConnection=m.XHRConnection;var g=n(245);e.JSONPBackend=g.JSONPBackend,e.JSONPConnection=g.JSONPConnection;var _=n(233);e.Http=_.Http,e.Jsonp=_.Jsonp;var b=n(236);e.Headers=b.Headers;var P=n(238);e.ResponseType=P.ResponseType,e.ReadyState=P.ReadyState,e.RequestMethod=P.RequestMethod;var E=n(240);e.URLSearchParams=E.URLSearchParams,e.HTTP_PROVIDERS=[r.provide(i.Http,{useFactory:function(t,e){return new i.Http(t,e)},deps:[o.XHRBackend,c.RequestOptions]}),a.BrowserXhr,r.provide(c.RequestOptions,{useClass:c.BaseRequestOptions}),r.provide(p.ResponseOptions,{useClass:p.BaseResponseOptions}),o.XHRBackend],e.HTTP_BINDINGS=e.HTTP_PROVIDERS,e.JSONP_PROVIDERS=[r.provide(i.Jsonp,{useFactory:function(t,e){return new i.Jsonp(t,e)},deps:[s.JSONPBackend,c.RequestOptions]}),u.BrowserJsonp,r.provide(c.RequestOptions,{useClass:c.BaseRequestOptions}),r.provide(p.ResponseOptions,{useClass:p.BaseResponseOptions}),r.provide(s.JSONPBackend,{useClass:s.JSONPBackend_})],e.JSON_BINDINGS=e.JSONP_PROVIDERS},function(t,e,n){"use strict";function r(t,e){return t.createConnection(e).response}function i(t,e,n,r){var i=t;return u.isPresent(e)?i.merge(new f.RequestOptions({method:e.method||n,url:e.url||r,search:e.search,headers:e.headers,body:e.body})):u.isPresent(n)?i.merge(new f.RequestOptions({method:n,url:r})):i.merge(new f.RequestOptions({url:r}))}var o=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},s=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},a=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},u=n(5),c=n(12),p=n(2),l=n(234),h=n(235),f=n(239),d=n(238),v=function(){function t(t,e){this._backend=t,this._defaultOptions=e}return t.prototype.request=function(t,e){var n;if(u.isString(t))n=r(this._backend,new h.Request(i(this._defaultOptions,e,d.RequestMethod.Get,t)));else{if(!(t instanceof h.Request))throw c.makeTypeError("First argument must be a url string or Request instance.");n=r(this._backend,t)}return n},t.prototype.get=function(t,e){return r(this._backend,new h.Request(i(this._defaultOptions,e,d.RequestMethod.Get,t)))},t.prototype.post=function(t,e,n){return r(this._backend,new h.Request(i(this._defaultOptions.merge(new f.RequestOptions({body:e})),n,d.RequestMethod.Post,t)))},t.prototype.put=function(t,e,n){return r(this._backend,new h.Request(i(this._defaultOptions.merge(new f.RequestOptions({body:e})),n,d.RequestMethod.Put,t)))},t.prototype["delete"]=function(t,e){return r(this._backend,new h.Request(i(this._defaultOptions,e,d.RequestMethod.Delete,t)))},t.prototype.patch=function(t,e,n){return r(this._backend,new h.Request(i(this._defaultOptions.merge(new f.RequestOptions({body:e})),n,d.RequestMethod.Patch,t)))},t.prototype.head=function(t,e){return r(this._backend,new h.Request(i(this._defaultOptions,e,d.RequestMethod.Head,t)))},t=s([p.Injectable(),a("design:paramtypes",[l.ConnectionBackend,f.RequestOptions])],t)}();e.Http=v;var y=function(t){function e(e,n){t.call(this,e,n)}return o(e,t),e.prototype.request=function(t,e){var n;if(u.isString(t)&&(t=new h.Request(i(this._defaultOptions,e,d.RequestMethod.Get,t))),!(t instanceof h.Request))throw c.makeTypeError("First argument must be a url string or Request instance.");return t.method!==d.RequestMethod.Get&&c.makeTypeError("JSONP requests must use GET request method."),n=r(this._backend,t)},e=s([p.Injectable(),a("design:paramtypes",[l.ConnectionBackend,f.RequestOptions])],e)}(v);e.Jsonp=y},function(t,e){"use strict";var n=function(){function t(){}return t}();e.ConnectionBackend=n;var r=function(){function t(){}return t}();e.Connection=r},function(t,e,n){"use strict";var r=n(236),i=n(237),o=n(5),s=function(){function t(t){var e=t.url;if(this.url=t.url,o.isPresent(t.search)){var n=t.search.toString();if(n.length>0){var s="?";o.StringWrapper.contains(this.url,"?")&&(s="&"==this.url[this.url.length-1]?"":"&"),this.url=e+s+n}}this._body=t.body,this.method=i.normalizeMethodName(t.method),this.headers=new r.Headers(t.headers)}return t.prototype.text=function(){return o.isPresent(this._body)?this._body.toString():""},t}();e.Request=s},function(t,e,n){"use strict";var r=n(5),i=n(12),o=n(15),s=function(){function t(e){var n=this;return e instanceof t?void(this._headersMap=e._headersMap):(this._headersMap=new o.Map,void(r.isBlank(e)||o.StringMapWrapper.forEach(e,function(t,e){n._headersMap.set(e,o.isListLikeIterable(t)?t:[t])})))}return t.fromResponseHeaderString=function(e){return e.trim().split("\n").map(function(t){return t.split(":")}).map(function(t){var e=t[0],n=t.slice(1);return[e.trim(),n.join(":").trim()]}).reduce(function(t,e){var n=e[0],r=e[1];return!t.set(n,r)&&t},new t)},t.prototype.append=function(t,e){var n=this._headersMap.get(t),r=o.isListLikeIterable(n)?n:[];r.push(e),this._headersMap.set(t,r)},t.prototype["delete"]=function(t){this._headersMap["delete"](t)},t.prototype.forEach=function(t){this._headersMap.forEach(t)},t.prototype.get=function(t){return o.ListWrapper.first(this._headersMap.get(t))},t.prototype.has=function(t){return this._headersMap.has(t)},t.prototype.keys=function(){return o.MapWrapper.keys(this._headersMap)},t.prototype.set=function(t,e){var n=[];if(o.isListLikeIterable(e)){var r=e.join(",");n.push(r)}else n.push(e);this._headersMap.set(t,n)},t.prototype.values=function(){return o.MapWrapper.values(this._headersMap)},t.prototype.toJSON=function(){var t={};return this._headersMap.forEach(function(e,n){var r=[];o.iterateListLike(e,function(t){return r=o.ListWrapper.concat(r,t.split(","))}),t[n]=r}),t},t.prototype.getAll=function(t){var e=this._headersMap.get(t);return o.isListLikeIterable(e)?e:[]},t.prototype.entries=function(){throw new i.BaseException('"entries" method is not implemented on Headers class')},t}();e.Headers=s},function(t,e,n){"use strict";function r(t){if(o.isString(t)){var e=t;if(t=t.replace(/(\w)(\w*)/g,function(t,e,n){return e.toUpperCase()+n.toLowerCase()}),t=s.RequestMethod[t],"number"!=typeof t)throw a.makeTypeError('Invalid request method. The method "'+e+'" is not supported.')}return t}function i(t){return"responseURL"in t?t.responseURL:/^X-Request-URL:/m.test(t.getAllResponseHeaders())?t.getResponseHeader("X-Request-URL"):void 0}var o=n(5),s=n(238),a=n(12);e.normalizeMethodName=r,e.isSuccess=function(t){return t>=200&&300>t},e.getResponseURL=i;var u=n(5);e.isJsObject=u.isJsObject},function(t,e){"use strict";!function(t){t[t.Get=0]="Get",t[t.Post=1]="Post",t[t.Put=2]="Put",t[t.Delete=3]="Delete",t[t.Options=4]="Options",t[t.Head=5]="Head",t[t.Patch=6]="Patch"}(e.RequestMethod||(e.RequestMethod={}));e.RequestMethod;!function(t){t[t.Unsent=0]="Unsent",t[t.Open=1]="Open",t[t.HeadersReceived=2]="HeadersReceived",t[t.Loading=3]="Loading",t[t.Done=4]="Done",t[t.Cancelled=5]="Cancelled"}(e.ReadyState||(e.ReadyState={}));e.ReadyState;!function(t){t[t.Basic=0]="Basic",t[t.Cors=1]="Cors",t[t.Default=2]="Default",t[t.Error=3]="Error",t[t.Opaque=4]="Opaque"}(e.ResponseType||(e.ResponseType={}));e.ResponseType},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(5),a=n(236),u=n(238),c=n(2),p=n(240),l=n(237),h=function(){function t(t){var e=void 0===t?{}:t,n=e.method,r=e.headers,i=e.body,o=e.url,a=e.search;this.method=s.isPresent(n)?l.normalizeMethodName(n):null,this.headers=s.isPresent(r)?r:null,this.body=s.isPresent(i)?i:null,this.url=s.isPresent(o)?o:null,this.search=s.isPresent(a)?s.isString(a)?new p.URLSearchParams(a):a:null}return t.prototype.merge=function(e){return new t({method:s.isPresent(e)&&s.isPresent(e.method)?e.method:this.method,headers:s.isPresent(e)&&s.isPresent(e.headers)?e.headers:this.headers,body:s.isPresent(e)&&s.isPresent(e.body)?e.body:this.body,url:s.isPresent(e)&&s.isPresent(e.url)?e.url:this.url,search:s.isPresent(e)&&s.isPresent(e.search)?s.isString(e.search)?new p.URLSearchParams(e.search):e.search.clone():this.search})},t}();e.RequestOptions=h;var f=function(t){function e(){t.call(this,{method:u.RequestMethod.Get,headers:new a.Headers})}return r(e,t),e=i([c.Injectable(),o("design:paramtypes",[])],e)}(h);e.BaseRequestOptions=f},function(t,e,n){"use strict";function r(t){void 0===t&&(t="");var e=new o.Map;if(t.length>0){var n=t.split("&");n.forEach(function(t){var n=t.split("="),r=n[0],o=n[1],s=i.isPresent(e.get(r))?e.get(r):[];s.push(o),e.set(r,s)})}return e}var i=n(5),o=n(15),s=function(){function t(t){void 0===t&&(t=""),this.rawParams=t,this.paramsMap=r(t)}return t.prototype.clone=function(){var e=new t;return e.appendAll(this),e},t.prototype.has=function(t){return this.paramsMap.has(t)},t.prototype.get=function(t){var e=this.paramsMap.get(t);return o.isListLikeIterable(e)?o.ListWrapper.first(e):null},t.prototype.getAll=function(t){var e=this.paramsMap.get(t);return i.isPresent(e)?e:[]},t.prototype.set=function(t,e){var n=this.paramsMap.get(t),r=i.isPresent(n)?n:[];o.ListWrapper.clear(r),r.push(e),this.paramsMap.set(t,r)},t.prototype.setAll=function(t){var e=this;t.paramsMap.forEach(function(t,n){var r=e.paramsMap.get(n),s=i.isPresent(r)?r:[];o.ListWrapper.clear(s),s.push(t[0]),e.paramsMap.set(n,s)})},t.prototype.append=function(t,e){var n=this.paramsMap.get(t),r=i.isPresent(n)?n:[];r.push(e),this.paramsMap.set(t,r)},t.prototype.appendAll=function(t){var e=this;t.paramsMap.forEach(function(t,n){for(var r=e.paramsMap.get(n),o=i.isPresent(r)?r:[],s=0;so?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(238),s=n(242),a=n(236),u=n(243),c=n(2),p=n(244),l=n(5),h=n(42),f=n(237),d=function(){function t(t,e,n){var r=this;this.request=t,this.response=new h.Observable(function(i){var c=e.build();c.open(o.RequestMethod[t.method].toUpperCase(),t.url);var p=function(){var t=l.isPresent(c.response)?c.response:c.responseText,e=a.Headers.fromResponseHeaderString(c.getAllResponseHeaders()),r=f.getResponseURL(c),o=1223===c.status?204:c.status;0===o&&(o=t?200:0);var p=new u.ResponseOptions({body:t,status:o,headers:e,url:r});l.isPresent(n)&&(p=n.merge(p));var h=new s.Response(p);return f.isSuccess(o)?(i.next(h),void i.complete()):void i.error(h)},h=function(t){var e=new u.ResponseOptions({body:t,type:o.ResponseType.Error});l.isPresent(n)&&(e=n.merge(e)),i.error(new s.Response(e))};return l.isPresent(t.headers)&&t.headers.forEach(function(t,e){return c.setRequestHeader(e,t.join(","))}),c.addEventListener("load",p),c.addEventListener("error",h),c.send(r.request.text()),function(){c.removeEventListener("load",p),c.removeEventListener("error",h),c.abort()}})}return t}();e.XHRConnection=d;var v=function(){function t(t,e){this._browserXHR=t,this._baseResponseOptions=e}return t.prototype.createConnection=function(t){return new d(t,this._browserXHR,this._baseResponseOptions)},t=r([c.Injectable(),i("design:paramtypes",[p.BrowserXhr,u.ResponseOptions])],t)}();e.XHRBackend=v},function(t,e,n){"use strict";var r=n(5),i=n(12),o=n(237),s=function(){function t(t){this._body=t.body,this.status=t.status,this.ok=this.status>=200&&this.status<=299,this.statusText=t.statusText,this.headers=t.headers,this.type=t.type,this.url=t.url}return t.prototype.blob=function(){throw new i.BaseException('"blob()" method not implemented on Response superclass')},t.prototype.json=function(){var t;return o.isJsObject(this._body)?t=this._body:r.isString(this._body)&&(t=r.Json.parse(this._body)),t},t.prototype.text=function(){return this._body.toString()},t.prototype.arrayBuffer=function(){throw new i.BaseException('"arrayBuffer()" method not implemented on Response superclass')},t}();e.Response=s},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(2),a=n(5),u=n(236),c=n(238),p=function(){function t(t){var e=void 0===t?{}:t,n=e.body,r=e.status,i=e.headers,o=e.statusText,s=e.type,u=e.url;this.body=a.isPresent(n)?n:null,this.status=a.isPresent(r)?r:null,this.headers=a.isPresent(i)?i:null,this.statusText=a.isPresent(o)?o:null,this.type=a.isPresent(s)?s:null,this.url=a.isPresent(u)?u:null}return t.prototype.merge=function(e){return new t({body:a.isPresent(e)&&a.isPresent(e.body)?e.body:this.body,status:a.isPresent(e)&&a.isPresent(e.status)?e.status:this.status,headers:a.isPresent(e)&&a.isPresent(e.headers)?e.headers:this.headers,statusText:a.isPresent(e)&&a.isPresent(e.statusText)?e.statusText:this.statusText,type:a.isPresent(e)&&a.isPresent(e.type)?e.type:this.type,url:a.isPresent(e)&&a.isPresent(e.url)?e.url:this.url})},t}();e.ResponseOptions=p;var l=function(t){function e(){t.call(this,{status:200,statusText:"Ok",type:c.ResponseType.Default,headers:new u.Headers})}return r(e,t),e=i([s.Injectable(),o("design:paramtypes",[])],e)}(p);e.BaseResponseOptions=l},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(2),s=function(){function t(){}return t.prototype.build=function(){return new XMLHttpRequest},t=r([o.Injectable(),i("design:paramtypes",[])],t)}();e.BrowserXhr=s},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(234),a=n(238),u=n(242),c=n(243),p=n(2),l=n(246),h=n(12),f=n(5),d=n(42),v="JSONP injected script did not invoke callback.",y="JSONP requests must use GET request method.",m=function(){function t(){}return t}();e.JSONPConnection=m;var g=function(t){function e(e,n,r){var i=this;if(t.call(this),this._dom=n,this.baseResponseOptions=r,this._finished=!1,e.method!==a.RequestMethod.Get)throw h.makeTypeError(y);this.request=e,this.response=new d.Observable(function(t){i.readyState=a.ReadyState.Loading;var o=i._id=n.nextRequestID();n.exposeConnection(o,i);var s=n.requestCallback(i._id),p=e.url;p.indexOf("=JSONP_CALLBACK&")>-1?p=f.StringWrapper.replace(p,"=JSONP_CALLBACK&","="+s+"&"):p.lastIndexOf("=JSONP_CALLBACK")===p.length-"=JSONP_CALLBACK".length&&(p=p.substring(0,p.length-"=JSONP_CALLBACK".length)+("="+s));var l=i._script=n.build(p),h=function(e){if(i.readyState!==a.ReadyState.Cancelled){if(i.readyState=a.ReadyState.Done,n.cleanup(l),!i._finished){var o=new c.ResponseOptions({body:v,type:a.ResponseType.Error,url:p});return f.isPresent(r)&&(o=r.merge(o)),void t.error(new u.Response(o))}var s=new c.ResponseOptions({body:i._responseData,url:p});f.isPresent(i.baseResponseOptions)&&(s=i.baseResponseOptions.merge(s)),t.next(new u.Response(s)),t.complete()}},d=function(e){if(i.readyState!==a.ReadyState.Cancelled){i.readyState=a.ReadyState.Done,n.cleanup(l);var o=new c.ResponseOptions({body:e.message,type:a.ResponseType.Error});f.isPresent(r)&&(o=r.merge(o)),t.error(new u.Response(o))}};return l.addEventListener("load",h),l.addEventListener("error",d),n.send(l),function(){i.readyState=a.ReadyState.Cancelled,l.removeEventListener("load",h),l.removeEventListener("error",d),f.isPresent(l)&&i._dom.cleanup(l)}})}return r(e,t),e.prototype.finished=function(t){this._finished=!0,this._dom.removeConnection(this._id),this.readyState!==a.ReadyState.Cancelled&&(this._responseData=t)},e}(m);e.JSONPConnection_=g;var _=function(t){function e(){t.apply(this,arguments)}return r(e,t),e}(s.ConnectionBackend);e.JSONPBackend=_;var b=function(t){function e(e,n){t.call(this),this._browserJSONP=e,this._baseResponseOptions=n}return r(e,t),e.prototype.createConnection=function(t){return new g(t,this._browserJSONP,this._baseResponseOptions)},e=i([p.Injectable(),o("design:paramtypes",[l.BrowserJsonp,c.ResponseOptions])],e)}(_);e.JSONPBackend_=b},function(t,e,n){"use strict";function r(){return null===c&&(c=a.global[e.JSONP_HOME]={}),c}var i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(2),a=n(5),u=0;e.JSONP_HOME="__ng_jsonp__";var c=null,p=function(){function t(){}return t.prototype.build=function(t){var e=document.createElement("script");return e.src=t,e},t.prototype.nextRequestID=function(){return"__req"+u++},t.prototype.requestCallback=function(t){return e.JSONP_HOME+"."+t+".finished"},t.prototype.exposeConnection=function(t,e){var n=r();n[t]=e},t.prototype.removeConnection=function(t){var e=r();e[t]=null},t.prototype.send=function(t){document.body.appendChild(t)},t.prototype.cleanup=function(t){t.parentNode&&t.parentNode.removeChild(t)},t=i([s.Injectable(),o("design:paramtypes",[])],t)}();e.BrowserJsonp=p},function(t,e,n){"use strict";function r(t){for(var n in t)e.hasOwnProperty(n)||(e[n]=t[n])}var i=n(248);e.Router=i.Router;var o=n(272);e.RouterOutlet=o.RouterOutlet;var s=n(274);e.RouterLink=s.RouterLink;var a=n(260);e.RouteParams=a.RouteParams,e.RouteData=a.RouteData;var u=n(256);e.RouteRegistry=u.RouteRegistry,e.ROUTER_PRIMARY_COMPONENT=u.ROUTER_PRIMARY_COMPONENT,r(n(269));var c=n(273);e.CanActivate=c.CanActivate;var p=n(260);e.Instruction=p.Instruction,e.ComponentInstruction=p.ComponentInstruction;var l=n(2);e.OpaqueToken=l.OpaqueToken;var h=n(275);e.ROUTER_PROVIDERS_COMMON=h.ROUTER_PROVIDERS_COMMON;var f=n(276);e.ROUTER_PROVIDERS=f.ROUTER_PROVIDERS,e.ROUTER_BINDINGS=f.ROUTER_BINDINGS;var d=n(272),v=n(274),y=n(5);e.ROUTER_DIRECTIVES=y.CONST_EXPR([d.RouterOutlet,v.RouterLink])},function(t,e,n){"use strict";function r(t,e){var n=y;return p.isBlank(t.component)?n:(p.isPresent(t.child)&&(n=r(t.child,p.isPresent(e)?e.child:null)),n.then(function(n){if(0==n)return!1;if(t.component.reuse)return!0;var r=v.getCanActivateHook(t.component.componentType);return p.isPresent(r)?r(t.component,p.isPresent(e)?e.component:null):!0}))}var i=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},o=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},s=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},a=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},u=n(40),c=n(15),p=n(5),l=n(12),h=n(249),f=n(2),d=n(256),v=n(270),y=u.PromiseWrapper.resolve(!0),m=u.PromiseWrapper.resolve(!1),g=function(){function t(t,e,n,r){this.registry=t,this.parent=e,this.hostComponent=n,this.root=r,this.navigating=!1,this.currentInstruction=null,this._currentNavigation=y,this._outlet=null,this._auxRouters=new c.Map,this._subject=new u.EventEmitter}return t.prototype.childRouter=function(t){return this._childRouter=new b(this,t)},t.prototype.auxRouter=function(t){return new b(this,t)},t.prototype.registerPrimaryOutlet=function(t){if(p.isPresent(t.name))throw new l.BaseException("registerPrimaryOutlet expects to be called with an unnamed outlet.");if(p.isPresent(this._outlet))throw new l.BaseException("Primary outlet is already registered.");return this._outlet=t,p.isPresent(this.currentInstruction)?this.commit(this.currentInstruction,!1):y},t.prototype.unregisterPrimaryOutlet=function(t){if(p.isPresent(t.name))throw new l.BaseException("registerPrimaryOutlet expects to be called with an unnamed outlet.");this._outlet=null},t.prototype.registerAuxOutlet=function(t){var e=t.name;if(p.isBlank(e))throw new l.BaseException("registerAuxOutlet expects to be called with an outlet with a name.");var n=this.auxRouter(this.hostComponent);this._auxRouters.set(e,n),n._outlet=t;var r;return p.isPresent(this.currentInstruction)&&p.isPresent(r=this.currentInstruction.auxInstruction[e])?n.commit(r):y},t.prototype.isRouteActive=function(t){var e=this,n=this;if(p.isBlank(this.currentInstruction))return!1;for(;p.isPresent(n.parent)&&p.isPresent(t.child);)n=n.parent,t=t.child;if(p.isBlank(t.component)||p.isBlank(this.currentInstruction.component)||this.currentInstruction.component.routeName!=t.component.routeName)return!1;var r=!0;return p.isPresent(this.currentInstruction.component.params)&&c.StringMapWrapper.forEach(t.component.params,function(t,n){e.currentInstruction.component.params[n]!==t&&(r=!1)}),r},t.prototype.config=function(t){var e=this;return t.forEach(function(t){e.registry.config(e.hostComponent,t)}),this.renavigate()},t.prototype.navigate=function(t){var e=this.generate(t);return this.navigateByInstruction(e,!1)},t.prototype.navigateByUrl=function(t,e){var n=this;return void 0===e&&(e=!1),this._currentNavigation=this._currentNavigation.then(function(r){return n.lastNavigationAttempt=t,n._startNavigating(),n._afterPromiseFinishNavigating(n.recognize(t).then(function(t){return p.isBlank(t)?!1:n._navigate(t,e)}))})},t.prototype.navigateByInstruction=function(t,e){var n=this;return void 0===e&&(e=!1),p.isBlank(t)?m:this._currentNavigation=this._currentNavigation.then(function(r){return n._startNavigating(),n._afterPromiseFinishNavigating(n._navigate(t,e))})},t.prototype._settleInstruction=function(t){var e=this;return t.resolveComponent().then(function(n){var r=[];return p.isPresent(t.component)&&(t.component.reuse=!1),p.isPresent(t.child)&&r.push(e._settleInstruction(t.child)),c.StringMapWrapper.forEach(t.auxInstruction,function(t,n){r.push(e._settleInstruction(t))}),u.PromiseWrapper.all(r)})},t.prototype._navigate=function(t,e){var n=this;return this._settleInstruction(t).then(function(e){return n._routerCanReuse(t)}).then(function(e){return n._canActivate(t)}).then(function(r){return r?n._routerCanDeactivate(t).then(function(r){return r?n.commit(t,e).then(function(e){return n._emitNavigationFinish(t.toRootUrl()),!0}):void 0}):!1})},t.prototype._emitNavigationFinish=function(t){u.ObservableWrapper.callEmit(this._subject,t)},t.prototype._emitNavigationFail=function(t){u.ObservableWrapper.callError(this._subject,t)},t.prototype._afterPromiseFinishNavigating=function(t){var e=this;return u.PromiseWrapper.catchError(t.then(function(t){return e._finishNavigating()}),function(t){throw e._finishNavigating(),t})},t.prototype._routerCanReuse=function(t){var e=this;return p.isBlank(this._outlet)?m:p.isBlank(t.component)?y:this._outlet.routerCanReuse(t.component).then(function(n){return t.component.reuse=n,n&&p.isPresent(e._childRouter)&&p.isPresent(t.child)?e._childRouter._routerCanReuse(t.child):void 0})},t.prototype._canActivate=function(t){return r(t,this.currentInstruction)},t.prototype._routerCanDeactivate=function(t){var e=this;if(p.isBlank(this._outlet))return y;var n,r=null,i=!1,o=null;return p.isPresent(t)&&(r=t.child,o=t.component,i=p.isBlank(t.component)||t.component.reuse),n=i?y:this._outlet.routerCanDeactivate(o),n.then(function(t){return 0==t?!1:p.isPresent(e._childRouter)?e._childRouter._routerCanDeactivate(r):!0})},t.prototype.commit=function(t,e){var n=this;void 0===e&&(e=!1),this.currentInstruction=t;var r=y;if(p.isPresent(this._outlet)&&p.isPresent(t.component)){var i=t.component;r=i.reuse?this._outlet.reuse(i):this.deactivate(t).then(function(t){return n._outlet.activate(i)}),p.isPresent(t.child)&&(r=r.then(function(e){return p.isPresent(n._childRouter)?n._childRouter.commit(t.child):void 0}))}var o=[];return this._auxRouters.forEach(function(e,n){p.isPresent(t.auxInstruction[n])&&o.push(e.commit(t.auxInstruction[n]))}),r.then(function(t){return u.PromiseWrapper.all(o)})},t.prototype._startNavigating=function(){this.navigating=!0},t.prototype._finishNavigating=function(){this.navigating=!1},t.prototype.subscribe=function(t,e){return u.ObservableWrapper.subscribe(this._subject,t,e)},t.prototype.deactivate=function(t){var e=this,n=null,r=null;p.isPresent(t)&&(n=t.child,r=t.component);var i=y;return p.isPresent(this._childRouter)&&(i=this._childRouter.deactivate(n)),p.isPresent(this._outlet)&&(i=i.then(function(t){return e._outlet.deactivate(r)})),i},t.prototype.recognize=function(t){var e=this._getAncestorInstructions();return this.registry.recognize(t,e)},t.prototype._getAncestorInstructions=function(){for(var t=[this.currentInstruction],e=this;p.isPresent(e=e.parent);)t.unshift(e.currentInstruction);return t},t.prototype.renavigate=function(){return p.isBlank(this.lastNavigationAttempt)?this._currentNavigation:this.navigateByUrl(this.lastNavigationAttempt)},t.prototype.generate=function(t){var e=this._getAncestorInstructions();return this.registry.generate(t,e)},t=o([f.Injectable(),s("design:paramtypes",[d.RouteRegistry,t,Object,t])],t)}();e.Router=g;var _=function(t){function e(e,n,r){var i=this;t.call(this,e,null,r),this.root=this,this._location=n,this._locationSub=this._location.subscribe(function(t){i.recognize(t.url).then(function(e){p.isPresent(e)?i.navigateByInstruction(e,p.isPresent(t.pop)).then(function(n){if(!p.isPresent(t.pop)||"hashchange"==t.type){var r=e.toUrlPath(),o=e.toUrlQuery();r.length>0&&"/"!=r[0]&&(r="/"+r),"hashchange"==t.type?e.toRootUrl()!=i._location.path()&&i._location.replaceState(r,o):i._location.go(r,o)}}):i._emitNavigationFail(t.url)})}),this.registry.configFromComponent(r),this.navigateByUrl(n.path())}return i(e,t),e.prototype.commit=function(e,n){var r=this;void 0===n&&(n=!1);var i=e.toUrlPath(),o=e.toUrlQuery();i.length>0&&"/"!=i[0]&&(i="/"+i);var s=t.prototype.commit.call(this,e);return n||(s=s.then(function(t){ -r._location.go(i,o)})),s},e.prototype.dispose=function(){p.isPresent(this._locationSub)&&(u.ObservableWrapper.dispose(this._locationSub),this._locationSub=null)},e=o([f.Injectable(),a(2,f.Inject(d.ROUTER_PRIMARY_COMPONENT)),s("design:paramtypes",[d.RouteRegistry,h.Location,p.Type])],e)}(g);e.RootRouter=_;var b=function(t){function e(e,n){t.call(this,e.registry,e,n,e.root),this.parent=e}return i(e,t),e.prototype.navigateByUrl=function(t,e){return void 0===e&&(e=!1),this.parent.navigateByUrl(t,e)},e.prototype.navigateByInstruction=function(t,e){return void 0===e&&(e=!1),this.parent.navigateByInstruction(t,e)},e}(g)},function(t,e,n){"use strict";function r(t){for(var n in t)e.hasOwnProperty(n)||(e[n]=t[n])}r(n(250))},function(t,e,n){"use strict";function r(t){for(var n in t)e.hasOwnProperty(n)||(e[n]=t[n])}r(n(251)),r(n(252)),r(n(253)),r(n(255)),r(n(254))},function(t,e){"use strict";var n=function(){function t(){}return Object.defineProperty(t.prototype,"pathname",{get:function(){return null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"search",{get:function(){return null},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"hash",{get:function(){return null},enumerable:!0,configurable:!0}),t}();e.PlatformLocation=n},function(t,e,n){"use strict";var r=n(5),i=n(2),o=function(){function t(){}return t}();e.LocationStrategy=o,e.APP_BASE_HREF=r.CONST_EXPR(new i.OpaqueToken("appBaseHref"))},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},a=n(2),u=n(252),c=n(254),p=n(251),l=n(5),h=function(t){function e(e,n){t.call(this),this._platformLocation=e,this._baseHref="",l.isPresent(n)&&(this._baseHref=n)}return r(e,t),e.prototype.onPopState=function(t){this._platformLocation.onPopState(t),this._platformLocation.onHashChange(t)},e.prototype.getBaseHref=function(){return this._baseHref},e.prototype.path=function(){var t=this._platformLocation.hash;return l.isPresent(t)||(t="#"),t.length>0?t.substring(1):t},e.prototype.prepareExternalUrl=function(t){var e=c.Location.joinWithSlash(this._baseHref,t);return e.length>0?"#"+e:e},e.prototype.pushState=function(t,e,n,r){var i=this.prepareExternalUrl(n+c.Location.normalizeQueryParams(r));0==i.length&&(i=this._platformLocation.pathname),this._platformLocation.pushState(t,e,i)},e.prototype.replaceState=function(t,e,n,r){var i=this.prepareExternalUrl(n+c.Location.normalizeQueryParams(r));0==i.length&&(i=this._platformLocation.pathname),this._platformLocation.replaceState(t,e,i)},e.prototype.forward=function(){this._platformLocation.forward()},e.prototype.back=function(){this._platformLocation.back()},e=i([a.Injectable(),s(1,a.Optional()),s(1,a.Inject(u.APP_BASE_HREF)),o("design:paramtypes",[p.PlatformLocation,String])],e)}(u.LocationStrategy);e.HashLocationStrategy=h},function(t,e,n){"use strict";function r(t,e){return t.length>0&&e.startsWith(t)?e.substring(t.length):e}function i(t){return/\/index.html$/g.test(t)?t.substring(0,t.length-11):t}var o=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},s=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},a=n(40),u=n(2),c=n(252),p=function(){function t(e){var n=this;this.platformStrategy=e,this._subject=new a.EventEmitter;var r=this.platformStrategy.getBaseHref();this._baseHref=t.stripTrailingSlash(i(r)),this.platformStrategy.onPopState(function(t){a.ObservableWrapper.callEmit(n._subject,{url:n.path(),pop:!0,type:t.type})})}return t.prototype.path=function(){return this.normalize(this.platformStrategy.path())},t.prototype.normalize=function(e){return t.stripTrailingSlash(r(this._baseHref,i(e)))},t.prototype.prepareExternalUrl=function(t){return t.length>0&&!t.startsWith("/")&&(t="/"+t),this.platformStrategy.prepareExternalUrl(t)},t.prototype.go=function(t,e){void 0===e&&(e=""),this.platformStrategy.pushState(null,"",t,e)},t.prototype.replaceState=function(t,e){void 0===e&&(e=""),this.platformStrategy.replaceState(null,"",t,e)},t.prototype.forward=function(){this.platformStrategy.forward()},t.prototype.back=function(){this.platformStrategy.back()},t.prototype.subscribe=function(t,e,n){return void 0===e&&(e=null),void 0===n&&(n=null),a.ObservableWrapper.subscribe(this._subject,t,e,n)},t.normalizeQueryParams=function(t){return t.length>0&&"?"!=t.substring(0,1)?"?"+t:t},t.joinWithSlash=function(t,e){if(0==t.length)return e;if(0==e.length)return t;var n=0;return t.endsWith("/")&&n++,e.startsWith("/")&&n++,2==n?t+e.substring(1):1==n?t+e:t+"/"+e},t.stripTrailingSlash=function(t){return/\/$/g.test(t)&&(t=t.substring(0,t.length-1)),t},t=o([u.Injectable(),s("design:paramtypes",[c.LocationStrategy])],t)}();e.Location=p},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},a=n(2),u=n(5),c=n(12),p=n(251),l=n(252),h=n(254),f=function(t){function e(e,n){if(t.call(this),this._platformLocation=e,u.isBlank(n)&&(n=this._platformLocation.getBaseHrefFromDOM()),u.isBlank(n))throw new c.BaseException("No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.");this._baseHref=n}return r(e,t),e.prototype.onPopState=function(t){this._platformLocation.onPopState(t),this._platformLocation.onHashChange(t)},e.prototype.getBaseHref=function(){return this._baseHref},e.prototype.prepareExternalUrl=function(t){return h.Location.joinWithSlash(this._baseHref,t)},e.prototype.path=function(){return this._platformLocation.pathname+h.Location.normalizeQueryParams(this._platformLocation.search)},e.prototype.pushState=function(t,e,n,r){var i=this.prepareExternalUrl(n+h.Location.normalizeQueryParams(r));this._platformLocation.pushState(t,e,i)},e.prototype.replaceState=function(t,e,n,r){var i=this.prepareExternalUrl(n+h.Location.normalizeQueryParams(r));this._platformLocation.replaceState(t,e,i)},e.prototype.forward=function(){this._platformLocation.forward()},e.prototype.back=function(){this._platformLocation.back()},e=i([a.Injectable(),s(1,a.Optional()),s(1,a.Inject(l.APP_BASE_HREF)),o("design:paramtypes",[p.PlatformLocation,String])],e)}(l.LocationStrategy);e.PathLocationStrategy=f},function(t,e,n){"use strict";function r(t){var e=[];return t.forEach(function(t){if(h.isString(t)){var n=t;e=e.concat(n.split("/"))}else e.push(t)}),e}function i(t){if(t=t.filter(function(t){return h.isPresent(t)}),0==t.length)return null;if(1==t.length)return t[0];var e=t[0],n=t.slice(1);return n.reduce(function(t,e){return-1==o(e.specificity,t.specificity)?e:t},e)}function o(t,e){for(var n=h.Math.min(t.length,e.length),r=0;n>r;r+=1){var i=h.StringWrapper.charCodeAt(t,r),o=h.StringWrapper.charCodeAt(e,r),s=o-i;if(0!=s)return s}return t.length-e.length}function s(t,e){if(h.isType(t)){var n=d.reflector.annotations(t);if(h.isPresent(n))for(var r=0;ro?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},u=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},c=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},p=n(15),l=n(40),h=n(5),f=n(12),d=n(18),v=n(2),y=n(257),m=n(258),g=n(261),_=n(260),b=n(268),P=n(259),E=l.PromiseWrapper.resolve(null);e.ROUTER_PRIMARY_COMPONENT=h.CONST_EXPR(new v.OpaqueToken("RouterPrimaryComponent"));var w=function(){function t(t){this._rootComponent=t,this._rules=new p.Map}return t.prototype.config=function(t,e){e=b.normalizeRouteConfig(e,this),e instanceof y.Route?b.assertComponentExists(e.component,e.path):e instanceof y.AuxRoute&&b.assertComponentExists(e.component,e.path);var n=this._rules.get(t);h.isBlank(n)&&(n=new g.RuleSet,this._rules.set(t,n));var r=n.config(e);e instanceof y.Route&&(r?s(e.component,e.path):this.configFromComponent(e.component))},t.prototype.configFromComponent=function(t){var e=this;if(h.isType(t)&&!this._rules.has(t)){var n=d.reflector.annotations(t);if(h.isPresent(n))for(var r=0;r0?[p.ListWrapper.last(e)]:[],i=r._auxRoutesToUnresolved(t.remainingAux,n),o=new _.ResolvedInstruction(t.instruction,null,i);if(h.isBlank(t.instruction)||t.instruction.terminal)return o;var s=e.concat([o]);return r._recognize(t.remaining,s).then(function(t){return h.isBlank(t)?null:t instanceof _.RedirectInstruction?t:(o.child=t,o)})}if(t instanceof m.RedirectMatch){var o=r.generate(t.redirectTo,e.concat([null]));return new _.RedirectInstruction(o.component,o.child,o.auxInstruction,t.specificity)}})});return!h.isBlank(t)&&""!=t.path||0!=u.length?l.PromiseWrapper.all(c).then(i):l.PromiseWrapper.resolve(this.generateDefault(s))},t.prototype._auxRoutesToUnresolved=function(t,e){var n=this,r={};return t.forEach(function(t){r[t.path]=new _.UnresolvedInstruction(function(){return n._recognize(t,e,!0)})}),r},t.prototype.generate=function(t,e,n){void 0===n&&(n=!1);var i,o=r(t);if(""==p.ListWrapper.first(o))o.shift(),i=p.ListWrapper.first(e),e=[];else if(i=e.length>0?e.pop():null,"."==p.ListWrapper.first(o))o.shift();else if(".."==p.ListWrapper.first(o))for(;".."==p.ListWrapper.first(o);){if(e.length<=0)throw new f.BaseException('Link "'+p.ListWrapper.toJSON(t)+'" has too many "../" segments.');i=e.pop(),o=p.ListWrapper.slice(o,1)}else{var s=p.ListWrapper.first(o),a=this._rootComponent,u=null;if(e.length>1){var c=e[e.length-1],l=e[e.length-2];a=c.component.componentType,u=l.component.componentType}else 1==e.length&&(a=e[0].component.componentType,u=this._rootComponent);var d=this.hasRoute(s,a),v=h.isPresent(u)&&this.hasRoute(s,u);if(v&&d){var y='Link "'+p.ListWrapper.toJSON(t)+'" is ambiguous, use "./" or "../" to disambiguate.';throw new f.BaseException(y)}v&&(i=e.pop())}if(""==o[o.length-1]&&o.pop(),o.length>0&&""==o[0]&&o.shift(),o.length<1){var y='Link "'+p.ListWrapper.toJSON(t)+'" must include a route name.';throw new f.BaseException(y)}for(var m=this._generate(o,e,i,n,t),g=e.length-1;g>=0;g--){var _=e[g];if(h.isBlank(_))break;m=_.replaceChild(m)}return m},t.prototype._generate=function(t,e,n,r,i){var o=this;void 0===r&&(r=!1);var s=this._rootComponent,a=null,u={},c=p.ListWrapper.last(e);if(h.isPresent(c)&&h.isPresent(c.component)&&(s=c.component.componentType),0==t.length){var l=this.generateDefault(s);if(h.isBlank(l))throw new f.BaseException('Link "'+p.ListWrapper.toJSON(i)+'" does not resolve to a terminal instruction.');return l}h.isPresent(n)&&!r&&(u=p.StringMapWrapper.merge(n.auxInstruction,u),a=n.component);var d=this._rules.get(s);if(h.isBlank(d))throw new f.BaseException('Component "'+h.getTypeNameForDebugging(s)+'" has no route config.');var v=0,y={};if(v=t.length;else{var O=e.concat([R]),T=t.slice(v);S=this._generate(T,O,null,!1,i)}R.child=S}return R},t.prototype.hasRoute=function(t,e){var n=this._rules.get(e);return h.isBlank(n)?!1:n.hasRoute(t)},t.prototype.generateDefault=function(t){var e=this;if(h.isBlank(t))return null;var n=this._rules.get(t);if(h.isBlank(n)||h.isBlank(n.defaultRule))return null;var r=null;if(h.isPresent(n.defaultRule.handler.componentType)){var i=n.defaultRule.generate({});return n.defaultRule.terminal||(r=this.generateDefault(n.defaultRule.handler.componentType)),new _.DefaultInstruction(i,r)}return new _.UnresolvedInstruction(function(){return n.defaultRule.handler.resolveComponentType().then(function(n){return e.generateDefault(t)})})},t=a([v.Injectable(),c(0,v.Inject(e.ROUTER_PRIMARY_COMPONENT)),u("design:paramtypes",[h.Type])],t)}();e.RouteRegistry=w},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(5),a=function(){function t(t){this.configs=t}return t=i([s.CONST(),o("design:paramtypes",[Array])],t)}();e.RouteConfig=a;var u=function(){function t(t){var e=t.name,n=t.useAsDefault,r=t.path,i=t.regex,o=t.serializer,s=t.data;this.name=e,this.useAsDefault=n,this.path=r,this.regex=i,this.serializer=o,this.data=s}return t=i([s.CONST(),o("design:paramtypes",[Object])],t)}();e.AbstractRoute=u;var c=function(t){function e(e){var n=e.name,r=e.useAsDefault,i=e.path,o=e.regex,s=e.serializer,a=e.data,u=e.component;t.call(this,{name:n,useAsDefault:r,path:i,regex:o,serializer:s,data:a}),this.aux=null,this.component=u}return r(e,t),e=i([s.CONST(),o("design:paramtypes",[Object])],e)}(u);e.Route=c;var p=function(t){function e(e){var n=e.name,r=e.useAsDefault,i=e.path,o=e.regex,s=e.serializer,a=e.data,u=e.component;t.call(this,{name:n,useAsDefault:r,path:i,regex:o,serializer:s,data:a}),this.component=u}return r(e,t),e=i([s.CONST(),o("design:paramtypes",[Object])],e)}(u);e.AuxRoute=p;var l=function(t){function e(e){var n=e.name,r=e.useAsDefault,i=e.path,o=e.regex,s=e.serializer,a=e.data,u=e.loader;t.call(this,{name:n,useAsDefault:r,path:i,regex:o,serializer:s,data:a}),this.aux=null,this.loader=u}return r(e,t),e=i([s.CONST(),o("design:paramtypes",[Object])],e)}(u);e.AsyncRoute=l;var h=function(t){function e(e){var n=e.name,r=e.useAsDefault,i=e.path,o=e.regex,s=e.serializer,a=e.data,u=e.redirectTo;t.call(this,{name:n,useAsDefault:r,path:i,regex:o,serializer:s,data:a}),this.redirectTo=u}return r(e,t),e=i([s.CONST(),o("design:paramtypes",[Object])],e)}(u);e.Redirect=h},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(5),o=n(12),s=n(41),a=n(15),u=n(259),c=n(260),p=function(){function t(){}return t}();e.RouteMatch=p;var l=function(t){function e(e,n,r){t.call(this),this.instruction=e,this.remaining=n,this.remainingAux=r}return r(e,t),e}(p);e.PathMatch=l;var h=function(t){function e(e,n){t.call(this),this.redirectTo=e,this.specificity=n}return r(e,t),e}(p);e.RedirectMatch=h;var f=function(){function t(t,e){this._pathRecognizer=t,this.redirectTo=e,this.hash=this._pathRecognizer.hash}return Object.defineProperty(t.prototype,"path",{get:function(){return this._pathRecognizer.toString()},set:function(t){throw new o.BaseException("you cannot set the path of a RedirectRule directly")},enumerable:!0,configurable:!0}),t.prototype.recognize=function(t){var e=null;return i.isPresent(this._pathRecognizer.matchUrl(t))&&(e=new h(this.redirectTo,this._pathRecognizer.specificity)),s.PromiseWrapper.resolve(e)},t.prototype.generate=function(t){throw new o.BaseException("Tried to generate a redirect.")},t}();e.RedirectRule=f;var d=function(){function t(t,e,n){this._routePath=t,this.handler=e,this._routeName=n,this._cache=new a.Map,this.specificity=this._routePath.specificity,this.hash=this._routePath.hash,this.terminal=this._routePath.terminal}return Object.defineProperty(t.prototype,"path",{get:function(){return this._routePath.toString()},set:function(t){throw new o.BaseException("you cannot set the path of a RouteRule directly")},enumerable:!0,configurable:!0}),t.prototype.recognize=function(t){var e=this,n=this._routePath.matchUrl(t);return i.isBlank(n)?null:this.handler.resolveComponentType().then(function(t){var r=e._getInstruction(n.urlPath,n.urlParams,n.allParams);return new l(r,n.rest,n.auxiliary)})},t.prototype.generate=function(t){var e=this._routePath.generateUrl(t),n=e.urlPath,r=e.urlParams;return this._getInstruction(n,u.convertUrlParamsToArray(r),t)},t.prototype.generateComponentPathValues=function(t){return this._routePath.generateUrl(t)},t.prototype._getInstruction=function(t,e,n){if(i.isBlank(this.handler.componentType))throw new o.BaseException("Tried to get instruction before the type was loaded.");var r=t+"?"+e.join("&");if(this._cache.has(r))return this._cache.get(r);var s=new c.ComponentInstruction(t,e,this.handler.data,this.handler.componentType,this.terminal,this.specificity,n,this._routeName);return this._cache.set(r,s),s},t}();e.RouteRule=d},function(t,e,n){"use strict";function r(t){var e=[];return p.isBlank(t)?[]:(c.StringMapWrapper.forEach(t,function(t,n){e.push(t===!0?n:n+"="+t)}),e)}function i(t,e){return void 0===e&&(e="&"),r(t).join(e)}function o(t){for(var e=new h(t[t.length-1]),n=t.length-2;n>=0;n-=1)e=new h(t[n],e);return e}function s(t){var e=p.RegExpWrapper.firstMatch(d,t);return p.isPresent(e)?e[0]:""}function a(t){var e=p.RegExpWrapper.firstMatch(v,t);return p.isPresent(e)?e[0]:""}var u=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},c=n(15),p=n(5),l=n(12);e.convertUrlParamsToArray=r,e.serializeParams=i;var h=function(){function t(t,e,n,r){void 0===e&&(e=null),void 0===n&&(n=p.CONST_EXPR([])),void 0===r&&(r=p.CONST_EXPR({})),this.path=t,this.child=e,this.auxiliary=n,this.params=r}return t.prototype.toString=function(){return this.path+this._matrixParamsToString()+this._auxToString()+this._childString()},t.prototype.segmentToString=function(){return this.path+this._matrixParamsToString()},t.prototype._auxToString=function(){return this.auxiliary.length>0?"("+this.auxiliary.map(function(t){return t.toString()}).join("//")+")":""},t.prototype._matrixParamsToString=function(){var t=i(this.params,";");return t.length>0?";"+t:""},t.prototype._childString=function(){return p.isPresent(this.child)?"/"+this.child.toString():""},t}();e.Url=h;var f=function(t){function e(e,n,r,i){void 0===n&&(n=null),void 0===r&&(r=p.CONST_EXPR([])),void 0===i&&(i=null),t.call(this,e,n,r,i)}return u(e,t),e.prototype.toString=function(){return this.path+this._auxToString()+this._childString()+this._queryParamsToString()},e.prototype.segmentToString=function(){return this.path+this._queryParamsToString()},e.prototype._queryParamsToString=function(){return p.isBlank(this.params)?"":"?"+i(this.params)},e}(h);e.RootUrl=f,e.pathSegmentsToUrl=o;var d=p.RegExpWrapper.create("^[^\\/\\(\\)\\?;=&#]+"),v=p.RegExpWrapper.create("^[^\\(\\)\\?;&#]+"),y=function(){function t(){}return t.prototype.peekStartsWith=function(t){return this._remaining.startsWith(t)},t.prototype.capture=function(t){if(!this._remaining.startsWith(t))throw new l.BaseException('Expected "'+t+'".');this._remaining=this._remaining.substring(t.length)},t.prototype.parse=function(t){return this._remaining=t,""==t||"/"==t?new h(""):this.parseRoot()},t.prototype.parseRoot=function(){this.peekStartsWith("/")&&this.capture("/");var t=s(this._remaining);this.capture(t);var e=[];this.peekStartsWith("(")&&(e=this.parseAuxiliaryRoutes()),this.peekStartsWith(";")&&this.parseMatrixParams();var n=null;this.peekStartsWith("/")&&!this.peekStartsWith("//")&&(this.capture("/"),n=this.parseSegment());var r=null;return this.peekStartsWith("?")&&(r=this.parseQueryParams()),new f(t,n,e,r)},t.prototype.parseSegment=function(){if(0==this._remaining.length)return null;this.peekStartsWith("/")&&this.capture("/");var t=s(this._remaining);this.capture(t);var e=null;this.peekStartsWith(";")&&(e=this.parseMatrixParams());var n=[];this.peekStartsWith("(")&&(n=this.parseAuxiliaryRoutes());var r=null;return this.peekStartsWith("/")&&!this.peekStartsWith("//")&&(this.capture("/"),r=this.parseSegment()),new h(t,r,n,e)},t.prototype.parseQueryParams=function(){var t={};for(this.capture("?"),this.parseQueryParam(t);this._remaining.length>0&&this.peekStartsWith("&");)this.capture("&"),this.parseQueryParam(t);return t},t.prototype.parseMatrixParams=function(){for(var t={};this._remaining.length>0&&this.peekStartsWith(";");)this.capture(";"),this.parseParam(t);return t},t.prototype.parseParam=function(t){var e=s(this._remaining);if(!p.isBlank(e)){this.capture(e);var n=!0;if(this.peekStartsWith("=")){this.capture("=");var r=s(this._remaining);p.isPresent(r)&&(n=r,this.capture(n))}t[e]=n}},t.prototype.parseQueryParam=function(t){var e=s(this._remaining);if(!p.isBlank(e)){this.capture(e);var n=!0;if(this.peekStartsWith("=")){this.capture("=");var r=a(this._remaining);p.isPresent(r)&&(n=r,this.capture(n))}t[e]=n}},t.prototype.parseAuxiliaryRoutes=function(){var t=[];for(this.capture("(");!this.peekStartsWith(")")&&this._remaining.length>0;)t.push(this.parseSegment()),this.peekStartsWith("//")&&this.capture("//");return this.capture(")"),t},t}();e.UrlParser=y,e.parser=new y},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=n(15),o=n(5),s=n(40),a=function(){function t(t){this.params=t}return t.prototype.get=function(t){return o.normalizeBlank(i.StringMapWrapper.get(this.params,t))},t}();e.RouteParams=a;var u=function(){function t(t){void 0===t&&(t=o.CONST_EXPR({})),this.data=t}return t.prototype.get=function(t){return o.normalizeBlank(i.StringMapWrapper.get(this.data,t))},t}();e.RouteData=u,e.BLANK_ROUTE_DATA=new u;var c=function(){function t(t,e,n){this.component=t,this.child=e,this.auxInstruction=n}return Object.defineProperty(t.prototype,"urlPath",{get:function(){return o.isPresent(this.component)?this.component.urlPath:""},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"urlParams",{get:function(){return o.isPresent(this.component)?this.component.urlParams:[]},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"specificity",{get:function(){var t="";return o.isPresent(this.component)&&(t+=this.component.specificity),o.isPresent(this.child)&&(t+=this.child.specificity),t},enumerable:!0,configurable:!0}),t.prototype.toRootUrl=function(){return this.toUrlPath()+this.toUrlQuery()},t.prototype._toNonRootUrl=function(){return this._stringifyPathMatrixAuxPrefixed()+(o.isPresent(this.child)?this.child._toNonRootUrl():"")},t.prototype.toUrlQuery=function(){return this.urlParams.length>0?"?"+this.urlParams.join("&"):""},t.prototype.replaceChild=function(t){return new p(this.component,t,this.auxInstruction)},t.prototype.toUrlPath=function(){return this.urlPath+this._stringifyAux()+(o.isPresent(this.child)?this.child._toNonRootUrl():"")},t.prototype.toLinkUrl=function(){return this.urlPath+this._stringifyAux()+(o.isPresent(this.child)?this.child._toLinkUrl():"")+this.toUrlQuery()},t.prototype._toLinkUrl=function(){return this._stringifyPathMatrixAuxPrefixed()+(o.isPresent(this.child)?this.child._toLinkUrl():"")},t.prototype._stringifyPathMatrixAuxPrefixed=function(){var t=this._stringifyPathMatrixAux();return t.length>0&&(t="/"+t),t},t.prototype._stringifyMatrixParams=function(){return this.urlParams.length>0?";"+this.urlParams.join(";"):""},t.prototype._stringifyPathMatrixAux=function(){return o.isBlank(this.component)?"":this.urlPath+this._stringifyMatrixParams()+this._stringifyAux()},t.prototype._stringifyAux=function(){var t=[];return i.StringMapWrapper.forEach(this.auxInstruction,function(e,n){t.push(e._stringifyPathMatrixAux())}),t.length>0?"("+t.join("//")+")":""},t}();e.Instruction=c;var p=function(t){function e(e,n,r){t.call(this,e,n,r)}return r(e,t),e.prototype.resolveComponent=function(){return s.PromiseWrapper.resolve(this.component)},e}(c);e.ResolvedInstruction=p;var l=function(t){function e(e,n){t.call(this,e,n,{})}return r(e,t),e.prototype.toLinkUrl=function(){return""},e.prototype._toLinkUrl=function(){return""},e}(p);e.DefaultInstruction=l;var h=function(t){function e(e,n,r){void 0===n&&(n=""),void 0===r&&(r=o.CONST_EXPR([])),t.call(this,null,null,{}),this._resolver=e,this._urlPath=n,this._urlParams=r}return r(e,t),Object.defineProperty(e.prototype,"urlPath",{get:function(){return o.isPresent(this.component)?this.component.urlPath:o.isPresent(this._urlPath)?this._urlPath:""},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"urlParams",{get:function(){return o.isPresent(this.component)?this.component.urlParams:o.isPresent(this._urlParams)?this._urlParams:[]},enumerable:!0,configurable:!0}),e.prototype.resolveComponent=function(){var t=this;return o.isPresent(this.component)?s.PromiseWrapper.resolve(this.component):this._resolver().then(function(e){return t.child=o.isPresent(e)?e.child:null,t.component=o.isPresent(e)?e.component:null})},e}(c);e.UnresolvedInstruction=h;var f=function(t){function e(e,n,r,i){t.call(this,e,n,r),this._specificity=i}return r(e,t),Object.defineProperty(e.prototype,"specificity",{get:function(){return this._specificity},enumerable:!0,configurable:!0}),e}(p);e.RedirectInstruction=f;var d=function(){function t(t,n,r,i,s,a,u,c){void 0===u&&(u=null),this.urlPath=t,this.urlParams=n,this.componentType=i,this.terminal=s,this.specificity=a,this.params=u,this.routeName=c,this.reuse=!1,this.routeData=o.isPresent(r)?r:e.BLANK_ROUTE_DATA}return t}();e.ComponentInstruction=d},function(t,e,n){"use strict";var r=n(5),i=n(12),o=n(15),s=n(40),a=n(258),u=n(257),c=n(262),p=n(263),l=n(264),h=n(267),f=function(){function t(){this.rulesByName=new o.Map,this.auxRulesByName=new o.Map,this.auxRulesByPath=new o.Map,this.rules=[],this.defaultRule=null}return t.prototype.config=function(t){var e;if(r.isPresent(t.name)&&t.name[0].toUpperCase()!=t.name[0]){var n=t.name[0].toUpperCase()+t.name.substring(1);throw new i.BaseException('Route "'+t.path+'" with name "'+t.name+'" does not begin with an uppercase letter. Route names should be CamelCase like "'+n+'".')}if(t instanceof u.AuxRoute){e=new p.SyncRouteHandler(t.component,t.data);var o=this._getRoutePath(t),s=new a.RouteRule(o,e,t.name);return this.auxRulesByPath.set(o.toString(),s),r.isPresent(t.name)&&this.auxRulesByName.set(t.name,s),s.terminal}var l=!1;if(t instanceof u.Redirect){var h=this._getRoutePath(t),f=new a.RedirectRule(h,t.redirectTo);return this._assertNoHashCollision(f.hash,t.path),this.rules.push(f),!0}t instanceof u.Route?(e=new p.SyncRouteHandler(t.component,t.data),l=r.isPresent(t.useAsDefault)&&t.useAsDefault):t instanceof u.AsyncRoute&&(e=new c.AsyncRouteHandler(t.loader,t.data),l=r.isPresent(t.useAsDefault)&&t.useAsDefault);var d=this._getRoutePath(t),v=new a.RouteRule(d,e,t.name);if(this._assertNoHashCollision(v.hash,t.path),l){if(r.isPresent(this.defaultRule))throw new i.BaseException("Only one route can be default");this.defaultRule=v}return this.rules.push(v),r.isPresent(t.name)&&this.rulesByName.set(t.name,v),v.terminal},t.prototype.recognize=function(t){var e=[];return this.rules.forEach(function(n){var i=n.recognize(t);r.isPresent(i)&&e.push(i)}),0==e.length&&r.isPresent(t)&&t.auxiliary.length>0?[s.PromiseWrapper.resolve(new a.PathMatch(null,null,t.auxiliary))]:e},t.prototype.recognizeAuxiliary=function(t){var e=this.auxRulesByPath.get(t.path);return r.isPresent(e)?[e.recognize(t)]:[s.PromiseWrapper.resolve(null)]},t.prototype.hasRoute=function(t){return this.rulesByName.has(t)},t.prototype.componentLoaded=function(t){return this.hasRoute(t)&&r.isPresent(this.rulesByName.get(t).handler.componentType)},t.prototype.loadComponent=function(t){return this.rulesByName.get(t).handler.resolveComponentType()},t.prototype.generate=function(t,e){var n=this.rulesByName.get(t);return r.isBlank(n)?null:n.generate(e)},t.prototype.generateAuxiliary=function(t,e){var n=this.auxRulesByName.get(t);return r.isBlank(n)?null:n.generate(e)},t.prototype._assertNoHashCollision=function(t,e){this.rules.forEach(function(n){if(t==n.hash)throw new i.BaseException("Configuration '"+e+"' conflicts with existing route '"+n.path+"'")})},t.prototype._getRoutePath=function(t){if(r.isPresent(t.regex)){if(r.isFunction(t.serializer))return new h.RegexRoutePath(t.regex,t.serializer);throw new i.BaseException("Route provides a regex property, '"+t.regex+"', but no serializer property")}if(r.isPresent(t.path)){var e=t instanceof u.AuxRoute&&t.path.startsWith("/")?t.path.substring(1):t.path;return new l.ParamRoutePath(e)}throw new i.BaseException("Route must provide either a path or regex property")},t}();e.RuleSet=f},function(t,e,n){"use strict";var r=n(5),i=n(260),o=function(){function t(t,e){void 0===e&&(e=null),this._loader=t,this._resolvedComponent=null,this.data=r.isPresent(e)?new i.RouteData(e):i.BLANK_ROUTE_DATA}return t.prototype.resolveComponentType=function(){ -var t=this;return r.isPresent(this._resolvedComponent)?this._resolvedComponent:this._resolvedComponent=this._loader().then(function(e){return t.componentType=e,e})},t}();e.AsyncRouteHandler=o},function(t,e,n){"use strict";var r=n(40),i=n(5),o=n(260),s=function(){function t(t,e){this.componentType=t,this._resolvedComponent=null,this._resolvedComponent=r.PromiseWrapper.resolve(t),this.data=i.isPresent(e)?new o.RouteData(e):o.BLANK_ROUTE_DATA}return t.prototype.resolveComponentType=function(){return this._resolvedComponent},t}();e.SyncRouteHandler=s},function(t,e,n){"use strict";function r(t){return o.isBlank(t)?null:(t=o.StringWrapper.replaceAll(t,y,"%25"),t=o.StringWrapper.replaceAll(t,m,"%2F"),t=o.StringWrapper.replaceAll(t,g,"%28"),t=o.StringWrapper.replaceAll(t,_,"%29"),t=o.StringWrapper.replaceAll(t,b,"%3B"))}function i(t){return o.isBlank(t)?null:(t=o.StringWrapper.replaceAll(t,P,";"),t=o.StringWrapper.replaceAll(t,E,")"),t=o.StringWrapper.replaceAll(t,w,"("),t=o.StringWrapper.replaceAll(t,C,"/"),t=o.StringWrapper.replaceAll(t,R,"%"))}var o=n(5),s=n(12),a=n(15),u=n(265),c=n(259),p=n(266),l=function(){function t(){this.name="",this.specificity="",this.hash="..."}return t.prototype.generate=function(t){return""},t.prototype.match=function(t){return!0},t}(),h=function(){function t(t){this.path=t,this.name="",this.specificity="2",this.hash=t}return t.prototype.match=function(t){return t==this.path},t.prototype.generate=function(t){return this.path},t}(),f=function(){function t(t){this.name=t,this.specificity="1",this.hash=":"}return t.prototype.match=function(t){return t.length>0},t.prototype.generate=function(t){if(!a.StringMapWrapper.contains(t.map,this.name))throw new s.BaseException("Route generator for '"+this.name+"' was not included in parameters passed.");return r(u.normalizeString(t.get(this.name)))},t.paramMatcher=/^:([^\/]+)$/g,t}(),d=function(){function t(t){this.name=t,this.specificity="0",this.hash="*"}return t.prototype.match=function(t){return!0},t.prototype.generate=function(t){return u.normalizeString(t.get(this.name))},t.wildcardMatcher=/^\*([^\/]+)$/g,t}(),v=function(){function t(t){this.routePath=t,this.terminal=!0,this._assertValidPath(t),this._parsePathString(t),this.specificity=this._calculateSpecificity(),this.hash=this._calculateHash();var e=this._segments[this._segments.length-1];this.terminal=!(e instanceof l)}return t.prototype.matchUrl=function(t){for(var e,n=t,r={},s=[],u=0;u=r;r++){var i,a=e[r];if(o.isPresent(i=o.RegExpWrapper.firstMatch(f.paramMatcher,a)))this._segments.push(new f(i[1]));else if(o.isPresent(i=o.RegExpWrapper.firstMatch(d.wildcardMatcher,a)))this._segments.push(new d(i[1]));else if("..."==a){if(n>r)throw new s.BaseException('Unexpected "..." before the end of the path for "'+t+'".');this._segments.push(new l)}else this._segments.push(new h(a))}},t.prototype._calculateSpecificity=function(){var t,e,n=this._segments.length;if(0==n)e+="2";else for(e="",t=0;n>t;t++)e+=this._segments[t].specificity;return e},t.prototype._calculateHash=function(){var t,e=this._segments.length,n=[];for(t=0;e>t;t++)n.push(this._segments[t].hash);return n.join("/")},t.prototype._assertValidPath=function(e){if(o.StringWrapper.contains(e,"#"))throw new s.BaseException('Path "'+e+'" should not include "#". Use "HashLocationStrategy" instead.');var n=o.RegExpWrapper.firstMatch(t.RESERVED_CHARS,e);if(o.isPresent(n))throw new s.BaseException('Path "'+e+'" contains "'+n[0]+'" which is not allowed in a route config.')},t.RESERVED_CHARS=o.RegExpWrapper.create("//|\\(|\\)|;|\\?|="),t}();e.ParamRoutePath=v;var y=/%/g,m=/\//g,g=/\(/g,_=/\)/g,b=/;/g,P=/%3B/gi,E=/%29/gi,w=/%28/gi,C=/%2F/gi,R=/%25/gi},function(t,e,n){"use strict";function r(t){return i.isBlank(t)?null:t.toString()}var i=n(5),o=n(15),s=function(){function t(t){var e=this;this.map={},this.keys={},i.isPresent(t)&&o.StringMapWrapper.forEach(t,function(t,n){e.map[n]=i.isPresent(t)?t.toString():null,e.keys[n]=!0})}return t.prototype.get=function(t){return o.StringMapWrapper["delete"](this.keys,t),this.map[t]},t.prototype.getUnused=function(){var t=this,e={},n=o.StringMapWrapper.keys(this.keys);return n.forEach(function(n){return e[n]=o.StringMapWrapper.get(t.map,n)}),e},t}();e.TouchMap=s,e.normalizeString=r},function(t,e){"use strict";var n=function(){function t(t,e,n,r,i){this.urlPath=t,this.urlParams=e,this.allParams=n,this.auxiliary=r,this.rest=i}return t}();e.MatchedUrl=n;var r=function(){function t(t,e){this.urlPath=t,this.urlParams=e}return t}();e.GeneratedUrl=r},function(t,e,n){"use strict";var r=n(5),i=n(266),o=function(){function t(t,e){this._reString=t,this._serializer=e,this.terminal=!0,this.specificity="2",this.hash=this._reString,this._regex=r.RegExpWrapper.create(this._reString)}return t.prototype.matchUrl=function(t){var e=t.toString(),n={},o=r.RegExpWrapper.matcher(this._regex,e),s=r.RegExpMatcherWrapper.next(o);if(r.isBlank(s))return null;for(var a=0;ao?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(5),s=function(){function t(t){this.name=t}return t=r([o.CONST(),i("design:paramtypes",[String])],t)}();e.RouteLifecycleHook=s;var a=function(){function t(t){this.fn=t}return t=r([o.CONST(),i("design:paramtypes",[Function])],t)}();e.CanActivate=a,e.routerCanReuse=o.CONST_EXPR(new s("routerCanReuse")),e.routerCanDeactivate=o.CONST_EXPR(new s("routerCanDeactivate")),e.routerOnActivate=o.CONST_EXPR(new s("routerOnActivate")),e.routerOnReuse=o.CONST_EXPR(new s("routerOnReuse")),e.routerOnDeactivate=o.CONST_EXPR(new s("routerOnDeactivate"))},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=this&&this.__param||function(t,e){return function(n,r){e(n,r,t)}},s=n(40),a=n(15),u=n(5),c=n(2),p=n(248),l=n(260),h=n(273),f=n(270),d=s.PromiseWrapper.resolve(!0),v=function(){function t(t,e,n,r){this._viewContainerRef=t,this._loader=e,this._parentRouter=n,this.name=null,this._componentRef=null,this._currentInstruction=null,this.activateEvents=new s.EventEmitter,u.isPresent(r)?(this.name=r,this._parentRouter.registerAuxOutlet(this)):this._parentRouter.registerPrimaryOutlet(this)}return t.prototype.activate=function(t){var e=this,n=this._currentInstruction;this._currentInstruction=t;var r=t.componentType,i=this._parentRouter.childRouter(r),o=c.ReflectiveInjector.resolve([c.provide(l.RouteData,{useValue:t.routeData}),c.provide(l.RouteParams,{useValue:new l.RouteParams(t.params)}),c.provide(p.Router,{useValue:i})]);return this._componentRef=this._loader.loadNextToLocation(r,this._viewContainerRef,o),this._componentRef.then(function(i){return e.activateEvents.emit(i.instance),f.hasLifecycleHook(h.routerOnActivate,r)?e._componentRef.then(function(e){return e.instance.routerOnActivate(t,n)}):i})},t.prototype.reuse=function(t){var e=this._currentInstruction;return this._currentInstruction=t,u.isBlank(this._componentRef)?this.activate(t):s.PromiseWrapper.resolve(f.hasLifecycleHook(h.routerOnReuse,this._currentInstruction.componentType)?this._componentRef.then(function(n){return n.instance.routerOnReuse(t,e)}):!0)},t.prototype.deactivate=function(t){var e=this,n=d;return u.isPresent(this._componentRef)&&u.isPresent(this._currentInstruction)&&f.hasLifecycleHook(h.routerOnDeactivate,this._currentInstruction.componentType)&&(n=this._componentRef.then(function(n){return n.instance.routerOnDeactivate(t,e._currentInstruction)})),n.then(function(t){if(u.isPresent(e._componentRef)){var n=e._componentRef.then(function(t){return t.destroy()});return e._componentRef=null,n}})},t.prototype.routerCanDeactivate=function(t){var e=this;return u.isBlank(this._currentInstruction)?d:f.hasLifecycleHook(h.routerCanDeactivate,this._currentInstruction.componentType)?this._componentRef.then(function(n){return n.instance.routerCanDeactivate(t,e._currentInstruction)}):d},t.prototype.routerCanReuse=function(t){var e,n=this;return e=u.isBlank(this._currentInstruction)||this._currentInstruction.componentType!=t.componentType?!1:f.hasLifecycleHook(h.routerCanReuse,this._currentInstruction.componentType)?this._componentRef.then(function(e){return e.instance.routerCanReuse(t,n._currentInstruction)}):t==this._currentInstruction||u.isPresent(t.params)&&u.isPresent(this._currentInstruction.params)&&a.StringMapWrapper.equals(t.params,this._currentInstruction.params),s.PromiseWrapper.resolve(e)},t.prototype.ngOnDestroy=function(){this._parentRouter.unregisterPrimaryOutlet(this)},r([c.Output("activate"),i("design:type",Object)],t.prototype,"activateEvents",void 0),t=r([c.Directive({selector:"router-outlet"}),o(3,c.Attribute("name")),i("design:paramtypes",[c.ViewContainerRef,c.DynamicComponentLoader,p.Router,String])],t)}();e.RouterOutlet=v},function(t,e,n){"use strict";var r=n(9),i=n(271),o=n(271);e.routerCanReuse=o.routerCanReuse,e.routerCanDeactivate=o.routerCanDeactivate,e.routerOnActivate=o.routerOnActivate,e.routerOnReuse=o.routerOnReuse,e.routerOnDeactivate=o.routerOnDeactivate,e.CanActivate=r.makeDecorator(i.CanActivate)},function(t,e,n){"use strict";var r=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},i=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},o=n(2),s=n(249),a=n(5),u=n(248),c=function(){function t(t,e){var n=this;this._router=t,this._location=e,this._router.subscribe(function(t){return n._updateLink()})}return t.prototype._updateLink=function(){this._navigationInstruction=this._router.generate(this._routeParams);var t=this._navigationInstruction.toLinkUrl();this.visibleHref=this._location.prepareExternalUrl(t)},Object.defineProperty(t.prototype,"isRouteActive",{get:function(){return this._router.isRouteActive(this._navigationInstruction)},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"routeParams",{set:function(t){this._routeParams=t,this._updateLink()},enumerable:!0,configurable:!0}),t.prototype.onClick=function(){return a.isString(this.target)&&"_self"!=this.target?!0:(this._router.navigateByInstruction(this._navigationInstruction),!1)},t=r([o.Directive({selector:"[routerLink]",inputs:["routeParams: routerLink","target: target"],host:{"(click)":"onClick()","[attr.href]":"visibleHref","[class.router-link-active]":"isRouteActive"}}),i("design:paramtypes",[u.Router,s.Location])],t)}();e.RouterLink=c},function(t,e,n){"use strict";function r(t,e,n,r){var i=new s.RootRouter(t,e,n);return r.registerDisposeListener(function(){return i.dispose()}),i}function i(t){if(0==t.componentTypes.length)throw new p.BaseException("Bootstrap at least one component before injecting Router.");return t.componentTypes[0]}var o=n(249),s=n(248),a=n(256),u=n(5),c=n(2),p=n(12);e.ROUTER_PROVIDERS_COMMON=u.CONST_EXPR([a.RouteRegistry,u.CONST_EXPR(new c.Provider(o.LocationStrategy,{useClass:o.PathLocationStrategy})),o.Location,u.CONST_EXPR(new c.Provider(s.Router,{useFactory:r,deps:u.CONST_EXPR([a.RouteRegistry,o.Location,a.ROUTER_PRIMARY_COMPONENT,c.ApplicationRef])})),u.CONST_EXPR(new c.Provider(a.ROUTER_PRIMARY_COMPONENT,{useFactory:i,deps:u.CONST_EXPR([c.ApplicationRef])}))])},function(t,e,n){"use strict";var r=n(275),i=n(2),o=n(277),s=n(249),a=n(5);e.ROUTER_PROVIDERS=a.CONST_EXPR([r.ROUTER_PROVIDERS_COMMON,a.CONST_EXPR(new i.Provider(s.PlatformLocation,{useClass:o.BrowserPlatformLocation}))]),e.ROUTER_BINDINGS=e.ROUTER_PROVIDERS},function(t,e,n){"use strict";var r=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},i=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},o=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},s=n(8),a=n(251),u=n(200),c=function(t){function e(){t.call(this),this._init()}return r(e,t),e.prototype._init=function(){this._location=u.DOM.getLocation(),this._history=u.DOM.getHistory()},Object.defineProperty(e.prototype,"location",{get:function(){return this._location},enumerable:!0,configurable:!0}),e.prototype.getBaseHrefFromDOM=function(){return u.DOM.getBaseHref()},e.prototype.onPopState=function(t){u.DOM.getGlobalEventTarget("window").addEventListener("popstate",t,!1)},e.prototype.onHashChange=function(t){u.DOM.getGlobalEventTarget("window").addEventListener("hashchange",t,!1)},Object.defineProperty(e.prototype,"pathname",{get:function(){return this._location.pathname},set:function(t){this._location.pathname=t},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"search",{get:function(){return this._location.search},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"hash",{get:function(){return this._location.hash},enumerable:!0,configurable:!0}),e.prototype.pushState=function(t,e,n){this._history.pushState(t,e,n)},e.prototype.replaceState=function(t,e,n){this._history.replaceState(t,e,n)},e.prototype.forward=function(){this._history.forward()},e.prototype.back=function(){this._history.back()},e=i([s.Injectable(),o("design:paramtypes",[])],e)}(a.PlatformLocation);e.BrowserPlatformLocation=c},function(t,e,n){"use strict";var r=n(137),i=n(2),o=n(279),s=n(5),a=n(279);e.RouterLinkTransform=a.RouterLinkTransform,e.ROUTER_LINK_DSL_PROVIDER=s.CONST_EXPR(new i.Provider(r.TEMPLATE_TRANSFORMS,{useClass:o.RouterLinkTransform,multi:!0}))},function(t,e,n){"use strict";function r(t,e){var n=new y(t,e.trim()).tokenize();return new m(n).generate()}var i=this&&this.__extends||function(t,e){function n(){this.constructor=t}for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},o=this&&this.__decorate||function(t,e,n,r){var i,o=arguments.length,s=3>o?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(s=(3>o?i(s):o>3?i(e,n,s):i(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s},s=this&&this.__metadata||function(t,e){return"object"==typeof Reflect&&"function"==typeof Reflect.metadata?Reflect.metadata(t,e):void 0},a=n(137),u=n(141),c=n(12),p=n(2),l=n(142),h=function(){function t(t){this.value=t}return t}(),f=function(){function t(){}return t}(),d=function(){function t(){}return t}(),v=function(){function t(t){this.ast=t}return t}(),y=function(){function t(t,e){this.parser=t,this.exp=e,this.index=0}return t.prototype.tokenize=function(){for(var t=[];this.indexn;n++)e.insertBefore(t[n],this.contentInsertionPoint)},t.prototype.setupOutputs=function(){for(var t=this,e=this.attrs,n=this.info.outputs,r=0;r1)throw new Error("Only support single directive definition for: "+this.name);var n=e[0];n.replace&&this.notSupported("replace"),n.terminal&&this.notSupported("terminal");var r=n.link;return"object"==typeof r&&r.post&&this.notSupported("link.post"),n},t.prototype.notSupported=function(t){throw new Error("Upgraded directive '"+this.name+"' does not support '"+t+"'.")},t.prototype.extractBindings=function(){var t="object"==typeof this.directive.bindToController;if(t&&Object.keys(this.directive.scope).length)throw new Error("Binding definitions on scope and controller at the same time are not supported.");var e=t?this.directive.bindToController:this.directive.scope;if("object"==typeof e)for(var n in e)if(e.hasOwnProperty(n)){var r=e[n],i=r.charAt(0);r=r.substr(1)||n;var o="output_"+n,s=o+": "+n,a=o+": "+n+"Change",u="input_"+n,c=u+": "+n;switch(i){case"=":this.propertyOutputs.push(o),this.checkProperties.push(r),this.outputs.push(o),this.outputsRename.push(a),this.propertyMap[o]=r;case"@":case"<":this.inputs.push(u),this.inputsRename.push(c),this.propertyMap[u]=r;break;case"&":this.outputs.push(o),this.outputsRename.push(s),this.propertyMap[o]=r;break;default:var p=JSON.stringify(e);throw new Error("Unexpected mapping '"+i+"' in '"+p+"' in '"+this.name+"' directive.")}}},t.prototype.compileTemplate=function(t,e,n){function r(e){var n=document.createElement("div");return n.innerHTML=e,t(n.childNodes)}var i=this;if(void 0!==this.directive.template)this.linkFn=r(this.directive.template);else{if(!this.directive.templateUrl)throw new Error("Directive '"+this.name+"' is not a component, it is missing template.");var o=this.directive.templateUrl,s=e.get(o);if(void 0===s)return new Promise(function(t,s){n("GET",o,null,function(n,a){200==n?t(i.linkFn=r(e.put(o,a))):s("GET "+o+" returned "+n+": "+a)})});this.linkFn=r(s)}return null},t.resolve=function(t,e){var n=[],r=e.get(i.NG1_COMPILE),o=e.get(i.NG1_TEMPLATE_CACHE),s=e.get(i.NG1_HTTP_BACKEND),a=e.get(i.NG1_CONTROLLER);for(var u in t)if(t.hasOwnProperty(u)){var c=t[u];c.directive=c.extractDirective(e),c.$controller=a,c.extractBindings();var p=c.compileTemplate(r,o,s);p&&n.push(p)}return Promise.all(n)},t}();e.UpgradeNg1ComponentAdapterBuilder=p;var l=function(){function t(t,e,n,i,a,p,l,h,f,d){this.linkFn=t,this.directive=n,this.inputs=p,this.outputs=l,this.propOuts=h,this.checkProperties=f,this.propertyMap=d,this.destinationObj=null,this.checkLastValues=[],this.element=i.nativeElement,this.componentScope=e.$new(!!n.scope);var v=s.element(this.element),y=n.controller,m=null;if(y){var g={$scope:this.componentScope,$element:v};m=a(y,g,null,n.controllerAs),v.data(o.controllerKey(n.name),m)}var _=n.link;if("object"==typeof _&&(_=_.pre),_){var b=c,P=c,E=this.resolveRequired(v,n.require);n.link(this.componentScope,v,b,E,P)}this.destinationObj=n.bindToController&&m?m:this.componentScope;for(var w=0;wr;r++)e.element.appendChild(t[r])},{parentBoundTranscludeFn:function(t,e){e(n)}}),this.destinationObj.$onInit&&this.destinationObj.$onInit()},t.prototype.ngOnChanges=function(t){for(var e in t)if(t.hasOwnProperty(e)){var n=t[e];this.setComponentProperty(e,n.currentValue)}},t.prototype.ngDoCheck=function(){for(var t=0,e=this.destinationObj,n=this.checkLastValues,r=this.checkProperties,i=0;i",this._properties=t&&t.properties||{},this._zoneDelegate=new d(this,this._parent&&this._parent._zoneDelegate,t)}return Object.defineProperty(e,"current",{get:function(){return m},enumerable:!0,configurable:!0}),Object.defineProperty(e,"currentTask",{get:function(){return T},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"parent",{get:function(){return this._parent},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"name",{get:function(){return this._name},enumerable:!0,configurable:!0}),e.prototype.get=function(e){for(var t=this;t;){if(t._properties.hasOwnProperty(e))return t._properties[e];t=t._parent}},e.prototype.fork=function(e){if(!e)throw new Error("ZoneSpec required!");return this._zoneDelegate.fork(this,e)},e.prototype.wrap=function(e,t){if("function"!=typeof e)throw new Error("Expecting function got: "+e);var n=this._zoneDelegate.intercept(this,e,t),r=this;return function(){return r.runGuarded(n,this,arguments,t)}},e.prototype.run=function(e,t,n,r){void 0===t&&(t=null),void 0===n&&(n=null),void 0===r&&(r=null);var o=m;m=this;try{return this._zoneDelegate.invoke(this,e,t,n,r)}finally{m=o}},e.prototype.runGuarded=function(e,t,n,r){void 0===t&&(t=null),void 0===n&&(n=null),void 0===r&&(r=null);var o=m;m=this;try{try{return this._zoneDelegate.invoke(this,e,t,n,r)}catch(a){if(this._zoneDelegate.handleError(this,a))throw a}}finally{m=o}},e.prototype.runTask=function(e,t,n){if(e.runCount++,e.zone!=this)throw new Error("A task can only be run in the zone which created it! (Creation: "+e.zone.name+"; Execution: "+this.name+")");var r=T;T=e;var o=m;m=this;try{"macroTask"==e.type&&e.data&&!e.data.isPeriodic&&(e.cancelFn=null);try{return this._zoneDelegate.invokeTask(this,e,t,n)}catch(a){if(this._zoneDelegate.handleError(this,a))throw a}}finally{m=o,T=r}},e.prototype.scheduleMicroTask=function(e,t,n,r){return this._zoneDelegate.scheduleTask(this,new v("microTask",this,e,t,n,r,null))},e.prototype.scheduleMacroTask=function(e,t,n,r,o){return this._zoneDelegate.scheduleTask(this,new v("macroTask",this,e,t,n,r,o))},e.prototype.scheduleEventTask=function(e,t,n,r,o){return this._zoneDelegate.scheduleTask(this,new v("eventTask",this,e,t,n,r,o))},e.prototype.cancelTask=function(e){var t=this._zoneDelegate.cancelTask(this,e);return e.runCount=-1,e.cancelFn=null,t},e.__symbol__=t,e}(),d=function(){function e(e,t,n){this._taskCounts={microTask:0,macroTask:0,eventTask:0},this.zone=e,this._parentDelegate=t,this._forkZS=n&&(n&&n.onFork?n:t._forkZS),this._forkDlgt=n&&(n.onFork?t:t._forkDlgt),this._interceptZS=n&&(n.onIntercept?n:t._interceptZS),this._interceptDlgt=n&&(n.onIntercept?t:t._interceptDlgt),this._invokeZS=n&&(n.onInvoke?n:t._invokeZS),this._invokeDlgt=n&&(n.onInvoke?t:t._invokeDlgt),this._handleErrorZS=n&&(n.onHandleError?n:t._handleErrorZS),this._handleErrorDlgt=n&&(n.onHandleError?t:t._handleErrorDlgt),this._scheduleTaskZS=n&&(n.onScheduleTask?n:t._scheduleTaskZS),this._scheduleTaskDlgt=n&&(n.onScheduleTask?t:t._scheduleTaskDlgt),this._invokeTaskZS=n&&(n.onInvokeTask?n:t._invokeTaskZS),this._invokeTaskDlgt=n&&(n.onInvokeTask?t:t._invokeTaskDlgt),this._cancelTaskZS=n&&(n.onCancelTask?n:t._cancelTaskZS),this._cancelTaskDlgt=n&&(n.onCancelTask?t:t._cancelTaskDlgt),this._hasTaskZS=n&&(n.onHasTask?n:t._hasTaskZS),this._hasTaskDlgt=n&&(n.onHasTask?t:t._hasTaskDlgt)}return e.prototype.fork=function(e,t){return this._forkZS?this._forkZS.onFork(this._forkDlgt,this.zone,e,t):new h(e,t)},e.prototype.intercept=function(e,t,n){return this._interceptZS?this._interceptZS.onIntercept(this._interceptDlgt,this.zone,e,t,n):t},e.prototype.invoke=function(e,t,n,r,o){return this._invokeZS?this._invokeZS.onInvoke(this._invokeDlgt,this.zone,e,t,n,r,o):t.apply(n,r)},e.prototype.handleError=function(e,t){return this._handleErrorZS?this._handleErrorZS.onHandleError(this._handleErrorDlgt,this.zone,e,t):!0},e.prototype.scheduleTask=function(e,t){try{if(this._scheduleTaskZS)return this._scheduleTaskZS.onScheduleTask(this._scheduleTaskDlgt,this.zone,e,t);if(t.scheduleFn)t.scheduleFn(t);else{if("microTask"!=t.type)throw new Error("Task is missing scheduleFn.");r(t)}return t}finally{e==this.zone&&this._updateTaskCount(t.type,1)}},e.prototype.invokeTask=function(e,t,n,r){try{return this._invokeTaskZS?this._invokeTaskZS.onInvokeTask(this._invokeTaskDlgt,this.zone,e,t,n,r):t.callback.apply(n,r)}finally{e!=this.zone||"eventTask"==t.type||t.data&&t.data.isPeriodic||this._updateTaskCount(t.type,-1)}},e.prototype.cancelTask=function(e,t){var n;if(this._cancelTaskZS)n=this._cancelTaskZS.onCancelTask(this._cancelTaskDlgt,this.zone,e,t);else{if(!t.cancelFn)throw new Error("Task does not support cancellation, or is already canceled.");n=t.cancelFn(t)}return e==this.zone&&this._updateTaskCount(t.type,-1),n},e.prototype.hasTask=function(e,t){return this._hasTaskZS&&this._hasTaskZS.onHasTask(this._hasTaskDlgt,this.zone,e,t)},e.prototype._updateTaskCount=function(e,t){var n=this._taskCounts,r=n[e],o=n[e]=r+t;if(0>o)throw new Error("More tasks executed then were scheduled.");if(0==r||0==o){var a={microTask:n.microTask>0,macroTask:n.macroTask>0,eventTask:n.eventTask>0,change:e};try{this.hasTask(this.zone,a)}finally{this._parentDelegate&&this._parentDelegate._updateTaskCount(e,t)}}},e}(),v=function(){function e(e,t,n,r,o,i,u){this.runCount=0,this.type=e,this.zone=t,this.source=n,this.data=o,this.scheduleFn=i,this.cancelFn=u,this.callback=r;var c=this;this.invoke=function(){try{return t.runTask(c,this,arguments)}finally{a()}}}return e}(),y=t("setTimeout"),g=t("Promise"),k=t("then"),m=new h(null,null),T=null,w=[],_=!1,b=[],E=!1,S=t("state"),O=t("value"),D="Promise.then",P=null,M=!0,z=!1,j=0,C=function(){function e(e){var t=this;t[S]=P,t[O]=[];try{e&&e(s(t,M),s(t,z))}catch(n){l(t,!1,n)}}return e.resolve=function(e){return l(new this(null),M,e)},e.reject=function(e){return l(new this(null),z,e)},e.race=function(e){function t(e){a&&(a=r(e))}function n(e){a&&(a=o(e))}for(var r,o,a=new this(function(e,t){r=e,o=t}),u=0,c=e;u=0;n--)"function"==typeof e[n]&&(e[n]=Zone.current.wrap(e[n],t+"_"+n));return e}function r(e,t){for(var r=e.constructor.name,o=function(o){var a=t[o],i=e[a];i&&(e[a]=function(e){return function(){return e.apply(this,n(arguments,r+"."+a))}}(i))},a=0;a1?new t(e,n):new t(e),i=Object.getOwnPropertyDescriptor(a,"onmessage");return i&&i.configurable===!1?(r=Object.create(a),["addEventListener","removeEventListener","send","close"].forEach(function(e){r[e]=function(){return a[e].apply(a,arguments)}})):r=a,o.patchOnProperties(r,["close","error","message","open"]),r};for(var n in t)e.WebSocket[n]=t[n]}var o=n(3);t.apply=r},function(e,t,n){"use strict";function r(e,t,n,r){function a(t){var n=t.data;return n.args[0]=t.invoke,n.handleId=u.apply(e,n.args),t}function i(e){return c(e.data.handleId)}var u=null,c=null;t+=r,n+=r,u=o.patchMethod(e,t,function(n){return function(o,u){if("function"==typeof u[0]){var c=Zone.current,s={handleId:null,isPeriodic:"Interval"===r,delay:"Timeout"===r||"Interval"===r?u[1]||0:null,args:u};return c.scheduleMacroTask(t,u[0],s,a,i)}return n.apply(e,u)}}),c=o.patchMethod(e,n,function(t){return function(n,r){var o=r[0];o&&"string"==typeof o.type?(o.cancelFn&&o.data.isPeriodic||0===o.runCount)&&o.zone.cancelTask(o):t.apply(e,r)}})}var o=n(3);t.patchTimer=r}]),function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t){"use strict";!function(){function e(){return new Error("STACKTRACE TRACKING")}function t(){try{throw e()}catch(t){return t}}function n(e){return e.stack?e.stack.split(u):[]}function r(e,t){for(var r=n(t),o=0;o0&&(e.push(n((new f).error)),a(e,t-1))}function i(){var e=[];a(e,2);for(var t=e[0],n=e[1],r=0;rthis.longStackTraceLimit&&(a.length=this.longStackTraceLimit),r.data||(r.data={}),r.data[l]=a,e.scheduleTask(n,r)},onHandleError:function(e,t,n,r){var a=Zone.currentTask;if(r instanceof Error&&a){var i=Object.getOwnPropertyDescriptor(r,"stack");if(i){var u=i.get,c=i.value;i={get:function(){return o(a.data&&a.data[l],u?u.apply(this):c)}},Object.defineProperty(r,"stack",i)}else r.stack=o(a.data&&a.data[l],r.stack)}return e.handleError(n,r)}},i()}()}]);var Reflect;!function(e){function t(e,t,n,r){if(_(r)){if(_(n)){if(!b(e))throw new TypeError;if(!S(t))throw new TypeError;return f(e,t)}if(!b(e))throw new TypeError;if(!E(t))throw new TypeError;return n=D(n),h(e,t,n)}if(!b(e))throw new TypeError;if(!E(t))throw new TypeError;if(_(n))throw new TypeError;if(!E(r))throw new TypeError;return n=D(n),p(e,t,n,r)}function n(e,t){function n(n,r){if(_(r)){if(!S(n))throw new TypeError;m(e,t,n,void 0)}else{if(!E(n))throw new TypeError;r=D(r),m(e,t,n,r)}}return n}function r(e,t,n,r){if(!E(n))throw new TypeError;return _(r)||(r=D(r)),m(e,t,n,r)}function o(e,t,n){if(!E(t))throw new TypeError;return _(n)||(n=D(n)),v(e,t,n)}function a(e,t,n){if(!E(t))throw new TypeError;return _(n)||(n=D(n)),y(e,t,n)}function i(e,t,n){if(!E(t))throw new TypeError;return _(n)||(n=D(n)),g(e,t,n)}function u(e,t,n){if(!E(t))throw new TypeError;return _(n)||(n=D(n)),k(e,t,n)}function c(e,t){if(!E(e))throw new TypeError;return _(t)||(t=D(t)),T(e,t)}function s(e,t){if(!E(e))throw new TypeError;return _(t)||(t=D(t)),w(e,t)}function l(e,t,n){if(!E(t))throw new TypeError;_(n)||(n=D(n));var r=d(t,n,!1);if(_(r))return!1;if(!r["delete"](e))return!1;if(r.size>0)return!0;var o=R.get(t);return o["delete"](n),o.size>0?!0:(R["delete"](t),!0)}function f(e,t){for(var n=e.length-1;n>=0;--n){var r=e[n],o=r(t);if(!_(o)){if(!S(o))throw new TypeError;t=o}}return t}function p(e,t,n,r){for(var o=e.length-1;o>=0;--o){var a=e[o],i=a(t,n,r);if(!_(i)){if(!E(i))throw new TypeError;r=i}}return r}function h(e,t,n){for(var r=e.length-1;r>=0;--r){var o=e[r];o(t,n)}}function d(e,t,n){var r=R.get(e);if(!r){if(!n)return;r=new Z,R.set(e,r)}var o=r.get(t);if(!o){if(!n)return;o=new Z,r.set(t,o)}return o}function v(e,t,n){var r=y(e,t,n);if(r)return!0;var o=P(t);return null!==o?v(e,o,n):!1}function y(e,t,n){var r=d(t,n,!1);return void 0===r?!1:Boolean(r.has(e))}function g(e,t,n){var r=y(e,t,n);if(r)return k(e,t,n);var o=P(t);return null!==o?g(e,o,n):void 0}function k(e,t,n){var r=d(t,n,!1);if(void 0!==r)return r.get(e)}function m(e,t,n,r){var o=d(n,r,!0);o.set(e,t)}function T(e,t){var n=w(e,t),r=P(e);if(null===r)return n;var o=T(r,t);if(o.length<=0)return n;if(n.length<=0)return o;for(var a=new I,i=[],u=0;u=0?(this._cache=e,!0):!1},get:function(e){var t=this._find(e);return t>=0?(this._cache=e,this._values[t]):void 0},set:function(e,t){return this["delete"](e),this._keys.push(e),this._values.push(t),this._cache=e,this},"delete":function(e){var n=this._find(e);return n>=0?(this._keys.splice(n,1),this._values.splice(n,1),this._cache=t,!0):!1},clear:function(){this._keys.length=0,this._values.length=0,this._cache=t},forEach:function(e,t){for(var n=this.size,r=0;n>r;++r){var o=this._keys[r],a=this._values[r];this._cache=o,e.call(this,a,o,this)}},_find:function(e){for(var t=this._keys,n=t.length,r=0;n>r;++r)if(t[r]===e)return r;return-1}},e}function z(){function e(){this._map=new Z}return e.prototype={get size(){return this._map.length},has:function(e){return this._map.has(e)},add:function(e){return this._map.set(e,e),this},"delete":function(e){return this._map["delete"](e)},clear:function(){this._map.clear()},forEach:function(e,t){this._map.forEach(e,t)}},e}function j(){function e(){this._key=o()}function t(e,t){for(var n=0;t>n;++n)e[n]=255*Math.random()|0}function n(e){if(c){var n=c.randomBytes(e);return n}if("function"==typeof Uint8Array){var n=new Uint8Array(e);return"undefined"!=typeof crypto?crypto.getRandomValues(n):"undefined"!=typeof msCrypto?msCrypto.getRandomValues(n):t(n,e),n}var n=new Array(e);return t(n,e),n}function r(){var e=n(i);e[6]=79&e[6]|64,e[8]=191&e[8]|128;for(var t="",r=0;i>r;++r){var o=e[r];(4===r||6===r||8===r)&&(t+="-"),16>o&&(t+="0"),t+=o.toString(16).toLowerCase()}return t}function o(){var e;do e="@@WeakMap@@"+r();while(s.call(l,e));return l[e]=!0,e}function a(e,t){if(!s.call(e,f)){if(!t)return;Object.defineProperty(e,f,{value:Object.create(null)})}return e[f]}var i=16,u="undefined"!=typeof global&&"[object process]"===Object.prototype.toString.call(global.process),c=u&&require("crypto"),s=Object.prototype.hasOwnProperty,l={},f=o();return e.prototype={has:function(e){var t=a(e,!1);return t?this._key in t:!1},get:function(e){var t=a(e,!1);return t?t[this._key]:void 0},set:function(e,t){var n=a(e,!0);return n[this._key]=t,this},"delete":function(e){var t=a(e,!1);return t&&this._key in t?delete t[this._key]:!1},clear:function(){this._key=o()}},e}var C=Object.getPrototypeOf(Function),Z="function"==typeof Map?Map:M(),I="function"==typeof Set?Set:z(),L="function"==typeof WeakMap?WeakMap:j(),R=new L;e.decorate=t,e.metadata=n,e.defineMetadata=r,e.hasMetadata=o,e.hasOwnMetadata=a,e.getMetadata=i,e.getOwnMetadata=u,e.getMetadataKeys=c,e.getOwnMetadataKeys=s,e.deleteMetadata=l,function(t){if("undefined"!=typeof t.Reflect){if(t.Reflect!==e)for(var n in e)t.Reflect[n]=e[n]}else t.Reflect=e}("undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope?self:"undefined"!=typeof global?global:Function("return this;")())}(Reflect||(Reflect={})); \ No newline at end of file diff --git a/accessible/libs/es6-shim.min.js b/accessible/libs/es6-shim.min.js deleted file mode 100644 index 9a11646fc88..00000000000 --- a/accessible/libs/es6-shim.min.js +++ /dev/null @@ -1,12 +0,0 @@ -/*! - * https://github.com/paulmillr/es6-shim - * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com) - * and contributors, MIT License - * es6-shim: v0.35.1 - * see https://github.com/paulmillr/es6-shim/blob/0.35.1/LICENSE - * Details and documentation: - * https://github.com/paulmillr/es6-shim/ - */ -(function(e,t){if(typeof define==="function"&&define.amd){define(t)}else if(typeof exports==="object"){module.exports=t()}else{e.returnExports=t()}})(this,function(){"use strict";var e=Function.call.bind(Function.apply);var t=Function.call.bind(Function.call);var r=Array.isArray;var n=Object.keys;var o=function notThunker(t){return function notThunk(){return!e(t,this,arguments)}};var i=function(e){try{e();return false}catch(t){return true}};var a=function valueOrFalseIfThrows(e){try{return e()}catch(t){return false}};var u=o(i);var f=function(){return!i(function(){Object.defineProperty({},"x",{get:function(){}})})};var s=!!Object.defineProperty&&f();var c=function foo(){}.name==="foo";var l=Function.call.bind(Array.prototype.forEach);var p=Function.call.bind(Array.prototype.reduce);var v=Function.call.bind(Array.prototype.filter);var y=Function.call.bind(Array.prototype.some);var h=function(e,t,r,n){if(!n&&t in e){return}if(s){Object.defineProperty(e,t,{configurable:true,enumerable:false,writable:true,value:r})}else{e[t]=r}};var b=function(e,t,r){l(n(t),function(n){var o=t[n];h(e,n,o,!!r)})};var g=Function.call.bind(Object.prototype.toString);var d=typeof/abc/==="function"?function IsCallableSlow(e){return typeof e==="function"&&g(e)==="[object Function]"}:function IsCallableFast(e){return typeof e==="function"};var O={getter:function(e,t,r){if(!s){throw new TypeError("getters require true ES5 support")}Object.defineProperty(e,t,{configurable:true,enumerable:false,get:r})},proxy:function(e,t,r){if(!s){throw new TypeError("getters require true ES5 support")}var n=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(r,t,{configurable:n.configurable,enumerable:n.enumerable,get:function getKey(){return e[t]},set:function setKey(r){e[t]=r}})},redefine:function(e,t,r){if(s){var n=Object.getOwnPropertyDescriptor(e,t);n.value=r;Object.defineProperty(e,t,n)}else{e[t]=r}},defineByDescriptor:function(e,t,r){if(s){Object.defineProperty(e,t,r)}else if("value"in r){e[t]=r.value}},preserveToString:function(e,t){if(t&&d(t.toString)){h(e,"toString",t.toString.bind(t),true)}}};var m=Object.create||function(e,t){var r=function Prototype(){};r.prototype=e;var o=new r;if(typeof t!=="undefined"){n(t).forEach(function(e){O.defineByDescriptor(o,e,t[e])})}return o};var w=function(e,t){if(!Object.setPrototypeOf){return false}return a(function(){var r=function Subclass(t){var r=new e(t);Object.setPrototypeOf(r,Subclass.prototype);return r};Object.setPrototypeOf(r,e);r.prototype=m(e.prototype,{constructor:{value:r}});return t(r)})};var j=function(){if(typeof self!=="undefined"){return self}if(typeof window!=="undefined"){return window}if(typeof global!=="undefined"){return global}throw new Error("unable to locate global object")};var S=j();var T=S.isFinite;var I=Function.call.bind(String.prototype.indexOf);var E=Function.apply.bind(Array.prototype.indexOf);var P=Function.call.bind(Array.prototype.concat);var C=Function.call.bind(String.prototype.slice);var M=Function.call.bind(Array.prototype.push);var x=Function.apply.bind(Array.prototype.push);var N=Function.call.bind(Array.prototype.shift);var A=Math.max;var R=Math.min;var _=Math.floor;var k=Math.abs;var F=Math.exp;var L=Math.log;var D=Math.sqrt;var z=Function.call.bind(Object.prototype.hasOwnProperty);var q;var W=function(){};var G=S.Symbol||{};var H=G.species||"@@species";var V=Number.isNaN||function isNaN(e){return e!==e};var B=Number.isFinite||function isFinite(e){return typeof e==="number"&&T(e)};var $=d(Math.sign)?Math.sign:function sign(e){var t=Number(e);if(t===0){return t}if(V(t)){return t}return t<0?-1:1};var U=function isArguments(e){return g(e)==="[object Arguments]"};var J=function isArguments(e){return e!==null&&typeof e==="object"&&typeof e.length==="number"&&e.length>=0&&g(e)!=="[object Array]"&&g(e.callee)==="[object Function]"};var X=U(arguments)?U:J;var K={primitive:function(e){return e===null||typeof e!=="function"&&typeof e!=="object"},string:function(e){return g(e)==="[object String]"},regex:function(e){return g(e)==="[object RegExp]"},symbol:function(e){return typeof S.Symbol==="function"&&typeof e==="symbol"}};var Z=function overrideNative(e,t,r){var n=e[t];h(e,t,r,true);O.preserveToString(e[t],n)};var Y=typeof G==="function"&&typeof G["for"]==="function"&&K.symbol(G());var Q=K.symbol(G.iterator)?G.iterator:"_es6-shim iterator_";if(S.Set&&typeof(new S.Set)["@@iterator"]==="function"){Q="@@iterator"}if(!S.Reflect){h(S,"Reflect",{},true)}var ee=S.Reflect;var te=String;var re={Call:function Call(t,r){var n=arguments.length>2?arguments[2]:[];if(!re.IsCallable(t)){throw new TypeError(t+" is not a function")}return e(t,r,n)},RequireObjectCoercible:function(e,t){if(e==null){throw new TypeError(t||"Cannot call method on "+e)}return e},TypeIsObject:function(e){if(e===void 0||e===null||e===true||e===false){return false}return typeof e==="function"||typeof e==="object"},ToObject:function(e,t){return Object(re.RequireObjectCoercible(e,t))},IsCallable:d,IsConstructor:function(e){return re.IsCallable(e)},ToInt32:function(e){return re.ToNumber(e)>>0},ToUint32:function(e){return re.ToNumber(e)>>>0},ToNumber:function(e){if(g(e)==="[object Symbol]"){throw new TypeError("Cannot convert a Symbol value to a number")}return+e},ToInteger:function(e){var t=re.ToNumber(e);if(V(t)){return 0}if(t===0||!B(t)){return t}return(t>0?1:-1)*_(k(t))},ToLength:function(e){var t=re.ToInteger(e);if(t<=0){return 0}if(t>Number.MAX_SAFE_INTEGER){return Number.MAX_SAFE_INTEGER}return t},SameValue:function(e,t){if(e===t){if(e===0){return 1/e===1/t}return true}return V(e)&&V(t)},SameValueZero:function(e,t){return e===t||V(e)&&V(t)},IsIterable:function(e){return re.TypeIsObject(e)&&(typeof e[Q]!=="undefined"||X(e))},GetIterator:function(e){if(X(e)){return new q(e,"value")}var t=re.GetMethod(e,Q);if(!re.IsCallable(t)){throw new TypeError("value is not an iterable")}var r=re.Call(t,e);if(!re.TypeIsObject(r)){throw new TypeError("bad iterator")}return r},GetMethod:function(e,t){var r=re.ToObject(e)[t];if(r===void 0||r===null){return void 0}if(!re.IsCallable(r)){throw new TypeError("Method not callable: "+t)}return r},IteratorComplete:function(e){return!!e.done},IteratorClose:function(e,t){var r=re.GetMethod(e,"return");if(r===void 0){return}var n,o;try{n=re.Call(r,e)}catch(i){o=i}if(t){return}if(o){throw o}if(!re.TypeIsObject(n)){throw new TypeError("Iterator's return method returned a non-object.")}},IteratorNext:function(e){var t=arguments.length>1?e.next(arguments[1]):e.next();if(!re.TypeIsObject(t)){throw new TypeError("bad iterator")}return t},IteratorStep:function(e){var t=re.IteratorNext(e);var r=re.IteratorComplete(t);return r?false:t},Construct:function(e,t,r,n){var o=typeof r==="undefined"?e:r;if(!n&&ee.construct){return ee.construct(e,t,o)}var i=o.prototype;if(!re.TypeIsObject(i)){i=Object.prototype}var a=m(i);var u=re.Call(e,a,t);return re.TypeIsObject(u)?u:a},SpeciesConstructor:function(e,t){var r=e.constructor;if(r===void 0){return t}if(!re.TypeIsObject(r)){throw new TypeError("Bad constructor")}var n=r[H];if(n===void 0||n===null){return t}if(!re.IsConstructor(n)){throw new TypeError("Bad @@species")}return n},CreateHTML:function(e,t,r,n){var o=re.ToString(e);var i="<"+t;if(r!==""){var a=re.ToString(n);var u=a.replace(/"/g,""");i+=" "+r+'="'+u+'"'}var f=i+">";var s=f+o;return s+""},IsRegExp:function IsRegExp(e){if(!re.TypeIsObject(e)){return false}var t=e[G.match];if(typeof t!=="undefined"){return!!t}return K.regex(e)},ToString:function ToString(e){return te(e)}};if(s&&Y){var ne=function defineWellKnownSymbol(e){if(K.symbol(G[e])){return G[e]}var t=G["for"]("Symbol."+e);Object.defineProperty(G,e,{configurable:false,enumerable:false,writable:false,value:t});return t};if(!K.symbol(G.search)){var oe=ne("search");var ie=String.prototype.search;h(RegExp.prototype,oe,function search(e){return re.Call(ie,e,[this])});var ae=function search(e){var t=re.RequireObjectCoercible(this);if(e!==null&&typeof e!=="undefined"){var r=re.GetMethod(e,oe);if(typeof r!=="undefined"){return re.Call(r,e,[t])}}return re.Call(ie,t,[re.ToString(e)])};Z(String.prototype,"search",ae)}if(!K.symbol(G.replace)){var ue=ne("replace");var fe=String.prototype.replace;h(RegExp.prototype,ue,function replace(e,t){return re.Call(fe,e,[this,t])});var se=function replace(e,t){var r=re.RequireObjectCoercible(this);if(e!==null&&typeof e!=="undefined"){var n=re.GetMethod(e,ue);if(typeof n!=="undefined"){return re.Call(n,e,[r,t])}}return re.Call(fe,r,[re.ToString(e),t])};Z(String.prototype,"replace",se)}if(!K.symbol(G.split)){var ce=ne("split");var le=String.prototype.split;h(RegExp.prototype,ce,function split(e,t){return re.Call(le,e,[this,t])});var pe=function split(e,t){var r=re.RequireObjectCoercible(this);if(e!==null&&typeof e!=="undefined"){var n=re.GetMethod(e,ce);if(typeof n!=="undefined"){return re.Call(n,e,[r,t])}}return re.Call(le,r,[re.ToString(e),t])};Z(String.prototype,"split",pe)}var ve=K.symbol(G.match);var ye=ve&&function(){var e={};e[G.match]=function(){return 42};return"a".match(e)!==42}();if(!ve||ye){var he=ne("match");var be=String.prototype.match;h(RegExp.prototype,he,function match(e){return re.Call(be,e,[this])});var ge=function match(e){var t=re.RequireObjectCoercible(this);if(e!==null&&typeof e!=="undefined"){var r=re.GetMethod(e,he);if(typeof r!=="undefined"){return re.Call(r,e,[t])}}return re.Call(be,t,[re.ToString(e)])};Z(String.prototype,"match",ge)}}var de=function wrapConstructor(e,t,r){O.preserveToString(t,e);if(Object.setPrototypeOf){Object.setPrototypeOf(e,t)}if(s){l(Object.getOwnPropertyNames(e),function(n){if(n in W||r[n]){return}O.proxy(e,n,t)})}else{l(Object.keys(e),function(n){if(n in W||r[n]){return}t[n]=e[n]})}t.prototype=e.prototype;O.redefine(e.prototype,"constructor",t)};var Oe=function(){return this};var me=function(e){if(s&&!z(e,H)){O.getter(e,H,Oe)}};var we=function(e,t){var r=t||function iterator(){return this};h(e,Q,r);if(!e[Q]&&K.symbol(Q)){e[Q]=r}};var je=function createDataProperty(e,t,r){if(s){Object.defineProperty(e,t,{configurable:true,enumerable:true,writable:true,value:r})}else{e[t]=r}};var Se=function createDataPropertyOrThrow(e,t,r){je(e,t,r);if(!re.SameValue(e[t],r)){throw new TypeError("property is nonconfigurable")}};var Te=function(e,t,r,n){if(!re.TypeIsObject(e)){throw new TypeError("Constructor requires `new`: "+t.name)}var o=t.prototype;if(!re.TypeIsObject(o)){o=r}var i=m(o);for(var a in n){if(z(n,a)){var u=n[a];h(i,a,u,true)}}return i};if(String.fromCodePoint&&String.fromCodePoint.length!==1){var Ie=String.fromCodePoint;Z(String,"fromCodePoint",function fromCodePoint(e){return re.Call(Ie,this,arguments)})}var Ee={fromCodePoint:function fromCodePoint(e){var t=[];var r;for(var n=0,o=arguments.length;n1114111){throw new RangeError("Invalid code point "+r)}if(r<65536){M(t,String.fromCharCode(r))}else{r-=65536;M(t,String.fromCharCode((r>>10)+55296));M(t,String.fromCharCode(r%1024+56320))}}return t.join("")},raw:function raw(e){var t=re.ToObject(e,"bad callSite");var r=re.ToObject(t.raw,"bad raw value");var n=r.length;var o=re.ToLength(n);if(o<=0){return""}var i=[];var a=0;var u,f,s,c;while(a=o){break}f=a+1=Ce){throw new RangeError("repeat count must be less than infinity and not overflow maximum string size")}return Pe(t,r)},startsWith:function startsWith(e){var t=re.ToString(re.RequireObjectCoercible(this));if(re.IsRegExp(e)){throw new TypeError('Cannot call method "startsWith" with a regex')}var r=re.ToString(e);var n;if(arguments.length>1){n=arguments[1]}var o=A(re.ToInteger(n),0);return C(t,o,o+r.length)===r},endsWith:function endsWith(e){var t=re.ToString(re.RequireObjectCoercible(this));if(re.IsRegExp(e)){throw new TypeError('Cannot call method "endsWith" with a regex')}var r=re.ToString(e);var n=t.length;var o;if(arguments.length>1){o=arguments[1]}var i=typeof o==="undefined"?n:re.ToInteger(o);var a=R(A(i,0),n);return C(t,a-r.length,a)===r},includes:function includes(e){if(re.IsRegExp(e)){throw new TypeError('"includes" does not accept a RegExp')}var t=re.ToString(e);var r;if(arguments.length>1){r=arguments[1]}return I(this,t,r)!==-1},codePointAt:function codePointAt(e){var t=re.ToString(re.RequireObjectCoercible(this));var r=re.ToInteger(e);var n=t.length;if(r>=0&&r56319||i){return o}var a=t.charCodeAt(r+1);if(a<56320||a>57343){return o}return(o-55296)*1024+(a-56320)+65536}}};if(String.prototype.includes&&"a".includes("a",Infinity)!==false){Z(String.prototype,"includes",Me.includes)}if(String.prototype.startsWith&&String.prototype.endsWith){var xe=i(function(){"/a/".startsWith(/a/)});var Ne=a(function(){return"abc".startsWith("a",Infinity)===false});if(!xe||!Ne){Z(String.prototype,"startsWith",Me.startsWith);Z(String.prototype,"endsWith",Me.endsWith)}}if(Y){var Ae=a(function(){var e=/a/;e[G.match]=false;return"/a/".startsWith(e)});if(!Ae){Z(String.prototype,"startsWith",Me.startsWith)}var Re=a(function(){var e=/a/;e[G.match]=false;return"/a/".endsWith(e)});if(!Re){Z(String.prototype,"endsWith",Me.endsWith)}var _e=a(function(){var e=/a/;e[G.match]=false;return"/a/".includes(e)});if(!_e){Z(String.prototype,"includes",Me.includes)}}b(String.prototype,Me);var ke=[" \n\x0B\f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003","\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028","\u2029\ufeff"].join("");var Fe=new RegExp("(^["+ke+"]+)|(["+ke+"]+$)","g");var Le=function trim(){return re.ToString(re.RequireObjectCoercible(this)).replace(Fe,"")};var De=["\x85","\u200b","\ufffe"].join("");var ze=new RegExp("["+De+"]","g");var qe=/^[\-+]0x[0-9a-f]+$/i;var We=De.trim().length!==De.length;h(String.prototype,"trim",Le,We);var Ge=function(e){return{value:e,done:arguments.length===0}};var He=function(e){re.RequireObjectCoercible(e);this._s=re.ToString(e);this._i=0};He.prototype.next=function(){var e=this._s;var t=this._i;if(typeof e==="undefined"||t>=e.length){this._s=void 0;return Ge()}var r=e.charCodeAt(t);var n,o;if(r<55296||r>56319||t+1===e.length){o=1}else{n=e.charCodeAt(t+1);o=n<56320||n>57343?1:2}this._i=t+o;return Ge(e.substr(t,o))};we(He.prototype);we(String.prototype,function(){return new He(this)});var Ve={from:function from(e){var r=this;var n;if(arguments.length>1){n=arguments[1]}var o,i;if(typeof n==="undefined"){o=false}else{if(!re.IsCallable(n)){throw new TypeError("Array.from: when provided, the second argument must be a function")}if(arguments.length>2){i=arguments[2]}o=true}var a=typeof(X(e)||re.GetMethod(e,Q))!=="undefined";var u,f,s;if(a){f=re.IsConstructor(r)?Object(new r):[];var c=re.GetIterator(e);var l,p;s=0;while(true){l=re.IteratorStep(c);if(l===false){break}p=l.value;try{if(o){p=typeof i==="undefined"?n(p,s):t(n,i,p,s)}f[s]=p}catch(v){re.IteratorClose(c,true);throw v}s+=1}u=s}else{var y=re.ToObject(e);u=re.ToLength(y.length);f=re.IsConstructor(r)?Object(new r(u)):new Array(u);var h;for(s=0;s2){f=arguments[2]}var s=typeof f==="undefined"?n:re.ToInteger(f);var c=s<0?A(n+s,0):R(s,n);var l=R(c-u,n-a);var p=1;if(u0){if(u in r){r[a]=r[u]}else{delete r[a]}u+=p;a+=p;l-=1}return r},fill:function fill(e){var t;if(arguments.length>1){t=arguments[1]}var r;if(arguments.length>2){r=arguments[2]}var n=re.ToObject(this);var o=re.ToLength(n.length);t=re.ToInteger(typeof t==="undefined"?0:t);r=re.ToInteger(typeof r==="undefined"?o:r);var i=t<0?A(o+t,0):R(t,o);var a=r<0?o+r:r;for(var u=i;u1?arguments[1]:null;for(var i=0,a;i1?arguments[1]:null;for(var i=0;i1&&typeof arguments[1]!=="undefined"){return re.Call(Ze,this,arguments)}else{return t(Ze,this,e)}})}var Ye=-(Math.pow(2,32)-1);var Qe=function(e,r){var n={length:Ye};n[r?(n.length>>>0)-1:0]=true;return a(function(){t(e,n,function(){throw new RangeError("should not reach here")},[]);return true})};if(!Qe(Array.prototype.forEach)){var et=Array.prototype.forEach;Z(Array.prototype,"forEach",function forEach(e){return re.Call(et,this.length>=0?this:[],arguments)},true)}if(!Qe(Array.prototype.map)){var tt=Array.prototype.map;Z(Array.prototype,"map",function map(e){return re.Call(tt,this.length>=0?this:[],arguments)},true)}if(!Qe(Array.prototype.filter)){var rt=Array.prototype.filter;Z(Array.prototype,"filter",function filter(e){return re.Call(rt,this.length>=0?this:[],arguments)},true)}if(!Qe(Array.prototype.some)){var nt=Array.prototype.some;Z(Array.prototype,"some",function some(e){return re.Call(nt,this.length>=0?this:[],arguments)},true)}if(!Qe(Array.prototype.every)){var ot=Array.prototype.every;Z(Array.prototype,"every",function every(e){return re.Call(ot,this.length>=0?this:[],arguments)},true)}if(!Qe(Array.prototype.reduce)){var it=Array.prototype.reduce;Z(Array.prototype,"reduce",function reduce(e){return re.Call(it,this.length>=0?this:[],arguments)},true)}if(!Qe(Array.prototype.reduceRight,true)){var at=Array.prototype.reduceRight;Z(Array.prototype,"reduceRight",function reduceRight(e){return re.Call(at,this.length>=0?this:[],arguments)},true)}var ut=Number("0o10")!==8;var ft=Number("0b10")!==2;var st=y(De,function(e){return Number(e+0+e)===0});if(ut||ft||st){var ct=Number;var lt=/^0b[01]+$/i;var pt=/^0o[0-7]+$/i;var vt=lt.test.bind(lt);var yt=pt.test.bind(pt);var ht=function(e){var t;if(typeof e.valueOf==="function"){t=e.valueOf();if(K.primitive(t)){return t}}if(typeof e.toString==="function"){t=e.toString();if(K.primitive(t)){return t}}throw new TypeError("No default value")};var bt=ze.test.bind(ze);var gt=qe.test.bind(qe);var dt=function(){var e=function Number(t){var r;if(arguments.length>0){r=K.primitive(t)?t:ht(t,"number")}else{r=0}if(typeof r==="string"){r=re.Call(Le,r);if(vt(r)){r=parseInt(C(r,2),2)}else if(yt(r)){r=parseInt(C(r,2),8)}else if(bt(r)||gt(r)){r=NaN}}var n=this;var o=a(function(){ct.prototype.valueOf.call(n);return true});if(n instanceof e&&!o){return new ct(r)}return ct(r)};return e}();de(ct,dt,{});b(dt,{NaN:ct.NaN,MAX_VALUE:ct.MAX_VALUE,MIN_VALUE:ct.MIN_VALUE,NEGATIVE_INFINITY:ct.NEGATIVE_INFINITY,POSITIVE_INFINITY:ct.POSITIVE_INFINITY});Number=dt;O.redefine(S,"Number",dt)}var Ot=Math.pow(2,53)-1;b(Number,{MAX_SAFE_INTEGER:Ot,MIN_SAFE_INTEGER:-Ot,EPSILON:2.220446049250313e-16,parseInt:S.parseInt,parseFloat:S.parseFloat,isFinite:B,isInteger:function isInteger(e){return B(e)&&re.ToInteger(e)===e},isSafeInteger:function isSafeInteger(e){return Number.isInteger(e)&&k(e)<=Number.MAX_SAFE_INTEGER},isNaN:V});h(Number,"parseInt",S.parseInt,Number.parseInt!==S.parseInt);if(![,1].find(function(e,t){return t===0})){Z(Array.prototype,"find",$e.find)}if([,1].findIndex(function(e,t){return t===0})!==0){Z(Array.prototype,"findIndex",$e.findIndex)}var mt=Function.bind.call(Function.bind,Object.prototype.propertyIsEnumerable);var wt=function ensureEnumerable(e,t){if(s&&mt(e,t)){Object.defineProperty(e,t,{enumerable:false})}};var jt=function sliceArgs(){var e=Number(this);var t=arguments.length;var r=t-e;var n=new Array(r<0?0:r);for(var o=e;o1){return NaN}if(t===-1){return-Infinity}if(t===1){return Infinity}if(t===0){return t}return.5*L((1+t)/(1-t))},cbrt:function cbrt(e){var t=Number(e);if(t===0){return t}var r=t<0;var n;if(r){t=-t}if(t===Infinity){n=Infinity}else{n=F(L(t)/3);n=(t/(n*n)+2*n)/3}return r?-n:n},clz32:function clz32(e){var t=Number(e);var r=re.ToUint32(t);if(r===0){return 32}return Or?re.Call(Or,r):31-_(L(r+.5)*gr)},cosh:function cosh(e){var t=Number(e);if(t===0){return 1}if(V(t)){return NaN}if(!T(t)){return Infinity}if(t<0){t=-t}if(t>21){return F(t)/2}return(F(t)+F(-t))/2},expm1:function expm1(e){var t=Number(e);if(t===-Infinity){return-1}if(!T(t)||t===0){return t}if(k(t)>.5){return F(t)-1}var r=t;var n=0;var o=1;while(n+r!==n){n+=r;o+=1;r*=t/o}return n},hypot:function hypot(e,t){var r=0;var n=0;for(var o=0;o0?i/n*(i/n):i}}return n===Infinity?Infinity:n*D(r)},log2:function log2(e){return L(e)*gr},log10:function log10(e){return L(e)*dr},log1p:function log1p(e){var t=Number(e);if(t<-1||V(t)){return NaN}if(t===0||t===Infinity){return t}if(t===-1){return-Infinity}return 1+t-1===0?t:t*(L(1+t)/(1+t-1))},sign:$,sinh:function sinh(e){var t=Number(e);if(!T(t)||t===0){return t}if(k(t)<1){return(Math.expm1(t)-Math.expm1(-t))/2}return(F(t-1)-F(-t-1))*br/2},tanh:function tanh(e){var t=Number(e);if(V(t)||t===0){return t}if(t>=20){return 1}if(t<=-20){return-1}return(Math.expm1(t)-Math.expm1(-t))/(F(t)+F(-t))},trunc:function trunc(e){var t=Number(e);return t<0?-_(-t):_(t)},imul:function imul(e,t){var r=re.ToUint32(e);var n=re.ToUint32(t);var o=r>>>16&65535;var i=r&65535;var a=n>>>16&65535;var u=n&65535;return i*u+(o*u+i*a<<16>>>0)|0},fround:function fround(e){var t=Number(e);if(t===0||t===Infinity||t===-Infinity||V(t)){return t}var r=$(t);var n=k(t);if(nyr||V(i)){return r*Infinity}return r*i}};b(Math,mr);h(Math,"log1p",mr.log1p,Math.log1p(-1e-17)!==-1e-17);h(Math,"asinh",mr.asinh,Math.asinh(-1e7)!==-Math.asinh(1e7));h(Math,"tanh",mr.tanh,Math.tanh(-2e-17)!==-2e-17);h(Math,"acosh",mr.acosh,Math.acosh(Number.MAX_VALUE)===Infinity);h(Math,"cbrt",mr.cbrt,Math.abs(1-Math.cbrt(1e-300)/1e-100)/Number.EPSILON>8);h(Math,"sinh",mr.sinh,Math.sinh(-2e-17)!==-2e-17);var wr=Math.expm1(10);h(Math,"expm1",mr.expm1,wr>22025.465794806718||wr<22025.465794806718);var jr=Math.round;var Sr=Math.round(.5-Number.EPSILON/4)===0&&Math.round(-.5+Number.EPSILON/3.99)===1;var Tr=lr+1;var Ir=2*lr-1;var Er=[Tr,Ir].every(function(e){return Math.round(e)===e});h(Math,"round",function round(e){var t=_(e);var r=t===-1?-0:t+1;return e-t<.5?t:r},!Sr||!Er);O.preserveToString(Math.round,jr);var Pr=Math.imul;if(Math.imul(4294967295,5)!==-5){Math.imul=mr.imul;O.preserveToString(Math.imul,Pr)}if(Math.imul.length!==2){Z(Math,"imul",function imul(e,t){return re.Call(Pr,Math,arguments); -})}var Cr=function(){var e=S.setTimeout;if(typeof e!=="function"&&typeof e!=="object"){return}re.IsPromise=function(e){if(!re.TypeIsObject(e)){return false}if(typeof e._promise==="undefined"){return false}return true};var r=function(e){if(!re.IsConstructor(e)){throw new TypeError("Bad promise constructor")}var t=this;var r=function(e,r){if(t.resolve!==void 0||t.reject!==void 0){throw new TypeError("Bad Promise implementation!")}t.resolve=e;t.reject=r};t.resolve=void 0;t.reject=void 0;t.promise=new e(r);if(!(re.IsCallable(t.resolve)&&re.IsCallable(t.reject))){throw new TypeError("Bad promise constructor")}};var n;if(typeof window!=="undefined"&&re.IsCallable(window.postMessage)){n=function(){var e=[];var t="zero-timeout-message";var r=function(r){M(e,r);window.postMessage(t,"*")};var n=function(r){if(r.source===window&&r.data===t){r.stopPropagation();if(e.length===0){return}var n=N(e);n()}};window.addEventListener("message",n,true);return r}}var o=function(){var e=S.Promise;var t=e&&e.resolve&&e.resolve();return t&&function(e){return t.then(e)}};var i=re.IsCallable(S.setImmediate)?S.setImmediate:typeof process==="object"&&process.nextTick?process.nextTick:o()||(re.IsCallable(n)?n():function(t){e(t,0)});var a=function(e){return e};var u=function(e){throw e};var f=0;var s=1;var c=2;var l=0;var p=1;var v=2;var y={};var h=function(e,t,r){i(function(){g(e,t,r)})};var g=function(e,t,r){var n,o;if(t===y){return e(r)}try{n=e(r);o=t.resolve}catch(i){n=i;o=t.reject}o(n)};var d=function(e,t){var r=e._promise;var n=r.reactionLength;if(n>0){h(r.fulfillReactionHandler0,r.reactionCapability0,t);r.fulfillReactionHandler0=void 0;r.rejectReactions0=void 0;r.reactionCapability0=void 0;if(n>1){for(var o=1,i=0;o0){h(r.rejectReactionHandler0,r.reactionCapability0,t);r.fulfillReactionHandler0=void 0;r.rejectReactions0=void 0;r.reactionCapability0=void 0;if(n>1){for(var o=1,i=0;o2&&arguments[2]===y;if(b&&o===E){i=y}else{i=new r(o)}var g=re.IsCallable(e)?e:a;var d=re.IsCallable(t)?t:u;var O=n._promise;var m;if(O.state===f){if(O.reactionLength===0){O.fulfillReactionHandler0=g;O.rejectReactionHandler0=d;O.reactionCapability0=i}else{var w=3*(O.reactionLength-1);O[w+l]=g;O[w+p]=d;O[w+v]=i}O.reactionLength+=1}else if(O.state===s){m=O.result;h(g,i,m)}else if(O.state===c){m=O.result;h(d,i,m)}else{throw new TypeError("unexpected Promise state")}return i.promise}});y=new r(E);I=T.then;return E}();if(S.Promise){delete S.Promise.accept;delete S.Promise.defer;delete S.Promise.prototype.chain}if(typeof Cr==="function"){b(S,{Promise:Cr});var Mr=w(S.Promise,function(e){return e.resolve(42).then(function(){})instanceof e});var xr=!i(function(){S.Promise.reject(42).then(null,5).then(null,W)});var Nr=i(function(){S.Promise.call(3,W)});var Ar=function(e){var t=e.resolve(5);t.constructor={};var r=e.resolve(t);try{r.then(null,W).then(null,W)}catch(n){return true}return t===r}(S.Promise);var Rr=s&&function(){var e=0;var t=Object.defineProperty({},"then",{get:function(){e+=1}});Promise.resolve(t);return e===1}();var _r=function BadResolverPromise(e){var t=new Promise(e);e(3,function(){});this.then=t.then;this.constructor=BadResolverPromise};_r.prototype=Promise.prototype;_r.all=Promise.all;var kr=a(function(){return!!_r.all([1,2])});if(!Mr||!xr||!Nr||Ar||!Rr||kr){Promise=Cr;Z(S,"Promise",Cr)}if(Promise.all.length!==1){var Fr=Promise.all;Z(Promise,"all",function all(e){return re.Call(Fr,this,arguments)})}if(Promise.race.length!==1){var Lr=Promise.race;Z(Promise,"race",function race(e){return re.Call(Lr,this,arguments)})}if(Promise.resolve.length!==1){var Dr=Promise.resolve;Z(Promise,"resolve",function resolve(e){return re.Call(Dr,this,arguments)})}if(Promise.reject.length!==1){var zr=Promise.reject;Z(Promise,"reject",function reject(e){return re.Call(zr,this,arguments)})}wt(Promise,"all");wt(Promise,"race");wt(Promise,"resolve");wt(Promise,"reject");me(Promise)}var qr=function(e){var t=n(p(e,function(e,t){e[t]=true;return e},{}));return e.join(":")===t.join(":")};var Wr=qr(["z","a","bb"]);var Gr=qr(["z",1,"a","3",2]);if(s){var Hr=function fastkey(e){if(!Wr){return null}if(typeof e==="undefined"||e===null){return"^"+re.ToString(e)}else if(typeof e==="string"){return"$"+e}else if(typeof e==="number"){if(!Gr){return"n"+e}return e}else if(typeof e==="boolean"){return"b"+e}return null};var Vr=function emptyObject(){return Object.create?Object.create(null):{}};var Br=function addIterableToMap(e,n,o){if(r(o)||K.string(o)){l(o,function(e){if(!re.TypeIsObject(e)){throw new TypeError("Iterator value "+e+" is not an entry object")}n.set(e[0],e[1])})}else if(o instanceof e){t(e.prototype.forEach,o,function(e,t){n.set(t,e)})}else{var i,a;if(o!==null&&typeof o!=="undefined"){a=n.set;if(!re.IsCallable(a)){throw new TypeError("bad map")}i=re.GetIterator(o)}if(typeof i!=="undefined"){while(true){var u=re.IteratorStep(i);if(u===false){break}var f=u.value;try{if(!re.TypeIsObject(f)){throw new TypeError("Iterator value "+f+" is not an entry object")}t(a,n,f[0],f[1])}catch(s){re.IteratorClose(i,true);throw s}}}}};var $r=function addIterableToSet(e,n,o){if(r(o)||K.string(o)){l(o,function(e){n.add(e)})}else if(o instanceof e){t(e.prototype.forEach,o,function(e){n.add(e)})}else{var i,a;if(o!==null&&typeof o!=="undefined"){a=n.add;if(!re.IsCallable(a)){throw new TypeError("bad set")}i=re.GetIterator(o)}if(typeof i!=="undefined"){while(true){var u=re.IteratorStep(i);if(u===false){break}var f=u.value;try{t(a,n,f)}catch(s){re.IteratorClose(i,true);throw s}}}}};var Ur={Map:function(){var e={};var r=function MapEntry(e,t){this.key=e;this.value=t;this.next=null;this.prev=null};r.prototype.isRemoved=function isRemoved(){return this.key===e};var n=function isMap(e){return!!e._es6map};var o=function requireMapSlot(e,t){if(!re.TypeIsObject(e)||!n(e)){throw new TypeError("Method Map.prototype."+t+" called on incompatible receiver "+re.ToString(e))}};var i=function MapIterator(e,t){o(e,"[[MapIterator]]");this.head=e._head;this.i=this.head;this.kind=t};i.prototype={next:function next(){var e=this.i;var t=this.kind;var r=this.head;if(typeof this.i==="undefined"){return Ge()}while(e.isRemoved()&&e!==r){e=e.prev}var n;while(e.next!==r){e=e.next;if(!e.isRemoved()){if(t==="key"){n=e.key}else if(t==="value"){n=e.value}else{n=[e.key,e.value]}this.i=e;return Ge(n)}}this.i=void 0;return Ge()}};we(i.prototype);var a;var u=function Map(){if(!(this instanceof Map)){throw new TypeError('Constructor Map requires "new"')}if(this&&this._es6map){throw new TypeError("Bad construction")}var e=Te(this,Map,a,{_es6map:true,_head:null,_storage:Vr(),_size:0});var t=new r(null,null);t.next=t.prev=t;e._head=t;if(arguments.length>0){Br(Map,e,arguments[0])}return e};a=u.prototype;O.getter(a,"size",function(){if(typeof this._size==="undefined"){throw new TypeError("size method called on incompatible Map")}return this._size});b(a,{get:function get(e){o(this,"get");var t=Hr(e);if(t!==null){var r=this._storage[t];if(r){return r.value}else{return}}var n=this._head;var i=n;while((i=i.next)!==n){if(re.SameValueZero(i.key,e)){return i.value}}},has:function has(e){o(this,"has");var t=Hr(e);if(t!==null){return typeof this._storage[t]!=="undefined"}var r=this._head;var n=r;while((n=n.next)!==r){if(re.SameValueZero(n.key,e)){return true}}return false},set:function set(e,t){o(this,"set");var n=this._head;var i=n;var a;var u=Hr(e);if(u!==null){if(typeof this._storage[u]!=="undefined"){this._storage[u].value=t;return this}else{a=this._storage[u]=new r(e,t);i=n.prev}}while((i=i.next)!==n){if(re.SameValueZero(i.key,e)){i.value=t;return this}}a=a||new r(e,t);if(re.SameValue(-0,e)){a.key=+0}a.next=this._head;a.prev=this._head.prev;a.prev.next=a;a.next.prev=a;this._size+=1;return this},"delete":function(t){o(this,"delete");var r=this._head;var n=r;var i=Hr(t);if(i!==null){if(typeof this._storage[i]==="undefined"){return false}n=this._storage[i].prev;delete this._storage[i]}while((n=n.next)!==r){if(re.SameValueZero(n.key,t)){n.key=n.value=e;n.prev.next=n.next;n.next.prev=n.prev;this._size-=1;return true}}return false},clear:function clear(){o(this,"clear");this._size=0;this._storage=Vr();var t=this._head;var r=t;var n=r.next;while((r=n)!==t){r.key=r.value=e;n=r.next;r.next=r.prev=t}t.next=t.prev=t},keys:function keys(){o(this,"keys");return new i(this,"key")},values:function values(){o(this,"values");return new i(this,"value")},entries:function entries(){o(this,"entries");return new i(this,"key+value")},forEach:function forEach(e){o(this,"forEach");var r=arguments.length>1?arguments[1]:null;var n=this.entries();for(var i=n.next();!i.done;i=n.next()){if(r){t(e,r,i.value[1],i.value[0],this)}else{e(i.value[1],i.value[0],this)}}}});we(a,a.entries);return u}(),Set:function(){var e=function isSet(e){return e._es6set&&typeof e._storage!=="undefined"};var r=function requireSetSlot(t,r){if(!re.TypeIsObject(t)||!e(t)){throw new TypeError("Set.prototype."+r+" called on incompatible receiver "+re.ToString(t))}};var o;var i=function Set(){if(!(this instanceof Set)){throw new TypeError('Constructor Set requires "new"')}if(this&&this._es6set){throw new TypeError("Bad construction")}var e=Te(this,Set,o,{_es6set:true,"[[SetData]]":null,_storage:Vr()});if(!e._es6set){throw new TypeError("bad set")}if(arguments.length>0){$r(Set,e,arguments[0])}return e};o=i.prototype;var a=function(e){var t=e;if(t==="^null"){return null}else if(t==="^undefined"){return void 0}else{var r=t.charAt(0);if(r==="$"){return C(t,1)}else if(r==="n"){return+C(t,1)}else if(r==="b"){return t==="btrue"}}return+t};var u=function ensureMap(e){if(!e["[[SetData]]"]){var t=e["[[SetData]]"]=new Ur.Map;l(n(e._storage),function(e){var r=a(e);t.set(r,r)});e["[[SetData]]"]=t}e._storage=null};O.getter(i.prototype,"size",function(){r(this,"size");if(this._storage){return n(this._storage).length}u(this);return this["[[SetData]]"].size});b(i.prototype,{has:function has(e){r(this,"has");var t;if(this._storage&&(t=Hr(e))!==null){return!!this._storage[t]}u(this);return this["[[SetData]]"].has(e)},add:function add(e){r(this,"add");var t;if(this._storage&&(t=Hr(e))!==null){this._storage[t]=true;return this}u(this);this["[[SetData]]"].set(e,e);return this},"delete":function(e){r(this,"delete");var t;if(this._storage&&(t=Hr(e))!==null){var n=z(this._storage,t);return delete this._storage[t]&&n}u(this);return this["[[SetData]]"]["delete"](e)},clear:function clear(){r(this,"clear");if(this._storage){this._storage=Vr()}if(this["[[SetData]]"]){this["[[SetData]]"].clear()}},values:function values(){r(this,"values");u(this);return this["[[SetData]]"].values()},entries:function entries(){r(this,"entries");u(this);return this["[[SetData]]"].entries()},forEach:function forEach(e){r(this,"forEach");var n=arguments.length>1?arguments[1]:null;var o=this;u(o);this["[[SetData]]"].forEach(function(r,i){if(n){t(e,n,i,i,o)}else{e(i,i,o)}})}});h(i.prototype,"keys",i.prototype.values,true);we(i.prototype,i.prototype.values);return i}()};if(S.Map||S.Set){var Jr=a(function(){return new Map([[1,2]]).get(1)===2});if(!Jr){var Xr=S.Map;S.Map=function Map(){if(!(this instanceof Map)){throw new TypeError('Constructor Map requires "new"')}var e=new Xr;if(arguments.length>0){Br(Map,e,arguments[0])}delete e.constructor;Object.setPrototypeOf(e,S.Map.prototype);return e};S.Map.prototype=m(Xr.prototype);h(S.Map.prototype,"constructor",S.Map,true);O.preserveToString(S.Map,Xr)}var Kr=new Map;var Zr=function(){var e=new Map([[1,0],[2,0],[3,0],[4,0]]);e.set(-0,e);return e.get(0)===e&&e.get(-0)===e&&e.has(0)&&e.has(-0)}();var Yr=Kr.set(1,2)===Kr;if(!Zr||!Yr){var Qr=Map.prototype.set;Z(Map.prototype,"set",function set(e,r){t(Qr,this,e===0?0:e,r);return this})}if(!Zr){var en=Map.prototype.get;var tn=Map.prototype.has;b(Map.prototype,{get:function get(e){return t(en,this,e===0?0:e)},has:function has(e){return t(tn,this,e===0?0:e)}},true);O.preserveToString(Map.prototype.get,en);O.preserveToString(Map.prototype.has,tn)}var rn=new Set;var nn=function(e){e["delete"](0);e.add(-0);return!e.has(0)}(rn);var on=rn.add(1)===rn;if(!nn||!on){var an=Set.prototype.add;Set.prototype.add=function add(e){t(an,this,e===0?0:e);return this};O.preserveToString(Set.prototype.add,an)}if(!nn){var un=Set.prototype.has;Set.prototype.has=function has(e){return t(un,this,e===0?0:e)};O.preserveToString(Set.prototype.has,un);var fn=Set.prototype["delete"];Set.prototype["delete"]=function SetDelete(e){return t(fn,this,e===0?0:e)};O.preserveToString(Set.prototype["delete"],fn)}var sn=w(S.Map,function(e){var t=new e([]);t.set(42,42);return t instanceof e});var cn=Object.setPrototypeOf&&!sn;var ln=function(){try{return!(S.Map()instanceof S.Map)}catch(e){return e instanceof TypeError}}();if(S.Map.length!==0||cn||!ln){var pn=S.Map;S.Map=function Map(){if(!(this instanceof Map)){throw new TypeError('Constructor Map requires "new"')}var e=new pn;if(arguments.length>0){Br(Map,e,arguments[0])}delete e.constructor;Object.setPrototypeOf(e,Map.prototype);return e};S.Map.prototype=pn.prototype;h(S.Map.prototype,"constructor",S.Map,true);O.preserveToString(S.Map,pn)}var vn=w(S.Set,function(e){var t=new e([]);t.add(42,42);return t instanceof e});var yn=Object.setPrototypeOf&&!vn;var hn=function(){try{return!(S.Set()instanceof S.Set)}catch(e){return e instanceof TypeError}}();if(S.Set.length!==0||yn||!hn){var bn=S.Set;S.Set=function Set(){if(!(this instanceof Set)){throw new TypeError('Constructor Set requires "new"')}var e=new bn;if(arguments.length>0){$r(Set,e,arguments[0])}delete e.constructor;Object.setPrototypeOf(e,Set.prototype);return e};S.Set.prototype=bn.prototype;h(S.Set.prototype,"constructor",S.Set,true);O.preserveToString(S.Set,bn)}var gn=new S.Map;var dn=!a(function(){return gn.keys().next().done});if(typeof S.Map.prototype.clear!=="function"||(new S.Set).size!==0||gn.size!==0||typeof S.Map.prototype.keys!=="function"||typeof S.Set.prototype.keys!=="function"||typeof S.Map.prototype.forEach!=="function"||typeof S.Set.prototype.forEach!=="function"||u(S.Map)||u(S.Set)||typeof gn.keys().next!=="function"||dn||!sn){b(S,{Map:Ur.Map,Set:Ur.Set},true)}if(S.Set.prototype.keys!==S.Set.prototype.values){h(S.Set.prototype,"keys",S.Set.prototype.values,true)}we(Object.getPrototypeOf((new S.Map).keys()));we(Object.getPrototypeOf((new S.Set).keys()));if(c&&S.Set.prototype.has.name!=="has"){var On=S.Set.prototype.has;Z(S.Set.prototype,"has",function has(e){return t(On,this,e)})}}b(S,Ur);me(S.Map);me(S.Set)}var mn=function throwUnlessTargetIsObject(e){if(!re.TypeIsObject(e)){throw new TypeError("target must be an object")}};var wn={apply:function apply(){return re.Call(re.Call,null,arguments)},construct:function construct(e,t){if(!re.IsConstructor(e)){throw new TypeError("First argument must be a constructor.")}var r=arguments.length>2?arguments[2]:e;if(!re.IsConstructor(r)){throw new TypeError("new.target must be a constructor.")}return re.Construct(e,t,r,"internal")},deleteProperty:function deleteProperty(e,t){mn(e);if(s){var r=Object.getOwnPropertyDescriptor(e,t);if(r&&!r.configurable){return false}}return delete e[t]},has:function has(e,t){mn(e);return t in e}};if(Object.getOwnPropertyNames){Object.assign(wn,{ownKeys:function ownKeys(e){mn(e);var t=Object.getOwnPropertyNames(e);if(re.IsCallable(Object.getOwnPropertySymbols)){x(t,Object.getOwnPropertySymbols(e))}return t}})}var jn=function ConvertExceptionToBoolean(e){return!i(e)};if(Object.preventExtensions){Object.assign(wn,{isExtensible:function isExtensible(e){mn(e);return Object.isExtensible(e)},preventExtensions:function preventExtensions(e){mn(e);return jn(function(){Object.preventExtensions(e)})}})}if(s){var Sn=function get(e,t,r){var n=Object.getOwnPropertyDescriptor(e,t);if(!n){var o=Object.getPrototypeOf(e);if(o===null){return void 0}return Sn(o,t,r)}if("value"in n){return n.value}if(n.get){return re.Call(n.get,r)}return void 0};var Tn=function set(e,r,n,o){var i=Object.getOwnPropertyDescriptor(e,r);if(!i){var a=Object.getPrototypeOf(e);if(a!==null){return Tn(a,r,n,o)}i={value:void 0,writable:true,enumerable:true,configurable:true}}if("value"in i){if(!i.writable){return false}if(!re.TypeIsObject(o)){return false}var u=Object.getOwnPropertyDescriptor(o,r);if(u){return ee.defineProperty(o,r,{value:n})}else{return ee.defineProperty(o,r,{value:n,writable:true,enumerable:true,configurable:true})}}if(i.set){t(i.set,o,n);return true}return false};Object.assign(wn,{defineProperty:function defineProperty(e,t,r){mn(e);return jn(function(){Object.defineProperty(e,t,r)})},getOwnPropertyDescriptor:function getOwnPropertyDescriptor(e,t){mn(e);return Object.getOwnPropertyDescriptor(e,t)},get:function get(e,t){mn(e);var r=arguments.length>2?arguments[2]:e;return Sn(e,t,r)},set:function set(e,t,r){mn(e);var n=arguments.length>3?arguments[3]:e;return Tn(e,t,r,n)}})}if(Object.getPrototypeOf){var In=Object.getPrototypeOf;wn.getPrototypeOf=function getPrototypeOf(e){mn(e);return In(e)}}if(Object.setPrototypeOf&&wn.getPrototypeOf){var En=function(e,t){var r=t;while(r){if(e===r){return true}r=wn.getPrototypeOf(r)}return false};Object.assign(wn,{setPrototypeOf:function setPrototypeOf(e,t){mn(e);if(t!==null&&!re.TypeIsObject(t)){throw new TypeError("proto must be an object or null")}if(t===ee.getPrototypeOf(e)){return true}if(ee.isExtensible&&!ee.isExtensible(e)){return false}if(En(e,t)){return false}Object.setPrototypeOf(e,t);return true}})}var Pn=function(e,t){if(!re.IsCallable(S.Reflect[e])){h(S.Reflect,e,t)}else{var r=a(function(){S.Reflect[e](1);S.Reflect[e](NaN);S.Reflect[e](true);return true});if(r){Z(S.Reflect,e,t)}}};Object.keys(wn).forEach(function(e){Pn(e,wn[e])});var Cn=S.Reflect.getPrototypeOf;if(c&&Cn&&Cn.name!=="getPrototypeOf"){Z(S.Reflect,"getPrototypeOf",function getPrototypeOf(e){return t(Cn,S.Reflect,e)})}if(S.Reflect.setPrototypeOf){if(a(function(){S.Reflect.setPrototypeOf(1,{});return true})){Z(S.Reflect,"setPrototypeOf",wn.setPrototypeOf)}}if(S.Reflect.defineProperty){if(!a(function(){var e=!S.Reflect.defineProperty(1,"test",{value:1});var t=typeof Object.preventExtensions!=="function"||!S.Reflect.defineProperty(Object.preventExtensions({}),"test",{});return e&&t})){Z(S.Reflect,"defineProperty",wn.defineProperty)}}if(S.Reflect.construct){if(!a(function(){var e=function F(){};return S.Reflect.construct(function(){},[],e)instanceof e})){Z(S.Reflect,"construct",wn.construct)}}if(String(new Date(NaN))!=="Invalid Date"){var Mn=Date.prototype.toString;var xn=function toString(){var e=+this;if(e!==e){return"Invalid Date"}return re.Call(Mn,this)};Z(Date.prototype,"toString",xn)}var Nn={anchor:function anchor(e){return re.CreateHTML(this,"a","name",e)},big:function big(){return re.CreateHTML(this,"big","","")},blink:function blink(){return re.CreateHTML(this,"blink","","")},bold:function bold(){return re.CreateHTML(this,"b","","")},fixed:function fixed(){return re.CreateHTML(this,"tt","","")},fontcolor:function fontcolor(e){return re.CreateHTML(this,"font","color",e)},fontsize:function fontsize(e){return re.CreateHTML(this,"font","size",e)},italics:function italics(){return re.CreateHTML(this,"i","","")},link:function link(e){return re.CreateHTML(this,"a","href",e)},small:function small(){return re.CreateHTML(this,"small","","")},strike:function strike(){return re.CreateHTML(this,"strike","","")},sub:function sub(){return re.CreateHTML(this,"sub","","")},sup:function sub(){return re.CreateHTML(this,"sup","","")}};l(Object.keys(Nn),function(e){var r=String.prototype[e];var n=false;if(re.IsCallable(r)){var o=t(r,"",' " ');var i=P([],o.match(/"/g)).length;n=o!==o.toLowerCase()||i>2}else{n=true}if(n){Z(String.prototype,e,Nn[e])}});var An=function(){if(!Y){return false}var e=typeof JSON==="object"&&typeof JSON.stringify==="function"?JSON.stringify:null;if(!e){return false}if(typeof e(G())!=="undefined"){return true}if(e([G()])!=="[null]"){return true}var t={a:G()};t[G()]=true;if(e(t)!=="{}"){return true}return false}();var Rn=a(function(){if(!Y){return true}return JSON.stringify(Object(G()))==="{}"&&JSON.stringify([Object(G())])==="[{}]"});if(An||!Rn){var _n=JSON.stringify;Z(JSON,"stringify",function stringify(e){if(typeof e==="symbol"){return}var n;if(arguments.length>1){n=arguments[1]}var o=[e];if(!r(n)){var i=re.IsCallable(n)?n:null;var a=function(e,r){var n=i?t(i,this,e,r):r;if(typeof n!=="symbol"){if(K.symbol(n)){return St({})(n)}else{return n}}};o.push(a)}else{o.push(n)}if(arguments.length>2){o.push(arguments[2])}return _n.apply(this,o)})}return S}); -//# sourceMappingURL=es6-shim.map diff --git a/accessible/media/accessible.css b/accessible/media/accessible.css deleted file mode 100644 index 500ee73231a..00000000000 --- a/accessible/media/accessible.css +++ /dev/null @@ -1,80 +0,0 @@ -.blocklyWorkspaceColumn { - float: left; - margin-right: 20px; - width: 800px; -} -.blocklySidebarColumn { - border-left: 1px solid #888; - float: left; - padding-left: 20px; - margin-top: 20px; - min-height: 700px; - width: 200px; -} - -.blocklySidebarButton { - background-color: #fff; - border: 1px solid #333; - border-radius: 4px; - color: #000; - font-size: 1em; - margin: 10px 0 10px 30px; - padding: 10px; - text-align: center; - vertical-align: middle; - white-space: nowrap; -} -.blocklySidebarButton[disabled] { - border: 1px solid #ccc; - opacity: 0.5; -} - -.blocklyAriaLiveStatus { - background: #c8f7be; - border-radius: 10px; - bottom: 80px; - left: 20px; - max-width: 275px; - padding: 10px; - position: fixed; -} - -.blocklyTree .blocklyActiveDescendant > label, -.blocklyTree .blocklyActiveDescendant > div > label, -.blocklyActiveDescendant > button, -.blocklyActiveDescendant > input, -.blocklyActiveDescendant > select, -.blocklyActiveDescendant > blockly-field-segment > label, -.blocklyActiveDescendant > blockly-field-segment > input, -.blocklyActiveDescendant > blockly-field-segment > select { - outline: 2px dotted #00f; -} - -.blocklyDropdownListItem[aria-selected="true"] button { - font-weight: bold; -} - -.blocklyModalCurtain { - background-color: rgba(0,0,0,0.4); - height: 100%; - left: 0; - overflow: auto; - position: fixed; - top: 0; - width: 100%; - z-index: 1; -} -.blocklyModal { - background-color: #fefefe; - border: 1px solid #888; - margin: 10% auto; - max-width: 600px; - padding: 20px; - width: 60%; -} -.blocklyModalButtonContainer { - margin: 10px 0; -} -.blocklyModal .activeButton { - border: 1px solid blue; -} diff --git a/accessible/media/click.mp3 b/accessible/media/click.mp3 deleted file mode 100644 index 4534b0ddca7..00000000000 Binary files a/accessible/media/click.mp3 and /dev/null differ diff --git a/accessible/media/click.ogg b/accessible/media/click.ogg deleted file mode 100644 index e8ae42a6106..00000000000 Binary files a/accessible/media/click.ogg and /dev/null differ diff --git a/accessible/media/click.wav b/accessible/media/click.wav deleted file mode 100644 index 41a50cd76f5..00000000000 Binary files a/accessible/media/click.wav and /dev/null differ diff --git a/accessible/media/delete.mp3 b/accessible/media/delete.mp3 deleted file mode 100644 index 1e71bdcf498..00000000000 Binary files a/accessible/media/delete.mp3 and /dev/null differ diff --git a/accessible/media/delete.ogg b/accessible/media/delete.ogg deleted file mode 100644 index a65b1122839..00000000000 Binary files a/accessible/media/delete.ogg and /dev/null differ diff --git a/accessible/media/delete.wav b/accessible/media/delete.wav deleted file mode 100644 index 455bcd3bb11..00000000000 Binary files a/accessible/media/delete.wav and /dev/null differ diff --git a/accessible/media/oops.mp3 b/accessible/media/oops.mp3 deleted file mode 100644 index 0c9507140b5..00000000000 Binary files a/accessible/media/oops.mp3 and /dev/null differ diff --git a/accessible/media/oops.ogg b/accessible/media/oops.ogg deleted file mode 100644 index 7bac05d97ed..00000000000 Binary files a/accessible/media/oops.ogg and /dev/null differ diff --git a/accessible/media/oops.wav b/accessible/media/oops.wav deleted file mode 100644 index 163df4f1cf7..00000000000 Binary files a/accessible/media/oops.wav and /dev/null differ diff --git a/accessible/messages.js b/accessible/messages.js deleted file mode 100644 index 60c51d34415..00000000000 --- a/accessible/messages.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @license - * Visual Blocks Language - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Translatable string constants for Accessible Blockly. - * @author madeeha@google.com (Madeeha Ghori) - */ -'use strict'; - -Blockly.Msg.WORKSPACE = 'Workspace'; -Blockly.Msg.WORKSPACE_BLOCK = - 'workspace block. Move right to edit. Press Enter for more options.'; - -Blockly.Msg.ATTACH_NEW_BLOCK_TO_LINK = 'Attach new block to link...'; -Blockly.Msg.CREATE_NEW_BLOCK_GROUP = 'Create new block group...'; -Blockly.Msg.ERASE_WORKSPACE = 'Erase Workspace'; -Blockly.Msg.NO_BLOCKS_IN_WORKSPACE = 'There are no blocks in the workspace.'; - -Blockly.Msg.COPY_BLOCK = 'Copy block'; -Blockly.Msg.DELETE = 'Delete block'; -Blockly.Msg.MARK_SPOT_BEFORE = 'Add link before'; -Blockly.Msg.MARK_SPOT_AFTER = 'Add link after'; -Blockly.Msg.MARK_THIS_SPOT = 'Add link inside'; -Blockly.Msg.MOVE_TO_MARKED_SPOT = 'Move to existing link'; -Blockly.Msg.PASTE_AFTER = 'Paste after'; -Blockly.Msg.PASTE_BEFORE = 'Paste before'; -Blockly.Msg.PASTE_INSIDE = 'Paste inside'; - -Blockly.Msg.BLOCK_OPTIONS = 'Block Options'; -Blockly.Msg.SELECT_A_BLOCK = 'Select a block...'; -Blockly.Msg.CANCEL = 'Cancel'; - -Blockly.Msg.ANY = 'any'; -Blockly.Msg.BLOCK = 'block'; -Blockly.Msg.BUTTON = 'Button.'; -Blockly.Msg.FOR = 'for'; -Blockly.Msg.VALUE = 'value'; - -Blockly.Msg.ADDED_LINK_MSG = 'Added link.'; -Blockly.Msg.ATTACHED_BLOCK_TO_LINK_MSG = 'attached to link. '; -Blockly.Msg.COPIED_BLOCK_MSG = 'copied. '; -Blockly.Msg.PASTED_BLOCK_FROM_CLIPBOARD_MSG = 'pasted. '; - -Blockly.Msg.PRESS_ENTER_TO_EDIT_NUMBER = 'Press Enter to edit number. '; -Blockly.Msg.PRESS_ENTER_TO_EDIT_TEXT = 'Press Enter to edit text. '; diff --git a/accessible/notifications.service.js b/accessible/notifications.service.js deleted file mode 100644 index b1ce22b27e6..00000000000 --- a/accessible/notifications.service.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Service for updating the ARIA live region that - * allows screenreaders to notify the user about actions that they have taken. - * @author sll@google.com (Sean Lip) - */ - -goog.provide('blocklyApp.NotificationsService'); - - -blocklyApp.NotificationsService = ng.core.Class({ - constructor: [function() { - this.currentMessage = ''; - this.timeouts = []; - }], - setDisplayedMessage_: function(newMessage) { - this.currentMessage = newMessage; - }, - getDisplayedMessage: function() { - return this.currentMessage; - }, - speak: function(newMessage) { - // Clear and reset any existing timeouts. - this.timeouts.forEach(function(timeout) { - clearTimeout(timeout); - }); - this.timeouts.length = 0; - - // Clear the current message, so that if, e.g., two operations of the same - // type are performed, both messages will be read in succession. - this.setDisplayedMessage_(''); - - // We need a non-zero timeout here, otherwise NVDA does not read the - // notification messages properly. - var that = this; - this.timeouts.push(setTimeout(function() { - that.setDisplayedMessage_(newMessage); - }, 20)); - this.timeouts.push(setTimeout(function() { - that.setDisplayedMessage_(''); - }, 5000)); - } -}); diff --git a/accessible/sidebar.component.js b/accessible/sidebar.component.js deleted file mode 100644 index 66f735eda26..00000000000 --- a/accessible/sidebar.component.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Component representing the sidebar that is shown next - * to the workspace. - * - * @author sll@google.com (Sean Lip) - */ - -goog.provide('blocklyApp.SidebarComponent'); - -goog.require('blocklyApp.UtilsService'); - -goog.require('blocklyApp.BlockConnectionService'); -goog.require('blocklyApp.ToolboxModalService'); -goog.require('blocklyApp.TranslatePipe'); -goog.require('blocklyApp.TreeService'); -goog.require('blocklyApp.VariableModalService'); - - -blocklyApp.SidebarComponent = ng.core.Component({ - selector: 'blockly-sidebar', - template: ` -
- - - - - -
- `, - pipes: [blocklyApp.TranslatePipe] -}) -.Class({ - constructor: [ - blocklyApp.BlockConnectionService, - blocklyApp.ToolboxModalService, - blocklyApp.TreeService, - blocklyApp.UtilsService, - blocklyApp.VariableModalService, - function( - blockConnectionService, toolboxModalService, treeService, - utilsService, variableService) { - // ACCESSIBLE_GLOBALS is a global variable defined by the containing - // page. It should contain a key, customSidebarButtons, describing - // additional buttons that should be displayed after the default ones. - // See README.md for details. - this.customSidebarButtons = - ACCESSIBLE_GLOBALS && ACCESSIBLE_GLOBALS.customSidebarButtons ? - ACCESSIBLE_GLOBALS.customSidebarButtons : []; - - this.blockConnectionService = blockConnectionService; - this.toolboxModalService = toolboxModalService; - this.treeService = treeService; - this.utilsService = utilsService; - this.variableModalService = variableService; - - this.ID_FOR_ATTACH_TO_LINK_BUTTON = 'blocklyAttachToLinkBtn'; - this.ID_FOR_CREATE_NEW_GROUP_BUTTON = 'blocklyCreateNewGroupBtn'; - } - ], - isAnyConnectionMarked: function() { - return this.blockConnectionService.isAnyConnectionMarked(); - }, - isWorkspaceEmpty: function() { - return this.utilsService.isWorkspaceEmpty(); - }, - hasVariableCategory: function() { - return this.toolboxModalService.toolboxHasVariableCategory(); - }, - clearWorkspace: function() { - blocklyApp.workspace.clear(); - this.treeService.clearAllActiveDescs(); - // The timeout is needed in order to give the blocks time to be cleared - // from the workspace, and for the 'workspace is empty' button to show up. - setTimeout(function() { - document.getElementById(blocklyApp.ID_FOR_EMPTY_WORKSPACE_BTN).focus(); - }, 50); - }, - showToolboxModalForAttachToMarkedConnection: function() { - this.toolboxModalService.showToolboxModalForAttachToMarkedConnection( - this.ID_FOR_ATTACH_TO_LINK_BUTTON); - }, - showToolboxModalForCreateNewGroup: function() { - this.toolboxModalService.showToolboxModalForCreateNewGroup( - this.ID_FOR_CREATE_NEW_GROUP_BUTTON); - }, - showAddVariableModal: function() { - this.variableModalService.showAddModal_("item"); - } -}); diff --git a/accessible/toolbox-modal.component.js b/accessible/toolbox-modal.component.js deleted file mode 100644 index 25358e82eef..00000000000 --- a/accessible/toolbox-modal.component.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Component representing the toolbox modal. - * - * @author sll@google.com (Sean Lip) - */ - -goog.provide('blocklyApp.ToolboxModalComponent'); - -goog.require('Blockly.CommonModal'); -goog.require('blocklyApp.AudioService'); -goog.require('blocklyApp.KeyboardInputService'); -goog.require('blocklyApp.ToolboxModalService'); -goog.require('blocklyApp.TranslatePipe'); -goog.require('blocklyApp.TreeService'); -goog.require('blocklyApp.UtilsService'); - - -blocklyApp.ToolboxModalComponent = ng.core.Component({ - selector: 'blockly-toolbox-modal', - template: ` -
- -
-

{{'SELECT_A_BLOCK'|translate}}

- -
-

{{toolboxCategory.categoryName}}

-
- -
-
-
-
- -
-
-
- `, - pipes: [blocklyApp.TranslatePipe] -}) -.Class({ - constructor: [ - blocklyApp.ToolboxModalService, blocklyApp.KeyboardInputService, - blocklyApp.AudioService, blocklyApp.UtilsService, blocklyApp.TreeService, - function( - toolboxModalService_, keyboardInputService_, audioService_, - utilsService_, treeService_) { - this.toolboxModalService = toolboxModalService_; - this.keyboardInputService = keyboardInputService_; - this.audioService = audioService_; - this.utilsService = utilsService_; - this.treeService = treeService_; - - this.modalIsVisible = false; - this.toolboxCategories = []; - this.onSelectBlockCallback = null; - this.onDismissCallback = null; - - this.firstBlockIndexes = []; - this.activeButtonIndex = -1; - this.totalNumBlocks = 0; - - var that = this; - this.toolboxModalService.registerPreShowHook( - function( - toolboxCategories, onSelectBlockCallback, onDismissCallback) { - that.modalIsVisible = true; - that.toolboxCategories = toolboxCategories; - that.onSelectBlockCallback = onSelectBlockCallback; - that.onDismissCallback = onDismissCallback; - - // The indexes of the buttons corresponding to the first block in - // each category, as well as the 'cancel' button at the end. - that.firstBlockIndexes = []; - that.activeButtonIndex = -1; - that.totalNumBlocks = 0; - - var cumulativeIndex = 0; - that.toolboxCategories.forEach(function(category) { - that.firstBlockIndexes.push(cumulativeIndex); - cumulativeIndex += category.blocks.length; - }); - that.firstBlockIndexes.push(cumulativeIndex); - that.totalNumBlocks = cumulativeIndex; - - Blockly.CommonModal.setupKeyboardOverrides(that); - that.keyboardInputService.addOverride('13', function(evt) { - evt.preventDefault(); - evt.stopPropagation(); - - if (that.activeButtonIndex == -1) { - return; - } - - var button = document.getElementById( - that.getOptionId(that.activeButtonIndex)); - - for (var i = 0; i < that.toolboxCategories.length; i++) { - if (that.firstBlockIndexes[i + 1] > that.activeButtonIndex) { - var categoryIndex = i; - var blockIndex = - that.activeButtonIndex - that.firstBlockIndexes[i]; - var block = that.getBlock(categoryIndex, blockIndex); - that.selectBlock(block); - return; - } - } - - // The 'Cancel' button has been pressed. - that.dismissModal(); - }); - - setTimeout(function() { - document.getElementById('toolboxModal').focus(); - }, 150); - } - ); - } - ], - // Closes the modal (on both success and failure). - hideModal_: Blockly.CommonModal.hideModal, - // Focuses on the button represented by the given index. - focusOnOption: function(index) { - var button = document.getElementById(this.getOptionId(index)); - button.focus(); - }, - // Counts the number of interactive elements for the modal. - numInteractiveElements: function() { - return this.totalNumBlocks + 1; - }, - getOverallIndex: function(categoryIndex, blockIndex) { - return this.firstBlockIndexes[categoryIndex] + blockIndex; - }, - getBlock: function(categoryIndex, blockIndex) { - return this.toolboxCategories[categoryIndex].blocks[blockIndex]; - }, - getBlockDescription: function(block) { - return this.utilsService.getBlockDescription(block); - }, - // Returns the ID for the corresponding option button. - getOptionId: function(index) { - return 'toolbox-modal-option-' + index; - }, - // Returns the ID for the "cancel" option button. - getCancelOptionId: function() { - return 'toolbox-modal-option-' + this.totalNumBlocks; - }, - selectBlock: function(block) { - this.onSelectBlockCallback(block); - this.hideModal_(); - }, - // Dismisses and closes the modal. - dismissModal: function() { - this.hideModal_(); - this.onDismissCallback(); - } -}); diff --git a/accessible/toolbox-modal.service.js b/accessible/toolbox-modal.service.js deleted file mode 100644 index fa860d58dcb..00000000000 --- a/accessible/toolbox-modal.service.js +++ /dev/null @@ -1,234 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Service for the toolbox modal. - * - * @author sll@google.com (Sean Lip) - */ - -goog.provide('blocklyApp.ToolboxModalService'); - -goog.require('blocklyApp.UtilsService'); - -goog.require('blocklyApp.BlockConnectionService'); -goog.require('blocklyApp.NotificationsService'); -goog.require('blocklyApp.TreeService'); - - -blocklyApp.ToolboxModalService = ng.core.Class({ - constructor: [ - blocklyApp.BlockConnectionService, - blocklyApp.NotificationsService, - blocklyApp.TreeService, - blocklyApp.UtilsService, - function( - blockConnectionService, notificationsService, treeService, - utilsService) { - this.blockConnectionService = blockConnectionService; - this.notificationsService = notificationsService; - this.treeService = treeService; - this.utilsService = utilsService; - - this.modalIsShown = false; - - this.selectedToolboxCategories = null; - this.onSelectBlockCallback = null; - this.onDismissCallback = null; - this.hasVariableCategory = null; - // The aim of the pre-show hook is to populate the modal component with - // the information it needs to display the modal (e.g., which categories - // and blocks to display). - this.preShowHook = function() { - throw Error( - 'A pre-show hook must be defined for the toolbox modal before it ' + - 'can be shown.'); - }; - } - ], - populateToolbox_: function() { - // Populate the toolbox categories. - this.allToolboxCategories = []; - var toolboxXmlElt = document.getElementById('blockly-toolbox-xml'); - var toolboxCategoryElts = toolboxXmlElt.getElementsByTagName('category'); - if (toolboxCategoryElts.length) { - this.allToolboxCategories = Array.from(toolboxCategoryElts).map( - function(categoryElt) { - var tmpWorkspace = new Blockly.Workspace(); - var custom = categoryElt.attributes.custom - // TODO (corydiers): Implement custom flyouts once #1153 is solved. - if (custom && custom.value == Blockly.VARIABLE_CATEGORY_NAME) { - var varBlocks = - Blockly.Variables.flyoutCategoryBlocks(blocklyApp.workspace); - varBlocks.forEach(function(block) { - Blockly.Xml.domToBlock(block, tmpWorkspace); - }); - } else { - Blockly.Xml.domToWorkspace(categoryElt, tmpWorkspace); - } - return { - categoryName: categoryElt.attributes.name.value, - blocks: tmpWorkspace.topBlocks_ - }; - } - ); - this.computeCategoriesForCreateNewGroupModal_(); - } else { - // A timeout seems to be needed in order for the .children accessor to - // work correctly. - var that = this; - setTimeout(function() { - // If there are no top-level categories, we create a single category - // containing all the top-level blocks. - var tmpWorkspace = new Blockly.Workspace(); - Array.from(toolboxXmlElt.children).forEach(function(topLevelNode) { - Blockly.Xml.domToBlock(tmpWorkspace, topLevelNode); - }); - - that.allToolboxCategories = [{ - categoryName: '', - blocks: tmpWorkspace.topBlocks_ - }]; - - that.computeCategoriesForCreateNewGroupModal_(); - }); - } - }, - computeCategoriesForCreateNewGroupModal_: function() { - // Precompute toolbox categories for blocks that have no output - // connection (and that can therefore be used as the base block of a - // "create new block group" action). - this.toolboxCategoriesForNewGroup = []; - var that = this; - this.allToolboxCategories.forEach(function(toolboxCategory) { - var baseBlocks = toolboxCategory.blocks.filter(function(block) { - return !block.outputConnection; - }); - - if (baseBlocks.length > 0) { - that.toolboxCategoriesForNewGroup.push({ - categoryName: toolboxCategory.categoryName, - blocks: baseBlocks - }); - } - }); - }, - registerPreShowHook: function(preShowHook) { - var that = this; - this.preShowHook = function() { - preShowHook( - that.selectedToolboxCategories, that.onSelectBlockCallback, - that.onDismissCallback); - }; - }, - isModalShown: function() { - return this.modalIsShown; - }, - toolboxHasVariableCategory: function() { - if (this.hasVariableCategory === null) { - var toolboxXmlElt = document.getElementById('blockly-toolbox-xml'); - var toolboxCategoryElts = toolboxXmlElt.getElementsByTagName('category'); - var that = this; - Array.from(toolboxCategoryElts).forEach( - function(categoryElt) { - var custom = categoryElt.attributes.custom; - if (custom && custom.value == Blockly.VARIABLE_CATEGORY_NAME) { - that.hasVariableCategory = true; - } - }); - - if (this.hasVariableCategory === null) { - this.hasVariableCategory = false; - } - } - - return this.hasVariableCategory; - }, - showModal_: function( - selectedToolboxCategories, onSelectBlockCallback, onDismissCallback) { - this.selectedToolboxCategories = selectedToolboxCategories; - this.onSelectBlockCallback = onSelectBlockCallback; - this.onDismissCallback = onDismissCallback; - - this.preShowHook(); - this.modalIsShown = true; - }, - hideModal: function() { - this.modalIsShown = false; - }, - showToolboxModalForAttachToMarkedConnection: function(sourceButtonId) { - var that = this; - - var selectedToolboxCategories = []; - this.populateToolbox_(); - this.allToolboxCategories.forEach(function(toolboxCategory) { - var selectedBlocks = toolboxCategory.blocks.filter(function(block) { - return that.blockConnectionService.canBeAttachedToMarkedConnection( - block); - }); - - if (selectedBlocks.length > 0) { - selectedToolboxCategories.push({ - categoryName: toolboxCategory.categoryName, - blocks: selectedBlocks - }); - } - }); - - this.showModal_(selectedToolboxCategories, function(block) { - var blockDescription = that.utilsService.getBlockDescription(block); - - // Clear the active desc for the destination tree, so that it can be - // cleanly reinstated after the new block is attached. - var destinationTreeId = that.treeService.getTreeIdForBlock( - that.blockConnectionService.getMarkedConnectionSourceBlock().id); - that.treeService.clearActiveDesc(destinationTreeId); - var newBlockId = that.blockConnectionService.attachToMarkedConnection( - block); - - // Invoke a digest cycle, so that the DOM settles. - setTimeout(function() { - that.treeService.focusOnBlock(newBlockId); - that.notificationsService.speak( - 'Attached. Now on, ' + blockDescription + ', block in workspace.'); - }); - }, function() { - document.getElementById(sourceButtonId).focus(); - }); - }, - showToolboxModalForCreateNewGroup: function(sourceButtonId) { - var that = this; - this.populateToolbox_(); - this.showModal_(this.toolboxCategoriesForNewGroup, function(block) { - var blockDescription = that.utilsService.getBlockDescription(block); - var xml = Blockly.Xml.blockToDom(block); - var newBlockId = Blockly.Xml.domToBlock(blocklyApp.workspace, xml).id; - - // Invoke a digest cycle, so that the DOM settles. - setTimeout(function() { - that.treeService.focusOnBlock(newBlockId); - that.notificationsService.speak( - 'Created new group in workspace. Now on, ' + blockDescription + - ', block in workspace.'); - }); - }, function() { - document.getElementById(sourceButtonId).focus(); - }); - } -}); diff --git a/accessible/translate.pipe.js b/accessible/translate.pipe.js deleted file mode 100644 index fef1940cd1f..00000000000 --- a/accessible/translate.pipe.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Pipe for internationalizing Blockly message strings. - * @author sll@google.com (Sean Lip) - */ - -goog.provide('blocklyApp.TranslatePipe'); - - -blocklyApp.TranslatePipe = ng.core.Pipe({ - name: 'translate' -}) -.Class({ - constructor: function() {}, - transform: function(messageId) { - return Blockly.Msg[messageId]; - } -}); diff --git a/accessible/tree.service.js b/accessible/tree.service.js deleted file mode 100644 index c5a73ac4c4a..00000000000 --- a/accessible/tree.service.js +++ /dev/null @@ -1,609 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Service that handles keyboard navigation on workspace - * block groups (internally represented as trees). This is a singleton service - * for the entire application. - * - * @author madeeha@google.com (Madeeha Ghori) - */ - -goog.provide('blocklyApp.TreeService'); - -goog.require('blocklyApp.UtilsService'); - -goog.require('blocklyApp.AudioService'); -goog.require('blocklyApp.BlockConnectionService'); -goog.require('blocklyApp.BlockOptionsModalService'); -goog.require('blocklyApp.NotificationsService'); -goog.require('blocklyApp.VariableModalService'); - - -blocklyApp.TreeService = ng.core.Class({ - constructor: [ - blocklyApp.AudioService, - blocklyApp.BlockConnectionService, - blocklyApp.BlockOptionsModalService, - blocklyApp.NotificationsService, - blocklyApp.UtilsService, - blocklyApp.VariableModalService, - function( - audioService, blockConnectionService, blockOptionsModalService, - notificationsService, utilsService, variableModalService) { - this.audioService = audioService; - this.blockConnectionService = blockConnectionService; - this.blockOptionsModalService = blockOptionsModalService; - this.notificationsService = notificationsService; - this.utilsService = utilsService; - this.variableModalService = variableModalService; - - // The suffix used for all IDs of block root elements. - this.BLOCK_ROOT_ID_SUFFIX_ = blocklyApp.BLOCK_ROOT_ID_SUFFIX; - // Maps tree IDs to the IDs of their active descendants. - this.activeDescendantIds_ = {}; - // Array containing all the sidebar button elements. - this.sidebarButtonElements_ = Array.from( - document.querySelectorAll('button.blocklySidebarButton')); - } - ], - scrollToElement_: function(elementId) { - var element = document.getElementById(elementId); - var documentElement = document.body || document.documentElement; - if (element.offsetTop < documentElement.scrollTop || - element.offsetTop > documentElement.scrollTop + window.innerHeight) { - window.scrollTo(0, element.offsetTop - 10); - } - }, - - isLi_: function(node) { - return node.tagName == 'LI'; - }, - getParentLi_: function(element) { - var nextNode = element.parentNode; - while (nextNode && !this.isLi_(nextNode)) { - nextNode = nextNode.parentNode; - } - return nextNode; - }, - getFirstChildLi_: function(element) { - var childList = element.children; - for (var i = 0; i < childList.length; i++) { - if (this.isLi_(childList[i])) { - return childList[i]; - } else { - var potentialElement = this.getFirstChildLi_(childList[i]); - if (potentialElement) { - return potentialElement; - } - } - } - return null; - }, - getLastChildLi_: function(element) { - var childList = element.children; - for (var i = childList.length - 1; i >= 0; i--) { - if (this.isLi_(childList[i])) { - return childList[i]; - } else { - var potentialElement = this.getLastChildLi_(childList[i]); - if (potentialElement) { - return potentialElement; - } - } - } - return null; - }, - getInitialSiblingLi_: function(element) { - while (true) { - var previousSibling = this.getPreviousSiblingLi_(element); - if (previousSibling && previousSibling.id != element.id) { - element = previousSibling; - } else { - return element; - } - } - }, - getPreviousSiblingLi_: function(element) { - if (element.previousElementSibling) { - var sibling = element.previousElementSibling; - return this.isLi_(sibling) ? sibling : this.getLastChildLi_(sibling); - } else { - var parent = element.parentNode; - while (parent && parent.tagName != 'OL') { - if (parent.previousElementSibling) { - var node = parent.previousElementSibling; - return this.isLi_(node) ? node : this.getLastChildLi_(node); - } else { - parent = parent.parentNode; - } - } - return null; - } - }, - getNextSiblingLi_: function(element) { - if (element.nextElementSibling) { - var sibling = element.nextElementSibling; - return this.isLi_(sibling) ? sibling : this.getFirstChildLi_(sibling); - } else { - var parent = element.parentNode; - while (parent && parent.tagName != 'OL') { - if (parent.nextElementSibling) { - var node = parent.nextElementSibling; - return this.isLi_(node) ? node : this.getFirstChildLi_(node); - } else { - parent = parent.parentNode; - } - } - return null; - } - }, - getFinalSiblingLi_: function(element) { - while (true) { - var nextSibling = this.getNextSiblingLi_(element); - if (nextSibling && nextSibling.id != element.id) { - element = nextSibling; - } else { - return element; - } - } - }, - - // Returns a list of all focus targets in the workspace, including the - // "Create new group" button that appears when no blocks are present. - getWorkspaceFocusTargets_: function() { - return Array.from( - document.querySelectorAll('.blocklyWorkspaceFocusTarget')); - }, - getAllFocusTargets_: function() { - return this.getWorkspaceFocusTargets_().concat(this.sidebarButtonElements_); - }, - getNextFocusTargetId_: function(treeId) { - var trees = this.getAllFocusTargets_(); - for (var i = 0; i < trees.length - 1; i++) { - if (trees[i].id == treeId) { - return trees[i + 1].id; - } - } - return null; - }, - getPreviousFocusTargetId_: function(treeId) { - var trees = this.getAllFocusTargets_(); - for (var i = trees.length - 1; i > 0; i--) { - if (trees[i].id == treeId) { - return trees[i - 1].id; - } - } - return null; - }, - - getActiveDescId: function(treeId) { - return this.activeDescendantIds_[treeId] || ''; - }, - // Set the active desc for this tree to its first child. - initActiveDesc: function(treeId) { - var tree = document.getElementById(treeId); - this.setActiveDesc(this.getFirstChildLi_(tree).id, treeId); - }, - // Make a given element the active descendant of a given tree. - setActiveDesc: function(newActiveDescId, treeId) { - if (this.getActiveDescId(treeId)) { - this.clearActiveDesc(treeId); - } - document.getElementById(newActiveDescId).classList.add( - 'blocklyActiveDescendant'); - this.activeDescendantIds_[treeId] = newActiveDescId; - - // Scroll the new active desc into view, if needed. This has no effect - // for blind users, but is helpful for sighted onlookers. - this.scrollToElement_(newActiveDescId); - }, - // This clears the active descendant of the given tree. It is used just - // before the tree is deleted. - clearActiveDesc: function(treeId) { - var activeDesc = document.getElementById(this.getActiveDescId(treeId)); - if (activeDesc) { - activeDesc.classList.remove('blocklyActiveDescendant'); - } - - if (this.activeDescendantIds_[treeId]) { - delete this.activeDescendantIds_[treeId]; - } - }, - clearAllActiveDescs: function() { - for (var treeId in this.activeDescendantIds_) { - var activeDesc = document.getElementById(this.getActiveDescId(treeId)); - if (activeDesc) { - activeDesc.classList.remove('blocklyActiveDescendant'); - } - } - - this.activeDescendantIds_ = {}; - }, - - isTreeRoot_: function(element) { - return element.classList.contains('blocklyTree'); - }, - getBlockRootId_: function(blockId) { - return blockId + this.BLOCK_ROOT_ID_SUFFIX_; - }, - // Return the 'lowest' Blockly block in the DOM tree that contains the given - // DOM element. - getContainingBlock_: function(domElement) { - var potentialBlockRoot = domElement; - while (potentialBlockRoot.id.indexOf(this.BLOCK_ROOT_ID_SUFFIX_) === -1) { - potentialBlockRoot = potentialBlockRoot.parentNode; - } - - var blockRootId = potentialBlockRoot.id; - var blockId = blockRootId.substring( - 0, blockRootId.length - this.BLOCK_ROOT_ID_SUFFIX_.length); - return blocklyApp.workspace.getBlockById(blockId); - }, - isTopLevelBlock_: function(block) { - return !block.getParent(); - }, - // Returns whether the given block is at the top level, and has no siblings. - isIsolatedTopLevelBlock_: function(block) { - var blockHasNoSiblings = ( - (!block.nextConnection || - !block.nextConnection.targetConnection) && - (!block.previousConnection || - !block.previousConnection.targetConnection)); - return this.isTopLevelBlock_(block) && blockHasNoSiblings; - }, - safelyRemoveBlock_: function(block, deleteBlockFunc, areNextBlocksRemoved) { - // Runs the given deleteBlockFunc (which should have the effect of deleting - // the given block, and possibly others after it if `areNextBlocksRemoved` - // is true) and then does one of two things: - // - If the deleted block was an isolated top-level block, or it is a top- - // level block and the next blocks are going to be removed, this means - // the current tree has no more blocks after the deletion. So, pick a new - // tree to focus on. - // - Otherwise, set the correct new active desc for the current tree. - var treeId = this.getTreeIdForBlock(block.id); - - var treeCeasesToExist = areNextBlocksRemoved ? - this.isTopLevelBlock_(block) : this.isIsolatedTopLevelBlock_(block); - - if (treeCeasesToExist) { - // Find the node to focus on after the deletion happens. - var nextElementToFocusOn = null; - var focusTargets = this.getWorkspaceFocusTargets_(); - for (var i = 0; i < focusTargets.length; i++) { - if (focusTargets[i].id == treeId) { - if (i + 1 < focusTargets.length) { - nextElementToFocusOn = focusTargets[i + 1]; - } else if (i > 0) { - nextElementToFocusOn = focusTargets[i - 1]; - } - break; - } - } - - this.clearActiveDesc(treeId); - deleteBlockFunc(); - // Invoke a digest cycle, so that the DOM settles (and the "Create new - // group" button in the workspace shows up, if applicable). - setTimeout(function() { - if (nextElementToFocusOn) { - nextElementToFocusOn.focus(); - } else { - document.getElementById( - blocklyApp.ID_FOR_EMPTY_WORKSPACE_BTN).focus(); - } - }); - } else { - var blockRootId = this.getBlockRootId_(block.id); - var blockRootElement = document.getElementById(blockRootId); - - // Find the new active desc for the current tree by trying the following - // possibilities in order: the parent, the next sibling, and the previous - // sibling. (If `areNextBlocksRemoved` is true, the next sibling would be - // moved together with the moved block, so we don't check it.) - if (areNextBlocksRemoved) { - var newActiveDesc = - this.getParentLi_(blockRootElement) || - this.getPreviousSiblingLi_(blockRootElement); - } else { - var newActiveDesc = - this.getParentLi_(blockRootElement) || - this.getNextSiblingLi_(blockRootElement) || - this.getPreviousSiblingLi_(blockRootElement); - } - - this.clearActiveDesc(treeId); - deleteBlockFunc(); - // Invoke a digest cycle, so that the DOM settles. - var that = this; - setTimeout(function() { - that.setActiveDesc(newActiveDesc.id, treeId); - document.getElementById(treeId).focus(); - }); - } - }, - getTreeIdForBlock: function(blockId) { - // Walk up the DOM until we get to the root element of the tree. - var potentialRoot = document.getElementById(this.getBlockRootId_(blockId)); - while (!this.isTreeRoot_(potentialRoot)) { - potentialRoot = potentialRoot.parentNode; - } - return potentialRoot.id; - }, - // Set focus to the tree containing the given block, and set the tree's - // active desc to the root element of the given block. - focusOnBlock: function(blockId) { - // Invoke a digest cycle, in order to allow the ID of the newly-created - // tree to be set in the DOM. - var that = this; - setTimeout(function() { - var treeId = that.getTreeIdForBlock(blockId); - document.getElementById(treeId).focus(); - that.setActiveDesc(that.getBlockRootId_(blockId), treeId); - }); - }, - showBlockOptionsModal: function(block) { - var that = this; - var actionButtonsInfo = []; - - if (block.previousConnection) { - actionButtonsInfo.push({ - action: function() { - that.blockConnectionService.markConnection(block.previousConnection); - that.focusOnBlock(block.id); - }, - translationIdForText: 'MARK_SPOT_BEFORE' - }); - } - - if (block.nextConnection) { - actionButtonsInfo.push({ - action: function() { - that.blockConnectionService.markConnection(block.nextConnection); - that.focusOnBlock(block.id); - }, - translationIdForText: 'MARK_SPOT_AFTER' - }); - } - - if (this.blockConnectionService.canBeMovedToMarkedConnection(block)) { - actionButtonsInfo.push({ - action: function() { - var blockDescription = that.utilsService.getBlockDescription(block); - var oldDestinationTreeId = that.getTreeIdForBlock( - that.blockConnectionService.getMarkedConnectionSourceBlock().id); - that.clearActiveDesc(oldDestinationTreeId); - - var newBlockId = that.blockConnectionService.attachToMarkedConnection( - block); - that.safelyRemoveBlock_(block, function() { - block.dispose(false); - }, true); - - // Invoke a digest cycle, so that the DOM settles. - setTimeout(function() { - that.focusOnBlock(newBlockId); - var newDestinationTreeId = that.getTreeIdForBlock(newBlockId); - - if (newDestinationTreeId != oldDestinationTreeId) { - // The tree ID for a moved block does not seem to behave - // predictably. E.g. start with two separate groups of one block - // each, add a link before the block in the second group, and - // move the block in the first group to that link. The tree ID of - // the resulting group ends up being the tree ID for the group - // that was originally first, not second as might be expected. - // Here, we double-check to ensure that all affected trees have - // an active desc set. - if (document.getElementById(oldDestinationTreeId)) { - var activeDescId = that.getActiveDescId(oldDestinationTreeId); - var activeDescTreeId = null; - if (activeDescId) { - var oldDestinationBlock = that.getContainingBlock_( - document.getElementById(activeDescId)); - activeDescTreeId = that.getTreeIdForBlock( - oldDestinationBlock); - if (activeDescTreeId != oldDestinationTreeId) { - that.clearActiveDesc(oldDestinationTreeId); - } - } - that.initActiveDesc(oldDestinationTreeId); - } - } - - that.notificationsService.speak( - blockDescription + ' ' + - Blockly.Msg.ATTACHED_BLOCK_TO_LINK_MSG + - '. Now on attached block in workspace.'); - }); - }, - translationIdForText: 'MOVE_TO_MARKED_SPOT' - }); - } - - actionButtonsInfo.push({ - action: function() { - var blockDescription = that.utilsService.getBlockDescription(block); - - that.safelyRemoveBlock_(block, function() { - block.dispose(true); - that.audioService.playDeleteSound(); - }, false); - - setTimeout(function() { - var message = blockDescription + ' deleted. ' + ( - that.utilsService.isWorkspaceEmpty() ? - 'Workspace is empty.' : 'Now on workspace.'); - that.notificationsService.speak(message); - }); - }, - translationIdForText: 'DELETE' - }); - - this.blockOptionsModalService.showModal(actionButtonsInfo, function() { - that.focusOnBlock(block.id); - }); - }, - - moveUpOneLevel_: function(treeId) { - var activeDesc = document.getElementById(this.getActiveDescId(treeId)); - var nextNode = this.getParentLi_(activeDesc); - if (nextNode) { - this.setActiveDesc(nextNode.id, treeId); - } else { - this.audioService.playOopsSound(); - } - }, - onKeypress: function(e, tree) { - // TODO(sll): Instead of this, have a common ActiveContextService which - // returns true if at least one modal is shown, and false otherwise. - if (this.blockOptionsModalService.isModalShown() || - this.variableModalService.isModalShown()) { - return; - } - - var treeId = tree.id; - var activeDesc = document.getElementById(this.getActiveDescId(treeId)); - if (!activeDesc) { - // The underlying Blockly instance may have decided blocks needed to - // be deleted. This is not necessarily an error, but needs to be repaired. - this.initActiveDesc(treeId); - activeDesc = document.getElementById(this.getActiveDescId(treeId)); - } - - if (e.altKey || e.ctrlKey) { - // Do not intercept combinations such as Alt+Home. - return; - } - - if (document.activeElement.tagName == 'INPUT' || - document.activeElement.tagName == 'SELECT') { - // For input fields, Esc, Enter, and Tab keystrokes are handled specially. - if (e.keyCode == 9 || e.keyCode == 13 || e.keyCode == 27) { - // Return the focus to the workspace tree containing the input field. - document.getElementById(treeId).focus(); - - // Note that Tab and Enter events stop propagating, this behavior is - // handled on other listeners. - if (e.keyCode == 27 || e.keyCode == 13) { - e.preventDefault(); - e.stopPropagation(); - } - } - } else { - // Outside an input field, Enter, Tab, Esc and navigation keys are all - // recognized. - if (e.keyCode == 13) { - // Enter key. The user wants to interact with a button, interact with - // an input field, or open the block options modal. - // Algorithm to find the field: do a DFS through the children until - // we find an INPUT, BUTTON or SELECT element (in which case we use it). - // Truncate the search at child LI elements. - e.stopPropagation(); - - var found = false; - var dfsStack = Array.from(activeDesc.children); - while (dfsStack.length) { - var currentNode = dfsStack.shift(); - if (currentNode.tagName == 'BUTTON') { - currentNode.click(); - found = true; - break; - } else if (currentNode.tagName == 'INPUT') { - currentNode.focus(); - currentNode.select(); - this.notificationsService.speak( - 'Type a value, then press Escape to exit'); - found = true; - break; - } else if (currentNode.tagName == 'SELECT') { - currentNode.focus(); - found = true; - return; - } else if (currentNode.tagName == 'LI') { - continue; - } - - if (currentNode.children) { - var reversedChildren = Array.from(currentNode.children).reverse(); - reversedChildren.forEach(function(childNode) { - dfsStack.unshift(childNode); - }); - } - } - - // If we cannot find a field to interact with, we open the modal for - // the current block instead. - if (!found) { - var block = this.getContainingBlock_(activeDesc); - this.showBlockOptionsModal(block); - } - } else if (e.keyCode == 9) { - // Tab key. The event is allowed to propagate through. - } else if ([27, 35, 36, 37, 38, 39, 40].indexOf(e.keyCode) !== -1) { - if (e.keyCode == 27 || e.keyCode == 37) { - // Esc or left arrow key. Go up a level, if possible. - this.moveUpOneLevel_(treeId); - } else if (e.keyCode == 35) { - // End key. Go to the last sibling in the subtree. - var potentialFinalSibling = this.getFinalSiblingLi_(activeDesc); - if (potentialFinalSibling) { - this.setActiveDesc(potentialFinalSibling.id, treeId); - } - } else if (e.keyCode == 36) { - // Home key. Go to the first sibling in the subtree. - var potentialInitialSibling = this.getInitialSiblingLi_(activeDesc); - if (potentialInitialSibling) { - this.setActiveDesc(potentialInitialSibling.id, treeId); - } - } else if (e.keyCode == 38) { - // Up arrow key. Go to the previous sibling, if possible. - var potentialPrevSibling = this.getPreviousSiblingLi_(activeDesc); - if (potentialPrevSibling) { - this.setActiveDesc(potentialPrevSibling.id, treeId); - } else { - var statusMessage = 'Reached top of list.'; - if (this.getParentLi_(activeDesc)) { - statusMessage += ' Press left to go to parent list.'; - } - this.audioService.playOopsSound(statusMessage); - } - } else if (e.keyCode == 39) { - // Right arrow key. Go down a level, if possible. - var potentialFirstChild = this.getFirstChildLi_(activeDesc); - if (potentialFirstChild) { - this.setActiveDesc(potentialFirstChild.id, treeId); - } else { - this.audioService.playOopsSound(); - } - } else if (e.keyCode == 40) { - // Down arrow key. Go to the next sibling, if possible. - var potentialNextSibling = this.getNextSiblingLi_(activeDesc); - if (potentialNextSibling) { - this.setActiveDesc(potentialNextSibling.id, treeId); - } else { - this.audioService.playOopsSound('Reached bottom of list.'); - } - } - - e.preventDefault(); - e.stopPropagation(); - } - } - } -}); diff --git a/accessible/utils.service.js b/accessible/utils.service.js deleted file mode 100644 index d78472dc8e1..00000000000 --- a/accessible/utils.service.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 utility service for multiple components. This is a - * singleton service that is used for the entire application. In general, it - * should only be used as a stateless adapter for native Blockly functions. - * - * @author madeeha@google.com (Madeeha Ghori) - */ - -goog.provide('blocklyApp.UtilsService'); - - -blocklyApp.ID_FOR_EMPTY_WORKSPACE_BTN = 'blocklyEmptyWorkspaceBtn'; -blocklyApp.BLOCK_ROOT_ID_SUFFIX = '-blockRoot'; - -blocklyApp.UtilsService = ng.core.Class({ - constructor: [function() {}], - getBlockDescription: function(block) { - // We use 'BLANK' instead of the default '?' so that the string is read - // out. (By default, screen readers tend to ignore punctuation.) - return block.toString(undefined, 'BLANK'); - }, - isWorkspaceEmpty: function() { - return !blocklyApp.workspace.topBlocks_.length; - } -}); diff --git a/accessible/variable-add-modal.component.js b/accessible/variable-add-modal.component.js deleted file mode 100644 index 1965e4e5e91..00000000000 --- a/accessible/variable-add-modal.component.js +++ /dev/null @@ -1,118 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Component representing the variable rename modal. - * - * @author corydiers@google.com (Cory Diers) - */ - -goog.provide('blocklyApp.VariableAddModalComponent'); - -goog.require('blocklyApp.AudioService'); -goog.require('blocklyApp.KeyboardInputService'); -goog.require('blocklyApp.TranslatePipe'); -goog.require('blocklyApp.VariableModalService'); - -goog.require('Blockly.CommonModal'); - - -blocklyApp.VariableAddModalComponent = ng.core.Component({ - selector: 'blockly-add-variable-modal', - template: ` -
- -
-

Add a variable...

- - -

New Variable Name: - -

-
- - - -
-
- `, - pipes: [blocklyApp.TranslatePipe] -}) -.Class({ - constructor: [ - blocklyApp.AudioService, blocklyApp.KeyboardInputService, blocklyApp.VariableModalService, - function(audioService, keyboardService, variableService) { - this.workspace = blocklyApp.workspace; - this.variableModalService = variableService; - this.audioService = audioService; - this.keyboardInputService = keyboardService - this.modalIsVisible = false; - this.activeButtonIndex = -1; - - var that = this; - this.variableModalService.registerPreAddShowHook( - function() { - that.modalIsVisible = true; - - Blockly.CommonModal.setupKeyboardOverrides(that); - - setTimeout(function() { - document.getElementById('varModal').focus(); - }, 150); - } - ); - } - ], - // Caches the current text variable as the user types. - setTextValue: function(newValue) { - this.variableName = newValue; - }, - // Closes the modal (on both success and failure). - hideModal_: Blockly.CommonModal.hideModal, - // Focuses on the button represented by the given index. - focusOnOption: Blockly.CommonModal.focusOnOption, - // Counts the number of interactive elements for the modal. - numInteractiveElements: Blockly.CommonModal.numInteractiveElements, - // Gets all the interactive elements for the modal. - getInteractiveElements: Blockly.CommonModal.getInteractiveElements, - // Gets the container with interactive elements. - getInteractiveContainer: function() { - return document.getElementById("varForm"); - }, - // Submits the name change for the variable. - submit: function() { - this.workspace.createVariable(this.variableName); - this.dismissModal(); - }, - // Dismisses and closes the modal. - dismissModal: function() { - this.variableModalService.hideModal(); - this.hideModal_(); - } -}) diff --git a/accessible/variable-modal.service.js b/accessible/variable-modal.service.js deleted file mode 100644 index df5555fae4f..00000000000 --- a/accessible/variable-modal.service.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Service for the variable modal. - * - * @author corydiers@google.com (Cory Diers) - */ - -goog.provide('blocklyApp.VariableModalService'); - -blocklyApp.VariableModalService = ng.core.Class({ - constructor: [ - function() { - this.modalIsShown = false; - } - ], - // Registers a hook to be called before the add modal is shown. - registerPreAddShowHook: function(preShowHook) { - this.preAddShowHook = function() { - preShowHook(); - }; - }, - // Registers a hook to be called before the rename modal is shown. - registerPreRenameShowHook: function(preShowHook) { - this.preRenameShowHook = function(oldName) { - preShowHook(oldName); - }; - }, - // Registers a hook to be called before the remove modal is shown. - registerPreRemoveShowHook: function(preShowHook) { - this.preRemoveShowHook = function(oldName, count) { - preShowHook(oldName, count); - }; - }, - // Returns true if the variable modal is shown. - isModalShown: function() { - return this.modalIsShown; - }, - // Show the add variable modal. - showAddModal_: function() { - this.preAddShowHook(); - this.modalIsShown = true; - }, - // Show the rename variable modal. - showRenameModal_: function(oldName) { - this.preRenameShowHook(oldName); - this.modalIsShown = true; - }, - // Show the remove variable modal. - showRemoveModal_: function(oldName) { - var count = this.getNumVariables(oldName); - this.modalIsShown = true; - if (count > 1) { - this.preRemoveShowHook(oldName, count); - } else { - var variable = blocklyApp.workspace.getVariable(oldName); - blocklyApp.workspace.deleteVariableInternal_(variable); - // Allow the execution loop to finish before "closing" the modal. While - // the modal never opens, its being "open" should prevent other keypresses - // anyway. - var that = this; - setTimeout(function() { - that.modalIsShown = false; - }); - } - }, - getNumVariables: function(oldName) { - return blocklyApp.workspace.getVariableUses(oldName).length; - }, - // Hide the variable modal. - hideModal: function() { - this.modalIsShown = false; - } -}); diff --git a/accessible/variable-remove-modal.component.js b/accessible/variable-remove-modal.component.js deleted file mode 100644 index b542e88a5fb..00000000000 --- a/accessible/variable-remove-modal.component.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Component representing the variable remove modal. - * - * @author corydiers@google.com (Cory Diers) - */ - -goog.provide('blocklyApp.VariableRemoveModalComponent'); - -goog.require('blocklyApp.AudioService'); -goog.require('blocklyApp.KeyboardInputService'); -goog.require('blocklyApp.TranslatePipe'); -goog.require('blocklyApp.TreeService'); -goog.require('blocklyApp.VariableModalService'); - -goog.require('Blockly.CommonModal'); - - -blocklyApp.VariableRemoveModalComponent = ng.core.Component({ - selector: 'blockly-remove-variable-modal', - template: ` -
- -
-

- Delete {{getNumVariables()}} uses of the "{{currentVariableName}}" - variable? -

- -
-
- - -
-
-
- `, - pipes: [blocklyApp.TranslatePipe] -}) -.Class({ - constructor: [ - blocklyApp.AudioService, - blocklyApp.KeyboardInputService, - blocklyApp.TreeService, - blocklyApp.VariableModalService, - function(audioService, keyboardService, treeService, variableService) { - this.workspace = blocklyApp.workspace; - this.treeService = treeService; - this.variableModalService = variableService; - this.audioService = audioService; - this.keyboardInputService = keyboardService - this.modalIsVisible = false; - this.activeButtonIndex = -1; - this.currentVariableName = ""; - this.count = 0; - - var that = this; - this.variableModalService.registerPreRemoveShowHook( - function(name, count) { - that.currentVariableName = name; - that.count = count - that.modalIsVisible = true; - - Blockly.CommonModal.setupKeyboardOverrides(that); - - setTimeout(function() { - document.getElementById('varModal').focus(); - }, 150); - } - ); - } - ], - // Closes the modal (on both success and failure). - hideModal_: Blockly.CommonModal.hideModal, - // Focuses on the button represented by the given index. - focusOnOption: Blockly.CommonModal.focusOnOption, - // Counts the number of interactive elements for the modal. - numInteractiveElements: Blockly.CommonModal.numInteractiveElements, - // Gets all the interactive elements for the modal. - getInteractiveElements: Blockly.CommonModal.getInteractiveElements, - // Gets the container with interactive elements. - getInteractiveContainer: function() { - return document.getElementById("varForm"); - }, - getNumVariables: function() { - return this.variableModalService.getNumVariables(this.currentVariableName); - }, - // Submits the name change for the variable. - submit: function() { - var variable = blocklyApp.workspace.getVariable(this.currentVariableName); - blocklyApp.workspace.deleteVariableInternal_(variable); - this.dismissModal(); - }, - // Dismisses and closes the modal. - dismissModal: function() { - this.variableModalService.hideModal(); - this.hideModal_(); - } -}) diff --git a/accessible/variable-rename-modal.component.js b/accessible/variable-rename-modal.component.js deleted file mode 100644 index e276e739cef..00000000000 --- a/accessible/variable-rename-modal.component.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Component representing the variable rename modal. - * - * @author corydiers@google.com (Cory Diers) - */ - -goog.provide('blocklyApp.VariableRenameModalComponent'); - -goog.require('Blockly.CommonModal'); -goog.require('blocklyApp.AudioService'); -goog.require('blocklyApp.KeyboardInputService'); -goog.require('blocklyApp.TranslatePipe'); -goog.require('blocklyApp.VariableModalService'); - - -blocklyApp.VariableRenameModalComponent = ng.core.Component({ - selector: 'blockly-rename-variable-modal', - template: ` -
- -
-

- Rename the "{{currentVariableName}}" variable... -

- -
-

New Variable Name: - -

-
- - -
-
-
- `, - pipes: [blocklyApp.TranslatePipe] -}) -.Class({ - constructor: [ - blocklyApp.AudioService, blocklyApp.KeyboardInputService, blocklyApp.VariableModalService, - function(audioService, keyboardService, variableService) { - this.workspace = blocklyApp.workspace; - this.variableModalService = variableService; - this.audioService = audioService; - this.keyboardInputService = keyboardService - this.modalIsVisible = false; - this.activeButtonIndex = -1; - this.currentVariableName = ""; - - var that = this; - this.variableModalService.registerPreRenameShowHook( - function(oldName) { - that.currentVariableName = oldName; - that.modalIsVisible = true; - - Blockly.CommonModal.setupKeyboardOverrides(that); - - setTimeout(function() { - document.getElementById('varModal').focus(); - }, 150); - } - ); - } - ], - // Caches the current text variable as the user types. - setTextValue: function(newValue) { - this.variableName = newValue; - }, - // Closes the modal (on both success and failure). - hideModal_: Blockly.CommonModal.hideModal, - // Focuses on the button represented by the given index. - focusOnOption: Blockly.CommonModal.focusOnOption, - // Counts the number of interactive elements for the modal. - numInteractiveElements: Blockly.CommonModal.numInteractiveElements, - // Gets all the interactive elements for the modal. - getInteractiveElements: Blockly.CommonModal.getInteractiveElements, - // Gets the container with interactive elements. - getInteractiveContainer: function() { - return document.getElementById("varForm"); - }, - // Submits the name change for the variable. - submit: function() { - this.workspace.renameVariable(this.currentVariableName, this.variableName); - this.dismissModal(); - }, - // Dismisses and closes the modal. - dismissModal: function() { - this.variableModalService.hideModal(); - this.hideModal_(); - } -}) diff --git a/accessible/workspace-block.component.js b/accessible/workspace-block.component.js deleted file mode 100644 index ca20b7fde33..00000000000 --- a/accessible/workspace-block.component.js +++ /dev/null @@ -1,216 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Component representing a Blockly.Block in the - * workspace. - * @author madeeha@google.com (Madeeha Ghori) - */ - -goog.provide('blocklyApp.WorkspaceBlockComponent'); - -goog.require('blocklyApp.UtilsService'); - -goog.require('blocklyApp.AudioService'); -goog.require('blocklyApp.BlockConnectionService'); -goog.require('blocklyApp.FieldSegmentComponent'); -goog.require('blocklyApp.TranslatePipe'); -goog.require('blocklyApp.TreeService'); - - -blocklyApp.WorkspaceBlockComponent = ng.core.Component({ - selector: 'blockly-workspace-block', - template: ` -
  • - - -
      - -
    -
  • - - - - `, - directives: [blocklyApp.FieldSegmentComponent, ng.core.forwardRef(function() { - return blocklyApp.WorkspaceBlockComponent; - })], - inputs: ['block', 'level', 'tree'], - pipes: [blocklyApp.TranslatePipe] -}) -.Class({ - constructor: [ - blocklyApp.AudioService, - blocklyApp.BlockConnectionService, - blocklyApp.TreeService, - blocklyApp.UtilsService, - function(audioService, blockConnectionService, treeService, utilsService) { - this.audioService = audioService; - this.blockConnectionService = blockConnectionService; - this.treeService = treeService; - this.utilsService = utilsService; - this.cachedBlockId = null; - } - ], - ngDoCheck: function() { - // The block ID can change if, for example, a block is spliced between two - // linked blocks. We need to refresh the fields and component IDs when this - // happens. - if (this.cachedBlockId != this.block.id) { - this.cachedBlockId = this.block.id; - - var SUPPORTED_FIELDS = [Blockly.FieldTextInput, Blockly.FieldDropdown]; - this.inputListAsFieldSegments = this.block.inputList.map(function(input) { - // Converts the input list to an array of field segments. Each field - // segment represents a user-editable field, prefixed by an arbitrary - // number of non-editable fields. - var fieldSegments = []; - - var bufferedFields = []; - input.fieldRow.forEach(function(field) { - var fieldIsSupported = SUPPORTED_FIELDS.some(function(fieldType) { - return (field instanceof fieldType); - }); - - if (fieldIsSupported) { - var fieldSegment = { - prefixFields: [], - mainField: field - }; - bufferedFields.forEach(function(bufferedField) { - fieldSegment.prefixFields.push(bufferedField); - }); - fieldSegments.push(fieldSegment); - bufferedFields = []; - } else { - bufferedFields.push(field); - } - }); - - // Handle leftover text at the end. - if (bufferedFields.length) { - fieldSegments.push({ - prefixFields: bufferedFields, - mainField: null - }); - } - - return fieldSegments; - }); - - // Generate unique IDs for elements in this component. - this.componentIds = {}; - this.componentIds.blockRoot = - this.block.id + blocklyApp.BLOCK_ROOT_ID_SUFFIX; - this.componentIds.blockSummary = this.block.id + '-blockSummary'; - - var that = this; - this.componentIds.inputs = this.block.inputList.map(function(input, i) { - var idsToGenerate = ['inputLi', 'fieldLabel']; - if (input.connection && !input.connection.targetBlock()) { - idsToGenerate.push('actionButtonLi', 'actionButton', 'buttonLabel'); - } - - var inputIds = {}; - idsToGenerate.forEach(function(idBaseString) { - inputIds[idBaseString] = [that.block.id, i, idBaseString].join('-'); - }); - - return inputIds; - }); - } - }, - ngAfterViewInit: function() { - // If this is a top-level tree in the workspace, ensure that it has an - // active descendant. (Note that a timeout is needed here in order to - // trigger Angular change detection.) - var that = this; - setTimeout(function() { - if (that.level === 0 && !that.treeService.getActiveDescId(that.tree.id)) { - that.treeService.setActiveDesc( - that.componentIds.blockRoot, that.tree.id); - } - }); - }, - addInteriorLink: function(connection) { - this.blockConnectionService.markConnection(connection); - }, - getBlockDescription: function() { - var blockDescription = this.utilsService.getBlockDescription(this.block); - - var parentBlock = this.block.getSurroundParent(); - if (parentBlock) { - var fullDescription = blockDescription + ' inside ' + - this.utilsService.getBlockDescription(parentBlock); - return fullDescription; - } else { - return blockDescription; - } - }, - getBlockNeededLabel: function(blockInput) { - // The input type name, or 'any' if any official input type qualifies. - var inputTypeLabel = ( - blockInput.connection.check_ ? - blockInput.connection.check_.join(', ') : Blockly.Msg.ANY); - var blockTypeLabel = ( - blockInput.type == Blockly.NEXT_STATEMENT ? - Blockly.Msg.BLOCK : Blockly.Msg.VALUE); - return inputTypeLabel + ' ' + blockTypeLabel + ' needed:'; - }, - generateAriaLabelledByAttr: function(mainLabel, secondLabel) { - return mainLabel + (secondLabel ? ' ' + secondLabel : ''); - } -}); diff --git a/accessible/workspace.component.js b/accessible/workspace.component.js deleted file mode 100644 index 08446011765..00000000000 --- a/accessible/workspace.component.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * AccessibleBlockly - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angular2 Component that details how a Blockly.Workspace is - * rendered in AccessibleBlockly. - * - * @author madeeha@google.com (Madeeha Ghori) - */ - -goog.provide('blocklyApp.WorkspaceComponent'); - -goog.require('blocklyApp.NotificationsService'); -goog.require('blocklyApp.ToolboxModalService'); -goog.require('blocklyApp.TranslatePipe'); -goog.require('blocklyApp.TreeService'); - -goog.require('blocklyApp.WorkspaceBlockComponent'); - - -blocklyApp.WorkspaceComponent = ng.core.Component({ - selector: 'blockly-workspace', - template: ` -
    -

    {{'WORKSPACE'|translate}}

    - -
    -
      - - -
    - - -

    - {{'NO_BLOCKS_IN_WORKSPACE'|translate}} - -

    -
    -
    -
    - `, - directives: [blocklyApp.WorkspaceBlockComponent], - pipes: [blocklyApp.TranslatePipe] -}) -.Class({ - constructor: [ - blocklyApp.NotificationsService, - blocklyApp.ToolboxModalService, - blocklyApp.TreeService, - function(notificationsService, toolboxModalService, treeService) { - this.notificationsService = notificationsService; - this.toolboxModalService = toolboxModalService; - this.treeService = treeService; - - this.ID_FOR_EMPTY_WORKSPACE_BTN = blocklyApp.ID_FOR_EMPTY_WORKSPACE_BTN; - this.workspace = blocklyApp.workspace; - this.currentTreeId = 0; - } - ], - getNewTreeId: function() { - this.currentTreeId++; - return 'blockly-tree-' + this.currentTreeId; - }, - getActiveDescId: function(treeId) { - return this.treeService.getActiveDescId(treeId); - }, - onKeypress: function(e, tree) { - this.treeService.onKeypress(e, tree); - }, - showToolboxModalForCreateNewGroup: function() { - this.toolboxModalService.showToolboxModalForCreateNewGroup( - this.ID_FOR_EMPTY_WORKSPACE_BTN); - }, - speakLocation: function(groupIndex, treeId) { - this.notificationsService.speak( - 'Now in workspace group ' + (groupIndex + 1) + ' of ' + - this.workspace.topBlocks_.length); - } -}); 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 new file mode 100644 index 00000000000..00d3868b177 --- /dev/null +++ b/appengine/.gcloudignore @@ -0,0 +1,20 @@ +# Do not upload these files. +.* +*.soy +*.komodoproject +deploy +/static/appengine/ +/static/demos/plane/soy/*.jar +/static/demos/plane/xlf/ +/static/externs/ +/static/msg/json/ +/static/scripts/ +/static/typings/ + +/static/eslintrc.json +/static/gulpfile.js +/static/jsconfig.json +/static/LICENSE +/static/package-lock.json +/static/package.json +/static/README.md diff --git a/appengine/README.txt b/appengine/README.txt index 6ba262bba75..caaa8e7b763 100644 --- a/appengine/README.txt +++ b/appengine/README.txt @@ -11,12 +11,13 @@ structure: blockly/ |- app.yaml + |- deploy |- index.yaml - |- index_redirect.py + |- main.py |- README.txt + |- requirements.txt |- storage.js |- storage.py - |- closure-library/ (Optional) `- static/ |- blocks/ |- core/ @@ -26,7 +27,7 @@ blockly/ |- msg/ |- tests/ |- blockly_compressed.js - |- blockly_uncompressed.js (Optional) + |- blockly_uncompressed.js |- blocks_compressed.js |- dart_compressed.js |- javascript_compressed.js @@ -34,11 +35,8 @@ blockly/ |- php_compressed.js `- python_compressed.js -Instructions for fetching the optional Closure library may be found here: - https://developers.google.com/blockly/guides/modify/web/closure - Go to https://appengine.google.com/ and create your App Engine application. -Modify the 'application' name of app.yaml to your App Engine application name. +Modify the 'PROJECT' name in the 'deploy' file to your App Engine application name. Finally, upload this directory structure to your App Engine account, -wait a minute, then go to http://YOURAPPNAME.appspot.com/ +then go to http://YOURAPPNAME.appspot.com/ diff --git a/appengine/add_timestamps.py b/appengine/add_timestamps.py new file mode 100644 index 00000000000..05ff7b92814 --- /dev/null +++ b/appengine/add_timestamps.py @@ -0,0 +1,69 @@ +"""Blockly Demo: Add timestamps + +Copyright 2020 Google LLC + +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. +""" + +"""A script to get all Xml entries in the datastore for Blockly demos +and reinsert any that do not have a last_accessed time. + +This script should only need to be run once, but may take a long time +to complete. + +NDB does not provide a way to query for all entities that are missing a +given property, so we have to get all of them and discard any that +already have a last_accessed time. + +Auth: `gcloud auth login` + +Set the correct project: `gcloud config set project blockly-demo` + +See the current project: `gcloud config get-value project` + +Start a venv: `python3 -m venv venv && source venv/bin/activate` +Inside your vm run `pip install google-cloud-ndb` +Run the script: `python3 add_timestamps.py` +""" + +__author__ = "fenichel@google.com (Rachel Fenichel)" + + +from google.cloud import ndb +from storage import Xml +import datetime + +PAGE_SIZE = 1000 + +def handle_results(results): + for x in results: + if (x.last_accessed is None): + x.put() + +def run_query(): + client = ndb.Client() + with client.context(): + query = Xml.query() + print(f'Total entries: {query.count()}') + cursor = None + more = True + page_count = 0 + result_count = 0 + while more: + results, cursor, more = query.fetch_page(PAGE_SIZE, start_cursor=cursor) + handle_results(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 8938830f849..9c93fb6873d 100644 --- a/appengine/app.yaml +++ b/appengine/app.yaml @@ -1,8 +1,4 @@ -application: blockly-demo -version: 1 -runtime: python27 -api_version: 1 -threadsafe: no +runtime: python312 handlers: # Redirect obsolete URLs. @@ -18,30 +14,68 @@ handlers: - url: /static/apps/.* static_files: redirect.html upload: redirect.html - - -# Storage API. -- url: /storage - script: storage.py - secure: always -- url: /storage\.js - static_files: storage.js - upload: storage\.js - secure: always +# 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 static_dir: static secure: always -# Closure library for uncompressed Blockly. -- url: /closure-library - static_dir: closure-library - secure: always - -# Redirect for root directory. -- url: / - script: index_redirect.py +# Storage API. +- url: /storage\.js + static_files: storage.js + upload: storage\.js secure: always # Favicon. @@ -64,24 +98,7 @@ handlers: upload: robots\.txt secure: always - -skip_files: -# App Engine default patterns. -- ^(.*/)?#.*#$ -- ^(.*/)?.*~$ -- ^(.*/)?.*\.py[co]$ -- ^(.*/)?.*/RCS/.*$ -- ^(.*/)?\..*$ -# Custom skip patterns. -- ^static/appengine/.*$ -- ^static/demos/plane/soy/.+\.jar$ -- ^static/demos/plane/template.soy$ -- ^static/demos/plane/xlf/.*$ -- ^static/i18n/.*$ -- ^static/msg/json/.*$ -- ^.+\.soy$ -- ^closure-library/.*_test.html$ -- ^closure-library/.*_test.js$ -- ^closure-library/closure/bin/.*$ -- ^closure-library/doc/.*$ -- ^closure-library/scripts/.*$ +# Dynamic content. +- url: /.* + script: auto + secure: always diff --git a/appengine/apple-touch-icon.png b/appengine/apple-touch-icon.png index 455abac2d71..38dc7ba1d44 100644 Binary files a/appengine/apple-touch-icon.png and b/appengine/apple-touch-icon.png differ diff --git a/appengine/blockly_compressed.js b/appengine/blockly_compressed.js new file mode 100644 index 00000000000..837d6bb3580 --- /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 { + // Can't alert? Probably node.js. +} diff --git a/appengine/expiration.py b/appengine/expiration.py new file mode 100644 index 00000000000..89d4a7a8d5d --- /dev/null +++ b/appengine/expiration.py @@ -0,0 +1,52 @@ +""" +Copyright 2020 Google LLC + +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. +""" + +"""Delete expired XML. +""" + +__author__ = "fenichel@google.com (Rachel Fenichel)" + + +import storage +import datetime + +from google.cloud import ndb + + +EXPIRATION_DAYS = 365 +# Limit the query to avoid timeouts. +QUERY_LIMIT = 1000 + +def delete_expired(): + """Deletes entries that have not been accessed in more than a year.""" + bestBefore = datetime.datetime.utcnow() - datetime.timedelta(days=EXPIRATION_DAYS) + client = ndb.Client() + with client.context(): + query = storage.Xml.query(storage.Xml.last_accessed < bestBefore) + results = query.fetch(limit=QUERY_LIMIT, keys_only=True) + for x in results: + x.delete() + return len(results) + + +def app(environ, start_response): + headers = [ + ("Content-Type", "text/plain") + ] + start_response("200 OK", headers) + n = delete_expired() + out = "%d records deleted." % n + return [out.encode("utf-8")] diff --git a/appengine/index_redirect.py b/appengine/index_redirect.py deleted file mode 100644 index 286a8e87cd1..00000000000 --- a/appengine/index_redirect.py +++ /dev/null @@ -1,2 +0,0 @@ -print("Status: 302") -print("Location: /static/demos/index.html") diff --git a/appengine/main.py b/appengine/main.py new file mode 100644 index 00000000000..765087c612c --- /dev/null +++ b/appengine/main.py @@ -0,0 +1,39 @@ +""" +Copyright 2020 Google LLC + +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. +""" + +import storage +import expiration + + +# Route to requested handler. +def app(environ, start_response): + if environ["PATH_INFO"] == "/": + return redirect(environ, start_response) + if environ["PATH_INFO"] == "/storage": + return storage.app(environ, start_response) + if environ["PATH_INFO"] == "/expiration": + return expiration.app(environ, start_response) + start_response("404 Not Found", []) + return [b"Page not found."] + + +# Redirect for root directory. +def redirect(environ, start_response): + headers = [ + ("Location", "static/demos/index.html") + ] + start_response("301 Found", headers) + return [] 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/requirements.txt b/appengine/requirements.txt new file mode 100644 index 00000000000..99d5d110e57 --- /dev/null +++ b/appengine/requirements.txt @@ -0,0 +1 @@ +google-cloud-ndb diff --git a/appengine/storage.js b/appengine/storage.js index 8141806d524..c1157177133 100644 --- a/appengine/storage.js +++ b/appengine/storage.js @@ -1,26 +1,11 @@ /** * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Loading and saving blocks with localStorage and cloud storage. - * @author q.neutron@gmail.com (Quynh Neutron) */ 'use strict'; @@ -59,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); } }; @@ -70,7 +55,16 @@ BlocklyStorage.restoreBlocks = function(opt_workspace) { */ BlocklyStorage.link = function(opt_workspace) { var workspace = opt_workspace || Blockly.getMainWorkspace(); - var xml = Blockly.Xml.workspaceToDom(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) { + var block = xml.querySelector('block'); + if (block) { + block.removeAttribute('x'); + block.removeAttribute('y'); + } + } var data = Blockly.Xml.domToText(xml); BlocklyStorage.makeRequest_('/storage', 'xml', data, workspace); }; @@ -121,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); } } @@ -158,12 +154,12 @@ 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(bindData); + workspace.removeChangeListener(change); } } - var bindData = workspace.addChangeListener(change); + workspace.addChangeListener(change); }; /** @@ -174,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 4a572073f4e..34db68b29ac 100644 --- a/appengine/storage.py +++ b/appengine/storage.py @@ -1,7 +1,6 @@ """Blockly Demo: Storage -Copyright 2012 Google Inc. -https://developers.google.com/blockly/ +Copyright 2012 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,70 +15,111 @@ 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 google.cloud import ndb from random import randint -from google.appengine.ext import db -from google.appengine.api import memcache -import logging +from urllib.parse import unquote + + +class Xml(ndb.Model): + # A row in the database. + xml_hash = ndb.IntegerProperty() + xml_content = ndb.TextProperty() + last_accessed = ndb.DateTimeProperty(auto_now=True) -print "Content-Type: text/plain\n" def keyGen(): # Generate a random string of length KEY_LEN. KEY_LEN = 6 - CHARS = "abcdefghijkmnopqrstuvwxyz23456789" # Exclude l, 0, 1. + CHARS = "abcdefghijkmnopqrstuvwxyz23456789" # Exclude l, 0, 1. max_index = len(CHARS) - 1 return "".join([CHARS[randint(0, max_index)] for x in range(KEY_LEN)]) -class Xml(db.Model): - # A row in the database. - xml_hash = db.IntegerProperty() - xml_content = db.TextProperty() - -forms = cgi.FieldStorage() -if "xml" in forms: - # Store XML and return a generated key. - xml_content = forms["xml"].value - xml_hash = hash(xml_content) - lookup_query = db.Query(Xml) - lookup_query.filter("xml_hash =", xml_hash) - lookup_result = lookup_query.get() - if lookup_result: - xml_key = lookup_result.key().name() - else: - trials = 0 - result = True - while result: - trials += 1 - if trials == 100: - raise Exception("Sorry, the generator failed to get a key for you.") - xml_key = keyGen() - result = db.get(db.Key.from_path("Xml", xml_key)) - xml = db.Text(xml_content, encoding="utf_8") - row = Xml(key_name = xml_key, xml_hash = xml_hash, xml_content = xml) - row.put() - print xml_key - -if "key" in forms: - # Retrieve stored XML based on the provided key. - key_provided = forms["key"].value + +# 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/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)) + 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() + else: + trials = 0 + result = True + while result: + trials += 1 + if trials == 100: + raise Exception("Sorry, the generator failed to get a key for you.") + xml_key = keyGen() + result = Xml.get_by_id(xml_key) + row = Xml(id = xml_key, xml_hash = xml_hash, xml_content = xml_content) + row.put() + return xml_key + + +def keyToXml(key_provided): + # Retrieve stored XML/JSON based on the provided key. # Normalize the string. key_provided = key_provided.lower().strip() - # Check memcache for a quick match. - xml = memcache.get("XML_" + key_provided) - if xml is None: - # Check datastore for a definitive match. - result = db.get(db.Key.from_path("Xml", key_provided)) - if not result: - xml = "" - else: - xml = result.xml_content - # Save to memcache for next hit. - if not memcache.add("XML_" + key_provided, xml, 3600): - logging.error("Memcache set failed.") - print xml.encode("utf-8") + # Check datastore for a match. + client = ndb.Client() + with client.context(): + result = Xml.get_by_id(key_provided) + if not result: + xml = "" + else: + # Put it back into the datastore immediately, which updates the last + # accessed time. + 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): + 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"]) + elif "key" in forms: + out = keyToXml(forms["key"]) + else: + out = "" + + start_response("200 OK", headers) + return [out.encode("utf-8")] diff --git a/blockly_accessible_compressed.js b/blockly_accessible_compressed.js deleted file mode 100644 index 8c2c13769e8..00000000000 --- a/blockly_accessible_compressed.js +++ /dev/null @@ -1,1650 +0,0 @@ -// Do not edit this file; automatically generated by build.py. -'use strict'; - -var COMPILED=!0,goog=goog||{};goog.global=this;goog.isDef=function(a){return void 0!==a};goog.isString=function(a){return"string"==typeof a};goog.isBoolean=function(a){return"boolean"==typeof a};goog.isNumber=function(a){return"number"==typeof a};goog.exportPath_=function(a,b,c){a=a.split(".");c=c||goog.global;a[0]in c||!c.execScript||c.execScript("var "+a[0]);for(var d;a.length&&(d=a.shift());)!a.length&&goog.isDef(b)?c[d]=b:c=c[d]&&c[d]!==Object.prototype[d]?c[d]:c[d]={}}; -goog.define=function(a,b){COMPILED||(goog.global.CLOSURE_UNCOMPILED_DEFINES&&void 0===goog.global.CLOSURE_UNCOMPILED_DEFINES.nodeType&&Object.prototype.hasOwnProperty.call(goog.global.CLOSURE_UNCOMPILED_DEFINES,a)?b=goog.global.CLOSURE_UNCOMPILED_DEFINES[a]:goog.global.CLOSURE_DEFINES&&void 0===goog.global.CLOSURE_DEFINES.nodeType&&Object.prototype.hasOwnProperty.call(goog.global.CLOSURE_DEFINES,a)&&(b=goog.global.CLOSURE_DEFINES[a]));goog.exportPath_(a,b)};goog.DEBUG=!1;goog.LOCALE="en"; -goog.TRUSTED_SITE=!0;goog.STRICT_MODE_COMPATIBLE=!1;goog.DISALLOW_TEST_ONLY_CODE=COMPILED&&!goog.DEBUG;goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING=!1;goog.provide=function(a){if(goog.isInModuleLoader_())throw Error("goog.provide can not be used within a goog.module.");if(!COMPILED&&goog.isProvided_(a))throw Error('Namespace "'+a+'" already declared.');goog.constructNamespace_(a)}; -goog.constructNamespace_=function(a,b){if(!COMPILED){delete goog.implicitNamespaces_[a];for(var c=a;(c=c.substring(0,c.lastIndexOf(".")))&&!goog.getObjectByName(c);)goog.implicitNamespaces_[c]=!0}goog.exportPath_(a,b)};goog.VALID_MODULE_RE_=/^[a-zA-Z_$][a-zA-Z0-9._$]*$/; -goog.module=function(a){if(!goog.isString(a)||!a||-1==a.search(goog.VALID_MODULE_RE_))throw Error("Invalid module identifier");if(!goog.isInModuleLoader_())throw Error("Module "+a+" has been loaded incorrectly. Note, modules cannot be loaded as normal scripts. They require some kind of pre-processing step. You're likely trying to load a module via a script tag or as a part of a concatenated bundle without rewriting the module. For more info see: https://github.com/google/closure-library/wiki/goog.module:-an-ES6-module-like-alternative-to-goog.provide.");if(goog.moduleLoaderState_.moduleName)throw Error("goog.module may only be called once per module."); -goog.moduleLoaderState_.moduleName=a;if(!COMPILED){if(goog.isProvided_(a))throw Error('Namespace "'+a+'" already declared.');delete goog.implicitNamespaces_[a]}};goog.module.get=function(a){return goog.module.getInternal_(a)};goog.module.getInternal_=function(a){if(!COMPILED){if(a in goog.loadedModules_)return goog.loadedModules_[a];if(!goog.implicitNamespaces_[a])return a=goog.getObjectByName(a),null!=a?a:null}return null};goog.moduleLoaderState_=null; -goog.isInModuleLoader_=function(){return null!=goog.moduleLoaderState_};goog.module.declareLegacyNamespace=function(){if(!COMPILED&&!goog.isInModuleLoader_())throw Error("goog.module.declareLegacyNamespace must be called from within a goog.module");if(!COMPILED&&!goog.moduleLoaderState_.moduleName)throw Error("goog.module must be called prior to goog.module.declareLegacyNamespace.");goog.moduleLoaderState_.declareLegacyNamespace=!0}; -goog.setTestOnly=function(a){if(goog.DISALLOW_TEST_ONLY_CODE)throw a=a||"",Error("Importing test-only code into non-debug environment"+(a?": "+a:"."));};goog.forwardDeclare=function(a){};COMPILED||(goog.isProvided_=function(a){return a in goog.loadedModules_||!goog.implicitNamespaces_[a]&&goog.isDefAndNotNull(goog.getObjectByName(a))},goog.implicitNamespaces_={"goog.module":!0}); -goog.getObjectByName=function(a,b){a=a.split(".");b=b||goog.global;for(var c;c=a.shift();)if(goog.isDefAndNotNull(b[c]))b=b[c];else return null;return b};goog.globalize=function(a,b){b=b||goog.global;for(var c in a)b[c]=a[c]}; -goog.addDependency=function(a,b,c,d){if(goog.DEPENDENCIES_ENABLED){var e;a=a.replace(/\\/g,"/");var f=goog.dependencies_;d&&"boolean"!==typeof d||(d=d?{module:"goog"}:{});for(var g=0;e=b[g];g++)f.nameToPath[e]=a,f.loadFlags[a]=d;for(d=0;b=c[d];d++)a in f.requires||(f.requires[a]={}),f.requires[a][b]=!0}};goog.ENABLE_DEBUG_LOADER=!0;goog.logToConsole_=function(a){goog.global.console&&goog.global.console.error(a)}; -goog.require=function(a){if(!COMPILED){goog.ENABLE_DEBUG_LOADER&&goog.IS_OLD_IE_&&goog.maybeProcessDeferredDep_(a);if(goog.isProvided_(a)){if(goog.isInModuleLoader_())return goog.module.getInternal_(a)}else if(goog.ENABLE_DEBUG_LOADER){var b=goog.getPathFromDeps_(a);if(b)goog.writeScripts_(b);else throw a="goog.require could not find: "+a,goog.logToConsole_(a),Error(a);}return null}};goog.basePath="";goog.nullFunction=function(){}; -goog.abstractMethod=function(){throw Error("unimplemented abstract method");};goog.addSingletonGetter=function(a){a.instance_=void 0;a.getInstance=function(){if(a.instance_)return a.instance_;goog.DEBUG&&(goog.instantiatedSingletons_[goog.instantiatedSingletons_.length]=a);return a.instance_=new a}};goog.instantiatedSingletons_=[];goog.LOAD_MODULE_USING_EVAL=!0;goog.SEAL_MODULE_EXPORTS=goog.DEBUG;goog.loadedModules_={};goog.DEPENDENCIES_ENABLED=!COMPILED&&goog.ENABLE_DEBUG_LOADER;goog.TRANSPILE="detect"; -goog.TRANSPILER="transpile.js"; -goog.DEPENDENCIES_ENABLED&&(goog.dependencies_={loadFlags:{},nameToPath:{},requires:{},visited:{},written:{},deferred:{}},goog.inHtmlDocument_=function(){var a=goog.global.document;return null!=a&&"write"in a},goog.findBasePath_=function(){if(goog.isDef(goog.global.CLOSURE_BASE_PATH)&&goog.isString(goog.global.CLOSURE_BASE_PATH))goog.basePath=goog.global.CLOSURE_BASE_PATH;else if(goog.inHtmlDocument_()){var a=goog.global.document;var b=a.currentScript;a=b?[b]:a.getElementsByTagName("SCRIPT");for(b= -a.length-1;0<=b;--b){var c=a[b].src,d=c.lastIndexOf("?"),d=-1==d?c.length:d;if("base.js"==c.substr(d-7,7)){goog.basePath=c.substr(0,d-7);break}}}},goog.importScript_=function(a,b){(goog.global.CLOSURE_IMPORT_SCRIPT||goog.writeScriptTag_)(a,b)&&(goog.dependencies_.written[a]=!0)},goog.IS_OLD_IE_=!(goog.global.atob||!goog.global.document||!goog.global.document.all),goog.oldIeWaiting_=!1,goog.importProcessedScript_=function(a,b,c){goog.importScript_("",'goog.retrieveAndExec_("'+a+'", '+b+", "+c+");")}, -goog.queuedModules_=[],goog.wrapModule_=function(a,b){return goog.LOAD_MODULE_USING_EVAL&&goog.isDef(goog.global.JSON)?"goog.loadModule("+goog.global.JSON.stringify(b+"\n//# sourceURL="+a+"\n")+");":'goog.loadModule(function(exports) {"use strict";'+b+"\n;return exports});\n//# sourceURL="+a+"\n"},goog.loadQueuedModules_=function(){var a=goog.queuedModules_.length;if(0\x3c/script>')},goog.appendScriptSrcNode_=function(a){var b=goog.global.document,c=b.createElement("script"); -c.type="text/javascript";c.src=a;c.defer=!1;c.async=!1;b.head.appendChild(c)},goog.writeScriptTag_=function(a,b){if(goog.inHtmlDocument_()){var c=goog.global.document;if(!goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING&&"complete"==c.readyState){if(/\bdeps.js$/.test(a))return!1;throw Error('Cannot write "'+a+'" after document load');}void 0===b?goog.IS_OLD_IE_?(goog.oldIeWaiting_=!0,b=" onreadystatechange='goog.onScriptLoad_(this, "+ ++goog.lastNonModuleScriptIndex_+")' ",c.write(''); - // Load fresh Closure Library. - document.write(''); - document.write(''); -} diff --git a/blockly_compressed.js b/blockly_compressed.js deleted file mode 100644 index 1b9939ddfd9..00000000000 --- a/blockly_compressed.js +++ /dev/null @@ -1,1586 +0,0 @@ -// Do not edit this file; automatically generated by build.py. -'use strict'; - -var COMPILED=!0,goog=goog||{};goog.global=this;goog.isDef=function(a){return void 0!==a};goog.isString=function(a){return"string"==typeof a};goog.isBoolean=function(a){return"boolean"==typeof a};goog.isNumber=function(a){return"number"==typeof a};goog.exportPath_=function(a,b,c){a=a.split(".");c=c||goog.global;a[0]in c||!c.execScript||c.execScript("var "+a[0]);for(var d;a.length&&(d=a.shift());)!a.length&&goog.isDef(b)?c[d]=b:c=c[d]&&c[d]!==Object.prototype[d]?c[d]:c[d]={}}; -goog.define=function(a,b){var c=b;COMPILED||(goog.global.CLOSURE_UNCOMPILED_DEFINES&&void 0===goog.global.CLOSURE_UNCOMPILED_DEFINES.nodeType&&Object.prototype.hasOwnProperty.call(goog.global.CLOSURE_UNCOMPILED_DEFINES,a)?c=goog.global.CLOSURE_UNCOMPILED_DEFINES[a]:goog.global.CLOSURE_DEFINES&&void 0===goog.global.CLOSURE_DEFINES.nodeType&&Object.prototype.hasOwnProperty.call(goog.global.CLOSURE_DEFINES,a)&&(c=goog.global.CLOSURE_DEFINES[a]));goog.exportPath_(a,c)};goog.DEBUG=!1;goog.LOCALE="en"; -goog.TRUSTED_SITE=!0;goog.STRICT_MODE_COMPATIBLE=!1;goog.DISALLOW_TEST_ONLY_CODE=COMPILED&&!goog.DEBUG;goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING=!1;goog.provide=function(a){if(goog.isInModuleLoader_())throw Error("goog.provide can not be used within a goog.module.");if(!COMPILED&&goog.isProvided_(a))throw Error('Namespace "'+a+'" already declared.');goog.constructNamespace_(a)}; -goog.constructNamespace_=function(a,b){if(!COMPILED){delete goog.implicitNamespaces_[a];for(var c=a;(c=c.substring(0,c.lastIndexOf(".")))&&!goog.getObjectByName(c);)goog.implicitNamespaces_[c]=!0}goog.exportPath_(a,b)};goog.VALID_MODULE_RE_=/^[a-zA-Z_$][a-zA-Z0-9._$]*$/; -goog.module=function(a){if(!goog.isString(a)||!a||-1==a.search(goog.VALID_MODULE_RE_))throw Error("Invalid module identifier");if(!goog.isInModuleLoader_())throw Error("Module "+a+" has been loaded incorrectly. Note, modules cannot be loaded as normal scripts. They require some kind of pre-processing step. You're likely trying to load a module via a script tag or as a part of a concatenated bundle without rewriting the module. For more info see: https://github.com/google/closure-library/wiki/goog.module:-an-ES6-module-like-alternative-to-goog.provide.");if(goog.moduleLoaderState_.moduleName)throw Error("goog.module may only be called once per module."); -goog.moduleLoaderState_.moduleName=a;if(!COMPILED){if(goog.isProvided_(a))throw Error('Namespace "'+a+'" already declared.');delete goog.implicitNamespaces_[a]}};goog.module.get=function(a){return goog.module.getInternal_(a)};goog.module.getInternal_=function(a){if(!COMPILED){if(a in goog.loadedModules_)return goog.loadedModules_[a];if(!goog.implicitNamespaces_[a])return a=goog.getObjectByName(a),null!=a?a:null}return null};goog.moduleLoaderState_=null; -goog.isInModuleLoader_=function(){return null!=goog.moduleLoaderState_};goog.module.declareLegacyNamespace=function(){if(!COMPILED&&!goog.isInModuleLoader_())throw Error("goog.module.declareLegacyNamespace must be called from within a goog.module");if(!COMPILED&&!goog.moduleLoaderState_.moduleName)throw Error("goog.module must be called prior to goog.module.declareLegacyNamespace.");goog.moduleLoaderState_.declareLegacyNamespace=!0}; -goog.setTestOnly=function(a){if(goog.DISALLOW_TEST_ONLY_CODE)throw a=a||"",Error("Importing test-only code into non-debug environment"+(a?": "+a:"."));};goog.forwardDeclare=function(a){};COMPILED||(goog.isProvided_=function(a){return a in goog.loadedModules_||!goog.implicitNamespaces_[a]&&goog.isDefAndNotNull(goog.getObjectByName(a))},goog.implicitNamespaces_={"goog.module":!0}); -goog.getObjectByName=function(a,b){for(var c=a.split("."),d=b||goog.global,e;e=c.shift();)if(goog.isDefAndNotNull(d[e]))d=d[e];else return null;return d};goog.globalize=function(a,b){var c=b||goog.global,d;for(d in a)c[d]=a[d]}; -goog.addDependency=function(a,b,c,d){if(goog.DEPENDENCIES_ENABLED){var e;a=a.replace(/\\/g,"/");var f=goog.dependencies_;d&&"boolean"!==typeof d||(d=d?{module:"goog"}:{});for(var g=0;e=b[g];g++)f.nameToPath[e]=a,f.loadFlags[a]=d;for(d=0;b=c[d];d++)a in f.requires||(f.requires[a]={}),f.requires[a][b]=!0}};goog.ENABLE_DEBUG_LOADER=!0;goog.logToConsole_=function(a){goog.global.console&&goog.global.console.error(a)}; -goog.require=function(a){if(!COMPILED){goog.ENABLE_DEBUG_LOADER&&goog.IS_OLD_IE_&&goog.maybeProcessDeferredDep_(a);if(goog.isProvided_(a)){if(goog.isInModuleLoader_())return goog.module.getInternal_(a)}else if(goog.ENABLE_DEBUG_LOADER){var b=goog.getPathFromDeps_(a);if(b)goog.writeScripts_(b);else throw a="goog.require could not find: "+a,goog.logToConsole_(a),Error(a);}return null}};goog.basePath="";goog.nullFunction=function(){}; -goog.abstractMethod=function(){throw Error("unimplemented abstract method");};goog.addSingletonGetter=function(a){a.instance_=void 0;a.getInstance=function(){if(a.instance_)return a.instance_;goog.DEBUG&&(goog.instantiatedSingletons_[goog.instantiatedSingletons_.length]=a);return a.instance_=new a}};goog.instantiatedSingletons_=[];goog.LOAD_MODULE_USING_EVAL=!0;goog.SEAL_MODULE_EXPORTS=goog.DEBUG;goog.loadedModules_={};goog.DEPENDENCIES_ENABLED=!COMPILED&&goog.ENABLE_DEBUG_LOADER;goog.TRANSPILE="detect"; -goog.TRANSPILER="transpile.js"; -goog.DEPENDENCIES_ENABLED&&(goog.dependencies_={loadFlags:{},nameToPath:{},requires:{},visited:{},written:{},deferred:{}},goog.inHtmlDocument_=function(){var a=goog.global.document;return null!=a&&"write"in a},goog.findBasePath_=function(){if(goog.isDef(goog.global.CLOSURE_BASE_PATH)&&goog.isString(goog.global.CLOSURE_BASE_PATH))goog.basePath=goog.global.CLOSURE_BASE_PATH;else if(goog.inHtmlDocument_()){var a=goog.global.document;var b=a.currentScript;a=b?[b]:a.getElementsByTagName("SCRIPT");for(b= -a.length-1;0<=b;--b){var c=a[b].src,d=c.lastIndexOf("?"),d=-1==d?c.length:d;if("base.js"==c.substr(d-7,7)){goog.basePath=c.substr(0,d-7);break}}}},goog.importScript_=function(a,b){(goog.global.CLOSURE_IMPORT_SCRIPT||goog.writeScriptTag_)(a,b)&&(goog.dependencies_.written[a]=!0)},goog.IS_OLD_IE_=!(goog.global.atob||!goog.global.document||!goog.global.document.all),goog.oldIeWaiting_=!1,goog.importProcessedScript_=function(a,b,c){goog.importScript_("",'goog.retrieveAndExec_("'+a+'", '+b+", "+c+");")}, -goog.queuedModules_=[],goog.wrapModule_=function(a,b){return goog.LOAD_MODULE_USING_EVAL&&goog.isDef(goog.global.JSON)?"goog.loadModule("+goog.global.JSON.stringify(b+"\n//# sourceURL="+a+"\n")+");":'goog.loadModule(function(exports) {"use strict";'+b+"\n;return exports});\n//# sourceURL="+a+"\n"},goog.loadQueuedModules_=function(){var a=goog.queuedModules_.length;if(0\x3c/script>')},goog.appendScriptSrcNode_=function(a){var b=goog.global.document,c=b.createElement("script"); -c.type="text/javascript";c.src=a;c.defer=!1;c.async=!1;b.head.appendChild(c)},goog.writeScriptTag_=function(a,b){if(goog.inHtmlDocument_()){var c=goog.global.document;if(!goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING&&"complete"==c.readyState){if(/\bdeps.js$/.test(a))return!1;throw Error('Cannot write "'+a+'" after document load');}if(void 0===b)if(goog.IS_OLD_IE_){goog.oldIeWaiting_=!0;var d=" onreadystatechange='goog.onScriptLoad_(this, "+ ++goog.lastNonModuleScriptIndex_+")' ";c.write(''); - // Load fresh Closure Library. - document.write(''); - document.write(''); -} diff --git a/blocks/blocks.ts b/blocks/blocks.ts new file mode 100644 index 00000000000..dc6ca386cd2 --- /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 type {BlockDefinition} from '../core/blocks.js'; +import * as lists from './lists.js'; +import * as logic from './logic.js'; +import * as loops from './loops.js'; +import * as math from './math.js'; +import * as procedures from './procedures.js'; +import * as texts from './text.js'; +import * as variables from './variables.js'; +import * as variablesDynamic from './variables_dynamic.js'; + +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 99e5aacdcaf..00000000000 --- a/blocks/colour.js +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 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.Blocks'); - - -/** - * Common HSV hue for all blocks in this category. - * This should be the same as Blockly.Msg.COLOUR_HUE. - * @readonly - */ -Blockly.Constants.Colour.HUE = 20; -/** @deprecated Use Blockly.Constants.Colour.HUE */ -Blockly.Blocks.colour.HUE = Blockly.Constants.Colour.HUE; - -Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT - // Block for colour picker. - { - "type": "colour_picker", - "message0": "%1", - "args0": [ - { - "type": "field_colour", - "name": "COLOUR", - "colour": "#ff0000" - } - ], - "output": "Colour", - "colour": "%{BKY_COLOUR_HUE}", - "helpUrl": "%{BKY_COLOUR_PICKER_HELPURL}", - "tooltip": "%{BKY_COLOUR_PICKER_TOOLTIP}", - "extensions": ["parent_tooltip_when_inline"] - }, - - // Block for random colour. - { - "type": "colour_random", - "message0": "%{BKY_COLOUR_RANDOM_TITLE}", - "output": "Colour", - "colour": "%{BKY_COLOUR_HUE}", - "helpUrl": "%{BKY_COLOUR_RANDOM_HELPURL}", - "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", - "colour": "%{BKY_COLOUR_HUE}", - "helpUrl": "%{BKY_COLOUR_RGB_HELPURL}", - "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", - "colour": "%{BKY_COLOUR_HUE}", - "helpUrl": "%{BKY_COLOUR_BLEND_HELPURL}", - "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 3c7ef341dc4..00000000000 --- a/blocks/lists.js +++ /dev/null @@ -1,846 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 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.Blocks'); - - -/** - * Common HSV hue for all blocks in this category. - * This should be the same as Blockly.Msg.LISTS_HUE. - * @readonly - */ -Blockly.Constants.Lists.HUE = 260; -/** @deprecated Use Blockly.Constants.Lists.HUE */ -Blockly.Blocks.lists.HUE = Blockly.Constants.Lists.HUE; - - -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", - "colour": "%{BKY_LISTS_HUE}", - "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", - "colour": "%{BKY_LISTS_HUE}", - "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, - "colour": "%{BKY_LISTS_HUE}", - "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", - "colour": "%{BKY_LISTS_HUE}", - "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", - "colour": "%{BKY_LISTS_HUE}", - "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.setColour(Blockly.Blocks.lists.HUE); - 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 = document.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); - 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.setColour(Blockly.Blocks.lists.HUE); - 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.setColour(Blockly.Blocks.lists.HUE); - 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.setColour(Blockly.Blocks.lists.HUE); - 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.setColour(Blockly.Blocks.lists.HUE); - var modeMenu = new Blockly.FieldDropdown(MODE, function(value) { - var isStatement = (value == 'REMOVE'); - this.sourceBlock_.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 = document.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.sourceBlock_; - 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.setColour(Blockly.Blocks.lists.HUE); - 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 = document.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.sourceBlock_; - 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.setColour(Blockly.Blocks.lists.HUE); - 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 = document.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, independant 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.sourceBlock_; - 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'); - } - } - 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", - "colour": Blockly.Blocks.lists.HUE, - "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.setColour(Blockly.Blocks.lists.HUE); - 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 '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) { - 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 = document.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..6754b6847db --- /dev/null +++ b/blocks/lists.ts @@ -0,0 +1,1068 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.libraryBlocks.lists + +import type {Block} from '../core/block.js'; +import type {BlockSvg} from '../core/block_svg.js'; +import { + createBlockDefinitionsFromJsonArray, + defineBlocks, +} from '../core/common.js'; +import type {Connection} from '../core/connection.js'; +import '../core/field_dropdown.js'; +import type {FieldDropdown} from '../core/field_dropdown.js'; +import * as fieldRegistry from '../core/field_registry.js'; +import {MutatorIcon} from '../core/icons/mutator_icon.js'; +import {Align} from '../core/inputs/align.js'; +import {ValueInput} from '../core/inputs/value_input.js'; +import {Msg} from '../core/msg.js'; +import * as xmlUtils from '../core/utils/xml.js'; +import type {Workspace} from '../core/workspace.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'); + const menu = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: this.WHERE_OPTIONS, + }) as FieldDropdown; + menu.setValidator( + /** @param value The input value. */ + function (this: FieldDropdown, value: string) { + const oldValue: string | null = this.getValue(); + const oldAt = oldValue === 'FROM_START' || oldValue === 'FROM_END'; + const newAt = value === 'FROM_START' || value === 'FROM_END'; + if (newAt !== oldAt) { + const block = this.getSourceBlock() as GetIndexBlock; + block.updateAt_(newAt); + } + return undefined; + }, + ); + this.appendDummyInput().appendField(menu, 'WHERE'); + 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'); + } + 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'); + const menu = fieldRegistry.fromJson({ + type: 'field_dropdown', + options: this.WHERE_OPTIONS, + }) as FieldDropdown; + menu.setValidator( + /** @param value The input value. */ + function (this: FieldDropdown, value: string) { + const oldValue: string | null = this.getValue(); + const oldAt = oldValue === 'FROM_START' || oldValue === 'FROM_END'; + const newAt = value === 'FROM_START' || value === 'FROM_END'; + if (newAt !== oldAt) { + const block = this.getSourceBlock() as SetIndexBlock; + block.updateAt_(newAt); + } + return undefined; + }, + ); + this.appendDummyInput().appendField(menu, 'WHERE'); + 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'); + } + this.moveInputBefore('AT', 'TO'); + if (this.getInput('ORDINAL')) { + this.moveInputBefore('ORDINAL', 'TO'); + } + }, +}; +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']); + const createMenu = (n: 1 | 2): FieldDropdown => { + 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. */ + function (this: FieldDropdown, value: string) { + const oldValue: string | null = this.getValue(); + const oldAt = oldValue === 'FROM_START' || oldValue === 'FROM_END'; + const newAt = value === 'FROM_START' || value === 'FROM_END'; + if (newAt !== oldAt) { + const block = this.getSourceBlock() as GetSublistBlock; + block.updateAt_(n, newAt); + } + return undefined; + }, + ); + return menu; + }; + this.appendDummyInput('WHERE1_INPUT').appendField(createMenu(1), 'WHERE1'); + this.appendDummyInput('AT1'); + this.appendDummyInput('WHERE2_INPUT').appendField(createMenu(2), 'WHERE2'); + 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); + } + if (n === 1) { + this.moveInputBefore('AT1', 'WHERE2_INPUT'); + if (this.getInput('ORDINAL1')) { + this.moveInputBefore('ORDINAL1', 'WHERE2_INPUT'); + } + } + 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 f27c4e76928..00000000000 --- a/blocks/logic.js +++ /dev/null @@ -1,621 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 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.Blocks'); - - -/** - * Common HSV hue for all blocks in this category. - * Should be the same as Blockly.Msg.LOGIC_HUE. - * @readonly - */ -Blockly.Constants.Logic.HUE = 210; -/** @deprecated Use Blockly.Constants.Logic.HUE */ -Blockly.Blocks.logic.HUE = Blockly.Constants.Logic.HUE; - -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", - "colour": "%{BKY_LOGIC_HUE}", - "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, - "colour": "%{BKY_LOGIC_HUE}", - "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, - "colour": "%{BKY_LOGIC_HUE}", - "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"], - ["<", "LT"], - ["\u2264", "LTE"], - [">", "GT"], - ["\u2265", "GTE"] - ] - }, - { - "type": "input_value", - "name": "B" - } - ], - "inputsInline": true, - "output": "Boolean", - "colour": "%{BKY_LOGIC_HUE}", - "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", - "colour": "%{BKY_LOGIC_HUE}", - "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", - "colour": "%{BKY_LOGIC_HUE}", - "tooltip": "%{BKY_LOGIC_NEGATE_TOOLTIP}", - "helpUrl": "%{BKY_LOGIC_NEGATE_HELPURL}" - }, - // Block for null data type. - { - "type": "logic_null", - "message0": "%{BKY_LOGIC_NULL}", - "output": null, - "colour": "%{BKY_LOGIC_HUE}", - "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, - "colour": "%{BKY_LOGIC_HUE}", - "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, - "colour": "%{BKY_LOGIC_HUE}", - "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, - "colour": "%{BKY_LOGIC_HUE}", - "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, - "colour": "%{BKY_LOGIC_HUE}", - "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, - - /** - * 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 = document.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.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('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 'Unknown block type.'; - } - clauseBlock = clauseBlock.nextConnection && - clauseBlock.nextConnection.targetBlock(); - } - this.updateShape_(); - // Reconnect any child blocks. - 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'); - }, - /** - * 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 'Unknown block type.'; - } - clauseBlock = clauseBlock.nextConnection && - clauseBlock.nextConnection.targetBlock(); - } - }, - /** - * 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 (var 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); - } - } -}; - -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); - -/** - * Corrects the logic_compare dropdown label with respect to language direction. - * @this Blockly.Block - * @package - */ -Blockly.Constants.Logic.fixLogicCompareRtlOpLabels = - function() { - var rtlOpLabels = { - 'LT': '\u200F<\u200F', - 'LTE': '\u200F\u2264\u200F', - 'GT': '\u200F>\u200F', - 'GTE': '\u200F\u2265\u200F' - }; - var opDropdown = this.getField('OP'); - if (opDropdown) { - var options = opDropdown.getOptions(); - for (var i = 0; i < options.length; ++i) { - var tuple = options[i]; - var op = tuple[1]; - var rtlLabel = rtlOpLabels[op]; - if (goog.isString(tuple[0]) && rtlLabel) { - // Replace LTR text label - tuple[0] = rtlLabel; - } - } - } - }; - -/** - * 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 = { - prevBlocks_: [null, null], - - /** - * 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) { - 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 && - !blockA.outputConnection.checkType_(blockB.outputConnection)) { - // Mismatch between two inputs. Disconnect previous and bump it away. - // Ensure that any disconnections are grouped with the causing event. - Blockly.Events.setGroup(e.group); - for (var i = 0; i < this.prevBlocks_.length; i++) { - var block = this.prevBlocks_[i]; - if (block === blockA || block === blockB) { - block.unplug(); - block.bumpNeighbours_(); - } - } - Blockly.Events.setGroup(false); - } - this.prevBlocks_[0] = blockA; - this.prevBlocks_[1] = blockB; - } -}; - -/** - * "logic_compare" extension function. Corrects direction of operators in the - * dropdown labels, and adds type left and right side type checking to - * "logic_compare" blocks. - * @this Blockly.Block - * @package - * @readonly - */ -Blockly.Constants.Logic.LOGIC_COMPARE_EXTENSION = function() { - // Fix operator labels in RTL - if (this.RTL) { - Blockly.Constants.Logic.fixLogicCompareRtlOpLabels.apply(this); - } - - // Add onchange handler to ensure types are compatable. - 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.outputConnection.checkType_(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..d2a7405fffa --- /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 type {Block} from '../core/block.js'; +import type {BlockSvg} from '../core/block_svg.js'; +import { + createBlockDefinitionsFromJsonArray, + defineBlocks, +} from '../core/common.js'; +import type {Connection} from '../core/connection.js'; +import * as Events from '../core/events/events.js'; +import type {Abstract as AbstractEvent} from '../core/events/events_abstract.js'; +import * as Extensions from '../core/extensions.js'; +import '../core/field_dropdown.js'; +import '../core/field_label.js'; +import '../core/icons/mutator_icon.js'; +import {Msg} from '../core/msg.js'; +import * as xmlUtils from '../core/utils/xml.js'; +import type {Workspace} from '../core/workspace.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 f0d2a98b986..00000000000 --- a/blocks/loops.js +++ /dev/null @@ -1,341 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 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.Blocks'); - - -/** - * Common HSV hue for all blocks in this category. - * Should be the same as Blockly.Msg.LOOPS_HUE - * @readonly - */ -Blockly.Constants.Loops.HUE = 120; -/** @deprecated Use Blockly.Constants.Loops.HUE */ -Blockly.Blocks.loops.HUE = Blockly.Constants.Loops.HUE; - -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, - "colour": "%{BKY_LOOPS_HUE}", - "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, - "colour": "%{BKY_LOOPS_HUE}", - "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, - "colour": "%{BKY_LOOPS_HUE}", - "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, - "colour": "%{BKY_LOOPS_HUE}", - "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, - "colour": "%{BKY_LOOPS_HUE}", - "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, - "colour": "%{BKY_LOOPS_HUE}", - "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) { - var varName = this.getFieldValue('VAR'); - if (!this.isCollapsed() && varName != null) { - var option = {enabled: true}; - option.text = - Blockly.Msg.VARIABLES_SET_CREATE_GET.replace('%1', varName); - var xmlField = goog.dom.createDom('field', null, varName); - xmlField.setAttribute('name', 'VAR'); - var xmlBlock = goog.dom.createDom('block', null, xmlField); - xmlBlock.setAttribute('type', 'variables_get'); - 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.buildTooltipWithFieldValue( - Blockly.Msg.CONTROLS_FOR_TOOLTIP, 'VAR')); - -Blockly.Extensions.register('controls_forEach_tooltip', - Blockly.Extensions.buildTooltipWithFieldValue( - Blockly.Msg.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_CHECK_IN_LOOP_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.Blocks['controls_flow_statements'].LOOP_TYPES.push('custom_loop'); - */ - LOOP_TYPES: ['controls_repeat', 'controls_repeat_ext', 'controls_forEach', - 'controls_for', 'controls_whileUntil'], - - /** - * 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 loop? - var block = this; - do { - if (this.LOOP_TYPES.indexOf(block.type) != -1) { - legal = true; - break; - } - block = block.getSurroundParent(); - } while (block); - if (legal) { - this.setWarningText(null); - if (!this.isInFlyout) { - this.setDisabled(false); - } - } else { - this.setWarningText(Blockly.Msg.CONTROLS_FLOW_STATEMENTS_WARNING); - if (!this.isInFlyout && !this.getInheritedDisabled()) { - this.setDisabled(true); - } - } - } -}; - -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..dd5a8116211 --- /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 {Block} from '../core/block.js'; +import { + createBlockDefinitionsFromJsonArray, + defineBlocks, +} from '../core/common.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 type {Abstract as AbstractEvent} from '../core/events/events_abstract.js'; +import * as eventUtils from '../core/events/utils.js'; +import * as Extensions from '../core/extensions.js'; +import '../core/field_dropdown.js'; +import '../core/field_label.js'; +import '../core/field_number.js'; +import '../core/field_variable.js'; +import {FieldVariable} from '../core/field_variable.js'; +import '../core/icons/warning_icon.js'; +import {Msg} from '../core/msg.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 0aff2ab3d46..00000000000 --- a/blocks/math.js +++ /dev/null @@ -1,566 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 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.Blocks'); - - -/** - * Common HSV hue for all blocks in this category. - * Should be the same as Blockly.Msg.MATH_HUE - * @readonly - */ -Blockly.Constants.Math.HUE = 230; -/** @deprecated Use Blockly.Constants.Math.HUE */ -Blockly.Blocks.math.HUE = Blockly.Constants.Math.HUE; - -Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT - // Block for numeric value. - { - "type": "math_number", - "message0": "%1", - "args0": [{ - "type": "field_number", - "name": "NUM", - "value": 0 - }], - "output": "Number", - "colour": "%{BKY_MATH_HUE}", - "helpUrl": "%{BKY_MATH_NUMBER_HELPURL}", - "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", - "colour": "%{BKY_MATH_HUE}", - "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", - "colour": "%{BKY_MATH_HUE}", - "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", - "colour": "%{BKY_MATH_HUE}", - "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", - "colour": "%{BKY_MATH_HUE}", - "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", - "colour": "%{BKY_MATH_HUE}", - "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, - "colour": "%{BKY_VARIABLES_HUE}", - "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", - "colour": "%{BKY_MATH_HUE}", - "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", - "colour": "%{BKY_MATH_HUE}", - "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", - "colour": "%{BKY_MATH_HUE}", - "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", - "colour": "%{BKY_MATH_HUE}", - "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", - "colour": "%{BKY_MATH_HUE}", - "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", - "colour": "%{BKY_MATH_HUE}", - "tooltip": "%{BKY_MATH_RANDOM_FLOAT_TOOLTIP}", - "helpUrl": "%{BKY_MATH_RANDOM_FLOAT_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 = document.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 "divisble 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.sourceBlock_.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. - * @this Blockly.Block - * @package - */ -Blockly.Constants.Math.CHANGE_TOOLTIP_EXTENSION = function() { - this.setTooltip(function() { - return Blockly.Msg.MATH_CHANGE_TOOLTIP.replace('%1', - this.getFieldValue('VAR')); - }.bind(this)); -}; - -Blockly.Extensions.register('math_change_tooltip', - Blockly.Extensions.buildTooltipWithFieldValue( - Blockly.Msg.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 = document.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..e5aef5fbb6e --- /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 type {Block} from '../core/block.js'; +import { + createBlockDefinitionsFromJsonArray, + defineBlocks, +} from '../core/common.js'; +import * as Extensions from '../core/extensions.js'; +import '../core/field_dropdown.js'; +import type {FieldDropdown} from '../core/field_dropdown.js'; +import '../core/field_label.js'; +import '../core/field_number.js'; +import '../core/field_variable.js'; +import * as xmlUtils from '../core/utils/xml.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 223a655cd19..00000000000 --- a/blocks/procedures.js +++ /dev/null @@ -1,889 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Procedure blocks for Blockly. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Blocks.procedures'); - -goog.require('Blockly.Blocks'); - - -/** - * Common HSV hue for all blocks in this category. - */ -Blockly.Blocks.procedures.HUE = 290; - -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.setColour(Blockly.Blocks.procedures.HUE); - this.setTooltip(Blockly.Msg.PROCEDURES_DEFNORETURN_TOOLTIP); - this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFNORETURN_HELPURL); - this.arguments_ = []; - 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. - * Display a warning if there are duplicately named parameters. - * @private - * @this Blockly.Block - */ - updateParams_: function() { - // Check for duplicated arguments. - var badArg = false; - var hash = {}; - for (var i = 0; i < this.arguments_.length; i++) { - if (hash['arg_' + this.arguments_[i].toLowerCase()]) { - badArg = true; - break; - } - hash['arg_' + this.arguments_[i].toLowerCase()] = true; - } - if (badArg) { - this.setWarningText(Blockly.Msg.PROCEDURES_DEF_DUPLICATE_WARNING); - } else { - this.setWarningText(null); - } - // 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 = document.createElement('mutation'); - if (opt_paramIds) { - container.setAttribute('name', this.getFieldValue('NAME')); - } - for (var i = 0; i < this.arguments_.length; i++) { - var parameter = document.createElement('arg'); - parameter.setAttribute('name', this.arguments_[i]); - 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_ = []; - for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { - if (childNode.nodeName.toLowerCase() == 'arg') { - this.arguments_.push(childNode.getAttribute('name')); - } - } - 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) { - var containerBlock = workspace.newBlock('procedures_mutatorcontainer'); - containerBlock.initSvg(); - - // Check/uncheck the allow statement box. - if (this.getInput('RETURN')) { - containerBlock.setFieldValue(this.hasStatements_ ? 'TRUE' : 'FALSE', - 'STATEMENTS'); - } else { - containerBlock.getInput('STATEMENT_INPUT').setVisible(false); - } - - // Parameter list. - var connection = containerBlock.getInput('STACK').connection; - for (var i = 0; i < this.arguments_.length; i++) { - var paramBlock = workspace.newBlock('procedures_mutatorarg'); - paramBlock.initSvg(); - paramBlock.setFieldValue(this.arguments_[i], 'NAME'); - // Store the old location. - paramBlock.oldLocation = i; - connection.connect(paramBlock.previousConnection); - connection = paramBlock.nextConnection; - } - // 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_ = []; - var paramBlock = containerBlock.getInputTargetBlock('STACK'); - while (paramBlock) { - this.arguments_.push(paramBlock.getFieldValue('NAME')); - 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_; - }, - /** - * Notification that a variable is renaming. - * If the name matches one of this block's variables, rename it. - * @param {string} oldName Previous name of variable. - * @param {string} newName Renamed variable. - * @this Blockly.Block - */ - renameVar: function(oldName, newName) { - var change = false; - for (var i = 0; i < this.arguments_.length; i++) { - if (Blockly.Names.equals(oldName, this.arguments_[i])) { - this.arguments_[i] = newName; - change = true; - } - } - if (change) { - this.updateParams_(); - // Update the mutator's variables if the mutator is open. - if (this.mutator.isVisible()) { - var blocks = this.mutator.workspace_.getAllBlocks(); - 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) { - // 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 = goog.dom.createDom('mutation'); - xmlMutation.setAttribute('name', name); - for (var i = 0; i < this.arguments_.length; i++) { - var xmlArg = goog.dom.createDom('arg'); - xmlArg.setAttribute('name', this.arguments_[i]); - xmlMutation.appendChild(xmlArg); - } - var xmlBlock = goog.dom.createDom('block', null, xmlMutation); - xmlBlock.setAttribute('type', this.callType_); - 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.arguments_.length; i++) { - var option = {enabled: true}; - var name = this.arguments_[i]; - option.text = Blockly.Msg.VARIABLES_SET_CREATE_GET.replace('%1', name); - var xmlField = goog.dom.createDom('field', null, name); - xmlField.setAttribute('name', 'VAR'); - var xmlBlock = goog.dom.createDom('block', null, xmlField); - xmlBlock.setAttribute('type', 'variables_get'); - option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); - options.push(option); - } - } - }, - 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.setColour(Blockly.Blocks.procedures.HUE); - this.setTooltip(Blockly.Msg.PROCEDURES_DEFRETURN_TOOLTIP); - this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFRETURN_HELPURL); - this.arguments_ = []; - 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, - renameVar: Blockly.Blocks['procedures_defnoreturn'].renameVar, - 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.setColour(Blockly.Blocks.procedures.HUE); - 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('x', this.validator_); - this.appendDummyInput() - .appendField(Blockly.Msg.PROCEDURES_MUTATORARG_TITLE) - .appendField(field, 'NAME'); - this.setPreviousStatement(true); - this.setNextStatement(true); - this.setColour(Blockly.Blocks.procedures.HUE); - 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.createNewVar_; - field.onFinishEditing_('x'); - }, - /** - * Obtain a valid name for the procedure. - * Merge runs of whitespace. Strip leading and trailing whitespace. - * Beyond this, all names are legal. - * @param {string} newVar User-supplied name. - * @return {?string} Valid name, or null if a name was not specified. - * @private - * @this Blockly.Block - */ - validator_: function(newVar) { - newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); - return newVar || null; - }, - /** - * Called when focusing away from the text field. - * Creates a new variable with this name. - * @param {string} newText The new variable name. - * @private - * @this Blockly.FieldTextInput - */ - createNewVar_: function(newText) { - var source = this.sourceBlock_; - if (source && source.workspace && source.workspace.options && - source.workspace.options.parentWorkspace) { - source.workspace.options.parentWorkspace.createVariable(newText); - } - } -}; - -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.setColour(Blockly.Blocks.procedures.HUE); - // Tooltip is set in renameProcedure. - this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLNORETURN_HELPURL); - this.arguments_ = []; - this.quarkConnections_ = {}; - this.quarkIds_ = null; - }, - /** - * 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'); - this.setTooltip( - (this.outputConnection ? Blockly.Msg.PROCEDURES_CALLRETURN_TOOLTIP : - Blockly.Msg.PROCEDURES_CALLNORETURN_TOOLTIP) - .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; - } - if (goog.array.equals(this.arguments_, paramNames)) { - // No change. - this.quarkIds_ = paramIds; - return; - } - if (paramIds.length != paramNames.length) { - throw 'Error: paramNames and paramIds must be the same length.'; - } - this.setCollapsed(false); - if (!this.quarkIds_) { - // Initialize tracking for this block. - this.quarkConnections_ = {}; - if (paramNames.join('\n') == this.arguments_.join('\n')) { - // No change to the parameters, allow quarkConnections_ to be - // populated with the existing connections. - this.quarkIds_ = paramIds; - } else { - 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); - 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 = document.createElement('mutation'); - container.setAttribute('name', this.getProcedureCall()); - for (var i = 0; i < this.arguments_.length; i++) { - var parameter = document.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); - }, - /** - * Notification that a variable is renaming. - * If the name matches one of this block's variables, rename it. - * @param {string} oldName Previous name of variable. - * @param {string} newName Renamed variable. - * @this Blockly.Block - */ - renameVar: function(oldName, newName) { - for (var i = 0; i < this.arguments_.length; i++) { - if (Blockly.Names.equals(oldName, this.arguments_[i])) { - this.arguments_[i] = newName; - this.getField('ARGNAME' + i).setValue(newName); - } - } - }, - /** - * 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.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.arguments_) != 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 = goog.dom.createDom('xml'); - var block = goog.dom.createDom('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 = goog.dom.createDom('field'); - field.setAttribute('name', 'NAME'); - field.appendChild(document.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, false); - Blockly.Events.setGroup(false); - } - } - }, - /** - * 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) { - 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); - def && 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.setColour(Blockly.Blocks.procedures.HUE); - // Tooltip is set in domToMutation. - this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLRETURN_HELPURL); - this.arguments_ = []; - this.quarkConnections_ = {}; - this.quarkIds_ = null; - }, - 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, - renameVar: Blockly.Blocks['procedures_callnoreturn'].renameVar, - 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.setColour(Blockly.Blocks.procedures.HUE); - 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 = document.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.setDisabled(false); - } - } else { - this.setWarningText(Blockly.Msg.PROCEDURES_IFRETURN_WARNING); - if (!this.isInFlyout && !this.getInheritedDisabled()) { - this.setDisabled(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: - * 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..20d8fa36bb0 --- /dev/null +++ b/blocks/procedures.ts @@ -0,0 +1,1363 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.libraryBlocks.procedures + +import type {Block} from '../core/block.js'; +import type {BlockSvg} from '../core/block_svg.js'; +import type {BlockDefinition} from '../core/blocks.js'; +import * as common from '../core/common.js'; +import {defineBlocks} from '../core/common.js'; +import {config} from '../core/config.js'; +import type {Connection} from '../core/connection.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 type {Abstract as AbstractEvent} from '../core/events/events_abstract.js'; +import type {BlockChange} from '../core/events/events_block_change.js'; +import type {BlockCreate} from '../core/events/events_block_create.js'; +import * as eventUtils from '../core/events/utils.js'; +import {FieldCheckbox} from '../core/field_checkbox.js'; +import {FieldLabel} from '../core/field_label.js'; +import * as fieldRegistry from '../core/field_registry.js'; +import {FieldTextInput} from '../core/field_textinput.js'; +import '../core/icons/comment_icon.js'; +import {MutatorIcon as Mutator} from '../core/icons/mutator_icon.js'; +import '../core/icons/warning_icon.js'; +import {Align} from '../core/inputs/align.js'; +import {Msg} from '../core/msg.js'; +import {Names} from '../core/names.js'; +import * as Procedures from '../core/procedures.js'; +import * as xmlUtils from '../core/utils/xml.js'; +import type {VariableModel} from '../core/variable_model.js'; +import * as Variables from '../core/variables.js'; +import type {Workspace} from '../core/workspace.js'; +import type {WorkspaceSvg} from '../core/workspace_svg.js'; +import * as Xml from '../core/xml.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 (!def.isEnabled()) { + this.setDisabledReason( + true, + DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON, + ); + this.setWarningText( + Msg['PROCEDURES_CALL_DISABLED_DEF_WARNING'].replace('%1', name), + ); + } + } 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 renameProcedure. + 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 348f9957924..00000000000 --- a/blocks/text.js +++ /dev/null @@ -1,892 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 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.Blocks'); - - -/** - * Common HSV hue for all blocks in this category. - * Should be the same as Blockly.Msg.TEXTS_HUE - * @readonly - */ -Blockly.Constants.Text.HUE = 160; -/** @deprecated Use Blockly.Constants.Text.HUE */ -Blockly.Blocks.texts.HUE = Blockly.Constants.Text.HUE; - -Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT - // Block for text value - { - "type": "text", - "message0": "%1", - "args0": [{ - "type": "field_input", - "name": "TEXT", - "text": "" - }], - "output": "String", - "colour": "%{BKY_TEXTS_HUE}", - "helpUrl": "%{BKY_TEXT_TEXT_HELPURL}", - "tooltip": "%{BKY_TEXT_TEXT_TOOLTIP}", - "extensions": [ - "text_quotes", - "parent_tooltip_when_inline" - ] - }, - { - "type": "text_join", - "message0": "", - "output": "String", - "colour": "%{BKY_TEXTS_HUE}", - "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" - }], - "colour": "%{BKY_TEXTS_HUE}", - "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, - "colour": "%{BKY_TEXTS_HUE}", - "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, - "colour": "%{BKY_TEXTS_HUE}", - "extensions": [ - "text_append_tooltip" - ] - }, - { - "type": "text_length", - "message0": "%{BKY_TEXT_LENGTH_TITLE}", - "args0": [ - { - "type": "input_value", - "name": "VALUE", - "check": ['String', 'Array'] - } - ], - "output": 'Number', - "colour": "%{BKY_TEXTS_HUE}", - "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', - "colour": "%{BKY_TEXTS_HUE}", - "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", - "colour": "%{BKY_TEXTS_HUE}", - "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": "input_dummy", - "name": "AT" - } - ], - "output": "String", - "colour": "%{BKY_TEXTS_HUE}", - "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.setColour(Blockly.Blocks.texts.HUE); - 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 = document.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, independant 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.sourceBlock_; - 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'); - } - } -}; - -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.setColour(Blockly.Blocks.texts.HUE); - 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.setColour(Blockly.Blocks.texts.HUE); - 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, - "colour": Blockly.Blocks.texts.HUE, - "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.setColour(Blockly.Blocks.texts.HUE); - // 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 = document.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.setColour(Blockly.Blocks.texts.HUE); - 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, - "colour": Blockly.Blocks.texts.HUE, - "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, - "colour": Blockly.Blocks.texts.HUE, - "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, - "colour": Blockly.Blocks.texts.HUE, - "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 couble quote). - * @readonly - */ - QUOTE_IMAGE_LEFT_DATAURI: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAn0lEQVQI1z3OMa5BURSF4f/cQhAKjUQhuQmFNwGJEUi0RKN5rU7FHKhpjEH3TEMtkdBSCY1EIv8r7nFX9e29V7EBAOvu7RPjwmWGH/VuF8CyN9/OAdvqIXYLvtRaNjx9mMTDyo+NjAN1HNcl9ZQ5oQMM3dgDUqDo1l8DzvwmtZN7mnD+PkmLa+4mhrxVA9fRowBWmVBhFy5gYEjKMfz9AylsaRRgGzvZAAAAAElFTkSuQmCC', - /** - * Image data URI of an LTR closing double quote (same as RTL opening couble quote). - * @readonly - */ - QUOTE_IMAGE_RIGHT_DATAURI: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAqUlEQVQI1z3KvUpCcRiA8ef9E4JNHhI0aFEacm1o0BsI0Slx8wa8gLauoDnoBhq7DcfWhggONDmJJgqCPA7neJ7p934EOOKOnM8Q7PDElo/4x4lFb2DmuUjcUzS3URnGib9qaPNbuXvBO3sGPHJDRG6fGVdMSeWDP2q99FQdFrz26Gu5Tq7dFMzUvbXy8KXeAj57cOklgA+u1B5AoslLtGIHQMaCVnwDnADZIFIrXsoXrgAAAABJRU5ErkJggg==', - /** - * 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. - */ - 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). - * @returns {!Blockly.FieldImage} The new field. - */ - 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. */ -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 = document.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); - 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. -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'])); -}; - -Blockly.Constants.Text.TEXT_APPEND_TOOLTIP_EXTENSION = function() { - // Assign 'this' to a variable for use in the tooltip closure below. - var thisBlock = this; - this.setTooltip(function() { - if (Blockly.Msg.TEXT_APPEND_TOOLTIP) { - return Blockly.Msg.TEXT_APPEND_TOOLTIP.replace('%1', - thisBlock.getFieldValue('VAR')); - } - return ''; - }); -}; - -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 = document.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' 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'); - } - if (Blockly.Msg.TEXT_CHARAT_TAIL) { - this.removeInput('TAIL', true); - this.appendDummyInput('TAIL') - .appendField(Blockly.Msg.TEXT_CHARAT_TAIL); - } - 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.sourceBlock_; - 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'); - } -}; - -// Does the initial mutator update of text_charAt and adds the tooltip -Blockly.Constants.Text.TEXT_CHARAT_EXTENSION = function() { - this.WHERE_OPTIONS = [ - [Blockly.Msg.TEXT_CHARAT_FROM_START, 'FROM_START'], - [Blockly.Msg.TEXT_CHARAT_FROM_END, 'FROM_END'], - [Blockly.Msg.TEXT_CHARAT_FIRST, 'FIRST'], - [Blockly.Msg.TEXT_CHARAT_LAST, 'LAST'], - [Blockly.Msg.TEXT_CHARAT_RANDOM, 'RANDOM'] - ]; - 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.register('text_append_tooltip', - Blockly.Constants.Text.TEXT_APPEND_TOOLTIP_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); \ No newline at end of file diff --git a/blocks/text.ts b/blocks/text.ts new file mode 100644 index 00000000000..a7ad5374ac4 --- /dev/null +++ b/blocks/text.ts @@ -0,0 +1,1001 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.libraryBlocks.texts + +import type {Block} from '../core/block.js'; +import type {BlockSvg} from '../core/block_svg.js'; +import { + createBlockDefinitionsFromJsonArray, + defineBlocks, +} from '../core/common.js'; +import {Connection} from '../core/connection.js'; +import * as Extensions from '../core/extensions.js'; +import {FieldDropdown} from '../core/field_dropdown.js'; +import {FieldImage} from '../core/field_image.js'; +import * as fieldRegistry from '../core/field_registry.js'; +import {FieldTextInput} from '../core/field_textinput.js'; +import '../core/field_variable.js'; +import {MutatorIcon} from '../core/icons/mutator_icon.js'; +import {Align} from '../core/inputs/align.js'; +import {ValueInput} from '../core/inputs/value_input.js'; +import {Msg} from '../core/msg.js'; +import * as xmlUtils from '../core/utils/xml.js'; +import type {Workspace} from '../core/workspace.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']); + const createMenu = (n: 1 | 2): FieldDropdown => { + 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. */ + function (this: FieldDropdown, value: any): null | undefined { + const oldValue: string | null = this.getValue(); + const oldAt = oldValue === 'FROM_START' || oldValue === 'FROM_END'; + const newAt = value === 'FROM_START' || value === 'FROM_END'; + if (newAt !== oldAt) { + const block = this.getSourceBlock() as GetSubstringBlock; + block.updateAt_(n, newAt); + } + return undefined; + }, + ); + return menu; + }; + this.appendDummyInput('WHERE1_INPUT').appendField(createMenu(1), 'WHERE1'); + this.appendDummyInput('AT1'); + this.appendDummyInput('WHERE2_INPUT').appendField(createMenu(2), 'WHERE2'); + 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']); + } + if (n === 1) { + this.moveInputBefore('AT1', 'WHERE2_INPUT'); + if (this.getInput('ORDINAL1')) { + this.moveInputBefore('ORDINAL1', 'WHERE2_INPUT'); + } + } + }, +}; + +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')!); + }, + + // These blocks do 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. +}; + +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']; + }); + }, +}; + +type PromptBlock = Block & PromptCommonMixin & QuoteImageMixin; + +blocks['text_prompt'] = { + ...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_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 07ae8e6e50e..00000000000 --- a/blocks/variables.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 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.Blocks'); - - -/** - * Common HSV hue for all blocks in this category. - * Should be the same as Blockly.Msg.VARIABLES_HUE. - * @readonly - */ -Blockly.Constants.Variables.HUE = 330; -/** @deprecated Use Blockly.Constants.Variables.HUE */ -Blockly.Blocks.variables.HUE = Blockly.Constants.Variables.HUE; - -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, - "colour": "%{BKY_VARIABLES_HUE}", - "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, - "colour": "%{BKY_VARIABLES_HUE}", - "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) { - // 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.getFieldValue('VAR'); - option.text = contextMenuMsg.replace('%1', name); - var xmlField = goog.dom.createDom('field', null, name); - xmlField.setAttribute('name', 'VAR'); - var xmlBlock = goog.dom.createDom('block', null, xmlField); - xmlBlock.setAttribute('type', opposite_type); - option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); - options.push(option); - } -}; - -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..0ec9112a3d6 --- /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 type {Block} from '../core/block.js'; +import { + createBlockDefinitionsFromJsonArray, + defineBlocks, +} from '../core/common.js'; +import * as ContextMenu from '../core/contextmenu.js'; +import type { + ContextMenuOption, + LegacyContextMenuOption, +} from '../core/contextmenu_registry.js'; +import * as Extensions from '../core/extensions.js'; +import '../core/field_label.js'; +import {FieldVariable} from '../core/field_variable.js'; +import {Msg} from '../core/msg.js'; +import * as Variables from '../core/variables.js'; +import type {WorkspaceSvg} from '../core/workspace_svg.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.ts b/blocks/variables_dynamic.ts new file mode 100644 index 00000000000..8e4ce290e09 --- /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 type {Block} from '../core/block.js'; +import { + createBlockDefinitionsFromJsonArray, + defineBlocks, +} from '../core/common.js'; +import * as ContextMenu from '../core/contextmenu.js'; +import type { + ContextMenuOption, + LegacyContextMenuOption, +} from '../core/contextmenu_registry.js'; +import {Abstract as AbstractEvent} from '../core/events/events_abstract.js'; +import * as Extensions from '../core/extensions.js'; +import '../core/field_label.js'; +import {FieldVariable} from '../core/field_variable.js'; +import {Msg} from '../core/msg.js'; +import * as Variables from '../core/variables.js'; +import type {WorkspaceSvg} from '../core/workspace_svg.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 c8d48e444f6..00000000000 --- a/blocks_compressed.js +++ /dev/null @@ -1,157 +0,0 @@ -// Do not edit this file; automatically generated by build.py. -'use strict'; - - -// Copyright 2012 Google Inc. Apache License 2.0 -Blockly.Blocks.lists={};Blockly.Constants={};Blockly.Constants.Lists={};Blockly.Constants.Lists.HUE=260;Blockly.Blocks.lists.HUE=Blockly.Constants.Lists.HUE; -Blockly.defineBlocksWithJsonArray([{type:"lists_create_empty",message0:"%{BKY_LISTS_CREATE_EMPTY_TITLE}",output:"Array",colour:"%{BKY_LISTS_HUE}",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",colour:"%{BKY_LISTS_HUE}",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,colour:"%{BKY_LISTS_HUE}",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",colour:"%{BKY_LISTS_HUE}",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",colour:"%{BKY_LISTS_HUE}",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.setColour(Blockly.Blocks.lists.HUE);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=document.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"],["\u2265","GTE"]]},{type:"input_value",name:"B"}],inputsInline:!0,output:"Boolean",colour:"%{BKY_LOGIC_HUE}",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",colour:"%{BKY_LOGIC_HUE}",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",colour:"%{BKY_LOGIC_HUE}",tooltip:"%{BKY_LOGIC_NEGATE_TOOLTIP}",helpUrl:"%{BKY_LOGIC_NEGATE_HELPURL}"},{type:"logic_null",message0:"%{BKY_LOGIC_NULL}",output:null,colour:"%{BKY_LOGIC_HUE}",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,colour:"%{BKY_LOGIC_HUE}",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,colour:"%{BKY_LOGIC_HUE}",tooltip:"%{BKY_CONTROLS_IF_IF_TOOLTIP}"},{type:"controls_if_elseif",message0:"%{BKY_CONTROLS_IF_ELSEIF_TITLE_ELSEIF}",previousStatement:null,nextStatement:null,enableContextMenu:!1,colour:"%{BKY_LOGIC_HUE}",tooltip:"%{BKY_CONTROLS_IF_ELSEIF_TOOLTIP}"},{type:"controls_if_else",message0:"%{BKY_CONTROLS_IF_ELSE_TITLE_ELSE}",previousStatement:null, -enableContextMenu:!1,colour:"%{BKY_LOGIC_HUE}",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,mutationToDom:function(){if(!this.elseifCount_&&!this.elseCount_)return null;var a=document.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.updateShape_()},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){var b=a.nextConnection.targetBlock();this.elseCount_=this.elseifCount_=0;a=[null];for(var c=[null],d=null;b;){switch(b.type){case "controls_if_elseif":this.elseifCount_++;a.push(b.valueConnection_);c.push(b.statementConnection_); -break;case "controls_if_else":this.elseCount_++;d=b.statementConnection_;break;default:throw"Unknown block type.";}b=b.nextConnection&&b.nextConnection.targetBlock()}this.updateShape_();for(b=1;b<=this.elseifCount_;b++)Blockly.Mutator.reconnect(a[b],this,"IF"+b),Blockly.Mutator.reconnect(c[b],this,"DO"+b);Blockly.Mutator.reconnect(d,this,"ELSE")},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"Unknown block type.";}a=a.nextConnection&&a.nextConnection.targetBlock()}},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)}};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.fixLogicCompareRtlOpLabels=function(){var a={LT:"\u200f<\u200f",LTE:"\u200f\u2264\u200f",GT:"\u200f>\u200f",GTE:"\u200f\u2265\u200f"},b=this.getField("OP");if(b)for(var b=b.getOptions(),c=0;ce;e++){var f=1==e?b:c;f&&!f.outputConnection.checkType_(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); \ No newline at end of file diff --git a/build.py b/build.py deleted file mode 100755 index e9f4f39e8ea..00000000000 --- a/build.py +++ /dev/null @@ -1,552 +0,0 @@ -#!/usr/bin/python2.7 -# Compresses the core Blockly files into a single JavaScript file. -# -# Copyright 2012 Google Inc. -# https://developers.google.com/blockly/ -# -# 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. - -# Usage: build.py <0 or more of accessible, core, generators, langfiles> -# build.py with no parameters builds all files. -# core builds blockly_compressed, blockly_uncompressed, and blocks_compressed. -# accessible builds blockly_accessible_compressed, -# blockly_accessible_uncompressed, and blocks_compressed. -# generators builds every _compressed.js. -# langfiles builds every msg/js/.js file. - -# This script generates four versions of Blockly's core files. The first pair -# are: -# 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 developement -# cycle since there is no need to rebuild or recompile, just reload. -# -# The second pair are: -# blockly_accessible_compressed.js -# blockly_accessible_uncompressed.js -# These files are analogous to blockly_compressed and blockly_uncompressed, -# but also include the visually-impaired module for Blockly. -# -# 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. -# dart_compressed.js: The compressed Dart generator. -# lua_compressed.js: The compressed Lua generator. -# msg/js/.js for every language defined in msg/js/.json. - -import sys -if sys.version_info[0] != 2: - raise Exception("Blockly build only compatible with Python 2.x.\n" - "You are using: " + sys.version) - -for arg in sys.argv[1:len(sys.argv)]: - if (arg != 'core' and - arg != 'accessible' and - arg != 'generators' and - arg != 'langfiles'): - raise Exception("Invalid argument: \"" + arg + "\". Usage: build.py <0 or more of accessible," + - " core, generators, langfiles>") - -import errno, glob, httplib, json, os, re, subprocess, threading, urllib - - -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(""" -var isNodeJS = !!(typeof module !== 'undefined' && module.exports && - typeof window === 'undefined'); - -if (isNodeJS) { - var window = {}; - require('closure-library'); -} - -window.BLOCKLY_DIR = (function() { - if (!isNodeJS) { - // 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 ''; -})(); - -window.BLOCKLY_BOOT = function() { - var dir = ''; - if (isNodeJS) { - require('closure-library'); - dir = 'blockly'; - } else { - // Execute after Closure has loaded. - if (!window.goog) { - alert('Error: Closure not found. Read this:\\n' + - 'developers.google.com/blockly/guides/modify/web/closure'); - } - dir = window.BLOCKLY_DIR.match(/[^\\/]+$/)[0]; - } -""") - 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) - # Find the Blockly directory name and replace it with a JS variable. - # This allows blockly_uncompressed.js to be compiled on one computer and be - # used on another, even if the directory name differs. - m = re.search('[\\/]([^\\/]+)[\\/]core[\\/]blockly.js', add_dependency) - add_dependency = re.sub('([\\/])' + re.escape(m.group(1)) + - '([\\/]core[\\/])', '\\1" + dir + "\\2', add_dependency) - f.write(add_dependency + '\n') - - provides = [] - for dep in calcdeps.BuildDependenciesFromFiles(self.search_paths): - if not dep.filename.startswith(os.pardir + os.sep): # '../' - provides.extend(dep.provides) - provides.sort() # Deterministic build. - f.write('\n') - f.write('// Load Blockly.\n') - for provide in provides: - f.write("goog.require('%s');\n" % provide) - - f.write(""" -delete this.BLOCKLY_DIR; -delete this.BLOCKLY_BOOT; -}; - -if (isNodeJS) { - window.BLOCKLY_BOOT(); - module.exports = Blockly; -} else { - // Delete any existing Closure (e.g. Soy's nogoog_shim). - document.write(''); - // Load fresh Closure Library. - 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 ('core' in self.bundles): - self.gen_core() - - if ('accessible' in self.bundles): - self.gen_accessible() - - if ('core' in self.bundles or 'accessible' in self.bundles): - self.gen_blocks() - - if ('generators' in self.bundles): - self.gen_generator("javascript") - self.gen_generator("python") - self.gen_generator("php") - self.gen_generator("dart") - self.gen_generator("lua") - - def gen_core(self): - target_filename = "blockly_compressed.js" - # Define the parameters for the POST request. - params = [ - ("compilation_level", "SIMPLE_OPTIMIZATIONS"), - ("use_closure_library", "true"), - ("output_format", "json"), - ("output_info", "compiled_code"), - ("output_info", "warnings"), - ("output_info", "errors"), - ("output_info", "statistics"), - ] - - # Read in all the source files. - filenames = calcdeps.CalculateDependencies(self.search_paths, - [os.path.join("core", "blockly.js")]) - filenames.sort() # Deterministic build. - for filename in filenames: - # Filter out the Closure files (the compiler will add them). - if filename.startswith(os.pardir + os.sep): # '../' - continue - f = open(filename) - params.append(("js_code", "".join(f.readlines()))) - f.close() - - self.do_compile(params, target_filename, filenames, "") - - def gen_accessible(self): - target_filename = "blockly_accessible_compressed.js" - # Define the parameters for the POST request. - params = [ - ("compilation_level", "SIMPLE_OPTIMIZATIONS"), - ("use_closure_library", "true"), - ("language_out", "ES5"), - ("output_format", "json"), - ("output_info", "compiled_code"), - ("output_info", "warnings"), - ("output_info", "errors"), - ("output_info", "statistics"), - ] - - # Read in all the source files. - filenames = calcdeps.CalculateDependencies(self.search_paths, - [os.path.join("accessible", "app.component.js")]) - filenames.sort() # Deterministic build. - for filename in filenames: - # Filter out the Closure files (the compiler will add them). - if filename.startswith(os.pardir + os.sep): # '../' - continue - f = open(filename) - params.append(("js_code", "".join(f.readlines()))) - 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"), - ] - - # Read in all the source files. - # Add Blockly.Blocks to be compatible with the compiler. - params.append(("js_code", "goog.provide('Blockly.Blocks');")) - filenames = glob.glob(os.path.join("blocks", "*.js")) - filenames.sort() # Deterministic build. - for filename in filenames: - f = open(filename) - params.append(("js_code", "".join(f.readlines()))) - f.close() - - # Remove Blockly.Blocks to be compatible with Blockly. - remove = "var Blockly={Blocks:{}};" - 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"), - ] - - # Read in all the source files. - # Add Blockly.Generator to be compatible with the compiler. - params.append(("js_code", "goog.provide('Blockly.Generator');")) - 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 = open(filename) - params.append(("js_code", "".join(f.readlines()))) - f.close() - filenames.insert(0, "[goog.provide]") - - # Remove Blockly.Generator to be compatible with Blockly. - remove = "var Blockly={Generator:{}};" - 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.HTTPConnection("closure-compiler.appspot.com") - conn.request("POST", "/compile", urllib.urlencode(params), headers) - response = conn.getresponse() - json_str = response.read() - conn.close() - - # Parse the JSON response. - json_data = json.loads(json_str) - - def file_lookup(name): - if not name.startswith("Input_"): - return "???" - n = int(name[6:]) - 1 - return filenames[n] - - if json_data.has_key("serverErrors"): - errors = json_data["serverErrors"] - for error in errors: - print("SERVER ERROR: %s" % target_filename) - print(error["error"]) - elif json_data.has_key("errors"): - 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 json_data.has_key("warnings"): - 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 json_data.has_key("compiledCode"): - print("FATAL ERROR: Compiler did not return compiledCode.") - sys.exit(1) - - code = HEADER + "\n" + json_data["compiledCode"] - code = code.replace(remove, "") - - # Trim down Google's Apache licences. - # The Closure Compiler used to preserve these until August 2015. - # Delete this in a few months if the licences don't return. - LICENSE = re.compile("""/\\* - - [\w ]+ - - (Copyright \\d+ Google Inc.) - https://developers.google.com/blockly/ - - 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. -\\*/""") - code = re.sub(LICENSE, r"\n// \1 Apache License 2.0", 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") - - -class Gen_langfiles(threading.Thread): - """Generate JavaScript file for each natural language supported. - - Runs in a separate thread. - """ - - def __init__(self, force_gen): - threading.Thread.__init__(self) - self.force_gen = force_gen - - def _rebuild(self, srcs, dests): - # Determine whether any of the files in srcs is newer than any in dests. - try: - return (max(os.path.getmtime(src) for src in srcs) > - min(os.path.getmtime(dest) for dest in dests)) - except OSError as e: - # Was a file not found? - if e.errno == errno.ENOENT: - # If it was a source file, we can't proceed. - if e.filename in srcs: - print("Source file missing: " + e.filename) - sys.exit(1) - else: - # If a destination file was missing, rebuild. - return True - else: - print("Error checking file creation times: " + e) - - def run(self): - # The files msg/json/{en,qqq,synonyms}.json depend on msg/messages.js. - if (self.force_gen or - self._rebuild([os.path.join("msg", "messages.js")], - [os.path.join("msg", "json", f) for f in - ["en.json", "qqq.json", "synonyms.json"]])): - try: - subprocess.check_call([ - "python", - os.path.join("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 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("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 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) - - -if __name__ == "__main__": - try: - calcdeps = import_path(os.path.join( - os.path.pardir, "closure-library", "closure", "bin", "calcdeps.py")) - except ImportError: - if os.path.isdir(os.path.join(os.path.pardir, "closure-library-read-only")): - # Dir got renamed when Closure moved from Google Code to GitHub in 2014. - print("Error: Closure directory needs to be renamed from" - "'closure-library-read-only' to 'closure-library'.\n" - "Please rename this directory.") - elif os.path.isdir(os.path.join(os.path.pardir, "google-closure-library")): - # When Closure is installed by npm, it is named "google-closure-library". - #calcdeps = import_path(os.path.join( - # os.path.pardir, "google-closure-library", "closure", "bin", "calcdeps.py")) - print("Error: Closure directory needs to be renamed from" - "'google-closure-library' to 'closure-library'.\n" - "Please rename this directory.") - else: - print("""Error: Closure not found. Read this: -developers.google.com/blockly/guides/modify/web/closure""") - sys.exit(1) - - core_search_paths = calcdeps.ExpandDirectories( - ["core", os.path.join(os.path.pardir, "closure-library")]) - core_search_paths.sort() # Deterministic build. - full_search_paths = calcdeps.ExpandDirectories( - ["accessible", "core", os.path.join(os.path.pardir, "closure-library")]) - full_search_paths.sort() # Deterministic build. - - if (len(sys.argv) == 1): - args = ['core', 'accessible', 'generators', 'defaultlangfiles'] - else: - args = sys.argv - - # Uncompressed and compressed are run in parallel threads. - # Uncompressed is limited by processor speed. - if ('core' in args): - Gen_uncompressed(core_search_paths, 'blockly_uncompressed.js').start() - - if ('accessible' in args): - Gen_uncompressed(full_search_paths, 'blockly_accessible_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 - # defaultlangfiles checks for changes in the msg files, while manually asking - # to build langfiles will force the messages to be rebuilt. - if ('langfiles' in args or 'defaultlangfiles' in args): - Gen_langfiles('langfiles' in args).start() diff --git a/core/any_aliases.ts b/core/any_aliases.ts new file mode 100644 index 00000000000..b04621726db --- /dev/null +++ b/core/any_aliases.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// eslint-disable-next-line +type AnyDuringMigration = any; diff --git a/core/block.js b/core/block.js deleted file mode 100644 index da77fe13514..00000000000 --- a/core/block.js +++ /dev/null @@ -1,1503 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2011 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 The class representing one block. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Block'); - -goog.require('Blockly.Blocks'); -goog.require('Blockly.Comment'); -goog.require('Blockly.Connection'); -goog.require('Blockly.Extensions'); -goog.require('Blockly.Input'); -goog.require('Blockly.Mutator'); -goog.require('Blockly.Warning'); -goog.require('Blockly.Workspace'); -goog.require('Blockly.Xml'); -goog.require('goog.array'); -goog.require('goog.asserts'); -goog.require('goog.math.Coordinate'); -goog.require('goog.string'); - - -/** - * Class for one block. - * Not normally called directly, workspace.newBlock() is preferred. - * @param {!Blockly.Workspace} workspace The block's workspace. - * @param {?string} prototypeName Name of the language object containing - * type-specific functions for this block. - * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise - * create a new id. - * @constructor - */ -Blockly.Block = function(workspace, prototypeName, opt_id) { - /** @type {string} */ - this.id = (opt_id && !workspace.getBlockById(opt_id)) ? - opt_id : Blockly.utils.genUid(); - workspace.blockDB_[this.id] = this; - /** @type {Blockly.Connection} */ - this.outputConnection = null; - /** @type {Blockly.Connection} */ - this.nextConnection = null; - /** @type {Blockly.Connection} */ - this.previousConnection = null; - /** @type {!Array.} */ - this.inputList = []; - /** @type {boolean|undefined} */ - this.inputsInline = undefined; - /** @type {boolean} */ - this.disabled = false; - /** @type {string|!Function} */ - this.tooltip = ''; - /** @type {boolean} */ - this.contextMenu = true; - - /** - * @type {Blockly.Block} - * @private - */ - this.parentBlock_ = null; - - /** - * @type {!Array.} - * @private - */ - this.childBlocks_ = []; - - /** - * @type {boolean} - * @private - */ - this.deletable_ = true; - - /** - * @type {boolean} - * @private - */ - this.movable_ = true; - - /** - * @type {boolean} - * @private - */ - this.editable_ = true; - - /** - * @type {boolean} - * @private - */ - this.isShadow_ = false; - - /** - * @type {boolean} - * @private - */ - this.collapsed_ = false; - - /** @type {string|Blockly.Comment} */ - this.comment = null; - - /** - * The block's position in workspace units. (0, 0) is at the workspace's - * origin; scale does not change this value. - * @type {!goog.math.Coordinate} - * @private - */ - this.xy_ = new goog.math.Coordinate(0, 0); - - /** @type {!Blockly.Workspace} */ - this.workspace = workspace; - /** @type {boolean} */ - this.isInFlyout = workspace.isFlyout; - /** @type {boolean} */ - this.isInMutator = workspace.isMutator; - - /** @type {boolean} */ - this.RTL = workspace.RTL; - - // Copy the type-specific functions and data from the prototype. - if (prototypeName) { - /** @type {string} */ - this.type = prototypeName; - var prototype = Blockly.Blocks[prototypeName]; - goog.asserts.assertObject(prototype, - 'Error: Unknown block type "%s".', prototypeName); - goog.mixin(this, prototype); - } - - workspace.addTopBlock(this); - - // Call an initialization function, if it exists. - if (goog.isFunction(this.init)) { - this.init(); - } - // Record initial inline state. - /** @type {boolean|undefined} */ - this.inputsInlineDefault = this.inputsInline; - if (Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.BlockCreate(this)); - } - // Bind an onchange function, if it exists. - if (goog.isFunction(this.onchange)) { - this.setOnChange(this.onchange); - } -}; - -/** - * Obtain a newly created block. - * @param {!Blockly.Workspace} workspace The block's workspace. - * @param {?string} prototypeName Name of the language object containing - * type-specific functions for this block. - * @return {!Blockly.Block} The created block. - * @deprecated December 2015 - */ -Blockly.Block.obtain = function(workspace, prototypeName) { - console.warn('Deprecated call to Blockly.Block.obtain, ' + - 'use workspace.newBlock instead.'); - return workspace.newBlock(prototypeName); -}; - -/** - * Optional text data that round-trips beween blocks and XML. - * Has no effect. May be used by 3rd parties for meta information. - * @type {?string} - */ -Blockly.Block.prototype.data = null; - -/** - * Colour of the block in '#RRGGBB' format. - * @type {string} - * @private - */ -Blockly.Block.prototype.colour_ = '#000000'; - -/** - * Dispose of this block. - * @param {boolean} healStack If true, then try to heal any gap by connecting - * the next statement with the previous statement. Otherwise, dispose of - * all children of this block. - */ -Blockly.Block.prototype.dispose = function(healStack) { - if (!this.workspace) { - // Already deleted. - return; - } - // Terminate onchange event calls. - if (this.onchangeWrapper_) { - this.workspace.removeChangeListener(this.onchangeWrapper_); - } - this.unplug(healStack); - if (Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.BlockDelete(this)); - } - Blockly.Events.disable(); - - try { - // This block is now at the top of the workspace. - // Remove this block from the workspace's list of top-most blocks. - if (this.workspace) { - this.workspace.removeTopBlock(this); - // Remove from block database. - delete this.workspace.blockDB_[this.id]; - this.workspace = null; - } - - // Just deleting this block from the DOM would result in a memory leak as - // well as corruption of the connection database. Therefore we must - // methodically step through the blocks and carefully disassemble them. - - // First, dispose of all my children. - for (var i = this.childBlocks_.length - 1; i >= 0; i--) { - this.childBlocks_[i].dispose(false); - } - // Then dispose of myself. - // Dispose of all inputs and their fields. - for (var i = 0, input; input = this.inputList[i]; i++) { - input.dispose(); - } - this.inputList.length = 0; - // Dispose of any remaining connections (next/previous/output). - var connections = this.getConnections_(true); - for (var i = 0; i < connections.length; i++) { - var connection = connections[i]; - if (connection.isConnected()) { - connection.disconnect(); - } - connections[i].dispose(); - } - } finally { - Blockly.Events.enable(); - } -}; - -/** - * Unplug this block from its superior block. If this block is a statement, - * optionally reconnect the block underneath with the block on top. - * @param {boolean} opt_healStack Disconnect child statement and reconnect - * stack. Defaults to false. - */ -Blockly.Block.prototype.unplug = function(opt_healStack) { - if (this.outputConnection) { - if (this.outputConnection.isConnected()) { - // Disconnect from any superior block. - this.outputConnection.disconnect(); - } - } else if (this.previousConnection) { - var previousTarget = null; - if (this.previousConnection.isConnected()) { - // Remember the connection that any next statements need to connect to. - previousTarget = this.previousConnection.targetConnection; - // Detach this block from the parent's tree. - this.previousConnection.disconnect(); - } - var nextBlock = this.getNextBlock(); - if (opt_healStack && nextBlock) { - // Disconnect the next statement. - var nextTarget = this.nextConnection.targetConnection; - nextTarget.disconnect(); - if (previousTarget && previousTarget.checkType_(nextTarget)) { - // Attach the next statement to the previous statement. - previousTarget.connect(nextTarget); - } - } - } -}; - -/** - * Returns all connections originating from this block. - * @return {!Array.} Array of connections. - * @private - */ -Blockly.Block.prototype.getConnections_ = function() { - var myConnections = []; - if (this.outputConnection) { - myConnections.push(this.outputConnection); - } - if (this.previousConnection) { - myConnections.push(this.previousConnection); - } - if (this.nextConnection) { - myConnections.push(this.nextConnection); - } - for (var i = 0, input; input = this.inputList[i]; i++) { - if (input.connection) { - myConnections.push(input.connection); - } - } - return myConnections; -}; - -/** - * Walks down a stack of blocks and finds the last next connection on the stack. - * @return {Blockly.Connection} The last next connection on the stack, or null. - * @package - */ -Blockly.Block.prototype.lastConnectionInStack_ = function() { - var nextConnection = this.nextConnection; - while (nextConnection) { - var nextBlock = nextConnection.targetBlock(); - if (!nextBlock) { - // Found a next connection with nothing on the other side. - return nextConnection; - } - nextConnection = nextBlock.nextConnection; - } - // Ran out of next connections. - return null; -}; - -/** - * Bump unconnected blocks out of alignment. Two blocks which aren't actually - * connected should not coincidentally line up on screen. - * @private - */ -Blockly.Block.prototype.bumpNeighbours_ = function() { - console.warn('Not expected to reach this bumpNeighbours_ function. The ' + - 'BlockSvg function for bumpNeighbours_ was expected to be called instead.'); -}; - -/** - * Return the parent block or null if this block is at the top level. - * @return {Blockly.Block} The block that holds the current block. - */ -Blockly.Block.prototype.getParent = function() { - // Look at the DOM to see if we are nested in another block. - return this.parentBlock_; -}; - -/** - * Return the input that connects to the specified block. - * @param {!Blockly.Block} block A block connected to an input on this block. - * @return {Blockly.Input} The input that connects to the specified block. - */ -Blockly.Block.prototype.getInputWithBlock = function(block) { - for (var i = 0, input; input = this.inputList[i]; i++) { - if (input.connection && input.connection.targetBlock() == block) { - return input; - } - } - return null; -}; - -/** - * Return the parent block that surrounds the current block, or null if this - * block has no surrounding block. A parent block might just be the previous - * statement, whereas the surrounding block is an if statement, while loop, etc. - * @return {Blockly.Block} The block that surrounds the current block. - */ -Blockly.Block.prototype.getSurroundParent = function() { - var block = this; - do { - var prevBlock = block; - block = block.getParent(); - if (!block) { - // Ran off the top. - return null; - } - } while (block.getNextBlock() == prevBlock); - // This block is an enclosing parent, not just a statement in a stack. - return block; -}; - -/** - * Return the next statement block directly connected to this block. - * @return {Blockly.Block} The next statement block or null. - */ -Blockly.Block.prototype.getNextBlock = function() { - return this.nextConnection && this.nextConnection.targetBlock(); -}; - -/** - * Return the top-most block in this block's tree. - * This will return itself if this block is at the top level. - * @return {!Blockly.Block} The root block. - */ -Blockly.Block.prototype.getRootBlock = function() { - var rootBlock; - var block = this; - do { - rootBlock = block; - block = rootBlock.parentBlock_; - } while (block); - return rootBlock; -}; - -/** - * Find all the blocks that are directly nested inside this one. - * Includes value and block inputs, as well as any following statement. - * Excludes any connection on an output tab or any preceding statement. - * @return {!Array.} Array of blocks. - */ -Blockly.Block.prototype.getChildren = function() { - return this.childBlocks_; -}; - -/** - * Set parent of this block to be a new block or null. - * @param {Blockly.Block} newParent New parent block. - */ -Blockly.Block.prototype.setParent = function(newParent) { - if (newParent == this.parentBlock_) { - return; - } - if (this.parentBlock_) { - // Remove this block from the old parent's child list. - goog.array.remove(this.parentBlock_.childBlocks_, this); - - // Disconnect from superior blocks. - if (this.previousConnection && this.previousConnection.isConnected()) { - throw 'Still connected to previous block.'; - } - if (this.outputConnection && this.outputConnection.isConnected()) { - throw 'Still connected to parent block.'; - } - this.parentBlock_ = null; - // This block hasn't actually moved on-screen, so there's no need to update - // its connection locations. - } else { - // Remove this block from the workspace's list of top-most blocks. - this.workspace.removeTopBlock(this); - } - - this.parentBlock_ = newParent; - if (newParent) { - // Add this block to the new parent's child list. - newParent.childBlocks_.push(this); - } else { - this.workspace.addTopBlock(this); - } -}; - -/** - * Find all the blocks that are directly or indirectly nested inside this one. - * Includes this block in the list. - * Includes value and block inputs, as well as any following statements. - * Excludes any connection on an output tab or any preceding statements. - * @return {!Array.} Flattened array of blocks. - */ -Blockly.Block.prototype.getDescendants = function() { - var blocks = [this]; - for (var child, x = 0; child = this.childBlocks_[x]; x++) { - blocks.push.apply(blocks, child.getDescendants()); - } - return blocks; -}; - -/** - * Get whether this block is deletable or not. - * @return {boolean} True if deletable. - */ -Blockly.Block.prototype.isDeletable = function() { - return this.deletable_ && !this.isShadow_ && - !(this.workspace && this.workspace.options.readOnly); -}; - -/** - * Set whether this block is deletable or not. - * @param {boolean} deletable True if deletable. - */ -Blockly.Block.prototype.setDeletable = function(deletable) { - this.deletable_ = deletable; -}; - -/** - * Get whether this block is movable or not. - * @return {boolean} True if movable. - */ -Blockly.Block.prototype.isMovable = function() { - return this.movable_ && !this.isShadow_ && - !(this.workspace && this.workspace.options.readOnly); -}; - -/** - * Set whether this block is movable or not. - * @param {boolean} movable True if movable. - */ -Blockly.Block.prototype.setMovable = function(movable) { - this.movable_ = movable; -}; - -/** - * Get whether this block is a shadow block or not. - * @return {boolean} True if a shadow. - */ -Blockly.Block.prototype.isShadow = function() { - return this.isShadow_; -}; - -/** - * Set whether this block is a shadow block or not. - * @param {boolean} shadow True if a shadow. - */ -Blockly.Block.prototype.setShadow = function(shadow) { - this.isShadow_ = shadow; -}; - -/** - * Get whether this block is editable or not. - * @return {boolean} True if editable. - */ -Blockly.Block.prototype.isEditable = function() { - return this.editable_ && !(this.workspace && this.workspace.options.readOnly); -}; - -/** - * Set whether this block is editable or not. - * @param {boolean} editable True if editable. - */ -Blockly.Block.prototype.setEditable = function(editable) { - this.editable_ = editable; - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { - field.updateEditable(); - } - } -}; - -/** - * Set whether the connections are hidden (not tracked in a database) or not. - * Recursively walk down all child blocks (except collapsed blocks). - * @param {boolean} hidden True if connections are hidden. - */ -Blockly.Block.prototype.setConnectionsHidden = function(hidden) { - if (!hidden && this.isCollapsed()) { - if (this.outputConnection) { - this.outputConnection.setHidden(hidden); - } - if (this.previousConnection) { - this.previousConnection.setHidden(hidden); - } - if (this.nextConnection) { - this.nextConnection.setHidden(hidden); - var child = this.nextConnection.targetBlock(); - if (child) { - child.setConnectionsHidden(hidden); - } - } - } else { - var myConnections = this.getConnections_(true); - for (var i = 0, connection; connection = myConnections[i]; i++) { - connection.setHidden(hidden); - if (connection.isSuperior()) { - var child = connection.targetBlock(); - if (child) { - child.setConnectionsHidden(hidden); - } - } - } - } -}; - -/** - * Set the URL of this block's help page. - * @param {string|Function} url URL string for block help, or function that - * returns a URL. Null for no help. - */ -Blockly.Block.prototype.setHelpUrl = function(url) { - this.helpUrl = url; -}; - -/** - * Change the tooltip text for a block. - * @param {string|!Function} newTip Text for tooltip or a parent element to - * link to for its tooltip. May be a function that returns a string. - */ -Blockly.Block.prototype.setTooltip = function(newTip) { - this.tooltip = newTip; -}; - -/** - * Get the colour of a block. - * @return {string} #RRGGBB string. - */ -Blockly.Block.prototype.getColour = function() { - return this.colour_; -}; - -/** - * Change the colour of a block. - * @param {number|string} colour HSV hue value, or #RRGGBB string. - */ -Blockly.Block.prototype.setColour = function(colour) { - var hue = Number(colour); - if (!isNaN(hue)) { - this.colour_ = Blockly.hueToRgb(hue); - } else if (goog.isString(colour) && colour.match(/^#[0-9a-fA-F]{6}$/)) { - this.colour_ = colour; - } else { - throw 'Invalid colour: ' + colour; - } -}; - -/** - * Sets a callback function to use whenever the block's parent workspace - * changes, replacing any prior onchange handler. This is usually only called - * from the constructor, the block type initializer function, or an extension - * initializer function. - * @param {function(Blockly.Events.Abstract)} onchangeFn The callback to call - * when the block's workspace changes. - * @throws {Error} if onchangeFn is not falsey or a function. - */ -Blockly.Block.prototype.setOnChange = function(onchangeFn) { - if (onchangeFn && !goog.isFunction(onchangeFn)) { - throw new Error("onchange must be a function."); - } - if (this.onchangeWrapper_) { - this.workspace.removeChangeListener(this.onchangeWrapper_); - } - this.onchange = onchangeFn; - if (this.onchange) { - this.onchangeWrapper_ = onchangeFn.bind(this); - this.workspace.addChangeListener(this.onchangeWrapper_); - } -}; - -/** - * Returns the named field from a block. - * @param {string} name The name of the field. - * @return {Blockly.Field} Named field, or null if field does not exist. - */ -Blockly.Block.prototype.getField = function(name) { - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { - if (field.name === name) { - return field; - } - } - } - return null; -}; - -/** - * Return all variables referenced by this block. - * @return {!Array.} List of variable names. - */ -Blockly.Block.prototype.getVars = function() { - var vars = []; - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { - if (field instanceof Blockly.FieldVariable) { - vars.push(field.getValue()); - } - } - } - return vars; -}; - -/** - * Notification that a variable is renaming. - * If the name matches one of this block's variables, rename it. - * @param {string} oldName Previous name of variable. - * @param {string} newName Renamed variable. - */ -Blockly.Block.prototype.renameVar = function(oldName, newName) { - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { - if (field instanceof Blockly.FieldVariable && - Blockly.Names.equals(oldName, field.getValue())) { - field.setValue(newName); - } - } - } -}; - -/** - * Returns the language-neutral value from the field of a block. - * @param {string} name The name of the field. - * @return {?string} Value from the field or null if field does not exist. - */ -Blockly.Block.prototype.getFieldValue = function(name) { - var field = this.getField(name); - if (field) { - return field.getValue(); - } - return null; -}; - -/** - * Change the field value for a block (e.g. 'CHOOSE' or 'REMOVE'). - * @param {string} newValue Value to be the new field. - * @param {string} name The name of the field. - */ -Blockly.Block.prototype.setFieldValue = function(newValue, name) { - var field = this.getField(name); - goog.asserts.assertObject(field, 'Field "%s" not found.', name); - field.setValue(newValue); -}; - -/** - * Set whether this block can chain onto the bottom of another block. - * @param {boolean} newBoolean True if there can be a previous statement. - * @param {string|Array.|null|undefined} opt_check Statement type or - * list of statement types. Null/undefined if any type could be connected. - */ -Blockly.Block.prototype.setPreviousStatement = function(newBoolean, opt_check) { - if (newBoolean) { - if (opt_check === undefined) { - opt_check = null; - } - if (!this.previousConnection) { - goog.asserts.assert(!this.outputConnection, - 'Remove output connection prior to adding previous connection.'); - this.previousConnection = - this.makeConnection_(Blockly.PREVIOUS_STATEMENT); - } - this.previousConnection.setCheck(opt_check); - } else { - if (this.previousConnection) { - goog.asserts.assert(!this.previousConnection.isConnected(), - 'Must disconnect previous statement before removing connection.'); - this.previousConnection.dispose(); - this.previousConnection = null; - } - } -}; - -/** - * Set whether another block can chain onto the bottom of this block. - * @param {boolean} newBoolean True if there can be a next statement. - * @param {string|Array.|null|undefined} opt_check Statement type or - * list of statement types. Null/undefined if any type could be connected. - */ -Blockly.Block.prototype.setNextStatement = function(newBoolean, opt_check) { - if (newBoolean) { - if (opt_check === undefined) { - opt_check = null; - } - if (!this.nextConnection) { - this.nextConnection = this.makeConnection_(Blockly.NEXT_STATEMENT); - } - this.nextConnection.setCheck(opt_check); - } else { - if (this.nextConnection) { - goog.asserts.assert(!this.nextConnection.isConnected(), - 'Must disconnect next statement before removing connection.'); - this.nextConnection.dispose(); - this.nextConnection = null; - } - } -}; - -/** - * Set whether this block returns a value. - * @param {boolean} newBoolean True if there is an output. - * @param {string|Array.|null|undefined} opt_check Returned type or list - * of returned types. Null or undefined if any type could be returned - * (e.g. variable get). - */ -Blockly.Block.prototype.setOutput = function(newBoolean, opt_check) { - if (newBoolean) { - if (opt_check === undefined) { - opt_check = null; - } - if (!this.outputConnection) { - goog.asserts.assert(!this.previousConnection, - 'Remove previous connection prior to adding output connection.'); - this.outputConnection = this.makeConnection_(Blockly.OUTPUT_VALUE); - } - this.outputConnection.setCheck(opt_check); - } else { - if (this.outputConnection) { - goog.asserts.assert(!this.outputConnection.isConnected(), - 'Must disconnect output value before removing connection.'); - this.outputConnection.dispose(); - this.outputConnection = null; - } - } -}; - -/** - * Set whether value inputs are arranged horizontally or vertically. - * @param {boolean} newBoolean True if inputs are horizontal. - */ -Blockly.Block.prototype.setInputsInline = function(newBoolean) { - if (this.inputsInline != newBoolean) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this, 'inline', null, this.inputsInline, newBoolean)); - this.inputsInline = newBoolean; - } -}; - -/** - * Get whether value inputs are arranged horizontally or vertically. - * @return {boolean} True if inputs are horizontal. - */ -Blockly.Block.prototype.getInputsInline = function() { - if (this.inputsInline != undefined) { - // Set explicitly. - return this.inputsInline; - } - // Not defined explicitly. Figure out what would look best. - for (var i = 1; i < this.inputList.length; i++) { - if (this.inputList[i - 1].type == Blockly.DUMMY_INPUT && - this.inputList[i].type == Blockly.DUMMY_INPUT) { - // Two dummy inputs in a row. Don't inline them. - return false; - } - } - for (var i = 1; i < this.inputList.length; i++) { - if (this.inputList[i - 1].type == Blockly.INPUT_VALUE && - this.inputList[i].type == Blockly.DUMMY_INPUT) { - // Dummy input after a value input. Inline them. - return true; - } - } - return false; -}; - -/** - * Set whether the block is disabled or not. - * @param {boolean} disabled True if disabled. - */ -Blockly.Block.prototype.setDisabled = function(disabled) { - if (this.disabled != disabled) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this, 'disabled', null, this.disabled, disabled)); - this.disabled = disabled; - } -}; - -/** - * Get whether the block is disabled or not due to parents. - * The block's own disabled property is not considered. - * @return {boolean} True if disabled. - */ -Blockly.Block.prototype.getInheritedDisabled = function() { - var ancestor = this.getSurroundParent(); - while (ancestor) { - if (ancestor.disabled) { - return true; - } - ancestor = ancestor.getSurroundParent(); - } - // Ran off the top. - return false; -}; - -/** - * Get whether the block is collapsed or not. - * @return {boolean} True if collapsed. - */ -Blockly.Block.prototype.isCollapsed = function() { - return this.collapsed_; -}; - -/** - * Set whether the block is collapsed or not. - * @param {boolean} collapsed True if collapsed. - */ -Blockly.Block.prototype.setCollapsed = function(collapsed) { - if (this.collapsed_ != collapsed) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this, 'collapsed', null, this.collapsed_, collapsed)); - this.collapsed_ = collapsed; - } -}; - -/** - * Create a human-readable text representation of this block and any children. - * @param {number=} opt_maxLength Truncate the string to this length. - * @param {string=} opt_emptyToken The placeholder string used to denote an - * empty field. If not specified, '?' is used. - * @return {string} Text of block. - */ -Blockly.Block.prototype.toString = function(opt_maxLength, opt_emptyToken) { - var text = []; - var emptyFieldPlaceholder = opt_emptyToken || '?'; - if (this.collapsed_) { - text.push(this.getInput('_TEMP_COLLAPSED_INPUT').fieldRow[0].text_); - } else { - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { - if (field instanceof Blockly.FieldDropdown && !field.getValue()) { - text.push(emptyFieldPlaceholder); - } else { - text.push(field.getText()); - } - } - if (input.connection) { - var child = input.connection.targetBlock(); - if (child) { - text.push(child.toString(undefined, opt_emptyToken)); - } else { - text.push(emptyFieldPlaceholder); - } - } - } - } - text = goog.string.trim(text.join(' ')) || '???'; - if (opt_maxLength) { - // TODO: Improve truncation so that text from this block is given priority. - // E.g. "1+2+3+4+5+6+7+8+9=0" should be "...6+7+8+9=0", not "1+2+3+4+5...". - // E.g. "1+2+3+4+5=6+7+8+9+0" should be "...4+5=6+7...". - text = goog.string.truncate(text, opt_maxLength); - } - return text; -}; - -/** - * Shortcut for appending a value input row. - * @param {string} name Language-neutral identifier which may used to find this - * input again. Should be unique to this block. - * @return {!Blockly.Input} The input object created. - */ -Blockly.Block.prototype.appendValueInput = function(name) { - return this.appendInput_(Blockly.INPUT_VALUE, name); -}; - -/** - * Shortcut for appending a statement input row. - * @param {string} name Language-neutral identifier which may used to find this - * input again. Should be unique to this block. - * @return {!Blockly.Input} The input object created. - */ -Blockly.Block.prototype.appendStatementInput = function(name) { - return this.appendInput_(Blockly.NEXT_STATEMENT, name); -}; - -/** - * Shortcut for appending a dummy input row. - * @param {string=} opt_name Language-neutral identifier which may used to find - * this input again. Should be unique to this block. - * @return {!Blockly.Input} The input object created. - */ -Blockly.Block.prototype.appendDummyInput = function(opt_name) { - return this.appendInput_(Blockly.DUMMY_INPUT, opt_name || ''); -}; - -/** - * Initialize this block using a cross-platform, internationalization-friendly - * JSON description. - * @param {!Object} json Structured data describing the block. - */ -Blockly.Block.prototype.jsonInit = function(json) { - - // Validate inputs. - goog.asserts.assert(json['output'] == undefined || - json['previousStatement'] == undefined, - 'Must not have both an output and a previousStatement.'); - - // Set basic properties of block. - if (json['colour'] !== undefined) { - var rawValue = json['colour']; - var colour = goog.isString(rawValue) ? - Blockly.utils.replaceMessageReferences(rawValue) : rawValue; - this.setColour(colour); - } - - // Interpolate the message blocks. - var i = 0; - while (json['message' + i] !== undefined) { - this.interpolate_(json['message' + i], json['args' + i] || [], - json['lastDummyAlign' + i]); - i++; - } - - if (json['inputsInline'] !== undefined) { - this.setInputsInline(json['inputsInline']); - } - // Set output and previous/next connections. - if (json['output'] !== undefined) { - this.setOutput(true, json['output']); - } - if (json['previousStatement'] !== undefined) { - this.setPreviousStatement(true, json['previousStatement']); - } - if (json['nextStatement'] !== undefined) { - this.setNextStatement(true, json['nextStatement']); - } - if (json['tooltip'] !== undefined) { - var rawValue = json['tooltip']; - var localizedText = Blockly.utils.replaceMessageReferences(rawValue); - this.setTooltip(localizedText); - } - if (json['enableContextMenu'] !== undefined) { - var rawValue = json['enableContextMenu']; - this.contextMenu = !!rawValue; - } - if (json['helpUrl'] !== undefined) { - var rawValue = json['helpUrl']; - var localizedValue = Blockly.utils.replaceMessageReferences(rawValue); - this.setHelpUrl(localizedValue); - } - if (goog.isString(json['extensions'])) { - console.warn('JSON attribute \'extensions\' should be an array of ' + - 'strings. Found raw string in JSON for \'' + json['type'] + '\' block.'); - json['extensions'] = [json['extensions']]; // Correct and continue. - } - - // Add the mutator to the block - if (json['mutator'] !== undefined) { - Blockly.Extensions.apply(json['mutator'], this, true); - } - - if (Array.isArray(json['extensions'])) { - var extensionNames = json['extensions']; - for (var i = 0; i < extensionNames.length; ++i) { - var extensionName = extensionNames[i]; - Blockly.Extensions.apply(extensionName, this, false); - } - } -}; - -/** - * Add key/values from mixinObj to this block object. By default, this method - * will check that the keys in mixinObj will not overwrite existing values in - * the block, including prototype values. This provides some insurance against - * mixin / extension incompatibilities with future block features. This check - * can be disabled by passing true as the second argument. - * @param {!Object} mixinObj The key/values pairs to add to this block object. - * @param {boolean=} opt_disableCheck Option flag to disable overwrite checks. - */ -Blockly.Block.prototype.mixin = function(mixinObj, opt_disableCheck) { - if (goog.isDef(opt_disableCheck) && !goog.isBoolean(opt_disableCheck)) { - throw new Error("opt_disableCheck must be a boolean if provided"); - } - if (!opt_disableCheck) { - var overwrites = []; - for (var key in mixinObj) { - if (this[key] !== undefined) { - overwrites.push(key); - } - } - if (overwrites.length) { - throw new Error('Mixin will overwrite block members: ' + - JSON.stringify(overwrites)); - } - } - goog.mixin(this, mixinObj); -}; - -/** - * Interpolate a message description onto the block. - * @param {string} message Text contains interpolation tokens (%1, %2, ...) - * that match with fields or inputs defined in the args array. - * @param {!Array} args Array of arguments to be interpolated. - * @param {string=} lastDummyAlign If a dummy input is added at the end, - * how should it be aligned? - * @private - */ -Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) { - var tokens = Blockly.utils.tokenizeInterpolation(message); - // Interpolate the arguments. Build a list of elements. - var indexDup = []; - var indexCount = 0; - var elements = []; - for (var i = 0; i < tokens.length; i++) { - var token = tokens[i]; - if (typeof token == 'number') { - if (token <= 0 || token > args.length) { - throw new Error('Block \"' + this.type + '\": ' + - 'Message index %' + token + ' out of range.'); - } - if (indexDup[token]) { - throw new Error('Block \"' + this.type + '\": ' + - 'Message index %' + token + ' duplicated.'); - } - indexDup[token] = true; - indexCount++; - elements.push(args[token - 1]); - } else { - token = token.trim(); - if (token) { - elements.push(token); - } - } - } - if(indexCount != args.length) { - throw new Error('Block \"' + this.type + '\": ' + - 'Message does not reference all ' + args.length + ' arg(s).'); - } - // Add last dummy input if needed. - if (elements.length && (typeof elements[elements.length - 1] == 'string' || - goog.string.startsWith(elements[elements.length - 1]['type'], - 'field_'))) { - var dummyInput = {type: 'input_dummy'}; - if (lastDummyAlign) { - dummyInput['align'] = lastDummyAlign; - } - elements.push(dummyInput); - } - // Lookup of alignment constants. - var alignmentLookup = { - 'LEFT': Blockly.ALIGN_LEFT, - 'RIGHT': Blockly.ALIGN_RIGHT, - 'CENTRE': Blockly.ALIGN_CENTRE - }; - // Populate block with inputs and fields. - var fieldStack = []; - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; - if (typeof element == 'string') { - fieldStack.push([element, undefined]); - } else { - var field = null; - var input = null; - do { - var altRepeat = false; - if (typeof element == 'string') { - field = new Blockly.FieldLabel(element); - } else { - switch (element['type']) { - case 'input_value': - input = this.appendValueInput(element['name']); - break; - case 'input_statement': - input = this.appendStatementInput(element['name']); - break; - case 'input_dummy': - input = this.appendDummyInput(element['name']); - break; - case 'field_label': - field = Blockly.Block.newFieldLabelFromJson_(element); - break; - case 'field_input': - field = Blockly.Block.newFieldTextInputFromJson_(element); - break; - case 'field_angle': - field = new Blockly.FieldAngle(element['angle']); - break; - case 'field_checkbox': - field = new Blockly.FieldCheckbox( - element['checked'] ? 'TRUE' : 'FALSE'); - break; - case 'field_colour': - field = new Blockly.FieldColour(element['colour']); - break; - case 'field_variable': - field = Blockly.Block.newFieldVariableFromJson_(element); - break; - case 'field_dropdown': - field = new Blockly.FieldDropdown(element['options']); - break; - case 'field_image': - field = Blockly.Block.newFieldImageFromJson_(element); - break; - case 'field_number': - field = new Blockly.FieldNumber(element['value'], - element['min'], element['max'], element['precision']); - break; - case 'field_date': - if (Blockly.FieldDate) { - field = new Blockly.FieldDate(element['date']); - break; - } - // Fall through if FieldDate is not compiled in. - default: - // Unknown field. - if (element['alt']) { - element = element['alt']; - altRepeat = true; - } - } - } - } while (altRepeat); - if (field) { - fieldStack.push([field, element['name']]); - } else if (input) { - if (element['check']) { - input.setCheck(element['check']); - } - if (element['align']) { - input.setAlign(alignmentLookup[element['align']]); - } - for (var j = 0; j < fieldStack.length; j++) { - input.appendField(fieldStack[j][0], fieldStack[j][1]); - } - fieldStack.length = 0; - } - } - } -}; - -/** - * Helper function to construct a FieldImage from a JSON arg object, - * dereferencing any string table references. - * @param {!Object} options A JSON object with options (src, width, height, and alt). - * @returns {!Blockly.FieldImage} The new image. - * @private - */ -Blockly.Block.newFieldImageFromJson_ = function(options) { - var src = Blockly.utils.replaceMessageReferences(options['src']); - var width = Number(Blockly.utils.replaceMessageReferences(options['width'])); - var height = - Number(Blockly.utils.replaceMessageReferences(options['height'])); - var alt = Blockly.utils.replaceMessageReferences(options['alt']); - return new Blockly.FieldImage(src, width, height, alt); -}; - -/** - * Helper function to construct a FieldLabel from a JSON arg object, - * dereferencing any string table references. - * @param {!Object} options A JSON object with options (text, and class). - * @returns {!Blockly.FieldLabel} The new label. - * @private - */ -Blockly.Block.newFieldLabelFromJson_ = function(options) { - var text = Blockly.utils.replaceMessageReferences(options['text']); - return new Blockly.FieldLabel(text, options['class']); -}; - -/** - * Helper function to construct a FieldTextInput from a JSON arg object, - * dereferencing any string table references. - * @param {!Object} options A JSON object with options (text, class, and - * spellcheck). - * @returns {!Blockly.FieldTextInput} The new text input. - * @private - */ -Blockly.Block.newFieldTextInputFromJson_ = function(options) { - var text = Blockly.utils.replaceMessageReferences(options['text']); - var field = new Blockly.FieldTextInput(text, options['class']); - if (typeof options['spellcheck'] == 'boolean') { - field.setSpellcheck(options['spellcheck']); - } - return field; -}; - -/** - * Helper function to construct a FieldVariable from a JSON arg object, - * dereferencing any string table references. - * @param {!Object} options A JSON object with options (variable). - * @returns {!Blockly.FieldVariable} The variable field. - * @private - */ -Blockly.Block.newFieldVariableFromJson_ = function(options) { - var varname = Blockly.utils.replaceMessageReferences(options['variable']); - return new Blockly.FieldVariable(varname); -}; - - -/** - * Add a value input, statement input or local variable to this block. - * @param {number} type Either Blockly.INPUT_VALUE or Blockly.NEXT_STATEMENT or - * Blockly.DUMMY_INPUT. - * @param {string} name Language-neutral identifier which may used to find this - * input again. Should be unique to this block. - * @return {!Blockly.Input} The input object created. - * @private - */ -Blockly.Block.prototype.appendInput_ = function(type, name) { - var connection = null; - if (type == Blockly.INPUT_VALUE || type == Blockly.NEXT_STATEMENT) { - connection = this.makeConnection_(type); - } - var input = new Blockly.Input(type, name, this, connection); - // Append input to list. - this.inputList.push(input); - return input; -}; - -/** - * Move a named input to a different location on this block. - * @param {string} name The name of the input to move. - * @param {?string} refName Name of input that should be after the moved input, - * or null to be the input at the end. - */ -Blockly.Block.prototype.moveInputBefore = function(name, refName) { - if (name == refName) { - return; - } - // Find both inputs. - var inputIndex = -1; - var refIndex = refName ? -1 : this.inputList.length; - for (var i = 0, input; input = this.inputList[i]; i++) { - if (input.name == name) { - inputIndex = i; - if (refIndex != -1) { - break; - } - } else if (refName && input.name == refName) { - refIndex = i; - if (inputIndex != -1) { - break; - } - } - } - goog.asserts.assert(inputIndex != -1, 'Named input "%s" not found.', name); - goog.asserts.assert(refIndex != -1, 'Reference input "%s" not found.', - refName); - this.moveNumberedInputBefore(inputIndex, refIndex); -}; - -/** - * Move a numbered input to a different location on this block. - * @param {number} inputIndex Index of the input to move. - * @param {number} refIndex Index of input that should be after the moved input. - */ -Blockly.Block.prototype.moveNumberedInputBefore = function( - inputIndex, refIndex) { - // Validate arguments. - goog.asserts.assert(inputIndex != refIndex, 'Can\'t move input to itself.'); - goog.asserts.assert(inputIndex < this.inputList.length, - 'Input index ' + inputIndex + ' out of bounds.'); - goog.asserts.assert(refIndex <= this.inputList.length, - 'Reference input ' + refIndex + ' out of bounds.'); - // Remove input. - var input = this.inputList[inputIndex]; - this.inputList.splice(inputIndex, 1); - if (inputIndex < refIndex) { - refIndex--; - } - // Reinsert input. - this.inputList.splice(refIndex, 0, input); -}; - -/** - * Remove an input from this block. - * @param {string} name The name of the input. - * @param {boolean=} opt_quiet True to prevent error if input is not present. - * @throws {goog.asserts.AssertionError} if the input is not present and - * opt_quiet is not true. - */ -Blockly.Block.prototype.removeInput = function(name, opt_quiet) { - for (var i = 0, input; input = this.inputList[i]; i++) { - if (input.name == name) { - if (input.connection && input.connection.isConnected()) { - input.connection.setShadowDom(null); - var block = input.connection.targetBlock(); - if (block.isShadow()) { - // Destroy any attached shadow block. - block.dispose(); - } else { - // Disconnect any attached normal block. - block.unplug(); - } - } - input.dispose(); - this.inputList.splice(i, 1); - return; - } - } - if (!opt_quiet) { - goog.asserts.fail('Input "%s" not found.', name); - } -}; - -/** - * Fetches the named input object. - * @param {string} name The name of the input. - * @return {Blockly.Input} The input object, or null if input does not exist. - */ -Blockly.Block.prototype.getInput = function(name) { - for (var i = 0, input; input = this.inputList[i]; i++) { - if (input.name == name) { - return input; - } - } - // This input does not exist. - return null; -}; - -/** - * Fetches the block attached to the named input. - * @param {string} name The name of the input. - * @return {Blockly.Block} The attached value block, or null if the input is - * either disconnected or if the input does not exist. - */ -Blockly.Block.prototype.getInputTargetBlock = function(name) { - var input = this.getInput(name); - return input && input.connection && input.connection.targetBlock(); -}; - -/** - * Returns the comment on this block (or '' if none). - * @return {string} Block's comment. - */ -Blockly.Block.prototype.getCommentText = function() { - return this.comment || ''; -}; - -/** - * Set this block's comment text. - * @param {?string} text The text, or null to delete. - */ -Blockly.Block.prototype.setCommentText = function(text) { - if (this.comment != text) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this, 'comment', null, this.comment, text || '')); - this.comment = text; - } -}; - -/** - * Set this block's warning text. - * @param {?string} text The text, or null to delete. - */ -Blockly.Block.prototype.setWarningText = function(/* text */) { - // NOP. -}; - -/** - * Give this block a mutator dialog. - * @param {Blockly.Mutator} mutator A mutator dialog instance or null to remove. - */ -Blockly.Block.prototype.setMutator = function(/* mutator */) { - // NOP. -}; - -/** - * Return the coordinates of the top-left corner of this block relative to the - * drawing surface's origin (0,0), in workspace units. - * @return {!goog.math.Coordinate} Object with .x and .y properties. - */ -Blockly.Block.prototype.getRelativeToSurfaceXY = function() { - return this.xy_; -}; - -/** - * Move a block by a relative offset. - * @param {number} dx Horizontal offset, in workspace units. - * @param {number} dy Vertical offset, in workspace units. - */ -Blockly.Block.prototype.moveBy = function(dx, dy) { - goog.asserts.assert(!this.parentBlock_, 'Block has parent.'); - var event = new Blockly.Events.BlockMove(this); - this.xy_.translate(dx, dy); - event.recordNew(); - Blockly.Events.fire(event); -}; - -/** - * Create a connection of the specified type. - * @param {number} type The type of the connection to create. - * @return {!Blockly.Connection} A new connection of the specified type. - * @private - */ -Blockly.Block.prototype.makeConnection_ = function(type) { - return new Blockly.Connection(this, type); -}; - -/** - * Recursively checks whether all statement and value inputs are filled with - * blocks. Also checks all following statement blocks in this stack. - * @param {boolean=} opt_shadowBlocksAreFilled An optional argument controlling - * whether shadow blocks are counted as filled. Defaults to true. - * @return {boolean} True if all inputs are filled, false otherwise. - */ -Blockly.Block.prototype.allInputsFilled = function(opt_shadowBlocksAreFilled) { - // Account for the shadow block filledness toggle. - if (opt_shadowBlocksAreFilled === undefined) { - opt_shadowBlocksAreFilled = true; - } - if (!opt_shadowBlocksAreFilled && this.isShadow()) { - return false; - } - - // Recursively check each input block of the current block. - for (var i = 0, input; input = this.inputList[i]; i++) { - if (!input.connection) { - continue; - } - var target = input.connection.targetBlock(); - if (!target || !target.allInputsFilled(opt_shadowBlocksAreFilled)) { - return false; - } - } - - // Recursively check the next block after the current block. - var next = this.getNextBlock(); - if (next) { - return next.allInputsFilled(opt_shadowBlocksAreFilled); - } - - return true; -}; - -/** - * This method returns a string describing this Block in developer terms (type - * name and ID; English only). - * - * Intended to on be used in console logs and errors. If you need a string that - * uses the user's native language (including block text, field values, and - * child blocks), use [toString()]{@link Blockly.Block#toString}. - * @return {string} The description. - */ -Blockly.Block.prototype.toDevString = function() { - var msg = this.type ? '"' + this.type + '" block' : 'Block'; - if (this.id) { - msg += ' (id="' + this.id + '")'; - } - return msg; -}; diff --git a/core/block.ts b/core/block.ts new file mode 100644 index 00000000000..0face8f8c9b --- /dev/null +++ b/core/block.ts @@ -0,0 +1,2540 @@ +/** + * @license + * Copyright 2011 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The class representing one block. + * + * @class + */ +// Former goog.module ID: Blockly.Block + +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_block_change.js'; +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_block_create.js'; +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_block_delete.js'; + +import {Blocks} from './blocks.js'; +import * as common from './common.js'; +import {Connection} from './connection.js'; +import {ConnectionType} from './connection_type.js'; +import * as constants from './constants.js'; +import type {Abstract} from './events/events_abstract.js'; +import type {BlockChange} from './events/events_block_change.js'; +import type {BlockMove} from './events/events_block_move.js'; +import {EventType} from './events/type.js'; +import * as eventUtils from './events/utils.js'; +import * as Extensions from './extensions.js'; +import type {Field} from './field.js'; +import * as fieldRegistry from './field_registry.js'; +import {DuplicateIconType} from './icons/exceptions.js'; +import {IconType} from './icons/icon_types.js'; +import type {MutatorIcon} from './icons/mutator_icon.js'; +import {Align} from './inputs/align.js'; +import {DummyInput} from './inputs/dummy_input.js'; +import {EndRowInput} from './inputs/end_row_input.js'; +import {Input} from './inputs/input.js'; +import {StatementInput} from './inputs/statement_input.js'; +import {ValueInput} from './inputs/value_input.js'; +import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js'; +import {isCommentIcon} from './interfaces/i_comment_icon.js'; +import {type IIcon} from './interfaces/i_icon.js'; +import * as registry from './registry.js'; +import * as Tooltip from './tooltip.js'; +import * as arrayUtils from './utils/array.js'; +import {Coordinate} from './utils/coordinate.js'; +import * as deprecation from './utils/deprecation.js'; +import * as idGenerator from './utils/idgenerator.js'; +import * as parsing from './utils/parsing.js'; +import {Size} from './utils/size.js'; +import type {VariableModel} from './variable_model.js'; +import type {Workspace} from './workspace.js'; + +/** + * Class for one block. + * Not normally called directly, workspace.newBlock() is preferred. + */ +export class Block implements IASTNodeLocation { + /** + * An optional callback method to use whenever the block's parent workspace + * changes. This is usually only called from the constructor, the block type + * initializer function, or an extension initializer function. + */ + onchange?: ((p1: Abstract) => void) | null; + + /** The language-neutral ID given to the collapsed input. */ + static readonly COLLAPSED_INPUT_NAME: string = constants.COLLAPSED_INPUT_NAME; + + /** The language-neutral ID given to the collapsed field. */ + static readonly COLLAPSED_FIELD_NAME: string = constants.COLLAPSED_FIELD_NAME; + + /** + * Optional text data that round-trips between blocks and XML. + * Has no effect. May be used by 3rd parties for meta information. + */ + data: string | null = null; + + /** + * Has this block been disposed of? + * + * @internal + */ + disposed = false; + + /** + * Colour of the block as HSV hue value (0-360) + * This may be null if the block colour was not set via a hue number. + */ + private hue: number | null = null; + + /** Colour of the block in '#RRGGBB' format. */ + protected colour_ = '#000000'; + + /** Name of the block style. */ + protected styleName_ = ''; + + /** An optional method called during initialization. */ + init?: () => void; + + /** An optional method called during disposal. */ + destroy?: () => void; + + /** + * An optional serialization method for defining how to serialize the + * mutation state to XML. This must be coupled with defining + * `domToMutation`. + */ + mutationToDom?: (...p1: AnyDuringMigration[]) => Element; + + /** + * An optional deserialization method for defining how to deserialize the + * mutation state from XML. This must be coupled with defining + * `mutationToDom`. + */ + domToMutation?: (p1: Element) => void; + + /** + * An optional serialization method for defining how to serialize the + * block's extra state (eg mutation state) to something JSON compatible. + * This must be coupled with defining `loadExtraState`. + * + * @param doFullSerialization Whether or not to serialize the full state of + * the extra state (rather than possibly saving a reference to some + * state). This is used during copy-paste. See the + * {@link https://developers.devsite.google.com/blockly/guides/create-custom-blocks/extensions#full_serialization_and_backing_data | block serialization docs} + * for more information. + */ + saveExtraState?: (doFullSerialization?: boolean) => AnyDuringMigration; + + /** + * An optional serialization method for defining how to deserialize the + * block's extra state (eg mutation state) from something JSON compatible. + * This must be coupled with defining `saveExtraState`. + */ + loadExtraState?: (p1: AnyDuringMigration) => void; + + /** + * An optional property for suppressing adding STATEMENT_PREFIX and + * STATEMENT_SUFFIX to generated code. + */ + suppressPrefixSuffix: boolean | null = false; + + /** + * An optional method for declaring developer variables, to be used + * by generators. Developer variables are never shown to the user, + * but are declared as global variables in the generated code. + * + * @returns a list of developer variable names. + */ + getDeveloperVariables?: () => string[]; + + /** + * An optional method that reconfigures the block based on the + * contents of the mutator dialog. + * + * @param rootBlock The root block in the mutator flyout. + */ + compose?: (rootBlock: Block) => void; + + /** + * An optional function that populates the mutator flyout with + * blocks representing this block's configuration. + * + * @param workspace The mutator flyout's workspace. + * @returns The root block created in the flyout's workspace. + */ + decompose?: (workspace: Workspace) => Block; + + id: string; + outputConnection: Connection | null = null; + nextConnection: Connection | null = null; + previousConnection: Connection | null = null; + inputList: Input[] = []; + inputsInline?: boolean; + icons: IIcon[] = []; + private disabledReasons = new Set(); + tooltip: Tooltip.TipInfo = ''; + contextMenu = true; + + protected parentBlock_: this | null = null; + + protected childBlocks_: this[] = []; + + private deletable = true; + + private movable = true; + + private editable = true; + + private shadow = false; + + protected collapsed_ = false; + protected outputShape_: number | null = null; + + /** + * Is the current block currently in the process of being disposed? + */ + protected disposing = false; + + /** + * Has this block been fully initialized? E.g. all fields initailized. + * + * @internal + */ + initialized = false; + + private readonly xy: Coordinate; + isInFlyout: boolean; + isInMutator: boolean; + RTL: boolean; + + /** True if this block is an insertion marker. */ + protected isInsertionMarker_ = false; + + /** Name of the type of hat. */ + hat?: string; + + /** Is this block a BlockSVG? */ + readonly rendered: boolean = false; + + /** + * String for block help, or function that returns a URL. Null for no help. + */ + helpUrl: string | (() => string) | null = null; + + /** A bound callback function to use when the parent workspace changes. */ + private onchangeWrapper: ((p1: Abstract) => void) | null = null; + + /** + * A count of statement inputs on the block. + * + * @internal + */ + statementInputCount = 0; + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + type!: string; + // Record initial inline state. + inputsInlineDefault?: boolean; + workspace: Workspace; + + /** + * @param workspace The block's workspace. + * @param prototypeName Name of the language object containing type-specific + * functions for this block. + * @param opt_id Optional ID. Use this ID if provided, otherwise create a new + * ID. + * @throws When the prototypeName is not valid or not allowed. + */ + constructor(workspace: Workspace, prototypeName: string, opt_id?: string) { + this.workspace = workspace; + + this.id = + opt_id && !workspace.getBlockById(opt_id) ? opt_id : idGenerator.genUid(); + workspace.setBlockById(this.id, this); + + /** + * The block's position in workspace units. (0, 0) is at the workspace's + * origin; scale does not change this value. + */ + this.xy = new Coordinate(0, 0); + this.isInFlyout = workspace.isFlyout; + this.isInMutator = workspace.isMutator; + + this.RTL = workspace.RTL; + + // Copy the type-specific functions and data from the prototype. + if (prototypeName) { + this.type = prototypeName; + const prototype = Blocks[prototypeName]; + if (!prototype || typeof prototype !== 'object') { + throw TypeError('Invalid block definition for type: ' + prototypeName); + } + Object.assign(this, prototype); + } + + workspace.addTopBlock(this); + workspace.addTypedBlock(this); + + if (new.target === Block) { + this.doInit_(); + } + } + + /** Calls the init() function and handles associated event firing, etc. */ + protected doInit_() { + // All events fired should be part of the same group. + // Any events fired during init should not be undoable, + // so that block creation is atomic. + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } + const initialUndoFlag = eventUtils.getRecordUndo(); + + try { + // Call an initialization function, if it exists. + if (typeof this.init === 'function') { + eventUtils.setRecordUndo(false); + this.init(); + eventUtils.setRecordUndo(initialUndoFlag); + } + + // Fire a create event. + if (eventUtils.isEnabled()) { + eventUtils.fire(new (eventUtils.get(EventType.BLOCK_CREATE))(this)); + } + } finally { + eventUtils.setGroup(existingGroup); + // In case init threw, recordUndo flag should still be reset. + eventUtils.setRecordUndo(initialUndoFlag); + } + this.inputsInlineDefault = this.inputsInline; + + // Bind an onchange function, if it exists. + if (typeof this.onchange === 'function') { + this.setOnChange(this.onchange); + } + } + + /** + * Dispose of this block. + * + * @param healStack If true, then try to heal any gap by connecting the next + * statement with the previous statement. Otherwise, dispose of all + * children of this block. + */ + dispose(healStack = false) { + this.disposing = true; + + // Dispose of this change listener before unplugging. + // Technically not necessary due to the event firing delay. + // But future-proofing. + if (this.onchangeWrapper) { + this.workspace.removeChangeListener(this.onchangeWrapper); + } + + this.unplug(healStack); + if (eventUtils.isEnabled()) { + // Constructing the delete event is costly. Only perform if necessary. + eventUtils.fire(new (eventUtils.get(EventType.BLOCK_DELETE))(this)); + } + this.workspace.removeTopBlock(this); + this.disposeInternal(); + } + + /** + * Disposes of this block without doing things required by the top block. + * E.g. does not fire events, unplug the block, etc. + */ + protected disposeInternal() { + this.disposing = true; + if (this.onchangeWrapper) { + this.workspace.removeChangeListener(this.onchangeWrapper); + } + + this.workspace.removeTypedBlock(this); + this.workspace.removeBlockById(this.id); + + if (typeof this.destroy === 'function') this.destroy(); + + this.childBlocks_.forEach((c) => c.disposeInternal()); + this.inputList.forEach((i) => i.dispose()); + this.inputList.length = 0; + this.getConnections_(true).forEach((c) => c.dispose()); + this.disposed = true; + } + + /** + * Returns true if the block is either in the process of being disposed, or + * is disposed. + * + * @internal + */ + isDeadOrDying(): boolean { + return this.disposing || this.disposed; + } + + /** + * Call initModel on all fields on the block. + * May be called more than once. + * Either initModel or initSvg must be called after creating a block and + * before the first interaction with it. Interactions include UI actions + * (e.g. clicking and dragging) and firing events (e.g. create, delete, and + * change). + */ + initModel() { + if (this.initialized) return; + for (const input of this.inputList) { + input.initModel(); + } + this.initialized = true; + } + + /** + * Unplug this block from its superior block. If this block is a statement, + * optionally reconnect the block underneath with the block on top. + * + * @param opt_healStack Disconnect child statement and reconnect stack. + * Defaults to false. + */ + unplug(opt_healStack?: boolean) { + if (this.outputConnection) { + this.unplugFromRow(opt_healStack); + } + if (this.previousConnection) { + this.unplugFromStack(opt_healStack); + } + } + + /** + * Unplug this block's output from an input on another block. Optionally + * reconnect the block's parent to the only child block, if possible. + * + * @param opt_healStack Disconnect right-side block and connect to left-side + * block. Defaults to false. + */ + private unplugFromRow(opt_healStack?: boolean) { + let parentConnection = null; + if (this.outputConnection?.isConnected()) { + parentConnection = this.outputConnection.targetConnection; + // Disconnect from any superior block. + this.outputConnection.disconnect(); + } + + // Return early in obvious cases. + if (!parentConnection || !opt_healStack) { + return; + } + + const thisConnection = this.getOnlyValueConnection(); + if ( + !thisConnection || + !thisConnection.isConnected() || + thisConnection.targetBlock()!.isShadow() + ) { + // Too many or too few possible connections on this block, or there's + // nothing on the other side of this connection. + return; + } + + const childConnection = thisConnection.targetConnection; + // Disconnect the child block. + childConnection?.disconnect(); + // Connect child to the parent if possible, otherwise bump away. + if ( + this.workspace.connectionChecker.canConnect( + childConnection, + parentConnection, + false, + ) + ) { + parentConnection.connect(childConnection!); + } else { + childConnection?.onFailedConnect(parentConnection); + } + } + + /** + * Returns the connection on the value input that is connected to another + * block. When an insertion marker is connected to a connection with a block + * already attached, the connected block is attached to the insertion marker. + * Since only one block can be displaced and attached to the insertion marker + * this should only ever return one connection. + * + * @returns The connection on the value input, or null. + */ + private getOnlyValueConnection(): Connection | null { + let connection = null; + for (let i = 0; i < this.inputList.length; i++) { + const thisConnection = this.inputList[i].connection; + if ( + thisConnection && + thisConnection.type === ConnectionType.INPUT_VALUE && + thisConnection.targetConnection + ) { + if (connection) { + return null; // More than one value input found. + } + connection = thisConnection; + } + } + return connection; + } + + /** + * Unplug this statement block from its superior block. Optionally reconnect + * the block underneath with the block on top. + * + * @param opt_healStack Disconnect child statement and reconnect stack. + * Defaults to false. + */ + private unplugFromStack(opt_healStack?: boolean) { + let previousTarget = null; + if (this.previousConnection?.isConnected()) { + // Remember the connection that any next statements need to connect to. + previousTarget = this.previousConnection.targetConnection; + // Detach this block from the parent's tree. + this.previousConnection.disconnect(); + } + const nextBlock = this.getNextBlock(); + if (opt_healStack && nextBlock && !nextBlock.isShadow()) { + // Disconnect the next statement. + const nextTarget = this.nextConnection?.targetConnection ?? null; + nextTarget?.disconnect(); + if ( + previousTarget && + this.workspace.connectionChecker.canConnect( + previousTarget, + nextTarget, + false, + ) + ) { + // Attach the next statement to the previous statement. + previousTarget.connect(nextTarget!); + } + } + } + + /** + * Returns all connections originating from this block. + * + * @param _all If true, return all connections even hidden ones. + * @returns Array of connections. + * @internal + */ + getConnections_(_all: boolean): Connection[] { + const myConnections = []; + if (this.outputConnection) { + myConnections.push(this.outputConnection); + } + if (this.previousConnection) { + myConnections.push(this.previousConnection); + } + if (this.nextConnection) { + myConnections.push(this.nextConnection); + } + for (let i = 0, input; (input = this.inputList[i]); i++) { + if (input.connection) { + myConnections.push(input.connection); + } + } + return myConnections; + } + + /** + * Walks down a stack of blocks and finds the last next connection on the + * stack. + * + * @param ignoreShadows If true,the last connection on a non-shadow block will + * be returned. If false, this will follow shadows to find the last + * connection. + * @returns The last next connection on the stack, or null. + * @internal + */ + lastConnectionInStack(ignoreShadows: boolean): Connection | null { + let nextConnection = this.nextConnection; + while (nextConnection) { + const nextBlock = nextConnection.targetBlock(); + if (!nextBlock || (ignoreShadows && nextBlock.isShadow())) { + return nextConnection; + } + nextConnection = nextBlock.nextConnection; + } + return null; + } + + /** + * Bump unconnected blocks out of alignment. Two blocks which aren't actually + * connected should not coincidentally line up on screen. + */ + bumpNeighbours() {} + + /** + * Return the parent block or null if this block is at the top level. The + * parent block is either the block connected to the previous connection (for + * a statement block) or the block connected to the output connection (for a + * value block). + * + * @returns The block (if any) that holds the current block. + */ + getParent(): this | null { + return this.parentBlock_; + } + + /** + * Return the input that connects to the specified block. + * + * @param block A block connected to an input on this block. + * @returns The input (if any) that connects to the specified block. + */ + getInputWithBlock(block: Block): Input | null { + for (let i = 0, input; (input = this.inputList[i]); i++) { + if (input.connection && input.connection.targetBlock() === block) { + return input; + } + } + return null; + } + + /** + * Return the parent block that surrounds the current block, or null if this + * block has no surrounding block. A parent block might just be the previous + * statement, whereas the surrounding block is an if statement, while loop, + * etc. + * + * @returns The block (if any) that surrounds the current block. + */ + getSurroundParent(): this | null { + /* eslint-disable-next-line @typescript-eslint/no-this-alias */ + let block: this | null = this; + let prevBlock; + do { + prevBlock = block; + block = block.getParent(); + if (!block) { + // Ran off the top. + return null; + } + } while (block.getNextBlock() === prevBlock); + // This block is an enclosing parent, not just a statement in a stack. + return block; + } + + /** + * Return the next statement block directly connected to this block. + * + * @returns The next statement block or null. + */ + getNextBlock(): Block | null { + return this.nextConnection && this.nextConnection.targetBlock(); + } + + /** + * Returns the block connected to the previous connection. + * + * @returns The previous statement block or null. + */ + getPreviousBlock(): Block | null { + return this.previousConnection && this.previousConnection.targetBlock(); + } + + /** + * Return the top-most block in this block's tree. + * This will return itself if this block is at the top level. + * + * @returns The root block. + */ + getRootBlock(): this { + let rootBlock: this; + /* eslint-disable-next-line @typescript-eslint/no-this-alias */ + let block: this | null = this; + do { + rootBlock = block; + block = rootBlock.parentBlock_; + } while (block); + return rootBlock; + } + + /** + * Walk up from the given block up through the stack of blocks to find + * the top block of the sub stack. If we are nested in a statement input only + * find the top-most nested block. Do not go all the way to the root block. + * + * @returns The top block in a stack. + * @internal + */ + getTopStackBlock(): this { + /* eslint-disable-next-line @typescript-eslint/no-this-alias */ + let block = this; + let previous; + do { + previous = block.getPreviousBlock(); + // AnyDuringMigration because: Type 'Block' is not assignable to type + // 'this'. + } while ( + previous && + previous.getNextBlock() === block && + (block = previous as AnyDuringMigration) + ); + return block; + } + + /** + * Find all the blocks that are directly nested inside this one. + * Includes value and statement inputs, as well as any following statement. + * Excludes any connection on an output tab or any preceding statement. + * Blocks are optionally sorted by position; top to bottom. + * + * @param ordered Sort the list if true. + * @returns Array of blocks. + */ + getChildren(ordered: boolean): Block[] { + if (!ordered) { + return this.childBlocks_; + } + const blocks = []; + for (let i = 0, input; (input = this.inputList[i]); i++) { + if (input.connection) { + const child = input.connection.targetBlock(); + if (child) { + blocks.push(child); + } + } + } + const next = this.getNextBlock(); + if (next) { + blocks.push(next); + } + return blocks; + } + + /** + * Set parent of this block to be a new block or null. + * + * @param newParent New parent block. + * @internal + */ + setParent(newParent: this | null) { + if (newParent === this.parentBlock_) { + return; + } + + // Check that block is connected to new parent if new parent is not null and + // that block is not connected to superior one if new parent is null. + const targetBlock = + (this.previousConnection && this.previousConnection.targetBlock()) || + (this.outputConnection && this.outputConnection.targetBlock()); + const isConnected = !!targetBlock; + + if (isConnected && newParent && targetBlock !== newParent) { + throw Error('Block connected to superior one that is not new parent.'); + } else if (!isConnected && newParent) { + throw Error('Block not connected to new parent.'); + } else if (isConnected && !newParent) { + throw Error( + 'Cannot set parent to null while block is still connected to' + + ' superior block.', + ); + } + + // This block hasn't actually moved on-screen, so there's no need to + // update its connection locations. + if (this.parentBlock_) { + // Remove this block from the old parent's child list. + arrayUtils.removeElem(this.parentBlock_.childBlocks_, this); + } else { + // New parent must be non-null so remove this block from the + // workspace's list of top-most blocks. + this.workspace.removeTopBlock(this); + } + + this.parentBlock_ = newParent; + if (newParent) { + // Add this block to the new parent's child list. + newParent.childBlocks_.push(this); + } else { + this.workspace.addTopBlock(this); + } + } + + /** + * Find all the blocks that are directly or indirectly nested inside this one. + * Includes this block in the list. + * Includes value and statement inputs, as well as any following statements. + * Excludes any connection on an output tab or any preceding statements. + * Blocks are optionally sorted by position; top to bottom. + * + * @param ordered Sort the list if true. + * @returns Flattened array of blocks. + */ + getDescendants(ordered: boolean): this[] { + const blocks = [this]; + const childBlocks = this.getChildren(ordered); + for (let child, i = 0; (child = childBlocks[i]); i++) { + // AnyDuringMigration because: Argument of type 'Block[]' is not + // assignable to parameter of type 'this[]'. + blocks.push(...(child.getDescendants(ordered) as AnyDuringMigration)); + } + return blocks; + } + + /** + * Get whether this block is deletable or not. + * + * @returns True if deletable. + */ + isDeletable(): boolean { + return ( + this.deletable && + !this.shadow && + !this.isDeadOrDying() && + !this.workspace.options.readOnly + ); + } + + /** + * Return whether this block's own deletable property is true or false. + * + * @returns True if the block's deletable property is true, false otherwise. + */ + isOwnDeletable(): boolean { + return this.deletable; + } + + /** + * Set whether this block is deletable or not. + * + * @param deletable True if deletable. + */ + setDeletable(deletable: boolean) { + this.deletable = deletable; + } + + /** + * Get whether this block is movable or not. + * + * @returns True if movable. + * @internal + */ + isMovable(): boolean { + return ( + this.movable && + !this.shadow && + !this.isDeadOrDying() && + !this.workspace.options.readOnly + ); + } + + /** + * Return whether this block's own movable property is true or false. + * + * @returns True if the block's movable property is true, false otherwise. + * @internal + */ + isOwnMovable(): boolean { + return this.movable; + } + + /** + * Set whether this block is movable or not. + * + * @param movable True if movable. + */ + setMovable(movable: boolean) { + this.movable = movable; + } + + /** + * Get whether is block is duplicatable or not. If duplicating this block and + * descendants will put this block over the workspace's capacity this block is + * not duplicatable. If duplicating this block and descendants will put any + * type over their maxInstances this block is not duplicatable. + * + * @returns True if duplicatable. + */ + isDuplicatable(): boolean { + if (!this.workspace.hasBlockLimits()) { + return true; + } + return this.workspace.isCapacityAvailable( + common.getBlockTypeCounts(this, true), + ); + } + + /** + * Get whether this block is a shadow block or not. + * + * @returns True if a shadow. + */ + isShadow(): boolean { + return this.shadow; + } + + /** + * Set whether this block is a shadow block or not. + * This method is internal and should not be called by users of Blockly. To + * create shadow blocks programmatically call connection.setShadowState + * + * @param shadow True if a shadow. + * @internal + */ + setShadow(shadow: boolean) { + this.shadow = shadow; + } + + /** + * Get whether this block is an insertion marker block or not. + * + * @returns True if an insertion marker. + */ + isInsertionMarker(): boolean { + return this.isInsertionMarker_; + } + + /** + * Set whether this block is an insertion marker block or not. + * Once set this cannot be unset. + * + * @param insertionMarker True if an insertion marker. + * @internal + */ + setInsertionMarker(insertionMarker: boolean) { + this.isInsertionMarker_ = insertionMarker; + } + + /** + * Get whether this block is editable or not. + * + * @returns True if editable. + * @internal + */ + isEditable(): boolean { + return ( + this.editable && !this.isDeadOrDying() && !this.workspace.options.readOnly + ); + } + + /** + * Return whether this block's own editable property is true or false. + * + * @returns True if the block's editable property is true, false otherwise. + */ + isOwnEditable(): boolean { + return this.editable; + } + + /** + * Set whether this block is editable or not. + * + * @param editable True if editable. + */ + setEditable(editable: boolean) { + this.editable = editable; + for (let i = 0, input; (input = this.inputList[i]); i++) { + for (let j = 0, field; (field = input.fieldRow[j]); j++) { + field.updateEditable(); + } + } + } + + /** + * Returns if this block has been disposed of / deleted. + * + * @returns True if this block has been disposed of / deleted. + */ + isDisposed(): boolean { + return this.disposed; + } + + /** + * @returns True if this block is a value block with a single editable field. + * @internal + */ + isSimpleReporter(): boolean { + if (!this.outputConnection) return false; + + for (const input of this.inputList) { + if (input.connection || input.fieldRow.length > 1) return false; + } + return true; + } + + /** + * Find the connection on this block that corresponds to the given connection + * on the other block. + * Used to match connections between a block and its insertion marker. + * + * @param otherBlock The other block to match against. + * @param conn The other connection to match. + * @returns The matching connection on this block, or null. + * @internal + */ + getMatchingConnection( + otherBlock: Block, + conn: Connection, + ): Connection | null { + const connections = this.getConnections_(true); + const otherConnections = otherBlock.getConnections_(true); + if (connections.length !== otherConnections.length) { + throw Error('Connection lists did not match in length.'); + } + for (let i = 0; i < otherConnections.length; i++) { + if (otherConnections[i] === conn) { + return connections[i]; + } + } + return null; + } + + /** + * Set the URL of this block's help page. + * + * @param url URL string for block help, or function that returns a URL. Null + * for no help. + */ + setHelpUrl(url: string | (() => string)) { + this.helpUrl = url; + } + + /** + * Sets the tooltip for this block. + * + * @param newTip The text for the tooltip, a function that returns the text + * for the tooltip, or a parent object whose tooltip will be used. To not + * display a tooltip pass the empty string. + */ + setTooltip(newTip: Tooltip.TipInfo) { + this.tooltip = newTip; + } + + /** + * Returns the tooltip text for this block. + * + * @returns The tooltip text for this block. + */ + getTooltip(): string { + return Tooltip.getTooltipOfObject(this); + } + + /** + * Get the colour of a block. + * + * @returns #RRGGBB string. + */ + getColour(): string { + return this.colour_; + } + + /** + * Get the name of the block style. + * + * @returns Name of the block style. + */ + getStyleName(): string { + return this.styleName_; + } + + /** + * Get the HSV hue value of a block. Null if hue not set. + * + * @returns Hue value (0-360). + */ + getHue(): number | null { + return this.hue; + } + + /** + * Change the colour of a block. + * + * @param colour HSV hue value (0 to 360), #RRGGBB string, or a message + * reference string pointing to one of those two values. + */ + setColour(colour: number | string) { + const parsed = parsing.parseBlockColour(colour); + this.hue = parsed.hue; + this.colour_ = parsed.hex; + } + + /** + * Set the style and colour values of a block. + * + * @param blockStyleName Name of the block style. + */ + setStyle(blockStyleName: string) { + this.styleName_ = blockStyleName; + } + + /** + * Sets a callback function to use whenever the block's parent workspace + * changes, replacing any prior onchange handler. This is usually only called + * from the constructor, the block type initializer function, or an extension + * initializer function. + * + * @param onchangeFn The callback to call when the block's workspace changes. + * @throws {Error} if onchangeFn is not falsey and not a function. + */ + setOnChange(onchangeFn: (p1: Abstract) => void) { + if (onchangeFn && typeof onchangeFn !== 'function') { + throw Error('onchange must be a function.'); + } + if (this.onchangeWrapper) { + this.workspace.removeChangeListener(this.onchangeWrapper); + } + this.onchange = onchangeFn; + this.onchangeWrapper = onchangeFn.bind(this); + this.workspace.addChangeListener(this.onchangeWrapper); + } + + /** + * Returns the named field from a block. + * + * @param name The name of the field. + * @returns Named field, or null if field does not exist. + */ + getField(name: string): Field | null { + if (typeof name !== 'string') { + throw TypeError( + 'Block.prototype.getField expects a string ' + + 'with the field name but received ' + + (name === undefined ? 'nothing' : name + ' of type ' + typeof name) + + ' instead', + ); + } + for (let i = 0, input; (input = this.inputList[i]); i++) { + for (let j = 0, field; (field = input.fieldRow[j]); j++) { + if (field.name === name) { + return field; + } + } + } + return null; + } + + /** + * Return all variables referenced by this block. + * + * @returns List of variable ids. + */ + getVars(): string[] { + const vars: string[] = []; + for (let i = 0, input; (input = this.inputList[i]); i++) { + for (let j = 0, field; (field = input.fieldRow[j]); j++) { + if (field.referencesVariables()) { + // NOTE: This only applies to `FieldVariable`, a `Field` + vars.push(field.getValue() as string); + } + } + } + return vars; + } + + /** + * Return all variables referenced by this block. + * + * @returns List of variable models. + * @internal + */ + getVarModels(): VariableModel[] { + const vars = []; + for (let i = 0, input; (input = this.inputList[i]); i++) { + for (let j = 0, field; (field = input.fieldRow[j]); j++) { + if (field.referencesVariables()) { + const model = this.workspace.getVariableById( + field.getValue() as string, + ); + // Check if the variable actually exists (and isn't just a potential + // variable). + if (model) { + vars.push(model); + } + } + } + } + return vars; + } + + /** + * 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. + * @internal + */ + updateVarName(variable: VariableModel) { + for (let i = 0, input; (input = this.inputList[i]); i++) { + for (let j = 0, field; (field = input.fieldRow[j]); j++) { + if ( + field.referencesVariables() && + variable.getId() === field.getValue() + ) { + field.refreshVariableName(); + } + } + } + } + + /** + * 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. + */ + renameVarById(oldId: string, newId: string) { + for (let i = 0, input; (input = this.inputList[i]); i++) { + for (let j = 0, field; (field = input.fieldRow[j]); j++) { + if (field.referencesVariables() && oldId === field.getValue()) { + field.setValue(newId); + } + } + } + } + + /** + * Returns the language-neutral value of the given field. + * + * @param name The name of the field. + * @returns Value of the field or null if field does not exist. + */ + getFieldValue(name: string): AnyDuringMigration { + const field = this.getField(name); + if (field) { + return field.getValue(); + } + return null; + } + + /** + * Sets the value of the given field for this block. + * + * @param newValue The value to set. + * @param name The name of the field to set the value of. + */ + setFieldValue(newValue: AnyDuringMigration, name: string) { + const field = this.getField(name); + if (!field) { + throw Error('Field "' + name + '" not found.'); + } + field.setValue(newValue); + } + + /** + * Set whether this block can chain onto the bottom of another block. + * + * @param newBoolean True if there can be a previous statement. + * @param opt_check Statement type or list of statement types. Null/undefined + * if any type could be connected. + */ + setPreviousStatement( + newBoolean: boolean, + opt_check?: string | string[] | null, + ) { + if (newBoolean) { + if (opt_check === undefined) { + opt_check = null; + } + if (!this.previousConnection) { + this.previousConnection = this.makeConnection_( + ConnectionType.PREVIOUS_STATEMENT, + ); + } + this.previousConnection.setCheck(opt_check); + } else { + if (this.previousConnection) { + if (this.previousConnection.isConnected()) { + throw Error( + 'Must disconnect previous statement before removing ' + + 'connection.', + ); + } + this.previousConnection.dispose(); + this.previousConnection = null; + } + } + } + + /** + * Set whether another block can chain onto the bottom of this block. + * + * @param newBoolean True if there can be a next statement. + * @param opt_check Statement type or list of statement types. Null/undefined + * if any type could be connected. + */ + setNextStatement(newBoolean: boolean, opt_check?: string | string[] | null) { + if (newBoolean) { + if (opt_check === undefined) { + opt_check = null; + } + if (!this.nextConnection) { + this.nextConnection = this.makeConnection_( + ConnectionType.NEXT_STATEMENT, + ); + } + this.nextConnection.setCheck(opt_check); + } else { + if (this.nextConnection) { + if (this.nextConnection.isConnected()) { + throw Error( + 'Must disconnect next statement before removing ' + 'connection.', + ); + } + this.nextConnection.dispose(); + this.nextConnection = null; + } + } + } + + /** + * Set whether this block returns a value. + * + * @param newBoolean True if there is an output. + * @param opt_check Returned type or list of returned types. Null or + * undefined if any type could be returned (e.g. variable get). + */ + setOutput(newBoolean: boolean, opt_check?: string | string[] | null) { + if (newBoolean) { + if (opt_check === undefined) { + opt_check = null; + } + if (!this.outputConnection) { + this.outputConnection = this.makeConnection_( + ConnectionType.OUTPUT_VALUE, + ); + } + this.outputConnection.setCheck(opt_check); + } else { + if (this.outputConnection) { + if (this.outputConnection.isConnected()) { + throw Error( + 'Must disconnect output value before removing connection.', + ); + } + this.outputConnection.dispose(); + this.outputConnection = null; + } + } + } + + /** + * Set whether value inputs are arranged horizontally or vertically. + * + * @param newBoolean True if inputs are horizontal. + */ + setInputsInline(newBoolean: boolean) { + if (this.inputsInline !== newBoolean) { + eventUtils.fire( + new (eventUtils.get(EventType.BLOCK_CHANGE))( + this, + 'inline', + null, + this.inputsInline, + newBoolean, + ), + ); + this.inputsInline = newBoolean; + } + } + + /** + * Get whether value inputs are arranged horizontally or vertically. + * + * @returns True if inputs are horizontal. + */ + getInputsInline(): boolean { + if (this.inputsInline !== undefined) { + // Set explicitly. + return this.inputsInline; + } + // Not defined explicitly. Figure out what would look best. + for (let i = 1; i < this.inputList.length; i++) { + if ( + this.inputList[i - 1] instanceof DummyInput && + this.inputList[i] instanceof DummyInput + ) { + // Two dummy inputs in a row. Don't inline them. + return false; + } + } + for (let i = 1; i < this.inputList.length; i++) { + if ( + this.inputList[i - 1] instanceof ValueInput && + this.inputList[i] instanceof DummyInput + ) { + // Dummy input after a value input. Inline them. + return true; + } + } + for (let i = 0; i < this.inputList.length; i++) { + if (this.inputList[i] instanceof EndRowInput) { + // A row-end input is present. Inline value inputs. + return true; + } + } + return false; + } + + /** + * Set the block's output shape. + * + * @param outputShape Value representing an output shape. + */ + setOutputShape(outputShape: number | null) { + this.outputShape_ = outputShape; + } + + /** + * Get the block's output shape. + * + * @returns Value representing output shape if one exists. + */ + getOutputShape(): number | null { + return this.outputShape_; + } + + /** + * Get whether this block is enabled or not. A block is considered enabled + * if there aren't any reasons why it would be disabled. A block may still + * be disabled for other reasons even if the user attempts to manually + * enable it, such as when the block is in an invalid location. + * + * @returns True if enabled. + */ + isEnabled(): boolean { + return this.disabledReasons.size === 0; + } + + /** @deprecated v11 - Get whether the block is manually disabled. */ + private get disabled(): boolean { + deprecation.warn( + 'disabled', + 'v11', + 'v12', + 'the isEnabled or hasDisabledReason methods of Block', + ); + return this.hasDisabledReason(constants.MANUALLY_DISABLED); + } + + /** @deprecated v11 - Set whether the block is manually disabled. */ + private set disabled(value: boolean) { + deprecation.warn( + 'disabled', + 'v11', + 'v12', + 'the setDisabledReason method of Block', + ); + this.setDisabledReason(value, constants.MANUALLY_DISABLED); + } + + /** + * @deprecated v11 - Set whether the block is manually enabled or disabled. + * The user can toggle whether a block is disabled from a context menu + * option. A block may still be disabled for other reasons even if the user + * attempts to manually enable it, such as when the block is in an invalid + * location. This method is deprecated and setDisabledReason should be used + * instead. + * + * @param enabled True if enabled. + */ + setEnabled(enabled: boolean) { + deprecation.warn( + 'setEnabled', + 'v11', + 'v12', + 'the setDisabledReason method of Block', + ); + this.setDisabledReason(!enabled, constants.MANUALLY_DISABLED); + } + + /** + * Add or remove a reason why the block might be disabled. If a block has + * any reasons to be disabled, then the block itself will be considered + * disabled. A block could be disabled for multiple independent reasons + * simultaneously, such as when the user manually disables it, or the block + * is invalid. + * + * @param disabled If true, then the block should be considered disabled for + * at least the provided reason, otherwise the block is no longer disabled + * for that reason. + * @param reason A language-neutral identifier for a reason why the block + * could be disabled. Call this method again with the same identifier to + * update whether the block is currently disabled for this reason. + */ + setDisabledReason(disabled: boolean, reason: string): void { + // Workspaces that were serialized before the reason for being disabled + // could be specified may have blocks that are disabled without a known + // reason. On being loaded, these blocks will default to having the manually + // disabled reason. However, if the user isn't allowed to manually disable + // or enable blocks, then this manually disabled reason cannot be removed. + // For backward compatibility with these legacy workspaces, when removing + // any disabled reason and the workspace does not allow manually disabling + // but the block is manually disabled, then remove the manually disabled + // reason in addition to the more specific reason. For example, when an + // orphaned block is no longer orphaned, the block should be enabled again. + if ( + !disabled && + !this.workspace.options.disable && + this.hasDisabledReason(constants.MANUALLY_DISABLED) && + reason != constants.MANUALLY_DISABLED + ) { + this.setDisabledReason(false, constants.MANUALLY_DISABLED); + } + + if (this.disabledReasons.has(reason) !== disabled) { + if (disabled) { + this.disabledReasons.add(reason); + } else { + this.disabledReasons.delete(reason); + } + const blockChangeEvent = new (eventUtils.get(EventType.BLOCK_CHANGE))( + this, + 'disabled', + /* name= */ null, + /* oldValue= */ !disabled, + /* newValue= */ disabled, + ) as BlockChange; + blockChangeEvent.setDisabledReason(reason); + eventUtils.fire(blockChangeEvent); + } + } + + /** + * Get whether the block is disabled or not due to parents. + * The block's own disabled property is not considered. + * + * @returns True if disabled. + */ + getInheritedDisabled(): boolean { + let ancestor = this.getSurroundParent(); + while (ancestor) { + if (!ancestor.isEnabled()) { + return true; + } + ancestor = ancestor.getSurroundParent(); + } + // Ran off the top. + return false; + } + + /** + * Get whether the block is currently disabled for the provided reason. + * + * @param reason A language-neutral identifier for a reason why the block + * could be disabled. + * @returns Whether the block is disabled for the provided reason. + */ + hasDisabledReason(reason: string): boolean { + return this.disabledReasons.has(reason); + } + + /** + * Get a set of reasons why the block is currently disabled, if any. If the + * block is enabled, this set will be empty. + * + * @returns The set of reasons why the block is disabled, if any. + */ + getDisabledReasons(): ReadonlySet { + return this.disabledReasons; + } + + /** + * Get whether the block is collapsed or not. + * + * @returns True if collapsed. + */ + isCollapsed(): boolean { + return this.collapsed_; + } + + /** + * Set whether the block is collapsed or not. + * + * @param collapsed True if collapsed. + */ + setCollapsed(collapsed: boolean) { + if (this.collapsed_ !== collapsed) { + eventUtils.fire( + new (eventUtils.get(EventType.BLOCK_CHANGE))( + this, + 'collapsed', + null, + this.collapsed_, + collapsed, + ), + ); + this.collapsed_ = collapsed; + } + } + + /** + * Create a human-readable text representation of this block and any children. + * + * @param opt_maxLength Truncate the string to this length. + * @param opt_emptyToken The placeholder string used to denote an empty input. + * If not specified, '?' is used. + * @returns Text of block. + */ + toString(opt_maxLength?: number, opt_emptyToken?: string): string { + const tokens = this.toTokens(opt_emptyToken); + + // Run through our tokens array and simplify expression to remove + // parentheses around single field blocks. + // E.g. ['repeat', '(', '10', ')', 'times', 'do', '?'] + for (let i = 2; i < tokens.length; i++) { + if (tokens[i - 2] === '(' && tokens[i] === ')') { + tokens[i - 2] = tokens[i - 1]; + tokens.splice(i - 1, 2); + } + } + + // Join the text array, removing the spaces around added parentheses. + let prev = ''; + let text: string = tokens.reduce((acc, curr) => { + const val = acc + (prev === '(' || curr === ')' ? '' : ' ') + curr; + prev = curr[curr.length - 1]; + return val; + }, ''); + + text = text.trim() || '???'; + if (opt_maxLength) { + // TODO: Improve truncation so that text from this block is given + // priority. E.g. "1+2+3+4+5+6+7+8+9=0" should be "...6+7+8+9=0", not + // "1+2+3+4+5...". E.g. "1+2+3+4+5=6+7+8+9+0" should be "...4+5=6+7...". + if (text.length > opt_maxLength) { + text = text.substring(0, opt_maxLength - 3) + '...'; + } + } + return text; + } + + /** + * Converts this block into string tokens. + * + * @param emptyToken The token to use in place of an empty input. + * Defaults to '?'. + * @returns The array of string tokens representing this block. + */ + private toTokens(emptyToken = '?'): string[] { + const tokens = []; + /** + * Whether or not to add parentheses around an input. + * + * @param connection The connection. + * @returns True if we should add parentheses around the input. + */ + function shouldAddParentheses(connection: Connection): boolean { + let checks = connection.getCheck(); + if (!checks && connection.targetConnection) { + checks = connection.targetConnection.getCheck(); + } + return ( + !!checks && (checks.includes('Boolean') || checks.includes('Number')) + ); + } + + for (const input of this.inputList) { + if (input.name == constants.COLLAPSED_INPUT_NAME) { + continue; + } + for (const field of input.fieldRow) { + tokens.push(field.getText()); + } + if (input.connection) { + const child = input.connection.targetBlock(); + if (child) { + const shouldAddParens = shouldAddParentheses(input.connection); + if (shouldAddParens) tokens.push('('); + tokens.push(...child.toTokens(emptyToken)); + if (shouldAddParens) tokens.push(')'); + } else { + tokens.push(emptyToken); + } + } + } + return tokens; + } + + /** + * Appends a value input row. + * + * @param name Language-neutral identifier which may used to find this input + * again. Should be unique to this block. + * @returns The input object created. + */ + appendValueInput(name: string): Input { + return this.appendInput(new ValueInput(name, this)); + } + + /** + * Appends a statement input row. + * + * @param name Language-neutral identifier which may used to find this input + * again. Should be unique to this block. + * @returns The input object created. + */ + appendStatementInput(name: string): Input { + this.statementInputCount++; + return this.appendInput(new StatementInput(name, this)); + } + + /** + * Appends a dummy input row. + * + * @param name Optional language-neutral identifier which may used to find + * this input again. Should be unique to this block. + * @returns The input object created. + */ + appendDummyInput(name = ''): Input { + return this.appendInput(new DummyInput(name, this)); + } + + /** + * Appends an input that ends the row. + * + * @param name Optional language-neutral identifier which may used to find + * this input again. Should be unique to this block. + * @returns The input object created. + */ + appendEndRowInput(name = ''): Input { + return this.appendInput(new EndRowInput(name, this)); + } + + /** + * Appends the given input row. + * + * Allows for custom inputs to be appended to the block. + */ + appendInput(input: Input): Input { + this.inputList.push(input); + return input; + } + + /** + * Appends an input with the given input type and name to the block after + * constructing it from the registry. + * + * @param type The name the input is registered under in the registry. + * @param name The name the input will have within the block. + * @returns The constucted input, or null if there was no constructor + * associated with the type. + */ + private appendInputFromRegistry(type: string, name: string): Input | null { + const inputConstructor = registry.getClass( + registry.Type.INPUT, + type, + false, + ); + if (!inputConstructor) return null; + return this.appendInput(new inputConstructor(name, this)); + } + + /** + * Initialize this block using a cross-platform, internationalization-friendly + * JSON description. + * + * @param json Structured data describing the block. + */ + jsonInit(json: AnyDuringMigration) { + const warningPrefix = json['type'] ? 'Block "' + json['type'] + '": ' : ''; + + // Validate inputs. + if (json['output'] && json['previousStatement']) { + throw Error( + warningPrefix + 'Must not have both an output and a previousStatement.', + ); + } + + // Validate that each arg has a corresponding message + let n = 0; + while (json['args' + n]) { + if (json['message' + n] === undefined) { + throw Error( + warningPrefix + + `args${n} must have a corresponding message (message${n}).`, + ); + } + n++; + } + + // Set basic properties of block. + // Makes styles backward compatible with old way of defining hat style. + if (json['style'] && json['style'].hat) { + this.hat = json['style'].hat; + // Must set to null so it doesn't error when checking for style and + // colour. + json['style'] = null; + } + + if (json['style'] && json['colour']) { + throw Error(warningPrefix + 'Must not have both a colour and a style.'); + } else if (json['style']) { + this.jsonInitStyle(json, warningPrefix); + } else { + this.jsonInitColour(json, warningPrefix); + } + + // Interpolate the message blocks. + let i = 0; + while (json['message' + i] !== undefined) { + this.interpolate( + json['message' + i], + json['args' + i] || [], + // Backwards compatibility: lastDummyAlign aliases implicitAlign. + json['implicitAlign' + i] || json['lastDummyAlign' + i], + warningPrefix, + ); + i++; + } + + if (json['inputsInline'] !== undefined) { + eventUtils.disable(); + this.setInputsInline(json['inputsInline']); + eventUtils.enable(); + } + + // Set output and previous/next connections. + if (json['output'] !== undefined) { + this.setOutput(true, json['output']); + } + if (json['outputShape'] !== undefined) { + this.setOutputShape(json['outputShape']); + } + if (json['previousStatement'] !== undefined) { + this.setPreviousStatement(true, json['previousStatement']); + } + if (json['nextStatement'] !== undefined) { + this.setNextStatement(true, json['nextStatement']); + } + if (json['tooltip'] !== undefined) { + const rawValue = json['tooltip']; + const localizedText = parsing.replaceMessageReferences(rawValue); + this.setTooltip(localizedText); + } + if (json['enableContextMenu'] !== undefined) { + this.contextMenu = !!json['enableContextMenu']; + } + if (json['suppressPrefixSuffix'] !== undefined) { + this.suppressPrefixSuffix = !!json['suppressPrefixSuffix']; + } + if (json['helpUrl'] !== undefined) { + const rawValue = json['helpUrl']; + const localizedValue = parsing.replaceMessageReferences(rawValue); + this.setHelpUrl(localizedValue); + } + if (typeof json['extensions'] === 'string') { + console.warn( + warningPrefix + + "JSON attribute 'extensions' should be an array of" + + " strings. Found raw string in JSON for '" + + json['type'] + + "' block.", + ); + json['extensions'] = [json['extensions']]; // Correct and continue. + } + + // Add the mutator to the block. + if (json['mutator'] !== undefined) { + Extensions.apply(json['mutator'], this, true); + } + + const extensionNames = json['extensions']; + if (Array.isArray(extensionNames)) { + for (let j = 0; j < extensionNames.length; j++) { + Extensions.apply(extensionNames[j], this, false); + } + } + } + + /** + * Initialize the colour of this block from the JSON description. + * + * @param json Structured data describing the block. + * @param warningPrefix Warning prefix string identifying block. + */ + private jsonInitColour(json: AnyDuringMigration, warningPrefix: string) { + if ('colour' in json) { + if (json['colour'] === undefined) { + console.warn(warningPrefix + 'Undefined colour value.'); + } else { + const rawValue = json['colour']; + try { + this.setColour(rawValue); + } catch { + console.warn(warningPrefix + 'Illegal colour value: ', rawValue); + } + } + } + } + + /** + * Initialize the style of this block from the JSON description. + * + * @param json Structured data describing the block. + * @param warningPrefix Warning prefix string identifying block. + */ + private jsonInitStyle(json: AnyDuringMigration, warningPrefix: string) { + const blockStyleName = json['style']; + try { + this.setStyle(blockStyleName); + } catch { + console.warn(warningPrefix + 'Style does not exist: ', blockStyleName); + } + } + + /** + * Add key/values from mixinObj to this block object. By default, this method + * will check that the keys in mixinObj will not overwrite existing values in + * the block, including prototype values. This provides some insurance against + * mixin / extension incompatibilities with future block features. This check + * can be disabled by passing true as the second argument. + * + * @param mixinObj The key/values pairs to add to this block object. + * @param opt_disableCheck Option flag to disable overwrite checks. + */ + mixin(mixinObj: AnyDuringMigration, opt_disableCheck?: boolean) { + if ( + opt_disableCheck !== undefined && + typeof opt_disableCheck !== 'boolean' + ) { + throw Error('opt_disableCheck must be a boolean if provided'); + } + if (!opt_disableCheck) { + const overwrites = []; + for (const key in mixinObj) { + if ((this as AnyDuringMigration)[key] !== undefined) { + overwrites.push(key); + } + } + if (overwrites.length) { + throw Error( + 'Mixin will overwrite block members: ' + JSON.stringify(overwrites), + ); + } + } + Object.assign(this, mixinObj); + } + + /** + * Interpolate a message description onto the block. + * + * @param message Text contains interpolation tokens (%1, %2, ...) that match + * with fields or inputs defined in the args array. + * @param args Array of arguments to be interpolated. + * @param implicitAlign If an implicit input is added at the end or in place + * of newline tokens, how should it be aligned? + * @param warningPrefix Warning prefix string identifying block. + */ + private interpolate( + message: string, + args: AnyDuringMigration[], + implicitAlign: string | undefined, + warningPrefix: string, + ) { + const tokens = parsing.tokenizeInterpolation(message); + this.validateTokens(tokens, args.length); + const elements = this.interpolateArguments(tokens, args, implicitAlign); + + // An array of [field, fieldName] tuples. + const fieldStack = []; + for (let i = 0, element; (element = elements[i]); i++) { + if (this.isInputKeyword(element['type'])) { + const input = this.inputFromJson(element, warningPrefix); + // Should never be null, but just in case. + if (input) { + for (let j = 0, tuple; (tuple = fieldStack[j]); j++) { + input.appendField(tuple[0], tuple[1]); + } + fieldStack.length = 0; + } + } else { + // All other types, including ones starting with 'input_' get routed + // here. + const field = this.fieldFromJson(element); + if (field) { + fieldStack.push([field, element['name']]); + } + } + } + } + + /** + * Validates that the tokens are within the correct bounds, with no + * duplicates, and that all of the arguments are referred to. Throws errors if + * any of these things are not true. + * + * @param tokens An array of tokens to validate + * @param argsCount The number of args that need to be referred to. + */ + private validateTokens(tokens: Array, argsCount: number) { + const visitedArgsHash = []; + let visitedArgsCount = 0; + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + if (typeof token !== 'number') { + continue; + } + if (token < 1 || token > argsCount) { + throw Error( + 'Block "' + + this.type + + '": ' + + 'Message index %' + + token + + ' out of range.', + ); + } + if (visitedArgsHash[token]) { + throw Error( + 'Block "' + + this.type + + '": ' + + 'Message index %' + + token + + ' duplicated.', + ); + } + visitedArgsHash[token] = true; + visitedArgsCount++; + } + if (visitedArgsCount !== argsCount) { + throw Error( + 'Block "' + + this.type + + '": ' + + 'Message does not reference all ' + + argsCount + + ' arg(s).', + ); + } + } + + /** + * Inserts args in place of numerical tokens. String args are converted to + * JSON that defines a label field. Newline characters are converted to + * end-row inputs, and if necessary an extra dummy input is added to the end + * of the elements. + * + * @param tokens The tokens to interpolate + * @param args The arguments to insert. + * @param implicitAlign The alignment to use for any implicitly added end-row + * or dummy inputs, if necessary. + * @returns The JSON definitions of field and inputs to add to the block. + */ + private interpolateArguments( + tokens: Array, + args: Array, + implicitAlign: string | undefined, + ): AnyDuringMigration[] { + const elements = []; + for (let i = 0; i < tokens.length; i++) { + let element = tokens[i]; + if (typeof element === 'number') { + element = args[element - 1]; + } + // Args can be strings, which is why this isn't elseif. + if (typeof element === 'string') { + if (element === '\n') { + // Convert newline tokens to end-row inputs. + const newlineInput = {'type': 'input_end_row'}; + if (implicitAlign) { + (newlineInput as AnyDuringMigration)['align'] = implicitAlign; + } + element = newlineInput as AnyDuringMigration; + } else { + // AnyDuringMigration because: Type '{ text: string; type: string; } + // | null' is not assignable to type 'string | number'. + element = this.stringToFieldJson(element) as AnyDuringMigration; + if (!element) { + continue; + } + } + } + elements.push(element); + } + + const length = elements.length; + if ( + length && + !this.isInputKeyword((elements as AnyDuringMigration)[length - 1]['type']) + ) { + const dummyInput = {'type': 'input_dummy'}; + if (implicitAlign) { + (dummyInput as AnyDuringMigration)['align'] = implicitAlign; + } + elements.push(dummyInput); + } + + return elements; + } + + /** + * Creates a field from the JSON definition of a field. If a field with the + * given type cannot be found, this attempts to create a different field using + * the 'alt' property of the JSON definition (if it exists). + * + * @param element The element to try to turn into a field. + * @returns The field defined by the JSON, or null if one couldn't be created. + */ + private fieldFromJson(element: { + alt?: string; + type: string; + text?: string; + }): Field | null { + const field = fieldRegistry.fromJson(element); + if (!field && element['alt']) { + if (typeof element['alt'] === 'string') { + const json = this.stringToFieldJson(element['alt']); + return json ? this.fieldFromJson(json) : null; + } + return this.fieldFromJson(element['alt']); + } + return field; + } + + /** + * Creates an input from the JSON definition of an input. Sets the input's + * check and alignment if they are provided. + * + * @param element The JSON to turn into an input. + * @param warningPrefix The prefix to add to warnings to help the developer + * debug. + * @returns The input that has been created, or null if one could not be + * created for some reason (should never happen). + */ + private inputFromJson( + element: AnyDuringMigration, + warningPrefix: string, + ): Input | null { + const alignmentLookup = { + 'LEFT': Align.LEFT, + 'RIGHT': Align.RIGHT, + 'CENTRE': Align.CENTRE, + 'CENTER': Align.CENTRE, + }; + + let input = null; + switch (element['type']) { + case 'input_value': + input = this.appendValueInput(element['name']); + break; + case 'input_statement': + input = this.appendStatementInput(element['name']); + break; + case 'input_dummy': + input = this.appendDummyInput(element['name']); + break; + case 'input_end_row': + input = this.appendEndRowInput(element['name']); + break; + default: { + input = this.appendInputFromRegistry(element['type'], element['name']); + break; + } + } + // Should never be hit because of interpolate_'s checks, but just in case. + if (!input) { + return null; + } + + if (element['check']) { + input.setCheck(element['check']); + } + if (element['align']) { + const alignment = (alignmentLookup as AnyDuringMigration)[ + element['align'].toUpperCase() + ]; + if (alignment === undefined) { + console.warn(warningPrefix + 'Illegal align value: ', element['align']); + } else { + input.setAlign(alignment); + } + } + return input; + } + + /** + * Returns true if the given string matches one of the input keywords. + * + * @param str The string to check. + * @returns True if the given string matches one of the input keywords, false + * otherwise. + */ + private isInputKeyword(str: string): boolean { + return ( + str === 'input_value' || + str === 'input_statement' || + str === 'input_dummy' || + str === 'input_end_row' || + registry.hasItem(registry.Type.INPUT, str) + ); + } + + /** + * Turns a string into the JSON definition of a label field. If the string + * becomes an empty string when trimmed, this returns null. + * + * @param str String to turn into the JSON definition of a label field. + * @returns The JSON definition or null. + */ + private stringToFieldJson(str: string): {text: string; type: string} | null { + str = str.trim(); + if (str) { + return { + 'type': 'field_label', + 'text': str, + }; + } + return null; + } + + /** + * Move a named input to a different location on this block. + * + * @param name The name of the input to move. + * @param refName Name of input that should be after the moved input, or null + * to be the input at the end. + */ + moveInputBefore(name: string, refName: string | null) { + if (name === refName) { + return; + } + // Find both inputs. + let inputIndex = -1; + let refIndex = refName ? -1 : this.inputList.length; + for (let i = 0, input; (input = this.inputList[i]); i++) { + if (input.name === name) { + inputIndex = i; + if (refIndex !== -1) { + break; + } + } else if (refName && input.name === refName) { + refIndex = i; + if (inputIndex !== -1) { + break; + } + } + } + if (inputIndex === -1) { + throw Error('Named input "' + name + '" not found.'); + } + if (refIndex === -1) { + throw Error('Reference input "' + refName + '" not found.'); + } + this.moveNumberedInputBefore(inputIndex, refIndex); + } + + /** + * Move a numbered input to a different location on this block. + * + * @param inputIndex Index of the input to move. + * @param refIndex Index of input that should be after the moved input. + */ + moveNumberedInputBefore(inputIndex: number, refIndex: number) { + // Validate arguments. + if (inputIndex === refIndex) { + throw Error("Can't move input to itself."); + } + if (inputIndex >= this.inputList.length) { + throw RangeError('Input index ' + inputIndex + ' out of bounds.'); + } + if (refIndex > this.inputList.length) { + throw RangeError('Reference input ' + refIndex + ' out of bounds.'); + } + // Remove input. + const input = this.inputList[inputIndex]; + this.inputList.splice(inputIndex, 1); + if (inputIndex < refIndex) { + refIndex--; + } + // Reinsert input. + this.inputList.splice(refIndex, 0, input); + } + + /** + * Remove an input from this block. + * + * @param name The name of the input. + * @param opt_quiet True to prevent an error if input is not present. + * @returns True if operation succeeds, false if input is not present and + * opt_quiet is true. + * @throws {Error} if the input is not present and opt_quiet is not true. + */ + removeInput(name: string, opt_quiet?: boolean): boolean { + for (let i = 0, input; (input = this.inputList[i]); i++) { + if (input.name === name) { + if (input instanceof StatementInput) this.statementInputCount--; + input.dispose(); + this.inputList.splice(i, 1); + return true; + } + } + if (opt_quiet) { + return false; + } + throw Error('Input not found: ' + name); + } + + /** + * Fetches the named input object. + * + * @param name The name of the input. + * @returns The input object, or null if input does not exist. + */ + getInput(name: string): Input | null { + for (let i = 0, input; (input = this.inputList[i]); i++) { + if (input.name === name) { + return input; + } + } + // This input does not exist. + return null; + } + + /** + * Fetches the block attached to the named input. + * + * @param name The name of the input. + * @returns The attached value block, or null if the input is either + * disconnected or if the input does not exist. + */ + getInputTargetBlock(name: string): Block | null { + const input = this.getInput(name); + return input && input.connection && input.connection.targetBlock(); + } + + /** + * Returns the comment on this block (or null if there is no comment). + * + * @returns Block's comment. + */ + getCommentText(): string | null { + const comment = this.getIcon(IconType.COMMENT); + return comment?.getText() ?? null; + } + + /** + * Set this block's comment text. + * + * @param text The text, or null to delete. + */ + setCommentText(text: string | null) { + const comment = this.getIcon(IconType.COMMENT); + const oldText = comment?.getText() ?? null; + if (oldText === text) return; + if (text !== null) { + let comment = this.getIcon(IconType.COMMENT); + if (!comment) { + const commentConstructor = registry.getClass( + registry.Type.ICON, + IconType.COMMENT.toString(), + false, + ); + if (!commentConstructor) { + throw new Error( + 'No comment icon class is registered, so a comment cannot be set', + ); + } + const icon = new commentConstructor(this); + if (!isCommentIcon(icon)) { + throw new Error( + 'The class registered as a comment icon does not conform to the ' + + 'ICommentIcon interface', + ); + } + comment = this.addIcon(icon); + } + eventUtils.disable(); + comment.setText(text); + eventUtils.enable(); + } else { + this.removeIcon(IconType.COMMENT); + } + + eventUtils.fire( + new (eventUtils.get(EventType.BLOCK_CHANGE))( + this, + 'comment', + null, + oldText, + text, + ), + ); + } + + /** + * Set this block's warning text. + * + * @param _text The text, or null to delete. + * @param _opt_id An optional ID for the warning text to be able to maintain + * multiple warnings. + */ + setWarningText(_text: string | null, _opt_id?: string) { + // NOOP. + } + + /** + * Give this block a mutator dialog. + * + * @param _mutator A mutator dialog instance or null to remove. + */ + setMutator(_mutator: MutatorIcon) { + // NOOP. + } + + /** Adds the given icon to the block. */ + addIcon(icon: T): T { + if (this.hasIcon(icon.getType())) throw new DuplicateIconType(icon); + this.icons.push(icon); + this.icons.sort((a, b) => a.getWeight() - b.getWeight()); + return icon; + } + + /** + * Removes the icon whose getType matches the given type iconType from the + * block. + * + * @param type The type of the icon to remove from the block. + * @returns True if an icon with the given type was found, false otherwise. + */ + removeIcon(type: IconType): boolean { + if (!this.hasIcon(type)) return false; + this.getIcon(type)?.dispose(); + this.icons = this.icons.filter((icon) => !icon.getType().equals(type)); + return true; + } + + /** + * @returns True if an icon with the given type exists on the block, + * false otherwise. + */ + hasIcon(type: IconType): boolean { + return this.icons.some((icon) => icon.getType().equals(type)); + } + + /** + * @param type The type of the icon to retrieve. Prefer passing an `IconType` + * for proper type checking when using typescript. + * @returns The icon with the given type if it exists on the block, undefined + * otherwise. + */ + getIcon(type: IconType | string): T | undefined { + if (type instanceof IconType) { + return this.icons.find((icon) => icon.getType().equals(type)) as T; + } else { + return this.icons.find((icon) => icon.getType().toString() === type) as T; + } + } + + /** @returns An array of the icons attached to this block. */ + getIcons(): IIcon[] { + return [...this.icons]; + } + + /** + * Return the coordinates of the top-left corner of this block relative to the + * drawing surface's origin (0,0), in workspace units. + * + * @returns Object with .x and .y properties. + */ + getRelativeToSurfaceXY(): Coordinate { + return this.xy; + } + + /** + * Move a block by a relative offset. + * + * @param dx Horizontal offset, in workspace units. + * @param dy Vertical offset, in workspace units. + * @param reason Why is this move happening? 'drag', 'bump', 'snap', ... + */ + moveBy(dx: number, dy: number, reason?: string[]) { + if (this.parentBlock_) { + throw Error('Block has parent'); + } + const event = new (eventUtils.get(EventType.BLOCK_MOVE))(this) as BlockMove; + if (reason) event.setReason(reason); + this.xy.translate(dx, dy); + event.recordNew(); + eventUtils.fire(event); + } + + /** + * Create a connection of the specified type. + * + * @param type The type of the connection to create. + * @returns A new connection of the specified type. + * @internal + */ + makeConnection_(type: ConnectionType): Connection { + return new Connection(this, type); + } + + /** + * Recursively checks whether all statement and value inputs are filled with + * blocks. Also checks all following statement blocks in this stack. + * + * @param opt_shadowBlocksAreFilled An optional argument controlling whether + * shadow blocks are counted as filled. Defaults to true. + * @returns True if all inputs are filled, false otherwise. + */ + allInputsFilled(opt_shadowBlocksAreFilled?: boolean): boolean { + // Account for the shadow block filledness toggle. + if (opt_shadowBlocksAreFilled === undefined) { + opt_shadowBlocksAreFilled = true; + } + if (!opt_shadowBlocksAreFilled && this.isShadow()) { + return false; + } + + // Recursively check each input block of the current block. + for (let i = 0, input; (input = this.inputList[i]); i++) { + if (!input.connection) { + continue; + } + const target = input.connection.targetBlock(); + if (!target || !target.allInputsFilled(opt_shadowBlocksAreFilled)) { + return false; + } + } + + // Recursively check the next block after the current block. + const next = this.getNextBlock(); + if (next) { + return next.allInputsFilled(opt_shadowBlocksAreFilled); + } + + return true; + } + + /** + * This method returns a string describing this Block in developer terms (type + * name and ID; English only). + * + * Intended to on be used in console logs and errors. If you need a string + * that uses the user's native language (including block text, field values, + * and child blocks), use [toString()]{@link Block#toString}. + * + * @returns The description. + */ + toDevString(): string { + let msg = this.type ? '"' + this.type + '" block' : 'Block'; + if (this.id) { + msg += ' (id="' + this.id + '")'; + } + return msg; + } +} + +export namespace Block { + export interface CommentModel { + text: string | null; + pinned: boolean; + size: Size; + } +} + +export type CommentModel = Block.CommentModel; diff --git a/core/block_animations.ts b/core/block_animations.ts new file mode 100644 index 00000000000..f3fc3d454f0 --- /dev/null +++ b/core/block_animations.ts @@ -0,0 +1,225 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.blockAnimations + +import type {BlockSvg} from './block_svg.js'; +import * as dom from './utils/dom.js'; +import {Svg} from './utils/svg.js'; + +/** A bounding box for a cloned block. */ +interface CloneRect { + x: number; + y: number; + width: number; + height: number; +} + +/** PID of disconnect UI animation. There can only be one at a time. */ +let disconnectPid: ReturnType | null = null; + +/** The wobbling block. There can only be one at a time. */ +let wobblingBlock: BlockSvg | null = null; + +/** + * Play some UI effects (sound, animation) when disposing of a block. + * + * @param block The block being disposed of. + * @internal + */ +export function disposeUiEffect(block: BlockSvg) { + // Disposing is going to take so long the animation won't play anyway. + if (block.getDescendants(false).length > 100) return; + + const workspace = block.workspace; + const svgGroup = block.getSvgRoot(); + workspace.getAudioManager().play('delete'); + + const xy = block.getRelativeToSurfaceXY(); + // Deeply clone the current block. + const clone: SVGGElement = svgGroup.cloneNode(true) as SVGGElement; + clone.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')'); + workspace.getLayerManager()?.appendToAnimationLayer({ + getSvgRoot: () => { + return clone; + }, + }); + const cloneRect = { + 'x': xy.x, + 'y': xy.y, + 'width': block.width, + 'height': block.height, + }; + disposeUiStep(clone, cloneRect, workspace.RTL, new Date()); +} +/** + * Animate a cloned block and eventually dispose of it. + * This is a class method, not an instance method since the original block has + * been destroyed and is no longer accessible. + * + * @param clone SVG element to animate and dispose of. + * @param rect Starting rect of the clone. + * @param rtl True if RTL, false if LTR. + * @param start Date of animation's start. + */ +function disposeUiStep( + clone: Element, + rect: CloneRect, + rtl: boolean, + start: Date, +) { + const ms = new Date().getTime() - start.getTime(); + const percent = ms / 150; + if (percent > 1) { + dom.removeNode(clone); + } else { + const x = rect.x + (((rtl ? -1 : 1) * rect.width) / 2) * percent; + const y = rect.y + (rect.height / 2) * percent; + const scale = 1 - percent; + clone.setAttribute( + 'transform', + 'translate(' + x + ',' + y + ')' + ' scale(' + scale + ')', + ); + setTimeout(disposeUiStep, 10, clone, rect, rtl, start); + } +} + +/** + * Play some UI effects (sound, ripple) after a connection has been established. + * + * @param block The block being connected. + * @internal + */ +export function connectionUiEffect(block: BlockSvg) { + const workspace = block.workspace; + const scale = workspace.scale; + workspace.getAudioManager().play('click'); + if (scale < 1) { + return; // Too small to care about visual effects. + } + // Determine the absolute coordinates of the inferior block. + const xy = workspace.getSvgXY(block.getSvgRoot()); + // Offset the coordinates based on the two connection types, fix scale. + if (block.outputConnection) { + xy.x += (block.RTL ? 3 : -3) * scale; + xy.y += 13 * scale; + } else if (block.previousConnection) { + xy.x += (block.RTL ? -23 : 23) * scale; + xy.y += 3 * scale; + } + const ripple = dom.createSvgElement( + Svg.CIRCLE, + { + 'cx': xy.x, + 'cy': xy.y, + 'r': 0, + 'fill': 'none', + 'stroke': '#888', + 'stroke-width': 10, + }, + workspace.getParentSvg(), + ); + + const scaleAnimation = dom.createSvgElement( + Svg.ANIMATE, + { + 'id': 'animationCircle', + 'begin': 'indefinite', + 'attributeName': 'r', + 'dur': '150ms', + 'from': 0, + 'to': 25 * scale, + }, + ripple, + ); + const opacityAnimation = dom.createSvgElement( + Svg.ANIMATE, + { + 'id': 'animationOpacity', + 'begin': 'indefinite', + 'attributeName': 'opacity', + 'dur': '150ms', + 'from': 1, + 'to': 0, + }, + ripple, + ); + + scaleAnimation.beginElement(); + opacityAnimation.beginElement(); + + setTimeout(() => void dom.removeNode(ripple), 150); +} + +/** + * Play some UI effects (sound, animation) when disconnecting a block. + * + * @param block The block being disconnected. + * @internal + */ +export function disconnectUiEffect(block: BlockSvg) { + disconnectUiStop(); + block.workspace.getAudioManager().play('disconnect'); + if (block.workspace.scale < 1) { + return; // Too small to care about visual effects. + } + // Horizontal distance for bottom of block to wiggle. + const DISPLACEMENT = 10; + // Scale magnitude of skew to height of block. + const height = block.getHeightWidth().height; + let magnitude = (Math.atan(DISPLACEMENT / height) / Math.PI) * 180; + if (!block.RTL) { + magnitude *= -1; + } + // Start the animation. + wobblingBlock = block; + disconnectUiStep(block, magnitude, new Date()); +} + +/** + * Animate a brief wiggle of a disconnected block. + * + * @param block Block to animate. + * @param magnitude Maximum degrees skew (reversed for RTL). + * @param start Date of animation's start. + */ +function disconnectUiStep(block: BlockSvg, magnitude: number, start: Date) { + const DURATION = 200; // Milliseconds. + const WIGGLES = 3; // Half oscillations. + + const ms = new Date().getTime() - start.getTime(); + const percent = ms / DURATION; + + let skew = ''; + if (percent <= 1) { + const val = Math.round( + Math.sin(percent * Math.PI * WIGGLES) * (1 - percent) * magnitude, + ); + skew = `skewX(${val})`; + disconnectPid = setTimeout(disconnectUiStep, 10, block, magnitude, start); + } + + block + .getSvgRoot() + .setAttribute('transform', `${block.getTranslation()} ${skew}`); +} + +/** + * Stop the disconnect UI animation immediately. + * + * @internal + */ +export function disconnectUiStop() { + if (!wobblingBlock) return; + if (disconnectPid) { + clearTimeout(disconnectPid); + disconnectPid = null; + } + wobblingBlock + .getSvgRoot() + .setAttribute('transform', wobblingBlock.getTranslation()); + wobblingBlock = null; +} diff --git a/core/block_drag_surface.js b/core/block_drag_surface.js deleted file mode 100644 index be72fa8e862..00000000000 --- a/core/block_drag_surface.js +++ /dev/null @@ -1,220 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 A class that manages a surface for dragging blocks. When a - * block drag is started, we move the block (and children) to a separate dom - * element that we move around using translate3d. At the end of the drag, the - * blocks are put back in into the svg they came from. This helps performance by - * avoiding repainting the entire svg on every mouse move while dragging blocks. - * @author picklesrus - */ - -'use strict'; - -goog.provide('Blockly.BlockDragSurfaceSvg'); -goog.require('Blockly.utils'); -goog.require('goog.asserts'); -goog.require('goog.math.Coordinate'); - - -/** - * Class for a drag surface for the currently dragged block. This is a separate - * SVG that contains only the currently moving block, or nothing. - * @param {!Element} container Containing element. - * @constructor - */ -Blockly.BlockDragSurfaceSvg = function(container) { - /** - * @type {!Element} - * @private - */ - this.container_ = container; - this.createDom(); -}; - -/** - * The SVG drag surface. Set once by Blockly.BlockDragSurfaceSvg.createDom. - * @type {Element} - * @private - */ -Blockly.BlockDragSurfaceSvg.prototype.SVG_ = null; - -/** - * This is where blocks live while they are being dragged if the drag surface - * is enabled. - * @type {Element} - * @private - */ -Blockly.BlockDragSurfaceSvg.prototype.dragGroup_ = null; - -/** - * Containing HTML element; parent of the workspace and the drag surface. - * @type {Element} - * @private - */ -Blockly.BlockDragSurfaceSvg.prototype.container_ = null; - -/** - * Cached value for the scale of the drag surface. - * Used to set/get the correct translation during and after a drag. - * @type {number} - * @private - */ -Blockly.BlockDragSurfaceSvg.prototype.scale_ = 1; - -/** - * Cached value for the translation of the drag surface. - * This translation is in pixel units, because the scale is applied to the - * drag group rather than the top-level SVG. - * @type {goog.math.Coordinate} - * @private - */ -Blockly.BlockDragSurfaceSvg.prototype.surfaceXY_ = null; - -/** - * Create the drag surface and inject it into the container. - */ -Blockly.BlockDragSurfaceSvg.prototype.createDom = function() { - if (this.SVG_) { - return; // Already created. - } - this.SVG_ = Blockly.utils.createSvgElement('svg', { - 'xmlns': Blockly.SVG_NS, - 'xmlns:html': Blockly.HTML_NS, - 'xmlns:xlink': 'http://www.w3.org/1999/xlink', - 'version': '1.1', - 'class': 'blocklyBlockDragSurface' - }, this.container_); - this.dragGroup_ = Blockly.utils.createSvgElement('g', {}, this.SVG_); -}; - -/** - * Set the SVG blocks on the drag surface's group and show the surface. - * Only one block group should be on the drag surface at a time. - * @param {!Element} blocks Block or group of blocks to place on the drag - * surface. - */ -Blockly.BlockDragSurfaceSvg.prototype.setBlocksAndShow = function(blocks) { - goog.asserts.assert(this.dragGroup_.childNodes.length == 0, - 'Already dragging a block.'); - // appendChild removes the blocks from the previous parent - this.dragGroup_.appendChild(blocks); - this.SVG_.style.display = 'block'; - this.surfaceXY_ = new goog.math.Coordinate(0, 0); -}; - -/** - * Translate and scale the entire drag surface group to the given position, to - * keep in sync with the workspace. - * @param {number} x X translation in workspace coordinates. - * @param {number} y Y translation in workspace coordinates. - * @param {number} scale Scale of the group. - */ -Blockly.BlockDragSurfaceSvg.prototype.translateAndScaleGroup = function(x, y, scale) { - this.scale_ = scale; - // This is a work-around to prevent a the blocks from rendering - // fuzzy while they are being dragged on the drag surface. - x = x.toFixed(0); - y = y.toFixed(0); - this.dragGroup_.setAttribute('transform', 'translate('+ x + ','+ y + ')' + - ' scale(' + scale + ')'); -}; - -/** - * Translate the drag surface's SVG based on its internal state. - * @private - */ -Blockly.BlockDragSurfaceSvg.prototype.translateSurfaceInternal_ = function() { - var x = this.surfaceXY_.x; - var y = this.surfaceXY_.y; - // This is a work-around to prevent a the blocks from rendering - // fuzzy while they are being dragged on the drag surface. - x = x.toFixed(0); - y = y.toFixed(0); - this.SVG_.style.display = 'block'; - - Blockly.utils.setCssTransform(this.SVG_, - 'translate3d(' + x + 'px, ' + y + 'px, 0px)'); -}; - -/** - * Translate the entire drag surface during a drag. - * We translate the drag surface instead of the blocks inside the surface - * so that the browser avoids repainting the SVG. - * Because of this, the drag coordinates must be adjusted by scale. - * @param {number} x X translation for the entire surface. - * @param {number} y Y translation for the entire surface. - */ -Blockly.BlockDragSurfaceSvg.prototype.translateSurface = function(x, y) { - this.surfaceXY_ = new goog.math.Coordinate(x * this.scale_, y * this.scale_); - this.translateSurfaceInternal_(); -}; - -/** - * Reports the surface translation in scaled workspace coordinates. - * Use this when finishing a drag to return blocks to the correct position. - * @return {!goog.math.Coordinate} Current translation of the surface. - */ -Blockly.BlockDragSurfaceSvg.prototype.getSurfaceTranslation = function() { - var xy = Blockly.utils.getRelativeXY(this.SVG_); - return new goog.math.Coordinate(xy.x / this.scale_, xy.y / this.scale_); -}; - -/** - * Provide a reference to the drag group (primarily for - * BlockSvg.getRelativeToSurfaceXY). - * @return {Element} Drag surface group element. - */ -Blockly.BlockDragSurfaceSvg.prototype.getGroup = function() { - return this.dragGroup_; -}; - -/** - * Get the current blocks on the drag surface, if any (primarily - * for BlockSvg.getRelativeToSurfaceXY). - * @return {!Element|undefined} Drag surface block DOM element, or undefined - * if no blocks exist. - */ -Blockly.BlockDragSurfaceSvg.prototype.getCurrentBlock = function() { - return this.dragGroup_.firstChild; -}; - -/** - * Clear the group and hide the surface; move the blocks off onto the provided - * element. - * If the block is being deleted it doesn't need to go back to the original - * surface, since it would be removed immediately during dispose. - * @param {Element} opt_newSurface Surface the dragging blocks should be moved - * to, or null if the blocks should be removed from this surface without - * being moved to a different surface. - */ -Blockly.BlockDragSurfaceSvg.prototype.clearAndHide = function(opt_newSurface) { - if (opt_newSurface) { - // appendChild removes the node from this.dragGroup_ - opt_newSurface.appendChild(this.getCurrentBlock()); - } else { - this.dragGroup_.removeChild(this.getCurrentBlock()); - } - this.SVG_.style.display = 'none'; - goog.asserts.assert(this.dragGroup_.childNodes.length == 0, - 'Drag group was not cleared.'); - this.surfaceXY_ = null; -}; diff --git a/core/block_dragger.js b/core/block_dragger.js deleted file mode 100644 index 0ba1749f074..00000000000 --- a/core/block_dragger.js +++ /dev/null @@ -1,324 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Methods for dragging a block visually. - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -goog.provide('Blockly.BlockDragger'); - -goog.require('Blockly.DraggedConnectionManager'); - -goog.require('goog.math.Coordinate'); -goog.require('goog.asserts'); - - -/** - * Class for a block dragger. It moves blocks around the workspace when they - * are being dragged by a mouse or touch. - * @param {!Blockly.Block} block The block to drag. - * @param {!Blockly.WorkspaceSvg} workspace The workspace to drag on. - * @constructor - */ -Blockly.BlockDragger = function(block, workspace) { - /** - * The top block in the stack that is being dragged. - * @type {!Blockly.BlockSvg} - * @private - */ - this.draggingBlock_ = block; - - /** - * The workspace on which the block is being dragged. - * @type {!Blockly.WorkspaceSvg} - * @private - */ - this.workspace_ = workspace; - - /** - * Object that keeps track of connections on dragged blocks. - * @type {!Blockly.DraggedConnectionManager} - * @private - */ - this.draggedConnectionManager_ = new Blockly.DraggedConnectionManager( - this.draggingBlock_); - - /** - * Which delete area the mouse pointer is over, if any. - * One of {@link Blockly.DELETE_AREA_TRASH}, - * {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}. - * @type {?number} - * @private - */ - this.deleteArea_ = null; - - /** - * Whether the block would be deleted if dropped immediately. - * @type {boolean} - * @private - */ - this.wouldDeleteBlock_ = false; - - /** - * The location of the top left corner of the dragging block at the beginning - * of the drag in workspace coordinates. - * @type {!goog.math.Coordinate} - * @private - */ - this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY(); - - /** - * A list of all of the icons (comment, warning, and mutator) that are - * on this block and its descendants. Moving an icon moves the bubble that - * extends from it if that bubble is open. - * @type {Array.} - * @private - */ - this.dragIconData_ = Blockly.BlockDragger.initIconData_(block); -}; - -/** - * Sever all links from this object. - * @package - */ -Blockly.BlockDragger.prototype.dispose = function() { - this.draggingBlock_ = null; - this.workspace_ = null; - this.startWorkspace_ = null; - this.dragIconData_.length = 0; - - if (this.draggedConnectionManager_) { - this.draggedConnectionManager_.dispose(); - this.draggedConnectionManager_ = null; - } -}; - -/** - * Make a list of all of the icons (comment, warning, and mutator) that are - * on this block and its descendants. Moving an icon moves the bubble that - * extends from it if that bubble is open. - * @param {!Blockly.BlockSvg} block The root block that is being dragged. - * @return {!Array.} The list of all icons and their locations. - * @private - */ -Blockly.BlockDragger.initIconData_ = function(block) { - // Build a list of icons that need to be moved and where they started. - var dragIconData = []; - var descendants = block.getDescendants(); - for (var i = 0, descendant; descendant = descendants[i]; i++) { - var icons = descendant.getIcons(); - for (var j = 0; j < icons.length; j++) { - var data = { - // goog.math.Coordinate with x and y properties (workspace coordinates). - location: icons[j].getIconLocation(), - // Blockly.Icon - icon: icons[j] - }; - dragIconData.push(data); - } - } - return dragIconData; -}; - -/** - * Start dragging a block. This includes moving it to the drag surface. - * @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has - * moved from the position at mouse down, in pixel units. - * @package - */ -Blockly.BlockDragger.prototype.startBlockDrag = function(currentDragDeltaXY) { - if (!Blockly.Events.getGroup()) { - Blockly.Events.setGroup(true); - } - - this.workspace_.setResizesEnabled(false); - Blockly.BlockSvg.disconnectUiStop_(); - - if (this.draggingBlock_.getParent()) { - this.draggingBlock_.unplug(); - var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); - var newLoc = goog.math.Coordinate.sum(this.startXY_, delta); - - this.draggingBlock_.translate(newLoc.x, newLoc.y); - this.draggingBlock_.disconnectUiEffect(); - } - this.draggingBlock_.setDragging(true); - // For future consideration: we may be able to put moveToDragSurface inside - // the block dragger, which would also let the block not track the block drag - // surface. - this.draggingBlock_.moveToDragSurface_(); - - if (this.workspace_.toolbox_) { - this.workspace_.toolbox_.addDeleteStyle(); - } -}; - -/** - * Execute a step of block dragging, based on the given event. Update the - * display accordingly. - * @param {!Event} e The most recent move event. - * @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has - * moved from the position at the start of the drag, in pixel units. - * @package - */ -Blockly.BlockDragger.prototype.dragBlock = function(e, currentDragDeltaXY) { - var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); - var newLoc = goog.math.Coordinate.sum(this.startXY_, delta); - - this.draggingBlock_.moveDuringDrag(newLoc); - this.dragIcons_(delta); - - this.deleteArea_ = this.workspace_.isDeleteArea(e); - this.draggedConnectionManager_.update(delta, this.deleteArea_); - - this.updateCursorDuringBlockDrag_(); -}; - -/** - * Finish a block drag and put the block back on the workspace. - * @param {!Event} e The mouseup/touchend event. - * @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has - * moved from the position at the start of the drag, in pixel units. - * @package - */ -Blockly.BlockDragger.prototype.endBlockDrag = function(e, currentDragDeltaXY) { - // Make sure internal state is fresh. - this.dragBlock(e, currentDragDeltaXY); - this.dragIconData_ = []; - - Blockly.BlockSvg.disconnectUiStop_(); - - var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); - var newLoc = goog.math.Coordinate.sum(this.startXY_, delta); - this.draggingBlock_.moveOffDragSurface_(newLoc); - - var deleted = this.maybeDeleteBlock_(); - if (!deleted) { - // These are expensive and don't need to be done if we're deleting. - this.draggingBlock_.moveConnections_(delta.x, delta.y); - this.draggingBlock_.setDragging(false); - this.draggedConnectionManager_.applyConnections(); - this.draggingBlock_.render(); - this.fireMoveEvent_(); - this.draggingBlock_.scheduleSnapAndBump(); - } - this.workspace_.setResizesEnabled(true); - - if (this.workspace_.toolbox_) { - this.workspace_.toolbox_.removeDeleteStyle(); - } - Blockly.Events.setGroup(false); -}; - -/** - * Fire a move event at the end of a block drag. - * @private - */ -Blockly.BlockDragger.prototype.fireMoveEvent_ = function() { - var event = new Blockly.Events.BlockMove(this.draggingBlock_); - event.oldCoordinate = this.startXY_; - event.recordNew(); - Blockly.Events.fire(event); -}; - -/** - * Shut the trash can and, if necessary, delete the dragging block. - * Should be called at the end of a block drag. - * @return {boolean} whether the block was deleted. - * @private - */ -Blockly.BlockDragger.prototype.maybeDeleteBlock_ = function() { - var trashcan = this.workspace_.trashcan; - - if (this.wouldDeleteBlock_) { - if (trashcan) { - goog.Timer.callOnce(trashcan.close, 100, trashcan); - } - // Fire a move event, so we know where to go back to for an undo. - this.fireMoveEvent_(); - this.draggingBlock_.dispose(false, true); - } else if (trashcan) { - // Make sure the trash can is closed. - trashcan.close(); - } - return this.wouldDeleteBlock_; -}; - -/** - * Update the cursor (and possibly the trash can lid) to reflect whether the - * dragging block would be deleted if released immediately. - * @private - */ -Blockly.BlockDragger.prototype.updateCursorDuringBlockDrag_ = function() { - this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock(); - var trashcan = this.workspace_.trashcan; - if (this.wouldDeleteBlock_) { - this.draggingBlock_.setDeleteStyle(true); - if (this.deleteArea_ == Blockly.DELETE_AREA_TRASH && trashcan) { - trashcan.setOpen_(true); - } - } else { - this.draggingBlock_.setDeleteStyle(false); - if (trashcan) { - trashcan.setOpen_(false); - } - } -}; - -/** - * Convert a coordinate object from pixels to workspace units, including a - * correction for mutator workspaces. - * This function does not consider differing origins. It simply scales the - * input's x and y values. - * @param {!goog.math.Coordinate} pixelCoord A coordinate with x and y values - * in css pixel units. - * @return {!goog.math.Coordinate} The input coordinate divided by the workspace - * scale. - * @private - */ -Blockly.BlockDragger.prototype.pixelsToWorkspaceUnits_ = function(pixelCoord) { - var result = new goog.math.Coordinate(pixelCoord.x / this.workspace_.scale, - pixelCoord.y / this.workspace_.scale); - if (this.workspace_.isMutator) { - // If we're in a mutator, its scale is always 1, purely because of some - // oddities in our rendering optimizations. The actual scale is the same as - // the scale on the parent workspace. - // Fix that for dragging. - var mainScale = this.workspace_.options.parentWorkspace.scale; - result = result.scale(1 / mainScale); - } - return result; -}; - -/** - * Move all of the icons connected to this drag. - * @param {!goog.math.Coordinate} dxy How far to move the icons from their - * original positions, in workspace units. - * @private - */ -Blockly.BlockDragger.prototype.dragIcons_ = function(dxy) { - // Moving icons moves their associated bubbles. - for (var i = 0; i < this.dragIconData_.length; i++) { - var data = this.dragIconData_[i]; - data.icon.setIconLocation(goog.math.Coordinate.sum(data.location, dxy)); - } -}; diff --git a/core/block_render_svg.js b/core/block_render_svg.js deleted file mode 100644 index 4c3bfb33b1a..00000000000 --- a/core/block_render_svg.js +++ /dev/null @@ -1,1000 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Methods for graphically rendering a block as SVG. - * @author fenichel@google.com (Rachel Fenichel) - */ - -'use strict'; - -goog.provide('Blockly.BlockSvg.render'); - -goog.require('Blockly.BlockSvg'); - -goog.require('goog.userAgent'); - - -// UI constants for rendering blocks. -/** - * Horizontal space between elements. - * @const - */ -Blockly.BlockSvg.SEP_SPACE_X = 10; -/** - * Vertical space between elements. - * @const - */ -Blockly.BlockSvg.SEP_SPACE_Y = 10; -/** - * Vertical padding around inline elements. - * @const - */ -Blockly.BlockSvg.INLINE_PADDING_Y = 5; -/** - * Minimum height of a block. - * @const - */ -Blockly.BlockSvg.MIN_BLOCK_Y = 25; -/** - * Height of horizontal puzzle tab. - * @const - */ -Blockly.BlockSvg.TAB_HEIGHT = 20; -/** - * Width of horizontal puzzle tab. - * @const - */ -Blockly.BlockSvg.TAB_WIDTH = 8; -/** - * Width of vertical tab (inc left margin). - * @const - */ -Blockly.BlockSvg.NOTCH_WIDTH = 30; -/** - * Rounded corner radius. - * @const - */ -Blockly.BlockSvg.CORNER_RADIUS = 8; -/** - * Do blocks with no previous or output connections have a 'hat' on top? - * @const - */ -Blockly.BlockSvg.START_HAT = false; -/** - * Height of the top hat. - * @const - */ -Blockly.BlockSvg.START_HAT_HEIGHT = 15; -/** - * Path of the top hat's curve. - * @const - */ -Blockly.BlockSvg.START_HAT_PATH = 'c 30,-' + - Blockly.BlockSvg.START_HAT_HEIGHT + ' 70,-' + - Blockly.BlockSvg.START_HAT_HEIGHT + ' 100,0'; -/** - * Path of the top hat's curve's highlight in LTR. - * @const - */ -Blockly.BlockSvg.START_HAT_HIGHLIGHT_LTR = - 'c 17.8,-9.2 45.3,-14.9 75,-8.7 M 100.5,0.5'; -/** - * Path of the top hat's curve's highlight in RTL. - * @const - */ -Blockly.BlockSvg.START_HAT_HIGHLIGHT_RTL = - 'm 25,-8.7 c 29.7,-6.2 57.2,-0.5 75,8.7'; -/** - * Distance from shape edge to intersect with a curved corner at 45 degrees. - * Applies to highlighting on around the inside of a curve. - * @const - */ -Blockly.BlockSvg.DISTANCE_45_INSIDE = (1 - Math.SQRT1_2) * - (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + 0.5; -/** - * Distance from shape edge to intersect with a curved corner at 45 degrees. - * Applies to highlighting on around the outside of a curve. - * @const - */ -Blockly.BlockSvg.DISTANCE_45_OUTSIDE = (1 - Math.SQRT1_2) * - (Blockly.BlockSvg.CORNER_RADIUS + 0.5) - 0.5; -/** - * SVG path for drawing next/previous notch from left to right. - * @const - */ -Blockly.BlockSvg.NOTCH_PATH_LEFT = 'l 6,4 3,0 6,-4'; -/** - * SVG path for drawing next/previous notch from left to right with - * highlighting. - * @const - */ -Blockly.BlockSvg.NOTCH_PATH_LEFT_HIGHLIGHT = 'l 6,4 3,0 6,-4'; -/** - * SVG path for drawing next/previous notch from right to left. - * @const - */ -Blockly.BlockSvg.NOTCH_PATH_RIGHT = 'l -6,4 -3,0 -6,-4'; -/** - * SVG path for drawing jagged teeth at the end of collapsed blocks. - * @const - */ -Blockly.BlockSvg.JAGGED_TEETH = 'l 8,0 0,4 8,4 -16,8 8,4'; -/** - * Height of SVG path for jagged teeth at the end of collapsed blocks. - * @const - */ -Blockly.BlockSvg.JAGGED_TEETH_HEIGHT = 20; -/** - * Width of SVG path for jagged teeth at the end of collapsed blocks. - * @const - */ -Blockly.BlockSvg.JAGGED_TEETH_WIDTH = 15; -/** - * SVG path for drawing a horizontal puzzle tab from top to bottom. - * @const - */ -Blockly.BlockSvg.TAB_PATH_DOWN = 'v 5 c 0,10 -' + Blockly.BlockSvg.TAB_WIDTH + - ',-8 -' + Blockly.BlockSvg.TAB_WIDTH + ',7.5 s ' + - Blockly.BlockSvg.TAB_WIDTH + ',-2.5 ' + Blockly.BlockSvg.TAB_WIDTH + ',7.5'; -/** - * SVG path for drawing a horizontal puzzle tab from top to bottom with - * highlighting from the upper-right. - * @const - */ -Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL = 'v 6.5 m -' + - (Blockly.BlockSvg.TAB_WIDTH * 0.97) + ',3 q -' + - (Blockly.BlockSvg.TAB_WIDTH * 0.05) + ',10 ' + - (Blockly.BlockSvg.TAB_WIDTH * 0.3) + ',9.5 m ' + - (Blockly.BlockSvg.TAB_WIDTH * 0.67) + ',-1.9 v 1.4'; - -/** - * SVG start point for drawing the top-left corner. - * @const - */ -Blockly.BlockSvg.TOP_LEFT_CORNER_START = - 'm 0,' + Blockly.BlockSvg.CORNER_RADIUS; -/** - * SVG start point for drawing the top-left corner's highlight in RTL. - * @const - */ -Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL = - 'm ' + Blockly.BlockSvg.DISTANCE_45_INSIDE + ',' + - Blockly.BlockSvg.DISTANCE_45_INSIDE; -/** - * SVG start point for drawing the top-left corner's highlight in LTR. - * @const - */ -Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR = - 'm 0.5,' + (Blockly.BlockSvg.CORNER_RADIUS - 0.5); -/** - * SVG path for drawing the rounded top-left corner. - * @const - */ -Blockly.BlockSvg.TOP_LEFT_CORNER = - 'A ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + - Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 ' + - Blockly.BlockSvg.CORNER_RADIUS + ',0'; -/** - * SVG path for drawing the highlight on the rounded top-left corner. - * @const - */ -Blockly.BlockSvg.TOP_LEFT_CORNER_HIGHLIGHT = - 'A ' + (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ',' + - (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ' 0 0,1 ' + - Blockly.BlockSvg.CORNER_RADIUS + ',0.5'; -/** - * SVG path for drawing the top-left corner of a statement input. - * Includes the top notch, a horizontal space, and the rounded inside corner. - * @const - */ -Blockly.BlockSvg.INNER_TOP_LEFT_CORNER = - Blockly.BlockSvg.NOTCH_PATH_RIGHT + ' h -' + - (Blockly.BlockSvg.NOTCH_WIDTH - 15 - Blockly.BlockSvg.CORNER_RADIUS) + - ' a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + - Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 -' + - Blockly.BlockSvg.CORNER_RADIUS + ',' + - Blockly.BlockSvg.CORNER_RADIUS; -/** - * SVG path for drawing the bottom-left corner of a statement input. - * Includes the rounded inside corner. - * @const - */ -Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER = - 'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + - Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' + - Blockly.BlockSvg.CORNER_RADIUS + ',' + - Blockly.BlockSvg.CORNER_RADIUS; -/** - * SVG path for drawing highlight on the top-left corner of a statement - * input in RTL. - * @const - */ -Blockly.BlockSvg.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL = - 'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + - Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' + - (-Blockly.BlockSvg.DISTANCE_45_OUTSIDE - 0.5) + ',' + - (Blockly.BlockSvg.CORNER_RADIUS - - Blockly.BlockSvg.DISTANCE_45_OUTSIDE); -/** - * SVG path for drawing highlight on the bottom-left corner of a statement - * input in RTL. - * @const - */ -Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL = - 'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ',' + - (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ' 0 0,0 ' + - (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ',' + - (Blockly.BlockSvg.CORNER_RADIUS + 0.5); -/** - * SVG path for drawing highlight on the bottom-left corner of a statement - * input in LTR. - * @const - */ -Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR = - 'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ',' + - (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ' 0 0,0 ' + - (Blockly.BlockSvg.CORNER_RADIUS - - Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' + - (Blockly.BlockSvg.DISTANCE_45_OUTSIDE + 0.5); - -/** - * Returns a bounding box describing the dimensions of this block - * and any blocks stacked below it. - * @return {!{height: number, width: number}} Object with height and width - * properties in workspace units. - */ -Blockly.BlockSvg.prototype.getHeightWidth = function() { - var height = this.height; - var width = this.width; - // Recursively add size of subsequent blocks. - var nextBlock = this.getNextBlock(); - if (nextBlock) { - var nextHeightWidth = nextBlock.getHeightWidth(); - height += nextHeightWidth.height - 4; // Height of tab. - width = Math.max(width, nextHeightWidth.width); - } else if (!this.nextConnection && !this.outputConnection) { - // Add a bit of margin under blocks with no bottom tab. - height += 2; - } - return {height: height, width: width}; -}; - -/** - * Render the block. - * Lays out and reflows a block based on its contents and settings. - * @param {boolean=} opt_bubble If false, just render this block. - * If true, also render block's parent, grandparent, etc. Defaults to true. - */ -Blockly.BlockSvg.prototype.render = function(opt_bubble) { - Blockly.Field.startCache(); - this.rendered = true; - - var cursorX = Blockly.BlockSvg.SEP_SPACE_X; - if (this.RTL) { - cursorX = -cursorX; - } - // Move the icons into position. - var icons = this.getIcons(); - for (var i = 0; i < icons.length; i++) { - cursorX = icons[i].renderIcon(cursorX); - } - cursorX += this.RTL ? - Blockly.BlockSvg.SEP_SPACE_X : -Blockly.BlockSvg.SEP_SPACE_X; - // If there are no icons, cursorX will be 0, otherwise it will be the - // width that the first label needs to move over by. - - var inputRows = this.renderCompute_(cursorX); - this.renderDraw_(cursorX, inputRows); - this.renderMoveConnections_(); - - if (opt_bubble !== false) { - // Render all blocks above this one (propagate a reflow). - var parentBlock = this.getParent(); - if (parentBlock) { - parentBlock.render(true); - } else { - // Top-most block. Fire an event to allow scrollbars to resize. - this.workspace.resizeContents(); - } - } - Blockly.Field.stopCache(); -}; - -/** - * Render a list of fields starting at the specified location. - * @param {!Array.} fieldList List of fields. - * @param {number} cursorX X-coordinate to start the fields. - * @param {number} cursorY Y-coordinate to start the fields. - * @return {number} X-coordinate of the end of the field row (plus a gap). - * @private - */ -Blockly.BlockSvg.prototype.renderFields_ = - function(fieldList, cursorX, cursorY) { - /* eslint-disable indent */ - cursorY += Blockly.BlockSvg.INLINE_PADDING_Y; - if (this.RTL) { - cursorX = -cursorX; - } - for (var t = 0, field; field = fieldList[t]; t++) { - var root = field.getSvgRoot(); - if (!root) { - continue; - } - - // Force a width re-calculation on IE and Edge to get around the issue - // described in Blockly.Field.getCachedWidth - if (goog.userAgent.IE || goog.userAgent.EDGE) { - field.updateWidth(); - } - - if (this.RTL) { - cursorX -= field.renderSep + field.renderWidth; - root.setAttribute('transform', - 'translate(' + cursorX + ',' + cursorY + ')'); - if (field.renderWidth) { - cursorX -= Blockly.BlockSvg.SEP_SPACE_X; - } - } else { - root.setAttribute('transform', - 'translate(' + (cursorX + field.renderSep) + ',' + cursorY + ')'); - if (field.renderWidth) { - cursorX += field.renderSep + field.renderWidth + - Blockly.BlockSvg.SEP_SPACE_X; - } - } - } - return this.RTL ? -cursorX : cursorX; -}; /* eslint-enable indent */ - -/** - * Computes the height and widths for each row and field. - * @param {number} iconWidth Offset of first row due to icons. - * @return {!Array.>} 2D array of objects, each containing - * position information. - * @private - */ -Blockly.BlockSvg.prototype.renderCompute_ = function(iconWidth) { - var inputList = this.inputList; - var inputRows = []; - inputRows.rightEdge = iconWidth + Blockly.BlockSvg.SEP_SPACE_X * 2; - if (this.previousConnection || this.nextConnection) { - inputRows.rightEdge = Math.max(inputRows.rightEdge, - Blockly.BlockSvg.NOTCH_WIDTH + Blockly.BlockSvg.SEP_SPACE_X); - } - var fieldValueWidth = 0; // Width of longest external value field. - var fieldStatementWidth = 0; // Width of longest statement field. - var hasValue = false; - var hasStatement = false; - var hasDummy = false; - var lastType = undefined; - var isInline = this.getInputsInline() && !this.isCollapsed(); - for (var i = 0, input; input = inputList[i]; i++) { - if (!input.isVisible()) { - continue; - } - var row; - if (!isInline || !lastType || - lastType == Blockly.NEXT_STATEMENT || - input.type == Blockly.NEXT_STATEMENT) { - // Create new row. - lastType = input.type; - row = []; - if (isInline && input.type != Blockly.NEXT_STATEMENT) { - row.type = Blockly.BlockSvg.INLINE; - } else { - row.type = input.type; - } - row.height = 0; - inputRows.push(row); - } else { - row = inputRows[inputRows.length - 1]; - } - row.push(input); - - // Compute minimum input size. - input.renderHeight = Blockly.BlockSvg.MIN_BLOCK_Y; - // The width is currently only needed for inline value inputs. - if (isInline && input.type == Blockly.INPUT_VALUE) { - input.renderWidth = Blockly.BlockSvg.TAB_WIDTH + - Blockly.BlockSvg.SEP_SPACE_X * 1.25; - } else { - input.renderWidth = 0; - } - // Expand input size if there is a connection. - if (input.connection && input.connection.isConnected()) { - var linkedBlock = input.connection.targetBlock(); - var bBox = linkedBlock.getHeightWidth(); - input.renderHeight = Math.max(input.renderHeight, bBox.height); - input.renderWidth = Math.max(input.renderWidth, bBox.width); - } - // Blocks have a one pixel shadow that should sometimes overhang. - if (!isInline && i == inputList.length - 1) { - // Last value input should overhang. - input.renderHeight--; - } else if (!isInline && input.type == Blockly.INPUT_VALUE && - inputList[i + 1] && inputList[i + 1].type == Blockly.NEXT_STATEMENT) { - // Value input above statement input should overhang. - input.renderHeight--; - } - - row.height = Math.max(row.height, input.renderHeight); - input.fieldWidth = 0; - if (inputRows.length == 1) { - // The first row gets shifted to accommodate any icons. - input.fieldWidth += this.RTL ? -iconWidth : iconWidth; - } - var previousFieldEditable = false; - for (var j = 0, field; field = input.fieldRow[j]; j++) { - if (j != 0) { - input.fieldWidth += Blockly.BlockSvg.SEP_SPACE_X; - } - // Get the dimensions of the field. - var fieldSize = field.getSize(); - field.renderWidth = fieldSize.width; - field.renderSep = (previousFieldEditable && field.EDITABLE) ? - Blockly.BlockSvg.SEP_SPACE_X : 0; - input.fieldWidth += field.renderWidth + field.renderSep; - row.height = Math.max(row.height, fieldSize.height); - previousFieldEditable = field.EDITABLE; - } - - if (row.type != Blockly.BlockSvg.INLINE) { - if (row.type == Blockly.NEXT_STATEMENT) { - hasStatement = true; - fieldStatementWidth = Math.max(fieldStatementWidth, input.fieldWidth); - } else { - if (row.type == Blockly.INPUT_VALUE) { - hasValue = true; - } else if (row.type == Blockly.DUMMY_INPUT) { - hasDummy = true; - } - fieldValueWidth = Math.max(fieldValueWidth, input.fieldWidth); - } - } - } - - // Make inline rows a bit thicker in order to enclose the values. - for (var y = 0, row; row = inputRows[y]; y++) { - row.thicker = false; - if (row.type == Blockly.BlockSvg.INLINE) { - for (var z = 0, input; input = row[z]; z++) { - if (input.type == Blockly.INPUT_VALUE) { - row.height += 2 * Blockly.BlockSvg.INLINE_PADDING_Y; - row.thicker = true; - break; - } - } - } - } - - // Compute the statement edge. - // This is the width of a block where statements are nested. - inputRows.statementEdge = 2 * Blockly.BlockSvg.SEP_SPACE_X + - fieldStatementWidth; - // Compute the preferred right edge. Inline blocks may extend beyond. - // This is the width of the block where external inputs connect. - if (hasStatement) { - inputRows.rightEdge = Math.max(inputRows.rightEdge, - inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH); - } - if (hasValue) { - inputRows.rightEdge = Math.max(inputRows.rightEdge, fieldValueWidth + - Blockly.BlockSvg.SEP_SPACE_X * 2 + Blockly.BlockSvg.TAB_WIDTH); - } else if (hasDummy) { - inputRows.rightEdge = Math.max(inputRows.rightEdge, fieldValueWidth + - Blockly.BlockSvg.SEP_SPACE_X * 2); - } - - inputRows.hasValue = hasValue; - inputRows.hasStatement = hasStatement; - inputRows.hasDummy = hasDummy; - return inputRows; -}; - - -/** - * Draw the path of the block. - * Move the fields to the correct locations. - * @param {number} iconWidth Offset of first row due to icons. - * @param {!Array.>} inputRows 2D array of objects, each - * containing position information. - * @private - */ -Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) { - this.startHat_ = false; - // Reset the height to zero and let the rendering process add in - // portions of the block height as it goes. (e.g. hats, inputs, etc.) - this.height = 0; - // Should the top and bottom left corners be rounded or square? - if (this.outputConnection) { - this.squareTopLeftCorner_ = true; - this.squareBottomLeftCorner_ = true; - } else { - this.squareTopLeftCorner_ = false; - this.squareBottomLeftCorner_ = false; - // If this block is in the middle of a stack, square the corners. - if (this.previousConnection) { - var prevBlock = this.previousConnection.targetBlock(); - if (prevBlock && prevBlock.getNextBlock() == this) { - this.squareTopLeftCorner_ = true; - } - } else if (Blockly.BlockSvg.START_HAT) { - // No output or previous connection. - this.squareTopLeftCorner_ = true; - this.startHat_ = true; - this.height += Blockly.BlockSvg.START_HAT_HEIGHT; - inputRows.rightEdge = Math.max(inputRows.rightEdge, 100); - } - var nextBlock = this.getNextBlock(); - if (nextBlock) { - this.squareBottomLeftCorner_ = true; - } - } - - // Assemble the block's path. - var steps = []; - var inlineSteps = []; - // The highlighting applies to edges facing the upper-left corner. - // Since highlighting is a two-pixel wide border, it would normally overhang - // the edge of the block by a pixel. So undersize all measurements by a pixel. - var highlightSteps = []; - var highlightInlineSteps = []; - - this.renderDrawTop_(steps, highlightSteps, inputRows.rightEdge); - var cursorY = this.renderDrawRight_(steps, highlightSteps, inlineSteps, - highlightInlineSteps, inputRows, iconWidth); - this.renderDrawBottom_(steps, highlightSteps, cursorY); - this.renderDrawLeft_(steps, highlightSteps); - - var pathString = steps.join(' ') + '\n' + inlineSteps.join(' '); - this.svgPath_.setAttribute('d', pathString); - this.svgPathDark_.setAttribute('d', pathString); - pathString = highlightSteps.join(' ') + '\n' + highlightInlineSteps.join(' '); - this.svgPathLight_.setAttribute('d', pathString); - if (this.RTL) { - // Mirror the block's path. - this.svgPath_.setAttribute('transform', 'scale(-1 1)'); - this.svgPathLight_.setAttribute('transform', 'scale(-1 1)'); - this.svgPathDark_.setAttribute('transform', 'translate(1,1) scale(-1 1)'); - } -}; - -/** - * Update all of the connections on this block with the new locations calculated - * in renderCompute. Also move all of the connected blocks based on the new - * connection locations. - * @private - */ -Blockly.BlockSvg.prototype.renderMoveConnections_ = function() { - var blockTL = this.getRelativeToSurfaceXY(); - // Don't tighten previous or output connections because they are inferior - // connections. - if (this.previousConnection) { - this.previousConnection.moveToOffset(blockTL); - } - if (this.outputConnection) { - this.outputConnection.moveToOffset(blockTL); - } - - for (var i = 0; i < this.inputList.length; i++) { - var conn = this.inputList[i].connection; - if (conn) { - conn.moveToOffset(blockTL); - if (conn.isConnected()) { - conn.tighten_(); - } - } - } - - if (this.nextConnection) { - this.nextConnection.moveToOffset(blockTL); - if (this.nextConnection.isConnected()) { - this.nextConnection.tighten_(); - } - } - -}; - -/** - * Render the top edge of the block. - * @param {!Array.} steps Path of block outline. - * @param {!Array.} highlightSteps Path of block highlights. - * @param {number} rightEdge Minimum width of block. - * @private - */ -Blockly.BlockSvg.prototype.renderDrawTop_ = - function(steps, highlightSteps, rightEdge) { - /* eslint-disable indent */ - // Position the cursor at the top-left starting point. - if (this.squareTopLeftCorner_) { - steps.push('m 0,0'); - highlightSteps.push('m 0.5,0.5'); - if (this.startHat_) { - steps.push(Blockly.BlockSvg.START_HAT_PATH); - highlightSteps.push(this.RTL ? - Blockly.BlockSvg.START_HAT_HIGHLIGHT_RTL : - Blockly.BlockSvg.START_HAT_HIGHLIGHT_LTR); - } - } else { - steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_START); - highlightSteps.push(this.RTL ? - Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL : - Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR); - // Top-left rounded corner. - steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER); - highlightSteps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_HIGHLIGHT); - } - - // Top edge. - if (this.previousConnection) { - steps.push('H', Blockly.BlockSvg.NOTCH_WIDTH - 15); - highlightSteps.push('H', Blockly.BlockSvg.NOTCH_WIDTH - 15); - steps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT); - highlightSteps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT_HIGHLIGHT); - - var connectionX = (this.RTL ? - -Blockly.BlockSvg.NOTCH_WIDTH : Blockly.BlockSvg.NOTCH_WIDTH); - this.previousConnection.setOffsetInBlock(connectionX, 0); - } - steps.push('H', rightEdge); - highlightSteps.push('H', rightEdge - 0.5); - this.width = rightEdge; -}; /* eslint-enable indent */ - -/** - * Render the right edge of the block. - * @param {!Array.} steps Path of block outline. - * @param {!Array.} highlightSteps Path of block highlights. - * @param {!Array.} inlineSteps Inline block outlines. - * @param {!Array.} highlightInlineSteps Inline block highlights. - * @param {!Array.>} inputRows 2D array of objects, each - * containing position information. - * @param {number} iconWidth Offset of first row due to icons. - * @return {number} Height of block. - * @private - */ -Blockly.BlockSvg.prototype.renderDrawRight_ = function(steps, highlightSteps, - inlineSteps, highlightInlineSteps, inputRows, iconWidth) { - var cursorX; - var cursorY = 0; - var connectionX, connectionY; - for (var y = 0, row; row = inputRows[y]; y++) { - cursorX = Blockly.BlockSvg.SEP_SPACE_X; - if (y == 0) { - cursorX += this.RTL ? -iconWidth : iconWidth; - } - highlightSteps.push('M', (inputRows.rightEdge - 0.5) + ',' + - (cursorY + 0.5)); - if (this.isCollapsed()) { - // Jagged right edge. - var input = row[0]; - var fieldX = cursorX; - var fieldY = cursorY; - this.renderFields_(input.fieldRow, fieldX, fieldY); - steps.push(Blockly.BlockSvg.JAGGED_TEETH); - highlightSteps.push('h 8'); - var remainder = row.height - Blockly.BlockSvg.JAGGED_TEETH_HEIGHT; - steps.push('v', remainder); - if (this.RTL) { - highlightSteps.push('v 3.9 l 7.2,3.4 m -14.5,8.9 l 7.3,3.5'); - highlightSteps.push('v', remainder - 0.7); - } - this.width += Blockly.BlockSvg.JAGGED_TEETH_WIDTH; - } else if (row.type == Blockly.BlockSvg.INLINE) { - // Inline inputs. - for (var x = 0, input; input = row[x]; x++) { - var fieldX = cursorX; - var fieldY = cursorY; - if (row.thicker) { - // Lower the field slightly. - fieldY += Blockly.BlockSvg.INLINE_PADDING_Y; - } - // TODO: Align inline field rows (left/right/centre). - cursorX = this.renderFields_(input.fieldRow, fieldX, fieldY); - if (input.type != Blockly.DUMMY_INPUT) { - cursorX += input.renderWidth + Blockly.BlockSvg.SEP_SPACE_X; - } - if (input.type == Blockly.INPUT_VALUE) { - inlineSteps.push('M', (cursorX - Blockly.BlockSvg.SEP_SPACE_X) + - ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y)); - inlineSteps.push('h', Blockly.BlockSvg.TAB_WIDTH - 2 - - input.renderWidth); - inlineSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN); - inlineSteps.push('v', input.renderHeight + 1 - - Blockly.BlockSvg.TAB_HEIGHT); - inlineSteps.push('h', input.renderWidth + 2 - - Blockly.BlockSvg.TAB_WIDTH); - inlineSteps.push('z'); - if (this.RTL) { - // Highlight right edge, around back of tab, and bottom. - highlightInlineSteps.push('M', - (cursorX - Blockly.BlockSvg.SEP_SPACE_X - 2.5 + - Blockly.BlockSvg.TAB_WIDTH - input.renderWidth) + ',' + - (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 0.5)); - highlightInlineSteps.push( - Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL); - highlightInlineSteps.push('v', - input.renderHeight - Blockly.BlockSvg.TAB_HEIGHT + 2.5); - highlightInlineSteps.push('h', - input.renderWidth - Blockly.BlockSvg.TAB_WIDTH + 2); - } else { - // Highlight right edge, bottom. - highlightInlineSteps.push('M', - (cursorX - Blockly.BlockSvg.SEP_SPACE_X + 0.5) + ',' + - (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 0.5)); - highlightInlineSteps.push('v', input.renderHeight + 1); - highlightInlineSteps.push('h', Blockly.BlockSvg.TAB_WIDTH - 2 - - input.renderWidth); - // Short highlight glint at bottom of tab. - highlightInlineSteps.push('M', - (cursorX - input.renderWidth - Blockly.BlockSvg.SEP_SPACE_X + - 0.9) + ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + - Blockly.BlockSvg.TAB_HEIGHT - 0.7)); - highlightInlineSteps.push('l', - (Blockly.BlockSvg.TAB_WIDTH * 0.46) + ',-2.1'); - } - // Create inline input connection. - if (this.RTL) { - connectionX = -cursorX - - Blockly.BlockSvg.TAB_WIDTH + Blockly.BlockSvg.SEP_SPACE_X + - input.renderWidth + 1; - } else { - connectionX = cursorX + - Blockly.BlockSvg.TAB_WIDTH - Blockly.BlockSvg.SEP_SPACE_X - - input.renderWidth - 1; - } - connectionY = cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 1; - input.connection.setOffsetInBlock(connectionX, connectionY); - } - } - - cursorX = Math.max(cursorX, inputRows.rightEdge); - this.width = Math.max(this.width, cursorX); - steps.push('H', cursorX); - highlightSteps.push('H', cursorX - 0.5); - steps.push('v', row.height); - if (this.RTL) { - highlightSteps.push('v', row.height - 1); - } - } else if (row.type == Blockly.INPUT_VALUE) { - // External input. - var input = row[0]; - var fieldX = cursorX; - var fieldY = cursorY; - if (input.align != Blockly.ALIGN_LEFT) { - var fieldRightX = inputRows.rightEdge - input.fieldWidth - - Blockly.BlockSvg.TAB_WIDTH - 2 * Blockly.BlockSvg.SEP_SPACE_X; - if (input.align == Blockly.ALIGN_RIGHT) { - fieldX += fieldRightX; - } else if (input.align == Blockly.ALIGN_CENTRE) { - fieldX += fieldRightX / 2; - } - } - this.renderFields_(input.fieldRow, fieldX, fieldY); - steps.push(Blockly.BlockSvg.TAB_PATH_DOWN); - var v = row.height - Blockly.BlockSvg.TAB_HEIGHT; - steps.push('v', v); - if (this.RTL) { - // Highlight around back of tab. - highlightSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL); - highlightSteps.push('v', v + 0.5); - } else { - // Short highlight glint at bottom of tab. - highlightSteps.push('M', (inputRows.rightEdge - 5) + ',' + - (cursorY + Blockly.BlockSvg.TAB_HEIGHT - 0.7)); - highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * 0.46) + - ',-2.1'); - } - // Create external input connection. - connectionX = this.RTL ? -inputRows.rightEdge - 1 : - inputRows.rightEdge + 1; - input.connection.setOffsetInBlock(connectionX, cursorY); - if (input.connection.isConnected()) { - this.width = Math.max(this.width, inputRows.rightEdge + - input.connection.targetBlock().getHeightWidth().width - - Blockly.BlockSvg.TAB_WIDTH + 1); - } - } else if (row.type == Blockly.DUMMY_INPUT) { - // External naked field. - var input = row[0]; - var fieldX = cursorX; - var fieldY = cursorY; - if (input.align != Blockly.ALIGN_LEFT) { - var fieldRightX = inputRows.rightEdge - input.fieldWidth - - 2 * Blockly.BlockSvg.SEP_SPACE_X; - if (inputRows.hasValue) { - fieldRightX -= Blockly.BlockSvg.TAB_WIDTH; - } - if (input.align == Blockly.ALIGN_RIGHT) { - fieldX += fieldRightX; - } else if (input.align == Blockly.ALIGN_CENTRE) { - fieldX += fieldRightX / 2; - } - } - this.renderFields_(input.fieldRow, fieldX, fieldY); - steps.push('v', row.height); - if (this.RTL) { - highlightSteps.push('v', row.height - 1); - } - } else if (row.type == Blockly.NEXT_STATEMENT) { - // Nested statement. - var input = row[0]; - if (y == 0) { - // If the first input is a statement stack, add a small row on top. - steps.push('v', Blockly.BlockSvg.SEP_SPACE_Y); - if (this.RTL) { - highlightSteps.push('v', Blockly.BlockSvg.SEP_SPACE_Y - 1); - } - cursorY += Blockly.BlockSvg.SEP_SPACE_Y; - } - var fieldX = cursorX; - var fieldY = cursorY; - if (input.align != Blockly.ALIGN_LEFT) { - var fieldRightX = inputRows.statementEdge - input.fieldWidth - - 2 * Blockly.BlockSvg.SEP_SPACE_X; - if (input.align == Blockly.ALIGN_RIGHT) { - fieldX += fieldRightX; - } else if (input.align == Blockly.ALIGN_CENTRE) { - fieldX += fieldRightX / 2; - } - } - this.renderFields_(input.fieldRow, fieldX, fieldY); - cursorX = inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH; - steps.push('H', cursorX); - steps.push(Blockly.BlockSvg.INNER_TOP_LEFT_CORNER); - steps.push('v', row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS); - steps.push(Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER); - steps.push('H', inputRows.rightEdge); - if (this.RTL) { - highlightSteps.push('M', - (cursorX - Blockly.BlockSvg.NOTCH_WIDTH + - Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + - ',' + (cursorY + Blockly.BlockSvg.DISTANCE_45_OUTSIDE)); - highlightSteps.push( - Blockly.BlockSvg.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL); - highlightSteps.push('v', - row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS); - highlightSteps.push( - Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL); - highlightSteps.push('H', inputRows.rightEdge - 0.5); - } else { - highlightSteps.push('M', - (cursorX - Blockly.BlockSvg.NOTCH_WIDTH + - Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' + - (cursorY + row.height - Blockly.BlockSvg.DISTANCE_45_OUTSIDE)); - highlightSteps.push( - Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR); - highlightSteps.push('H', inputRows.rightEdge - 0.5); - } - // Create statement connection. - connectionX = this.RTL ? -cursorX : cursorX + 1; - input.connection.setOffsetInBlock(connectionX, cursorY + 1); - - if (input.connection.isConnected()) { - this.width = Math.max(this.width, inputRows.statementEdge + - input.connection.targetBlock().getHeightWidth().width); - } - if (y == inputRows.length - 1 || - inputRows[y + 1].type == Blockly.NEXT_STATEMENT) { - // If the final input is a statement stack, add a small row underneath. - // Consecutive statement stacks are also separated by a small divider. - steps.push('v', Blockly.BlockSvg.SEP_SPACE_Y); - if (this.RTL) { - highlightSteps.push('v', Blockly.BlockSvg.SEP_SPACE_Y - 1); - } - cursorY += Blockly.BlockSvg.SEP_SPACE_Y; - } - } - cursorY += row.height; - } - if (!inputRows.length) { - cursorY = Blockly.BlockSvg.MIN_BLOCK_Y; - steps.push('V', cursorY); - if (this.RTL) { - highlightSteps.push('V', cursorY - 1); - } - } - return cursorY; -}; - -/** - * Render the bottom edge of the block. - * @param {!Array.} steps Path of block outline. - * @param {!Array.} highlightSteps Path of block highlights. - * @param {number} cursorY Height of block. - * @private - */ -Blockly.BlockSvg.prototype.renderDrawBottom_ = - function(steps, highlightSteps, cursorY) { - /* eslint-disable indent */ - this.height += cursorY + 1; // Add one for the shadow. - if (this.nextConnection) { - steps.push('H', (Blockly.BlockSvg.NOTCH_WIDTH + (this.RTL ? 0.5 : - 0.5)) + - ' ' + Blockly.BlockSvg.NOTCH_PATH_RIGHT); - // Create next block connection. - var connectionX; - if (this.RTL) { - connectionX = -Blockly.BlockSvg.NOTCH_WIDTH; - } else { - connectionX = Blockly.BlockSvg.NOTCH_WIDTH; - } - this.nextConnection.setOffsetInBlock(connectionX, cursorY + 1); - this.height += 4; // Height of tab. - } - - // Should the bottom-left corner be rounded or square? - if (this.squareBottomLeftCorner_) { - steps.push('H 0'); - if (!this.RTL) { - highlightSteps.push('M', '0.5,' + (cursorY - 0.5)); - } - } else { - steps.push('H', Blockly.BlockSvg.CORNER_RADIUS); - steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' + - Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 -' + - Blockly.BlockSvg.CORNER_RADIUS + ',-' + - Blockly.BlockSvg.CORNER_RADIUS); - if (!this.RTL) { - highlightSteps.push('M', Blockly.BlockSvg.DISTANCE_45_INSIDE + ',' + - (cursorY - Blockly.BlockSvg.DISTANCE_45_INSIDE)); - highlightSteps.push('A', (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ',' + - (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ' 0 0,1 ' + - '0.5,' + (cursorY - Blockly.BlockSvg.CORNER_RADIUS)); - } - } -}; /* eslint-enable indent */ - -/** - * Render the left edge of the block. - * @param {!Array.} steps Path of block outline. - * @param {!Array.} highlightSteps Path of block highlights. - * @private - */ -Blockly.BlockSvg.prototype.renderDrawLeft_ = function(steps, highlightSteps) { - if (this.outputConnection) { - // Create output connection. - this.outputConnection.setOffsetInBlock(0, 0); - steps.push('V', Blockly.BlockSvg.TAB_HEIGHT); - steps.push('c 0,-10 -' + Blockly.BlockSvg.TAB_WIDTH + ',8 -' + - Blockly.BlockSvg.TAB_WIDTH + ',-7.5 s ' + Blockly.BlockSvg.TAB_WIDTH + - ',2.5 ' + Blockly.BlockSvg.TAB_WIDTH + ',-7.5'); - if (this.RTL) { - highlightSteps.push('M', (Blockly.BlockSvg.TAB_WIDTH * -0.25) + ',8.4'); - highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * -0.45) + ',-2.1'); - } else { - highlightSteps.push('V', Blockly.BlockSvg.TAB_HEIGHT - 1.5); - highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * -0.92) + - ',-0.5 q ' + (Blockly.BlockSvg.TAB_WIDTH * -0.19) + - ',-5.5 0,-11'); - highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * 0.92) + - ',1 V 0.5 H 1'); - } - this.width += Blockly.BlockSvg.TAB_WIDTH; - } else if (!this.RTL) { - if (this.squareTopLeftCorner_) { - // Statement block in a stack. - highlightSteps.push('V', 0.5); - } else { - highlightSteps.push('V', Blockly.BlockSvg.CORNER_RADIUS); - } - } - steps.push('z'); -}; diff --git a/core/block_svg.js b/core/block_svg.js deleted file mode 100644 index 6c57850ea39..00000000000 --- a/core/block_svg.js +++ /dev/null @@ -1,1548 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Methods for graphically rendering a block as SVG. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.BlockSvg'); - -goog.require('Blockly.Block'); -goog.require('Blockly.ContextMenu'); -goog.require('Blockly.Grid'); -goog.require('Blockly.RenderedConnection'); -goog.require('Blockly.Touch'); -goog.require('Blockly.utils'); -goog.require('goog.Timer'); -goog.require('goog.asserts'); -goog.require('goog.dom'); -goog.require('goog.math.Coordinate'); -goog.require('goog.userAgent'); - - -/** - * Class for a block's SVG representation. - * Not normally called directly, workspace.newBlock() is preferred. - * @param {!Blockly.Workspace} workspace The block's workspace. - * @param {?string} prototypeName Name of the language object containing - * type-specific functions for this block. - * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise - * create a new id. - * @extends {Blockly.Block} - * @constructor - */ -Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { - // Create core elements for the block. - /** - * @type {SVGElement} - * @private - */ - this.svgGroup_ = Blockly.utils.createSvgElement('g', {}, null); - this.svgGroup_.translate_ = ''; - - /** - * @type {SVGElement} - * @private - */ - this.svgPathDark_ = Blockly.utils.createSvgElement('path', - {'class': 'blocklyPathDark', 'transform': 'translate(1,1)'}, - this.svgGroup_); - - /** - * @type {SVGElement} - * @private - */ - this.svgPath_ = Blockly.utils.createSvgElement('path', {'class': 'blocklyPath'}, - this.svgGroup_); - - /** - * @type {SVGElement} - * @private - */ - this.svgPathLight_ = Blockly.utils.createSvgElement('path', - {'class': 'blocklyPathLight'}, this.svgGroup_); - this.svgPath_.tooltip = this; - - /** @type {boolean} */ - this.rendered = false; - - /** - * Whether to move the block to the drag surface when it is dragged. - * True if it should move, false if it should be translated directly. - * @type {boolean} - * @private - */ - this.useDragSurface_ = Blockly.utils.is3dSupported() && !!workspace.blockDragSurface_; - - Blockly.Tooltip.bindMouseEvents(this.svgPath_); - Blockly.BlockSvg.superClass_.constructor.call(this, - workspace, prototypeName, opt_id); -}; -goog.inherits(Blockly.BlockSvg, Blockly.Block); - -/** - * Height of this block, not including any statement blocks above or below. - * Height is in workspace units. - */ -Blockly.BlockSvg.prototype.height = 0; -/** - * Width of this block, including any connected value blocks. - * Width is in workspace units. - */ -Blockly.BlockSvg.prototype.width = 0; - -/** - * Original location of block being dragged. - * @type {goog.math.Coordinate} - * @private - */ -Blockly.BlockSvg.prototype.dragStartXY_ = null; - -/** - * Constant for identifying rows that are to be rendered inline. - * Don't collide with Blockly.INPUT_VALUE and friends. - * @const - */ -Blockly.BlockSvg.INLINE = -1; - -/** - * Create and initialize the SVG representation of the block. - * May be called more than once. - */ -Blockly.BlockSvg.prototype.initSvg = function() { - goog.asserts.assert(this.workspace.rendered, 'Workspace is headless.'); - for (var i = 0, input; input = this.inputList[i]; i++) { - input.init(); - } - var icons = this.getIcons(); - for (var i = 0; i < icons.length; i++) { - icons[i].createIcon(); - } - this.updateColour(); - this.updateMovable(); - if (!this.workspace.options.readOnly && !this.eventsInit_) { - Blockly.bindEventWithChecks_(this.getSvgRoot(), 'mousedown', this, - this.onMouseDown_); - } - this.eventsInit_ = true; - - if (!this.getSvgRoot().parentNode) { - this.workspace.getCanvas().appendChild(this.getSvgRoot()); - } -}; - -/** - * Select this block. Highlight it visually. - */ -Blockly.BlockSvg.prototype.select = function() { - if (this.isShadow() && this.getParent()) { - // Shadow blocks should not be selected. - this.getParent().select(); - return; - } - if (Blockly.selected == this) { - return; - } - var oldId = null; - if (Blockly.selected) { - oldId = Blockly.selected.id; - // Unselect any previously selected block. - Blockly.Events.disable(); - try { - Blockly.selected.unselect(); - } finally { - Blockly.Events.enable(); - } - } - var event = new Blockly.Events.Ui(null, 'selected', oldId, this.id); - event.workspaceId = this.workspace.id; - Blockly.Events.fire(event); - Blockly.selected = this; - this.addSelect(); -}; - -/** - * Unselect this block. Remove its highlighting. - */ -Blockly.BlockSvg.prototype.unselect = function() { - if (Blockly.selected != this) { - return; - } - var event = new Blockly.Events.Ui(null, 'selected', this.id, null); - event.workspaceId = this.workspace.id; - Blockly.Events.fire(event); - Blockly.selected = null; - this.removeSelect(); -}; - -/** - * Block's mutator icon (if any). - * @type {Blockly.Mutator} - */ -Blockly.BlockSvg.prototype.mutator = null; - -/** - * Block's comment icon (if any). - * @type {Blockly.Comment} - */ -Blockly.BlockSvg.prototype.comment = null; - -/** - * Block's warning icon (if any). - * @type {Blockly.Warning} - */ -Blockly.BlockSvg.prototype.warning = null; - -/** - * Returns a list of mutator, comment, and warning icons. - * @return {!Array} List of icons. - */ -Blockly.BlockSvg.prototype.getIcons = function() { - var icons = []; - if (this.mutator) { - icons.push(this.mutator); - } - if (this.comment) { - icons.push(this.comment); - } - if (this.warning) { - icons.push(this.warning); - } - return icons; -}; - -/** - * Set parent of this block to be a new block or null. - * @param {Blockly.BlockSvg} newParent New parent block. - */ -Blockly.BlockSvg.prototype.setParent = function(newParent) { - if (newParent == this.parentBlock_) { - return; - } - var svgRoot = this.getSvgRoot(); - if (this.parentBlock_ && svgRoot) { - // Move this block up the DOM. Keep track of x/y translations. - var xy = this.getRelativeToSurfaceXY(); - this.workspace.getCanvas().appendChild(svgRoot); - svgRoot.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')'); - } - - Blockly.Field.startCache(); - Blockly.BlockSvg.superClass_.setParent.call(this, newParent); - Blockly.Field.stopCache(); - - if (newParent) { - var oldXY = this.getRelativeToSurfaceXY(); - newParent.getSvgRoot().appendChild(svgRoot); - var newXY = this.getRelativeToSurfaceXY(); - // Move the connections to match the child's new position. - this.moveConnections_(newXY.x - oldXY.x, newXY.y - oldXY.y); - } -}; - -/** - * Return the coordinates of the top-left corner of this block relative to the - * drawing surface's origin (0,0), in workspace units. - * If the block is on the workspace, (0, 0) is the origin of the workspace - * coordinate system. - * This does not change with workspace scale. - * @return {!goog.math.Coordinate} Object with .x and .y properties in - * workspace coordinates. - */ -Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() { - var x = 0; - var y = 0; - - var dragSurfaceGroup = this.useDragSurface_ ? - this.workspace.blockDragSurface_.getGroup() : null; - - var element = this.getSvgRoot(); - if (element) { - do { - // Loop through this block and every parent. - var xy = Blockly.utils.getRelativeXY(element); - x += xy.x; - y += xy.y; - // If this element is the current element on the drag surface, include - // the translation of the drag surface itself. - if (this.useDragSurface_ && - this.workspace.blockDragSurface_.getCurrentBlock() == element) { - var surfaceTranslation = this.workspace.blockDragSurface_.getSurfaceTranslation(); - x += surfaceTranslation.x; - y += surfaceTranslation.y; - } - element = element.parentNode; - } while (element && element != this.workspace.getCanvas() && - element != dragSurfaceGroup); - } - return new goog.math.Coordinate(x, y); -}; - -/** - * Move a block by a relative offset. - * @param {number} dx Horizontal offset in workspace units. - * @param {number} dy Vertical offset in workspace units. - */ -Blockly.BlockSvg.prototype.moveBy = function(dx, dy) { - goog.asserts.assert(!this.parentBlock_, 'Block has parent.'); - var event = new Blockly.Events.BlockMove(this); - var xy = this.getRelativeToSurfaceXY(); - this.translate(xy.x + dx, xy.y + dy); - this.moveConnections_(dx, dy); - event.recordNew(); - this.workspace.resizeContents(); - Blockly.Events.fire(event); -}; - -/** - * Transforms a block by setting the translation on the transform attribute - * of the block's SVG. - * @param {number} x The x coordinate of the translation in workspace units. - * @param {number} y The y coordinate of the translation in workspace units. - */ -Blockly.BlockSvg.prototype.translate = function(x, y) { - this.getSvgRoot().setAttribute('transform', - 'translate(' + x + ',' + y + ')'); -}; - -/** - * Move this block to its workspace's drag surface, accounting for positioning. - * Generally should be called at the same time as setDragging_(true). - * Does nothing if useDragSurface_ is false. - * @private - */ -Blockly.BlockSvg.prototype.moveToDragSurface_ = function() { - if (!this.useDragSurface_) { - return; - } - // The translation for drag surface blocks, - // is equal to the current relative-to-surface position, - // to keep the position in sync as it move on/off the surface. - // This is in workspace coordinates. - var xy = this.getRelativeToSurfaceXY(); - this.clearTransformAttributes_(); - this.workspace.blockDragSurface_.translateSurface(xy.x, xy.y); - // Execute the move on the top-level SVG component - this.workspace.blockDragSurface_.setBlocksAndShow(this.getSvgRoot()); -}; - -/** - * Move this block back to the workspace block canvas. - * Generally should be called at the same time as setDragging_(false). - * Does nothing if useDragSurface_ is false. - * @param {!goog.math.Coordinate} newXY The position the block should take on - * on the workspace canvas, in workspace coordinates. - * @private - */ -Blockly.BlockSvg.prototype.moveOffDragSurface_ = function(newXY) { - if (!this.useDragSurface_) { - return; - } - // Translate to current position, turning off 3d. - this.translate(newXY.x, newXY.y); - this.workspace.blockDragSurface_.clearAndHide(this.workspace.getCanvas()); -}; - -/** - * Move this block during a drag, taking into account whether we are using a - * drag surface to translate blocks. - * This block must be a top-level block. - * @param {!goog.math.Coordinate} newLoc The location to translate to, in - * workspace coordinates. - * @package - */ -Blockly.BlockSvg.prototype.moveDuringDrag = function(newLoc) { - if (this.useDragSurface_) { - this.workspace.blockDragSurface_.translateSurface(newLoc.x, newLoc.y); - } else { - this.svgGroup_.translate_ = 'translate(' + newLoc.x + ',' + newLoc.y + ')'; - this.svgGroup_.setAttribute('transform', - this.svgGroup_.translate_ + this.svgGroup_.skew_); - } -}; - -/** - * Clear the block of transform="..." attributes. - * Used when the block is switching from 3d to 2d transform or vice versa. - * @private - */ -Blockly.BlockSvg.prototype.clearTransformAttributes_ = function() { - Blockly.utils.removeAttribute(this.getSvgRoot(), 'transform'); -}; - -/** - * Snap this block to the nearest grid point. - */ -Blockly.BlockSvg.prototype.snapToGrid = function() { - if (!this.workspace) { - return; // Deleted block. - } - if (this.workspace.isDragging()) { - return; // Don't bump blocks during a drag. - } - if (this.getParent()) { - return; // Only snap top-level blocks. - } - if (this.isInFlyout) { - return; // Don't move blocks around in a flyout. - } - var grid = this.workspace.getGrid(); - if (!grid || !grid.shouldSnap()) { - return; // Config says no snapping. - } - var spacing = grid.getSpacing(); - var half = spacing / 2; - var xy = this.getRelativeToSurfaceXY(); - var dx = Math.round((xy.x - half) / spacing) * spacing + half - xy.x; - var dy = Math.round((xy.y - half) / spacing) * spacing + half - xy.y; - dx = Math.round(dx); - dy = Math.round(dy); - if (dx != 0 || dy != 0) { - this.moveBy(dx, dy); - } -}; - -/** - * Returns the coordinates of a bounding box describing the dimensions of this - * block and any blocks stacked below it. - * Coordinate system: workspace coordinates. - * @return {!{topLeft: goog.math.Coordinate, bottomRight: goog.math.Coordinate}} - * Object with top left and bottom right coordinates of the bounding box. - */ -Blockly.BlockSvg.prototype.getBoundingRectangle = function() { - var blockXY = this.getRelativeToSurfaceXY(this); - var tab = this.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; - var blockBounds = this.getHeightWidth(); - var topLeft; - var bottomRight; - if (this.RTL) { - // Width has the tab built into it already so subtract it here. - topLeft = new goog.math.Coordinate(blockXY.x - (blockBounds.width - tab), - blockXY.y); - // Add the width of the tab/puzzle piece knob to the x coordinate - // since X is the corner of the rectangle, not the whole puzzle piece. - bottomRight = new goog.math.Coordinate(blockXY.x + tab, - blockXY.y + blockBounds.height); - } else { - // Subtract the width of the tab/puzzle piece knob to the x coordinate - // since X is the corner of the rectangle, not the whole puzzle piece. - topLeft = new goog.math.Coordinate(blockXY.x - tab, blockXY.y); - // Width has the tab built into it already so subtract it here. - bottomRight = new goog.math.Coordinate(blockXY.x + blockBounds.width - tab, - blockXY.y + blockBounds.height); - } - return {topLeft: topLeft, bottomRight: bottomRight}; -}; - -/** - * Set whether the block is collapsed or not. - * @param {boolean} collapsed True if collapsed. - */ -Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) { - if (this.collapsed_ == collapsed) { - return; - } - var renderList = []; - // Show/hide the inputs. - for (var i = 0, input; input = this.inputList[i]; i++) { - renderList.push.apply(renderList, input.setVisible(!collapsed)); - } - - var COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT'; - if (collapsed) { - var icons = this.getIcons(); - for (var i = 0; i < icons.length; i++) { - icons[i].setVisible(false); - } - var text = this.toString(Blockly.COLLAPSE_CHARS); - this.appendDummyInput(COLLAPSED_INPUT_NAME).appendField(text).init(); - } else { - this.removeInput(COLLAPSED_INPUT_NAME); - // Clear any warnings inherited from enclosed blocks. - this.setWarningText(null); - } - Blockly.BlockSvg.superClass_.setCollapsed.call(this, collapsed); - - if (!renderList.length) { - // No child blocks, just render this block. - renderList[0] = this; - } - if (this.rendered) { - for (var i = 0, block; block = renderList[i]; i++) { - block.render(); - } - // Don't bump neighbours. - // Although bumping neighbours would make sense, users often collapse - // all their functions and store them next to each other. Expanding and - // bumping causes all their definitions to go out of alignment. - } -}; - -/** - * Open the next (or previous) FieldTextInput. - * @param {Blockly.Field|Blockly.Block} start Current location. - * @param {boolean} forward If true go forward, otherwise backward. - */ -Blockly.BlockSvg.prototype.tab = function(start, forward) { - // This function need not be efficient since it runs once on a keypress. - // Create an ordered list of all text fields and connected inputs. - var list = []; - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { - if (field instanceof Blockly.FieldTextInput) { - // TODO: Also support dropdown fields. - list.push(field); - } - } - if (input.connection) { - var block = input.connection.targetBlock(); - if (block) { - list.push(block); - } - } - } - var i = list.indexOf(start); - if (i == -1) { - // No start location, start at the beginning or end. - i = forward ? -1 : list.length; - } - var target = list[forward ? i + 1 : i - 1]; - if (!target) { - // Ran off of list. - var parent = this.getParent(); - if (parent) { - parent.tab(this, forward); - } - } else if (target instanceof Blockly.Field) { - target.showEditor_(); - } else { - target.tab(null, forward); - } -}; - -/** - * Handle a mouse-down on an SVG block. - * @param {!Event} e Mouse down event or touch start event. - * @private - */ -Blockly.BlockSvg.prototype.onMouseDown_ = function(e) { - var gesture = this.workspace.getGesture(e); - if (gesture) { - gesture.handleBlockStart(e, this); - } -}; - -/** - * Load the block's help page in a new window. - * @private - */ -Blockly.BlockSvg.prototype.showHelp_ = function() { - var url = goog.isFunction(this.helpUrl) ? this.helpUrl() : this.helpUrl; - if (url) { - window.open(url); - } -}; - -/** - * Show the context menu for this block. - * @param {!Event} e Mouse event. - * @private - */ -Blockly.BlockSvg.prototype.showContextMenu_ = function(e) { - if (this.workspace.options.readOnly || !this.contextMenu) { - return; - } - // Save the current block in a variable for use in closures. - var block = this; - var menuOptions = []; - - if (this.isDeletable() && this.isMovable() && !block.isInFlyout) { - // Option to duplicate this block. - var duplicateOption = { - text: Blockly.Msg.DUPLICATE_BLOCK, - enabled: true, - callback: function() { - Blockly.duplicate_(block); - } - }; - if (this.getDescendants().length > this.workspace.remainingCapacity()) { - duplicateOption.enabled = false; - } - menuOptions.push(duplicateOption); - - if (this.isEditable() && !this.collapsed_ && - this.workspace.options.comments) { - // Option to add/remove a comment. - var commentOption = {enabled: !goog.userAgent.IE}; - if (this.comment) { - commentOption.text = Blockly.Msg.REMOVE_COMMENT; - commentOption.callback = function() { - block.setCommentText(null); - }; - } else { - commentOption.text = Blockly.Msg.ADD_COMMENT; - commentOption.callback = function() { - block.setCommentText(''); - }; - } - menuOptions.push(commentOption); - } - - // Option to make block inline. - if (!this.collapsed_) { - for (var i = 1; i < this.inputList.length; i++) { - if (this.inputList[i - 1].type != Blockly.NEXT_STATEMENT && - this.inputList[i].type != Blockly.NEXT_STATEMENT) { - // Only display this option if there are two value or dummy inputs - // next to each other. - var inlineOption = {enabled: true}; - var isInline = this.getInputsInline(); - inlineOption.text = isInline ? - Blockly.Msg.EXTERNAL_INPUTS : Blockly.Msg.INLINE_INPUTS; - inlineOption.callback = function() { - block.setInputsInline(!isInline); - }; - menuOptions.push(inlineOption); - break; - } - } - } - - if (this.workspace.options.collapse) { - // Option to collapse/expand block. - if (this.collapsed_) { - var expandOption = {enabled: true}; - expandOption.text = Blockly.Msg.EXPAND_BLOCK; - expandOption.callback = function() { - block.setCollapsed(false); - }; - menuOptions.push(expandOption); - } else { - var collapseOption = {enabled: true}; - collapseOption.text = Blockly.Msg.COLLAPSE_BLOCK; - collapseOption.callback = function() { - block.setCollapsed(true); - }; - menuOptions.push(collapseOption); - } - } - - if (this.workspace.options.disable) { - // Option to disable/enable block. - var disableOption = { - text: this.disabled ? - Blockly.Msg.ENABLE_BLOCK : Blockly.Msg.DISABLE_BLOCK, - enabled: !this.getInheritedDisabled(), - callback: function() { - block.setDisabled(!block.disabled); - } - }; - menuOptions.push(disableOption); - } - - // Option to delete this block. - // Count the number of blocks that are nested in this block. - var descendantCount = this.getDescendants().length; - var nextBlock = this.getNextBlock(); - if (nextBlock) { - // Blocks in the current stack would survive this block's deletion. - descendantCount -= nextBlock.getDescendants().length; - } - var deleteOption = { - text: descendantCount == 1 ? Blockly.Msg.DELETE_BLOCK : - Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(descendantCount)), - enabled: true, - callback: function() { - Blockly.Events.setGroup(true); - block.dispose(true, true); - Blockly.Events.setGroup(false); - } - }; - menuOptions.push(deleteOption); - } - - // Option to get help. - var url = goog.isFunction(this.helpUrl) ? this.helpUrl() : this.helpUrl; - var helpOption = {enabled: !!url}; - helpOption.text = Blockly.Msg.HELP; - helpOption.callback = function() { - block.showHelp_(); - }; - menuOptions.push(helpOption); - - // Allow the block to add or modify menuOptions. - if (this.customContextMenu && !block.isInFlyout) { - this.customContextMenu(menuOptions); - } - - Blockly.ContextMenu.show(e, menuOptions, this.RTL); - Blockly.ContextMenu.currentBlock = this; -}; - -/** - * Move the connections for this block and all blocks attached under it. - * Also update any attached bubbles. - * @param {number} dx Horizontal offset from current location, in workspace - * units. - * @param {number} dy Vertical offset from current location, in workspace - * units. - * @private - */ -Blockly.BlockSvg.prototype.moveConnections_ = function(dx, dy) { - if (!this.rendered) { - // Rendering is required to lay out the blocks. - // This is probably an invisible block attached to a collapsed block. - return; - } - var myConnections = this.getConnections_(false); - for (var i = 0; i < myConnections.length; i++) { - myConnections[i].moveBy(dx, dy); - } - var icons = this.getIcons(); - for (var i = 0; i < icons.length; i++) { - icons[i].computeIconLocation(); - } - - // Recurse through all blocks attached under this one. - for (var i = 0; i < this.childBlocks_.length; i++) { - this.childBlocks_[i].moveConnections_(dx, dy); - } -}; - -/** - * Recursively adds or removes the dragging class to this node and its children. - * @param {boolean} adding True if adding, false if removing. - * @package - */ -Blockly.BlockSvg.prototype.setDragging = function(adding) { - if (adding) { - var group = this.getSvgRoot(); - group.translate_ = ''; - group.skew_ = ''; - Blockly.draggingConnections_ = - Blockly.draggingConnections_.concat(this.getConnections_(true)); - Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyDragging'); - } else { - Blockly.draggingConnections_ = []; - Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyDragging'); - } - // Recurse through all blocks attached under this one. - for (var i = 0; i < this.childBlocks_.length; i++) { - this.childBlocks_[i].setDragging(adding); - } -}; - -/** - * Add or remove the UI indicating if this block is movable or not. - */ -Blockly.BlockSvg.prototype.updateMovable = function() { - if (this.isMovable()) { - Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyDraggable'); - } else { - Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyDraggable'); - } -}; - -/** - * Set whether this block is movable or not. - * @param {boolean} movable True if movable. - */ -Blockly.BlockSvg.prototype.setMovable = function(movable) { - Blockly.BlockSvg.superClass_.setMovable.call(this, movable); - this.updateMovable(); -}; - -/** - * Set whether this block is editable or not. - * @param {boolean} editable True if editable. - */ -Blockly.BlockSvg.prototype.setEditable = function(editable) { - Blockly.BlockSvg.superClass_.setEditable.call(this, editable); - var icons = this.getIcons(); - for (var i = 0; i < icons.length; i++) { - icons[i].updateEditable(); - } -}; - -/** - * Set whether this block is a shadow block or not. - * @param {boolean} shadow True if a shadow. - */ -Blockly.BlockSvg.prototype.setShadow = function(shadow) { - Blockly.BlockSvg.superClass_.setShadow.call(this, shadow); - this.updateColour(); -}; - -/** - * Return the root node of the SVG or null if none exists. - * @return {Element} The root SVG node (probably a group). - */ -Blockly.BlockSvg.prototype.getSvgRoot = function() { - return this.svgGroup_; -}; - -/** - * Dispose of this block. - * @param {boolean} healStack If true, then try to heal any gap by connecting - * the next statement with the previous statement. Otherwise, dispose of - * all children of this block. - * @param {boolean} animate If true, show a disposal animation and sound. - */ -Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { - if (!this.workspace) { - // The block has already been deleted. - return; - } - Blockly.Tooltip.hide(); - Blockly.Field.startCache(); - // Save the block's workspace temporarily so we can resize the - // contents once the block is disposed. - var blockWorkspace = this.workspace; - // If this block is being dragged, unlink the mouse events. - if (Blockly.selected == this) { - this.unselect(); - this.workspace.cancelCurrentGesture(); - } - // If this block has a context menu open, close it. - if (Blockly.ContextMenu.currentBlock == this) { - Blockly.ContextMenu.hide(); - } - - if (animate && this.rendered) { - this.unplug(healStack); - this.disposeUiEffect(); - } - // Stop rerendering. - this.rendered = false; - - Blockly.Events.disable(); - try { - var icons = this.getIcons(); - for (var i = 0; i < icons.length; i++) { - icons[i].dispose(); - } - } finally { - Blockly.Events.enable(); - } - Blockly.BlockSvg.superClass_.dispose.call(this, healStack); - - goog.dom.removeNode(this.svgGroup_); - blockWorkspace.resizeContents(); - // Sever JavaScript to DOM connections. - this.svgGroup_ = null; - this.svgPath_ = null; - this.svgPathLight_ = null; - this.svgPathDark_ = null; - Blockly.Field.stopCache(); -}; - -/** - * Play some UI effects (sound, animation) when disposing of a block. - */ -Blockly.BlockSvg.prototype.disposeUiEffect = function() { - this.workspace.getAudioManager().play('delete'); - - var xy = this.workspace.getSvgXY(/** @type {!Element} */ (this.svgGroup_)); - // Deeply clone the current block. - var clone = this.svgGroup_.cloneNode(true); - clone.translateX_ = xy.x; - clone.translateY_ = xy.y; - clone.setAttribute('transform', - 'translate(' + clone.translateX_ + ',' + clone.translateY_ + ')'); - this.workspace.getParentSvg().appendChild(clone); - clone.bBox_ = clone.getBBox(); - // Start the animation. - Blockly.BlockSvg.disposeUiStep_(clone, this.RTL, new Date, - this.workspace.scale); -}; - -/** - * Animate a cloned block and eventually dispose of it. - * This is a class method, not an instance method since the original block has - * been destroyed and is no longer accessible. - * @param {!Element} clone SVG element to animate and dispose of. - * @param {boolean} rtl True if RTL, false if LTR. - * @param {!Date} start Date of animation's start. - * @param {number} workspaceScale Scale of workspace. - * @private - */ -Blockly.BlockSvg.disposeUiStep_ = function(clone, rtl, start, workspaceScale) { - var ms = new Date - start; - var percent = ms / 150; - if (percent > 1) { - goog.dom.removeNode(clone); - } else { - var x = clone.translateX_ + - (rtl ? -1 : 1) * clone.bBox_.width * workspaceScale / 2 * percent; - var y = clone.translateY_ + clone.bBox_.height * workspaceScale * percent; - var scale = (1 - percent) * workspaceScale; - clone.setAttribute('transform', 'translate(' + x + ',' + y + ')' + - ' scale(' + scale + ')'); - var closure = function() { - Blockly.BlockSvg.disposeUiStep_(clone, rtl, start, workspaceScale); - }; - setTimeout(closure, 10); - } -}; - -/** - * Play some UI effects (sound, ripple) after a connection has been established. - */ -Blockly.BlockSvg.prototype.connectionUiEffect = function() { - this.workspace.getAudioManager().play('click'); - if (this.workspace.scale < 1) { - return; // Too small to care about visual effects. - } - // Determine the absolute coordinates of the inferior block. - var xy = this.workspace.getSvgXY(/** @type {!Element} */ (this.svgGroup_)); - // Offset the coordinates based on the two connection types, fix scale. - if (this.outputConnection) { - xy.x += (this.RTL ? 3 : -3) * this.workspace.scale; - xy.y += 13 * this.workspace.scale; - } else if (this.previousConnection) { - xy.x += (this.RTL ? -23 : 23) * this.workspace.scale; - xy.y += 3 * this.workspace.scale; - } - var ripple = Blockly.utils.createSvgElement('circle', - {'cx': xy.x, 'cy': xy.y, 'r': 0, 'fill': 'none', - 'stroke': '#888', 'stroke-width': 10}, - this.workspace.getParentSvg()); - // Start the animation. - Blockly.BlockSvg.connectionUiStep_(ripple, new Date, this.workspace.scale); -}; - -/** - * Expand a ripple around a connection. - * @param {!Element} ripple Element to animate. - * @param {!Date} start Date of animation's start. - * @param {number} workspaceScale Scale of workspace. - * @private - */ -Blockly.BlockSvg.connectionUiStep_ = function(ripple, start, workspaceScale) { - var ms = new Date - start; - var percent = ms / 150; - if (percent > 1) { - goog.dom.removeNode(ripple); - } else { - ripple.setAttribute('r', percent * 25 * workspaceScale); - ripple.style.opacity = 1 - percent; - var closure = function() { - Blockly.BlockSvg.connectionUiStep_(ripple, start, workspaceScale); - }; - Blockly.BlockSvg.disconnectUiStop_.pid_ = setTimeout(closure, 10); - } -}; - -/** - * Play some UI effects (sound, animation) when disconnecting a block. - */ -Blockly.BlockSvg.prototype.disconnectUiEffect = function() { - this.workspace.getAudioManager().play('disconnect'); - if (this.workspace.scale < 1) { - return; // Too small to care about visual effects. - } - // Horizontal distance for bottom of block to wiggle. - var DISPLACEMENT = 10; - // Scale magnitude of skew to height of block. - var height = this.getHeightWidth().height; - var magnitude = Math.atan(DISPLACEMENT / height) / Math.PI * 180; - if (!this.RTL) { - magnitude *= -1; - } - // Start the animation. - Blockly.BlockSvg.disconnectUiStep_(this.svgGroup_, magnitude, new Date); -}; - -/** - * Animate a brief wiggle of a disconnected block. - * @param {!Element} group SVG element to animate. - * @param {number} magnitude Maximum degrees skew (reversed for RTL). - * @param {!Date} start Date of animation's start. - * @private - */ -Blockly.BlockSvg.disconnectUiStep_ = function(group, magnitude, start) { - var DURATION = 200; // Milliseconds. - var WIGGLES = 3; // Half oscillations. - - var ms = new Date - start; - var percent = ms / DURATION; - - if (percent > 1) { - group.skew_ = ''; - } else { - var skew = Math.round(Math.sin(percent * Math.PI * WIGGLES) * - (1 - percent) * magnitude); - group.skew_ = 'skewX(' + skew + ')'; - var closure = function() { - Blockly.BlockSvg.disconnectUiStep_(group, magnitude, start); - }; - Blockly.BlockSvg.disconnectUiStop_.group = group; - Blockly.BlockSvg.disconnectUiStop_.pid = setTimeout(closure, 10); - } - group.setAttribute('transform', group.translate_ + group.skew_); -}; - -/** - * Stop the disconnect UI animation immediately. - * @private - */ -Blockly.BlockSvg.disconnectUiStop_ = function() { - if (Blockly.BlockSvg.disconnectUiStop_.group) { - clearTimeout(Blockly.BlockSvg.disconnectUiStop_.pid); - var group = Blockly.BlockSvg.disconnectUiStop_.group; - group.skew_ = ''; - group.setAttribute('transform', group.translate_); - Blockly.BlockSvg.disconnectUiStop_.group = null; - } -}; - -/** - * PID of disconnect UI animation. There can only be one at a time. - * @type {number} - */ -Blockly.BlockSvg.disconnectUiStop_.pid = 0; - -/** - * SVG group of wobbling block. There can only be one at a time. - * @type {Element} - */ -Blockly.BlockSvg.disconnectUiStop_.group = null; - -/** - * Change the colour of a block. - */ -Blockly.BlockSvg.prototype.updateColour = function() { - if (this.disabled) { - // Disabled blocks don't have colour. - return; - } - var hexColour = this.getColour(); - var rgb = goog.color.hexToRgb(hexColour); - if (this.isShadow()) { - rgb = goog.color.lighten(rgb, 0.6); - hexColour = goog.color.rgbArrayToHex(rgb); - this.svgPathLight_.style.display = 'none'; - this.svgPathDark_.setAttribute('fill', hexColour); - } else { - this.svgPathLight_.style.display = ''; - var hexLight = goog.color.rgbArrayToHex(goog.color.lighten(rgb, 0.3)); - var hexDark = goog.color.rgbArrayToHex(goog.color.darken(rgb, 0.2)); - this.svgPathLight_.setAttribute('stroke', hexLight); - this.svgPathDark_.setAttribute('fill', hexDark); - } - this.svgPath_.setAttribute('fill', hexColour); - - var icons = this.getIcons(); - for (var i = 0; i < icons.length; i++) { - icons[i].updateColour(); - } - - // Bump every dropdown to change its colour. - for (var x = 0, input; input = this.inputList[x]; x++) { - for (var y = 0, field; field = input.fieldRow[y]; y++) { - field.setText(null); - } - } -}; - -/** - * Enable or disable a block. - */ -Blockly.BlockSvg.prototype.updateDisabled = function() { - if (this.disabled || this.getInheritedDisabled()) { - if (Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyDisabled')) { - this.svgPath_.setAttribute('fill', - 'url(#' + this.workspace.options.disabledPatternId + ')'); - } - } else { - if (Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyDisabled')) { - this.updateColour(); - } - } - var children = this.getChildren(); - for (var i = 0, child; child = children[i]; i++) { - child.updateDisabled(); - } -}; - -/** - * Returns the comment on this block (or '' if none). - * @return {string} Block's comment. - */ -Blockly.BlockSvg.prototype.getCommentText = function() { - if (this.comment) { - var comment = this.comment.getText(); - // Trim off trailing whitespace. - return comment.replace(/\s+$/, '').replace(/ +\n/g, '\n'); - } - return ''; -}; - -/** - * Set this block's comment text. - * @param {?string} text The text, or null to delete. - */ -Blockly.BlockSvg.prototype.setCommentText = function(text) { - var changedState = false; - if (goog.isString(text)) { - if (!this.comment) { - this.comment = new Blockly.Comment(this); - changedState = true; - } - this.comment.setText(/** @type {string} */ (text)); - } else { - if (this.comment) { - this.comment.dispose(); - changedState = true; - } - } - if (changedState && this.rendered) { - this.render(); - // Adding or removing a comment icon will cause the block to change shape. - this.bumpNeighbours_(); - } -}; - -/** - * Set this block's warning text. - * @param {?string} text The text, or null to delete. - * @param {string=} opt_id An optional ID for the warning text to be able to - * maintain multiple warnings. - */ -Blockly.BlockSvg.prototype.setWarningText = function(text, opt_id) { - if (!this.setWarningText.pid_) { - // Create a database of warning PIDs. - // Only runs once per block (and only those with warnings). - this.setWarningText.pid_ = Object.create(null); - } - var id = opt_id || ''; - if (!id) { - // Kill all previous pending processes, this edit supersedes them all. - for (var n in this.setWarningText.pid_) { - clearTimeout(this.setWarningText.pid_[n]); - delete this.setWarningText.pid_[n]; - } - } else if (this.setWarningText.pid_[id]) { - // Only queue up the latest change. Kill any earlier pending process. - clearTimeout(this.setWarningText.pid_[id]); - delete this.setWarningText.pid_[id]; - } - if (this.workspace.isDragging()) { - // Don't change the warning text during a drag. - // Wait until the drag finishes. - var thisBlock = this; - this.setWarningText.pid_[id] = setTimeout(function() { - if (thisBlock.workspace) { // Check block wasn't deleted. - delete thisBlock.setWarningText.pid_[id]; - thisBlock.setWarningText(text, id); - } - }, 100); - return; - } - if (this.isInFlyout) { - text = null; - } - - // Bubble up to add a warning on top-most collapsed block. - var parent = this.getSurroundParent(); - var collapsedParent = null; - while (parent) { - if (parent.isCollapsed()) { - collapsedParent = parent; - } - parent = parent.getSurroundParent(); - } - if (collapsedParent) { - collapsedParent.setWarningText(text, 'collapsed ' + this.id + ' ' + id); - } - - var changedState = false; - if (goog.isString(text)) { - if (!this.warning) { - this.warning = new Blockly.Warning(this); - changedState = true; - } - this.warning.setText(/** @type {string} */ (text), id); - } else { - // Dispose all warnings if no id is given. - if (this.warning && !id) { - this.warning.dispose(); - changedState = true; - } else if (this.warning) { - var oldText = this.warning.getText(); - this.warning.setText('', id); - var newText = this.warning.getText(); - if (!newText) { - this.warning.dispose(); - } - changedState = oldText != newText; - } - } - if (changedState && this.rendered) { - this.render(); - // Adding or removing a warning icon will cause the block to change shape. - this.bumpNeighbours_(); - } -}; - -/** - * Give this block a mutator dialog. - * @param {Blockly.Mutator} mutator A mutator dialog instance or null to remove. - */ -Blockly.BlockSvg.prototype.setMutator = function(mutator) { - if (this.mutator && this.mutator !== mutator) { - this.mutator.dispose(); - } - if (mutator) { - mutator.block_ = this; - this.mutator = mutator; - mutator.createIcon(); - } -}; - -/** - * Set whether the block is disabled or not. - * @param {boolean} disabled True if disabled. - */ -Blockly.BlockSvg.prototype.setDisabled = function(disabled) { - if (this.disabled != disabled) { - Blockly.BlockSvg.superClass_.setDisabled.call(this, disabled); - if (this.rendered) { - this.updateDisabled(); - } - } -}; - -/** - * Set whether the block is highlighted or not. Block highlighting is - * often used to visually mark blocks currently being executed. - * @param {boolean} highlighted True if highlighted. - */ -Blockly.BlockSvg.prototype.setHighlighted = function(highlighted) { - if (!this.rendered) { - return; - } - if (highlighted) { - this.svgPath_.setAttribute('filter', - 'url(#' + this.workspace.options.embossFilterId + ')'); - this.svgPathLight_.style.display = 'none'; - } else { - Blockly.utils.removeAttribute(this.svgPath_, 'filter'); - delete this.svgPathLight_.style.display; - } -}; - -/** - * Select this block. Highlight it visually. - */ -Blockly.BlockSvg.prototype.addSelect = function() { - Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklySelected'); -}; - -/** - * Unselect this block. Remove its highlighting. - */ -Blockly.BlockSvg.prototype.removeSelect = function() { - Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklySelected'); -}; - -/** - * Update the cursor over this block by adding or removing a class. - * @param {boolean} enable True if the delete cursor should be shown, false - * otherwise. - * @package - */ -Blockly.BlockSvg.prototype.setDeleteStyle = function(enable) { - if (enable) { - Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyDraggingDelete'); - } else { - Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyDraggingDelete'); - } -}; - -// Overrides of functions on Blockly.Block that take into account whether the -// block has been rendered. - -/** - * Change the colour of a block. - * @param {number|string} colour HSV hue value, or #RRGGBB string. - */ -Blockly.BlockSvg.prototype.setColour = function(colour) { - Blockly.BlockSvg.superClass_.setColour.call(this, colour); - - if (this.rendered) { - this.updateColour(); - } -}; - -/** - * Move this block to the front of the visible workspace. - * tags do not respect z-index so svg renders them in the - * order that they are in the dom. By placing this block first within the - * block group's , it will render on top of any other blocks. - * @package - */ -Blockly.BlockSvg.prototype.bringToFront = function() { - var block = this; - do { - var root = block.getSvgRoot(); - root.parentNode.appendChild(root); - block = block.getParent(); - } while (block); -}; - -/** - * Set whether this block can chain onto the bottom of another block. - * @param {boolean} newBoolean True if there can be a previous statement. - * @param {string|Array.|null|undefined} opt_check Statement type or - * list of statement types. Null/undefined if any type could be connected. - */ -Blockly.BlockSvg.prototype.setPreviousStatement = - function(newBoolean, opt_check) { - /* eslint-disable indent */ - Blockly.BlockSvg.superClass_.setPreviousStatement.call(this, newBoolean, - opt_check); - - if (this.rendered) { - this.render(); - this.bumpNeighbours_(); - } -}; /* eslint-enable indent */ - -/** - * Set whether another block can chain onto the bottom of this block. - * @param {boolean} newBoolean True if there can be a next statement. - * @param {string|Array.|null|undefined} opt_check Statement type or - * list of statement types. Null/undefined if any type could be connected. - */ -Blockly.BlockSvg.prototype.setNextStatement = function(newBoolean, opt_check) { - Blockly.BlockSvg.superClass_.setNextStatement.call(this, newBoolean, - opt_check); - - if (this.rendered) { - this.render(); - this.bumpNeighbours_(); - } -}; - -/** - * Set whether this block returns a value. - * @param {boolean} newBoolean True if there is an output. - * @param {string|Array.|null|undefined} opt_check Returned type or list - * of returned types. Null or undefined if any type could be returned - * (e.g. variable get). - */ -Blockly.BlockSvg.prototype.setOutput = function(newBoolean, opt_check) { - Blockly.BlockSvg.superClass_.setOutput.call(this, newBoolean, opt_check); - - if (this.rendered) { - this.render(); - this.bumpNeighbours_(); - } -}; - -/** - * Set whether value inputs are arranged horizontally or vertically. - * @param {boolean} newBoolean True if inputs are horizontal. - */ -Blockly.BlockSvg.prototype.setInputsInline = function(newBoolean) { - Blockly.BlockSvg.superClass_.setInputsInline.call(this, newBoolean); - - if (this.rendered) { - this.render(); - this.bumpNeighbours_(); - } -}; - -/** - * Remove an input from this block. - * @param {string} name The name of the input. - * @param {boolean=} opt_quiet True to prevent error if input is not present. - * @throws {goog.asserts.AssertionError} if the input is not present and - * opt_quiet is not true. - */ -Blockly.BlockSvg.prototype.removeInput = function(name, opt_quiet) { - Blockly.BlockSvg.superClass_.removeInput.call(this, name, opt_quiet); - - if (this.rendered) { - this.render(); - // Removing an input will cause the block to change shape. - this.bumpNeighbours_(); - } -}; - -/** - * Move a numbered input to a different location on this block. - * @param {number} inputIndex Index of the input to move. - * @param {number} refIndex Index of input that should be after the moved input. - */ -Blockly.BlockSvg.prototype.moveNumberedInputBefore = function( - inputIndex, refIndex) { - Blockly.BlockSvg.superClass_.moveNumberedInputBefore.call(this, inputIndex, - refIndex); - - if (this.rendered) { - this.render(); - // Moving an input will cause the block to change shape. - this.bumpNeighbours_(); - } -}; - -/** - * Add a value input, statement input or local variable to this block. - * @param {number} type Either Blockly.INPUT_VALUE or Blockly.NEXT_STATEMENT or - * Blockly.DUMMY_INPUT. - * @param {string} name Language-neutral identifier which may used to find this - * input again. Should be unique to this block. - * @return {!Blockly.Input} The input object created. - * @private - */ -Blockly.BlockSvg.prototype.appendInput_ = function(type, name) { - var input = Blockly.BlockSvg.superClass_.appendInput_.call(this, type, name); - - if (this.rendered) { - this.render(); - // Adding an input will cause the block to change shape. - this.bumpNeighbours_(); - } - return input; -}; - -/** - * Returns connections originating from this block. - * @param {boolean} all If true, return all connections even hidden ones. - * Otherwise, for a non-rendered block return an empty list, and for a - * collapsed block don't return inputs connections. - * @return {!Array.} Array of connections. - * @package - */ -Blockly.BlockSvg.prototype.getConnections_ = function(all) { - var myConnections = []; - if (all || this.rendered) { - if (this.outputConnection) { - myConnections.push(this.outputConnection); - } - if (this.previousConnection) { - myConnections.push(this.previousConnection); - } - if (this.nextConnection) { - myConnections.push(this.nextConnection); - } - if (all || !this.collapsed_) { - for (var i = 0, input; input = this.inputList[i]; i++) { - if (input.connection) { - myConnections.push(input.connection); - } - } - } - } - return myConnections; -}; - -/** - * Create a connection of the specified type. - * @param {number} type The type of the connection to create. - * @return {!Blockly.RenderedConnection} A new connection of the specified type. - * @private - */ -Blockly.BlockSvg.prototype.makeConnection_ = function(type) { - return new Blockly.RenderedConnection(this, type); -}; - -/** - * Bump unconnected blocks out of alignment. Two blocks which aren't actually - * connected should not coincidentally line up on screen. - * @private - */ -Blockly.BlockSvg.prototype.bumpNeighbours_ = function() { - if (!this.workspace) { - return; // Deleted block. - } - if (Blockly.dragMode_ != Blockly.DRAG_NONE) { - return; // Don't bump blocks during a drag. - } - var rootBlock = this.getRootBlock(); - if (rootBlock.isInFlyout) { - return; // Don't move blocks around in a flyout. - } - // Loop through every connection on this block. - var myConnections = this.getConnections_(false); - for (var i = 0, connection; connection = myConnections[i]; i++) { - - // Spider down from this block bumping all sub-blocks. - if (connection.isConnected() && connection.isSuperior()) { - connection.targetBlock().bumpNeighbours_(); - } - - var neighbours = connection.neighbours_(Blockly.SNAP_RADIUS); - for (var j = 0, otherConnection; otherConnection = neighbours[j]; j++) { - - // If both connections are connected, that's probably fine. But if - // either one of them is unconnected, then there could be confusion. - if (!connection.isConnected() || !otherConnection.isConnected()) { - // Only bump blocks if they are from different tree structures. - if (otherConnection.getSourceBlock().getRootBlock() != rootBlock) { - - // Always bump the inferior block. - if (connection.isSuperior()) { - otherConnection.bumpAwayFrom_(connection); - } else { - connection.bumpAwayFrom_(otherConnection); - } - } - } - } - } -}; - -/** - * Schedule snapping to grid and bumping neighbours to occur after a brief - * delay. - * @package - */ -Blockly.BlockSvg.prototype.scheduleSnapAndBump = function() { - var block = this; - // Ensure that any snap and bump are part of this move's event group. - var group = Blockly.Events.getGroup(); - - setTimeout(function() { - Blockly.Events.setGroup(group); - block.snapToGrid(); - Blockly.Events.setGroup(false); - }, Blockly.BUMP_DELAY / 2); - - setTimeout(function() { - Blockly.Events.setGroup(group); - block.bumpNeighbours_(); - Blockly.Events.setGroup(false); - }, Blockly.BUMP_DELAY); -}; diff --git a/core/block_svg.ts b/core/block_svg.ts new file mode 100644 index 00000000000..1c1de49ec2b --- /dev/null +++ b/core/block_svg.ts @@ -0,0 +1,1730 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Methods for graphically rendering a block as SVG. + * + * @class + */ +// Former goog.module ID: Blockly.BlockSvg + +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_selected.js'; + +import {Block} from './block.js'; +import * as blockAnimations from './block_animations.js'; +import {IDeletable} from './blockly.js'; +import * as browserEvents from './browser_events.js'; +import {BlockCopyData, BlockPaster} from './clipboard/block_paster.js'; +import * as common from './common.js'; +import {config} from './config.js'; +import type {Connection} from './connection.js'; +import {ConnectionType} from './connection_type.js'; +import * as constants from './constants.js'; +import * as ContextMenu from './contextmenu.js'; +import { + ContextMenuOption, + ContextMenuRegistry, + LegacyContextMenuOption, +} from './contextmenu_registry.js'; +import {BlockDragStrategy} from './dragging/block_drag_strategy.js'; +import type {BlockMove} from './events/events_block_move.js'; +import {EventType} from './events/type.js'; +import * as eventUtils from './events/utils.js'; +import type {Field} from './field.js'; +import {FieldLabel} from './field_label.js'; +import {IconType} from './icons/icon_types.js'; +import {MutatorIcon} from './icons/mutator_icon.js'; +import {WarningIcon} from './icons/warning_icon.js'; +import type {Input} from './inputs/input.js'; +import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {ICopyable} from './interfaces/i_copyable.js'; +import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js'; +import {IIcon} from './interfaces/i_icon.js'; +import * as internalConstants from './internal_constants.js'; +import {ASTNode} from './keyboard_nav/ast_node.js'; +import {TabNavigateCursor} from './keyboard_nav/tab_navigate_cursor.js'; +import {MarkerManager} from './marker_manager.js'; +import {Msg} from './msg.js'; +import * as renderManagement from './render_management.js'; +import {RenderedConnection} from './rendered_connection.js'; +import type {IPathObject} from './renderers/common/i_path_object.js'; +import * as blocks from './serialization/blocks.js'; +import type {BlockStyle} from './theme.js'; +import * as Tooltip from './tooltip.js'; +import {Coordinate} from './utils/coordinate.js'; +import * as deprecation from './utils/deprecation.js'; +import * as dom from './utils/dom.js'; +import {Rect} from './utils/rect.js'; +import {Svg} from './utils/svg.js'; +import * as svgMath from './utils/svg_math.js'; +import {FlyoutItemInfo} from './utils/toolbox.js'; +import type {Workspace} from './workspace.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Class for a block's SVG representation. + * Not normally called directly, workspace.newBlock() is preferred. + */ +export class BlockSvg + extends Block + implements + IASTNodeLocationSvg, + IBoundedElement, + ICopyable, + IDraggable, + IDeletable +{ + /** + * Constant for identifying rows that are to be rendered inline. + * Don't collide with Blockly.inputTypes. + */ + static readonly INLINE = -1; + + /** + * ID to give the "collapsed warnings" warning. Allows us to remove the + * "collapsed warnings" warning without removing any warnings that belong to + * the block. + */ + static readonly COLLAPSED_WARNING_ID = 'TEMP_COLLAPSED_WARNING_'; + override decompose?: (p1: Workspace) => BlockSvg; + // override compose?: ((p1: BlockSvg) => void)|null; + + /** + * An optional method which saves a record of blocks connected to + * this block so they can be later restored after this block is + * recoomposed (reconfigured). Typically records the connected + * blocks on properties on blocks in the mutator flyout, so that + * rearranging those component blocks will automatically rearrange + * the corresponding connected blocks on this block after this block + * is recomposed. + * + * To keep the saved connection information up-to-date, MutatorIcon + * arranges for an event listener to call this method any time the + * mutator flyout is open and a change occurs on this block's + * workspace. + * + * @param rootBlock The root block in the mutator flyout. + */ + saveConnections?: (rootBlock: BlockSvg) => void; + + customContextMenu?: ( + p1: Array, + ) => void; + + /** + * Height of this block, not including any statement blocks above or below. + * Height is in workspace units. + */ + height = 0; + + /** + * Width of this block, including any connected value blocks. + * Width is in workspace units. + */ + width = 0; + + /** + * Width of this block, not including any connected value blocks. + * Width is in workspace units. + * + * @internal + */ + childlessWidth = 0; + + /** + * Map from IDs for warnings text to PIDs of functions to apply them. + * Used to be able to maintain multiple warnings. + */ + private warningTextDb = new Map>(); + + /** Block's mutator icon (if any). */ + mutator: MutatorIcon | null = null; + + private svgGroup: SVGGElement; + style: BlockStyle; + /** @internal */ + pathObject: IPathObject; + + /** Is this block a BlockSVG? */ + override readonly rendered = true; + + private visuallyDisabled = false; + + override workspace: WorkspaceSvg; + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + override outputConnection!: RenderedConnection; + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + override nextConnection!: RenderedConnection; + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + override previousConnection!: RenderedConnection; + + private translation = ''; + + /** Whether this block is currently being dragged. */ + private dragging = false; + + /** + * The location of the top left of this block (in workspace coordinates) + * relative to either its parent block, or the workspace origin if it has no + * parent. + * + * @internal + */ + relativeCoords = new Coordinate(0, 0); + + private dragStrategy: IDragStrategy = new BlockDragStrategy(this); + + /** + * @param workspace The block's workspace. + * @param prototypeName Name of the language object containing type-specific + * functions for this block. + * @param opt_id Optional ID. Use this ID if provided, otherwise create a new + * ID. + */ + constructor(workspace: WorkspaceSvg, prototypeName: string, opt_id?: string) { + super(workspace, prototypeName, opt_id); + if (!workspace.rendered) { + throw TypeError('Cannot create a rendered block in a headless workspace'); + } + this.workspace = workspace; + this.svgGroup = dom.createSvgElement(Svg.G, {}); + + /** A block style object. */ + this.style = workspace.getRenderer().getConstants().getBlockStyle(null); + + /** The renderer's path object. */ + this.pathObject = workspace + .getRenderer() + .makePathObject(this.svgGroup, this.style); + + const svgPath = this.pathObject.svgPath; + (svgPath as any).tooltip = this; + Tooltip.bindMouseEvents(svgPath); + + // Expose this block's ID on its top-level SVG group. + this.svgGroup.setAttribute('data-id', this.id); + + this.doInit_(); + } + + /** + * Create and initialize the SVG representation of the block. + * May be called more than once. + */ + initSvg() { + if (this.initialized) return; + for (const input of this.inputList) { + input.init(); + } + for (const icon of this.getIcons()) { + icon.initView(this.createIconPointerDownListener(icon)); + icon.updateEditable(); + } + this.applyColour(); + this.pathObject.updateMovable(this.isMovable() || this.isInFlyout); + const svg = this.getSvgRoot(); + if (!this.workspace.options.readOnly && svg) { + browserEvents.conditionalBind(svg, 'pointerdown', this, this.onMouseDown); + } + + if (!svg.parentNode) { + this.workspace.getCanvas().appendChild(svg); + } + this.initialized = true; + } + + /** + * Get the secondary colour of a block. + * + * @returns #RRGGBB string. + */ + getColourSecondary(): string | undefined { + return this.style.colourSecondary; + } + + /** + * Get the tertiary colour of a block. + * + * @returns #RRGGBB string. + */ + getColourTertiary(): string | undefined { + return this.style.colourTertiary; + } + + /** Selects this block. Highlights the block visually. */ + select() { + if (this.isShadow()) { + this.getParent()?.select(); + return; + } + this.addSelect(); + } + + /** Unselects this block. Unhighlights the block visually. */ + unselect() { + if (this.isShadow()) { + this.getParent()?.unselect(); + return; + } + this.removeSelect(); + } + + /** + * Sets the parent of this block to be a new block or null. + * + * @param newParent New parent block. + * @internal + */ + override setParent(newParent: this | null) { + const oldParent = this.parentBlock_; + if (newParent === oldParent) { + return; + } + + dom.startTextWidthCache(); + super.setParent(newParent); + dom.stopTextWidthCache(); + + const svgRoot = this.getSvgRoot(); + + // Bail early if workspace is clearing, or we aren't rendered. + // We won't need to reattach ourselves anywhere. + if (this.workspace.isClearing || !svgRoot) { + return; + } + + const oldXY = this.getRelativeToSurfaceXY(); + if (newParent) { + (newParent as BlockSvg).getSvgRoot().appendChild(svgRoot); + } else if (oldParent) { + // If we are losing a parent, we want to move our DOM element to the + // root of the workspace. + const draggingBlock = this.workspace + .getCanvas() + .querySelector('.blocklyDragging'); + if (draggingBlock) { + this.workspace.getCanvas().insertBefore(svgRoot, draggingBlock); + } else { + this.workspace.getCanvas().appendChild(svgRoot); + } + this.translate(oldXY.x, oldXY.y); + } + + this.applyColour(); + } + + /** + * Return the coordinates of the top-left corner of this block relative to the + * drawing surface's origin (0,0), in workspace units. + * If the block is on the workspace, (0, 0) is the origin of the workspace + * coordinate system. + * This does not change with workspace scale. + * + * @returns Object with .x and .y properties in workspace coordinates. + */ + override getRelativeToSurfaceXY(): Coordinate { + const layerManger = this.workspace.getLayerManager(); + if (!layerManger) { + throw new Error( + 'Cannot calculate position because the workspace has not been appended', + ); + } + let x = 0; + let y = 0; + + let element: SVGElement = this.getSvgRoot(); + if (element) { + do { + // Loop through this block and every parent. + const xy = svgMath.getRelativeXY(element); + x += xy.x; + y += xy.y; + element = element.parentNode as SVGElement; + } while (element && !layerManger.hasLayer(element)); + } + return new Coordinate(x, y); + } + + /** + * Move a block by a relative offset. + * + * @param dx Horizontal offset in workspace units. + * @param dy Vertical offset in workspace units. + * @param reason Why is this move happening? 'drag', 'bump', 'snap', ... + */ + override moveBy(dx: number, dy: number, reason?: string[]) { + if (this.parentBlock_) { + throw Error('Block has parent'); + } + const eventsEnabled = eventUtils.isEnabled(); + let event: BlockMove | null = null; + if (eventsEnabled) { + event = new (eventUtils.get(EventType.BLOCK_MOVE)!)(this) as BlockMove; + if (reason) event.setReason(reason); + } + + const delta = new Coordinate(dx, dy); + const currLoc = this.getRelativeToSurfaceXY(); + const newLoc = Coordinate.sum(currLoc, delta); + this.translate(newLoc.x, newLoc.y); + this.updateComponentLocations(newLoc); + + if (eventsEnabled && event) { + event!.recordNew(); + eventUtils.fire(event); + } + this.workspace.resizeContents(); + } + + /** + * Transforms a block by setting the translation on the transform attribute + * of the block's SVG. + * + * @param x The x coordinate of the translation in workspace units. + * @param y The y coordinate of the translation in workspace units. + */ + translate(x: number, y: number) { + this.translation = `translate(${x}, ${y})`; + this.relativeCoords = new Coordinate(x, y); + this.getSvgRoot().setAttribute('transform', this.getTranslation()); + } + + /** + * Returns the SVG translation of this block. + * + * @internal + */ + getTranslation(): string { + return this.translation; + } + + /** + * Move a block to a position. + * + * @param xy The position to move to in workspace units. + * @param reason Why is this move happening? 'drag', 'bump', 'snap', ... + */ + moveTo(xy: Coordinate, reason?: string[]) { + const curXY = this.getRelativeToSurfaceXY(); + this.moveBy(xy.x - curXY.x, xy.y - curXY.y, reason); + } + + /** + * Move this block during a drag. + * This block must be a top-level block. + * + * @param newLoc The location to translate to, in workspace coordinates. + * @internal + */ + moveDuringDrag(newLoc: Coordinate) { + this.translate(newLoc.x, newLoc.y); + this.getSvgRoot().setAttribute('transform', this.getTranslation()); + this.updateComponentLocations(newLoc); + } + + /** Snap this block to the nearest grid point. */ + snapToGrid() { + if (this.isDeadOrDying()) return; + if (this.getParent()) return; + if (this.isInFlyout) return; + const grid = this.workspace.getGrid(); + if (!grid?.shouldSnap()) return; + const currentXY = this.getRelativeToSurfaceXY(); + const alignedXY = grid.alignXY(currentXY); + if (alignedXY !== currentXY) { + this.moveTo(alignedXY, ['snap']); + } + } + + /** + * Returns the coordinates of a bounding box describing the dimensions of this + * block and any blocks stacked below it. + * Coordinate system: workspace coordinates. + * + * @returns Object with coordinates of the bounding box. + */ + getBoundingRectangle(): Rect { + return this.getBoundingRectangleWithDimensions(this.getHeightWidth()); + } + + /** + * Returns the coordinates of a bounding box describing the dimensions of this + * block alone. + * Coordinate system: workspace coordinates. + * + * @returns Object with coordinates of the bounding box. + */ + getBoundingRectangleWithoutChildren(): Rect { + return this.getBoundingRectangleWithDimensions({ + height: this.height, + width: this.childlessWidth, + }); + } + + private getBoundingRectangleWithDimensions(blockBounds: { + height: number; + width: number; + }) { + const blockXY = this.getRelativeToSurfaceXY(); + let left; + let right; + if (this.RTL) { + left = blockXY.x - blockBounds.width; + right = blockXY.x; + } else { + left = blockXY.x; + right = blockXY.x + blockBounds.width; + } + return new Rect(blockXY.y, blockXY.y + blockBounds.height, left, right); + } + + /** + * Notify every input on this block to mark its fields as dirty. + * A dirty field is a field that needs to be re-rendered. + */ + markDirty() { + this.pathObject.constants = this.workspace.getRenderer().getConstants(); + for (let i = 0, input; (input = this.inputList[i]); i++) { + input.markDirty(); + } + } + + /** + * Set whether the block is collapsed or not. + * + * @param collapsed True if collapsed. + */ + override setCollapsed(collapsed: boolean) { + if (this.collapsed_ === collapsed) { + return; + } + super.setCollapsed(collapsed); + this.updateCollapsed(); + } + + /** + * Makes sure that when the block is collapsed, it is rendered correctly + * for that state. + */ + private updateCollapsed() { + const collapsed = this.isCollapsed(); + const collapsedInputName = constants.COLLAPSED_INPUT_NAME; + const collapsedFieldName = constants.COLLAPSED_FIELD_NAME; + + for (let i = 0, input; (input = this.inputList[i]); i++) { + if (input.name !== collapsedInputName) { + input.setVisible(!collapsed); + } + } + + for (const icon of this.getIcons()) { + icon.updateCollapsed(); + } + + if (!collapsed) { + this.updateDisabled(); + this.removeInput(collapsedInputName); + return; + } + + const text = this.toString(internalConstants.COLLAPSE_CHARS); + const field = this.getField(collapsedFieldName); + if (field) { + field.setValue(text); + return; + } + const input = + this.getInput(collapsedInputName) || + this.appendDummyInput(collapsedInputName); + input.appendField(new FieldLabel(text), collapsedFieldName); + } + + /** + * Open the next (or previous) FieldTextInput. + * + * @param start Current field. + * @param forward If true go forward, otherwise backward. + */ + tab(start: Field, forward: boolean) { + const tabCursor = new TabNavigateCursor(); + tabCursor.setCurNode(ASTNode.createFieldNode(start)!); + const currentNode = tabCursor.getCurNode(); + + if (forward) { + tabCursor.next(); + } else { + tabCursor.prev(); + } + + const nextNode = tabCursor.getCurNode(); + if (nextNode && nextNode !== currentNode) { + const nextField = nextNode.getLocation() as Field; + nextField.showEditor(); + + // Also move the cursor if we're in keyboard nav mode. + if (this.workspace.keyboardAccessibilityMode) { + this.workspace.getCursor()!.setCurNode(nextNode); + } + } + } + + /** + * Handle a pointerdown on an SVG block. + * + * @param e Pointer down event. + */ + private onMouseDown(e: PointerEvent) { + const gesture = this.workspace.getGesture(e); + if (gesture) { + gesture.handleBlockStart(e, this); + } + } + + /** + * Load the block's help page in a new window. + * + * @internal + */ + showHelp() { + const url = + typeof this.helpUrl === 'function' ? this.helpUrl() : this.helpUrl; + if (url) { + window.open(url); + } + } + + /** + * Generate the context menu for this block. + * + * @returns Context menu options or null if no menu. + */ + protected generateContextMenu(): Array< + ContextMenuOption | LegacyContextMenuOption + > | null { + if (this.workspace.options.readOnly || !this.contextMenu) { + return null; + } + const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( + ContextMenuRegistry.ScopeType.BLOCK, + {block: this}, + ); + + // Allow the block to add or modify menuOptions. + if (this.customContextMenu) { + this.customContextMenu(menuOptions); + } + + return menuOptions; + } + + /** + * Show the context menu for this block. + * + * @param e Mouse event. + * @internal + */ + showContextMenu(e: PointerEvent) { + const menuOptions = this.generateContextMenu(); + + if (menuOptions && menuOptions.length) { + ContextMenu.show(e, menuOptions, this.RTL, this.workspace); + ContextMenu.setCurrentBlock(this); + } + } + + /** + * Updates the locations of any parts of the block that need to know where + * they are (e.g. connections, icons). + * + * @param blockOrigin The top-left of this block in workspace coordinates. + * @internal + */ + updateComponentLocations(blockOrigin: Coordinate) { + if (!this.dragging) this.updateConnectionLocations(blockOrigin); + this.updateIconLocations(blockOrigin); + this.updateFieldLocations(blockOrigin); + + for (const child of this.getChildren(false)) { + child.updateComponentLocations( + Coordinate.sum(blockOrigin, child.relativeCoords), + ); + } + } + + private updateConnectionLocations(blockOrigin: Coordinate) { + for (const conn of this.getConnections_(false)) { + conn.moveToOffset(blockOrigin); + } + } + + private updateIconLocations(blockOrigin: Coordinate) { + for (const icon of this.getIcons()) { + icon.onLocationChange(blockOrigin); + } + } + + private updateFieldLocations(blockOrigin: Coordinate) { + for (const input of this.inputList) { + for (const field of input.fieldRow) { + field.onLocationChange(blockOrigin); + } + } + } + + /** + * Recursively adds or removes the dragging class to this node and its + * children. + * + * @param adding True if adding, false if removing. + * @internal + */ + setDragging(adding: boolean) { + this.dragging = adding; + if (adding) { + this.translation = ''; + common.draggingConnections.push(...this.getConnections_(true)); + dom.addClass(this.svgGroup, 'blocklyDragging'); + } else { + common.draggingConnections.length = 0; + dom.removeClass(this.svgGroup, 'blocklyDragging'); + } + // Recurse through all blocks attached under this one. + for (let i = 0; i < this.childBlocks_.length; i++) { + (this.childBlocks_[i] as BlockSvg).setDragging(adding); + } + } + + /** + * Set whether this block is movable or not. + * + * @param movable True if movable. + */ + override setMovable(movable: boolean) { + super.setMovable(movable); + this.pathObject.updateMovable(movable); + } + + /** + * Set whether this block is editable or not. + * + * @param editable True if editable. + */ + override setEditable(editable: boolean) { + super.setEditable(editable); + const icons = this.getIcons(); + for (let i = 0; i < icons.length; i++) { + icons[i].updateEditable(); + } + } + + /** + * Sets whether this block is a shadow block or not. + * This method is internal and should not be called by users of Blockly. To + * create shadow blocks programmatically call connection.setShadowState + * + * @param shadow True if a shadow. + * @internal + */ + override setShadow(shadow: boolean) { + super.setShadow(shadow); + this.applyColour(); + } + + /** + * Set whether this block is an insertion marker block or not. + * Once set this cannot be unset. + * + * @param insertionMarker True if an insertion marker. + * @internal + */ + override setInsertionMarker(insertionMarker: boolean) { + if (this.isInsertionMarker_ === insertionMarker) { + return; // No change. + } + this.isInsertionMarker_ = insertionMarker; + if (this.isInsertionMarker_) { + this.setColour( + this.workspace.getRenderer().getConstants().INSERTION_MARKER_COLOUR, + ); + this.pathObject.updateInsertionMarker(true); + } + } + + /** + * Return the root node of the SVG or null if none exists. + * + * @returns The root SVG node (probably a group). + */ + getSvgRoot(): SVGGElement { + return this.svgGroup; + } + + /** + * Dispose of this block. + * + * @param healStack If true, then try to heal any gap by connecting the next + * statement with the previous statement. Otherwise, dispose of all + * children of this block. + * @param animate If true, show a disposal animation and sound. + */ + override dispose(healStack?: boolean, animate?: boolean) { + this.disposing = true; + + Tooltip.dispose(); + ContextMenu.hide(); + + if (animate) { + this.unplug(healStack); + blockAnimations.disposeUiEffect(this); + } + + // Selecting a shadow block highlights an ancestor block, but that highlight + // should be removed if the shadow block will be deleted. So, before + // deleting blocks and severing the connections between them, check whether + // doing so would delete a selected block and make sure that any associated + // parent is updated. + const selection = common.getSelected(); + if (selection instanceof Block) { + let selectionAncestor: Block | null = selection; + while (selectionAncestor !== null) { + if (selectionAncestor === this) { + // The block to be deleted contains the selected block, so remove any + // selection highlight associated with the selected block before + // deleting them. + selection.unselect(); + } + selectionAncestor = selectionAncestor.getParent(); + } + } + + super.dispose(!!healStack); + dom.removeNode(this.svgGroup); + } + + /** + * Disposes of this block without doing things required by the top block. + * E.g. does trigger UI effects, remove nodes, etc. + */ + override disposeInternal() { + this.disposing = true; + super.disposeInternal(); + + if (common.getSelected() === this) { + this.unselect(); + this.workspace.cancelCurrentGesture(); + } + + [...this.warningTextDb.values()].forEach((n) => clearTimeout(n)); + this.warningTextDb.clear(); + + this.getIcons().forEach((i) => i.dispose()); + } + + /** + * Delete a block and hide chaff when doing so. The block will not be deleted + * if it's in a flyout. This is called from the context menu and keyboard + * shortcuts as the full delete action. If you are disposing of a block from + * the workspace and don't need to perform flyout checks, handle event + * grouping, or hide chaff, then use `block.dispose()` directly. + */ + checkAndDelete() { + if (this.workspace.isFlyout) { + return; + } + eventUtils.setGroup(true); + this.workspace.hideChaff(); + if (this.outputConnection) { + // Do not attempt to heal rows + // (https://github.com/google/blockly/issues/4832) + this.dispose(false, true); + } else { + this.dispose(/* heal */ true, true); + } + eventUtils.setGroup(false); + } + + /** + * Encode a block for copying. + * + * @returns Copy metadata, or null if the block is an insertion marker. + */ + toCopyData(): BlockCopyData | null { + if (this.isInsertionMarker_) { + return null; + } + return { + paster: BlockPaster.TYPE, + blockState: blocks.save(this, { + addCoordinates: true, + addNextBlocks: false, + }) as blocks.State, + typeCounts: common.getBlockTypeCounts(this, true), + }; + } + + /** + * Updates the colour of the block to match the block's state. + * + * @internal + */ + applyColour() { + this.pathObject.applyColour(this); + + const icons = this.getIcons(); + for (let i = 0; i < icons.length; i++) { + icons[i].applyColour(); + } + + for (let x = 0, input; (input = this.inputList[x]); x++) { + for (let y = 0, field; (field = input.fieldRow[y]); y++) { + field.applyColour(); + } + } + } + + /** + * Updates the colour of the block (and children) to match the current + * disabled state. + * + * @internal + */ + updateDisabled() { + const disabled = !this.isEnabled() || this.getInheritedDisabled(); + + if (this.visuallyDisabled === disabled) { + this.getNextBlock()?.updateDisabled(); + return; + } + + this.applyColour(); + this.visuallyDisabled = disabled; + for (const child of this.getChildren(false)) { + child.updateDisabled(); + } + } + + /** + * Set this block's warning text. + * + * @param text The text, or null to delete. + * @param id An optional ID for the warning text to be able to maintain + * multiple warnings. + */ + override setWarningText(text: string | null, id: string = '') { + if (!id) { + // Kill all previous pending processes, this edit supersedes them all. + for (const timeout of this.warningTextDb.values()) { + clearTimeout(timeout); + } + this.warningTextDb.clear(); + } else if (this.warningTextDb.has(id)) { + // Only queue up the latest change. Kill any earlier pending process. + clearTimeout(this.warningTextDb.get(id)!); + this.warningTextDb.delete(id); + } + if (this.workspace.isDragging()) { + // Don't change the warning text during a drag. + // Wait until the drag finishes. + this.warningTextDb.set( + id, + setTimeout(() => { + if (!this.isDeadOrDying()) { + this.warningTextDb.delete(id); + this.setWarningText(text, id); + } + }, 100), + ); + return; + } + if (this.isInFlyout) { + text = null; + } + + const icon = this.getIcon(WarningIcon.TYPE) as WarningIcon | undefined; + if (text) { + // Bubble up to add a warning on top-most collapsed block. + // TODO(#6020): This warning is never removed. + let parent = this.getSurroundParent(); + let collapsedParent = null; + while (parent) { + if (parent.isCollapsed()) { + collapsedParent = parent; + } + parent = parent.getSurroundParent(); + } + if (collapsedParent) { + collapsedParent.setWarningText( + Msg['COLLAPSED_WARNINGS_WARNING'], + BlockSvg.COLLAPSED_WARNING_ID, + ); + } + + if (icon) { + (icon as WarningIcon).addMessage(text, id); + } else { + this.addIcon(new WarningIcon(this).addMessage(text, id)); + } + } else if (icon) { + // Dispose all warnings if no ID is given. + if (!id) { + this.removeIcon(WarningIcon.TYPE); + } else { + // Remove just this warning id's message. + icon.addMessage('', id); + // Then remove the entire icon if there is no longer any text. + if (!icon.getText()) this.removeIcon(WarningIcon.TYPE); + } + } + } + + /** + * Give this block a mutator dialog. + * + * @param mutator A mutator dialog instance or null to remove. + */ + override setMutator(mutator: MutatorIcon | null) { + this.removeIcon(MutatorIcon.TYPE); + if (mutator) this.addIcon(mutator); + } + + override addIcon(icon: T): T { + super.addIcon(icon); + + if (icon instanceof MutatorIcon) this.mutator = icon; + + icon.initView(this.createIconPointerDownListener(icon)); + icon.applyColour(); + icon.updateEditable(); + this.queueRender(); + + return icon; + } + + /** + * Creates a pointer down event listener for the icon to append to its + * root svg. + */ + private createIconPointerDownListener(icon: IIcon) { + return (e: PointerEvent) => { + if (this.isDeadOrDying()) return; + const gesture = this.workspace.getGesture(e); + if (gesture) { + gesture.setStartIcon(icon); + } + }; + } + + override removeIcon(type: IconType): boolean { + const removed = super.removeIcon(type); + + if (type.equals(MutatorIcon.TYPE)) this.mutator = null; + + this.queueRender(); + + return removed; + } + + /** + * @deprecated v11 - Set whether the block is manually enabled or disabled. + * The user can toggle whether a block is disabled from a context menu + * option. A block may still be disabled for other reasons even if the user + * attempts to manually enable it, such as when the block is in an invalid + * location. This method is deprecated and setDisabledReason should be used + * instead. + * + * @param enabled True if enabled. + */ + override setEnabled(enabled: boolean) { + deprecation.warn( + 'setEnabled', + 'v11', + 'v12', + 'the setDisabledReason method of BlockSvg', + ); + const wasEnabled = this.isEnabled(); + super.setEnabled(enabled); + if (this.isEnabled() !== wasEnabled && !this.getInheritedDisabled()) { + this.updateDisabled(); + } + } + + /** + * Add or remove a reason why the block might be disabled. If a block has + * any reasons to be disabled, then the block itself will be considered + * disabled. A block could be disabled for multiple independent reasons + * simultaneously, such as when the user manually disables it, or the block + * is invalid. + * + * @param disabled If true, then the block should be considered disabled for + * at least the provided reason, otherwise the block is no longer disabled + * for that reason. + * @param reason A language-neutral identifier for a reason why the block + * could be disabled. Call this method again with the same identifier to + * update whether the block is currently disabled for this reason. + */ + override setDisabledReason(disabled: boolean, reason: string): void { + const wasEnabled = this.isEnabled(); + super.setDisabledReason(disabled, reason); + if (this.isEnabled() !== wasEnabled && !this.getInheritedDisabled()) { + this.updateDisabled(); + } + } + + /** + * Set whether the block is highlighted or not. Block highlighting is + * often used to visually mark blocks currently being executed. + * + * @param highlighted True if highlighted. + */ + setHighlighted(highlighted: boolean) { + this.pathObject.updateHighlighted(highlighted); + } + + /** + * Adds the visual "select" effect to the block, but does not actually select + * it or fire an event. + * + * @see BlockSvg#select + */ + addSelect() { + this.pathObject.updateSelected(true); + } + + /** + * Removes the visual "select" effect from the block, but does not actually + * unselect it or fire an event. + * + * @see BlockSvg#unselect + */ + removeSelect() { + this.pathObject.updateSelected(false); + } + + /** + * Update the cursor over this block by adding or removing a class. + * + * @param enable True if the delete cursor should be shown, false otherwise. + * @internal + */ + setDeleteStyle(enable: boolean) { + this.pathObject.updateDraggingDelete(enable); + } + + // Overrides of functions on Blockly.Block that take into account whether the + // block has been rendered. + + /** + * Get the colour of a block. + * + * @returns #RRGGBB string. + */ + override getColour(): string { + return this.style.colourPrimary; + } + + /** + * Change the colour of a block. + * + * @param colour HSV hue value, or #RRGGBB string. + */ + override setColour(colour: number | string) { + super.setColour(colour); + const styleObj = this.workspace + .getRenderer() + .getConstants() + .getBlockStyleForColour(this.colour_); + + this.pathObject.setStyle(styleObj.style); + this.style = styleObj.style; + this.styleName_ = styleObj.name; + + this.applyColour(); + } + + /** + * Set the style and colour values of a block. + * + * @param blockStyleName Name of the block style. + * @throws {Error} if the block style does not exist. + */ + override setStyle(blockStyleName: string) { + const blockStyle = this.workspace + .getRenderer() + .getConstants() + .getBlockStyle(blockStyleName); + this.styleName_ = blockStyleName; + + if (blockStyle) { + this.hat = blockStyle.hat; + this.pathObject.setStyle(blockStyle); + // Set colour to match Block. + this.colour_ = blockStyle.colourPrimary; + this.style = blockStyle; + + this.applyColour(); + } else { + throw Error('Invalid style name: ' + blockStyleName); + } + } + + /** + * Move this block to the front of the visible workspace. + * tags do not respect z-index so SVG renders them in the + * order that they are in the DOM. By placing this block first within the + * block group's , it will render on top of any other blocks. + * Use sparingly, this method is expensive because it reorders the DOM + * nodes. + * + * @param blockOnly True to only move this block to the front without + * adjusting its parents. + */ + bringToFront(blockOnly = false) { + /* eslint-disable-next-line @typescript-eslint/no-this-alias */ + let block: this | null = this; + if (block.isDeadOrDying()) { + return; + } + do { + const root = block.getSvgRoot(); + const parent = root.parentNode; + const childNodes = parent!.childNodes; + // Avoid moving the block if it's already at the bottom. + if (childNodes[childNodes.length - 1] !== root) { + parent!.appendChild(root); + } + if (blockOnly) break; + block = block.getParent(); + } while (block); + } + + /** + * Set whether this block can chain onto the bottom of another block. + * + * @param newBoolean True if there can be a previous statement. + * @param opt_check Statement type or list of statement types. Null/undefined + * if any type could be connected. + */ + override setPreviousStatement( + newBoolean: boolean, + opt_check?: string | string[] | null, + ) { + super.setPreviousStatement(newBoolean, opt_check); + this.queueRender(); + } + + /** + * Set whether another block can chain onto the bottom of this block. + * + * @param newBoolean True if there can be a next statement. + * @param opt_check Statement type or list of statement types. Null/undefined + * if any type could be connected. + */ + override setNextStatement( + newBoolean: boolean, + opt_check?: string | string[] | null, + ) { + super.setNextStatement(newBoolean, opt_check); + this.queueRender(); + } + + /** + * Set whether this block returns a value. + * + * @param newBoolean True if there is an output. + * @param opt_check Returned type or list of returned types. Null or + * undefined if any type could be returned (e.g. variable get). + */ + override setOutput( + newBoolean: boolean, + opt_check?: string | string[] | null, + ) { + super.setOutput(newBoolean, opt_check); + this.queueRender(); + } + + /** + * Set whether value inputs are arranged horizontally or vertically. + * + * @param newBoolean True if inputs are horizontal. + */ + override setInputsInline(newBoolean: boolean) { + super.setInputsInline(newBoolean); + this.queueRender(); + } + + /** + * Remove an input from this block. + * + * @param name The name of the input. + * @param opt_quiet True to prevent error if input is not present. + * @returns True if operation succeeds, false if input is not present and + * opt_quiet is true + * @throws {Error} if the input is not present and opt_quiet is not true. + */ + override removeInput(name: string, opt_quiet?: boolean): boolean { + const removed = super.removeInput(name, opt_quiet); + this.queueRender(); + return removed; + } + + /** + * Move a numbered input to a different location on this block. + * + * @param inputIndex Index of the input to move. + * @param refIndex Index of input that should be after the moved input. + */ + override moveNumberedInputBefore(inputIndex: number, refIndex: number) { + super.moveNumberedInputBefore(inputIndex, refIndex); + this.queueRender(); + } + + /** @override */ + override appendInput(input: Input): Input { + super.appendInput(input); + this.queueRender(); + return input; + } + + /** + * Sets whether this block's connections are tracked in the database or not. + * + * Used by the deserializer to be more efficient. Setting a connection's + * tracked_ value to false keeps it from adding itself to the db when it + * gets its first moveTo call, saving expensive ops for later. + * + * @param track If true, start tracking. If false, stop tracking. + * @internal + */ + setConnectionTracking(track: boolean) { + if (this.previousConnection) { + this.previousConnection.setTracking(track); + } + if (this.outputConnection) { + this.outputConnection.setTracking(track); + } + if (this.nextConnection) { + this.nextConnection.setTracking(track); + const child = this.nextConnection.targetBlock(); + if (child) { + child.setConnectionTracking(track); + } + } + + if (this.collapsed_) { + // When track is true, we don't want to start tracking collapsed + // connections. When track is false, we're already not tracking + // collapsed connections, so no need to update. + return; + } + + for (let i = 0; i < this.inputList.length; i++) { + const conn = this.inputList[i].connection as RenderedConnection; + if (conn) { + conn.setTracking(track); + + // Pass tracking on down the chain. + const block = conn.targetBlock(); + if (block) { + block.setConnectionTracking(track); + } + } + } + } + + /** + * Returns connections originating from this block. + * + * @param all If true, return all connections even hidden ones. + * Otherwise, for a collapsed block don't return inputs connections. + * @returns Array of connections. + * @internal + */ + override getConnections_(all: boolean): RenderedConnection[] { + const myConnections = []; + if (this.outputConnection) { + myConnections.push(this.outputConnection); + } + if (this.previousConnection) { + myConnections.push(this.previousConnection); + } + if (this.nextConnection) { + myConnections.push(this.nextConnection); + } + if (all || !this.collapsed_) { + for (let i = 0, input; (input = this.inputList[i]); i++) { + if (input.connection) { + myConnections.push(input.connection as RenderedConnection); + } + } + } + return myConnections; + } + + /** + * Walks down a stack of blocks and finds the last next connection on the + * stack. + * + * @param ignoreShadows If true,the last connection on a non-shadow block will + * be returned. If false, this will follow shadows to find the last + * connection. + * @returns The last next connection on the stack, or null. + * @internal + */ + override lastConnectionInStack( + ignoreShadows: boolean, + ): RenderedConnection | null { + return super.lastConnectionInStack(ignoreShadows) as RenderedConnection; + } + + /** + * Find the connection on this block that corresponds to the given connection + * on the other block. + * Used to match connections between a block and its insertion marker. + * + * @param otherBlock The other block to match against. + * @param conn The other connection to match. + * @returns The matching connection on this block, or null. + * @internal + */ + override getMatchingConnection( + otherBlock: Block, + conn: Connection, + ): RenderedConnection | null { + return super.getMatchingConnection(otherBlock, conn) as RenderedConnection; + } + + /** + * Create a connection of the specified type. + * + * @param type The type of the connection to create. + * @returns A new connection of the specified type. + * @internal + */ + override makeConnection_(type: ConnectionType): RenderedConnection { + return new RenderedConnection(this, type); + } + + /** + * Return the next statement block directly connected to this block. + * + * @returns The next statement block or null. + */ + override getNextBlock(): BlockSvg | null { + return super.getNextBlock() as BlockSvg; + } + + /** + * Returns the block connected to the previous connection. + * + * @returns The previous statement block or null. + */ + override getPreviousBlock(): BlockSvg | null { + return super.getPreviousBlock() as BlockSvg; + } + + /** + * Bumps unconnected blocks out of alignment. + * + * Two blocks which aren't actually connected should not coincidentally line + * up on screen, because that creates confusion for end-users. + */ + override bumpNeighbours() { + const root = this.getRootBlock(); + if ( + this.isDeadOrDying() || + this.workspace.isDragging() || + root.isInFlyout + ) { + return; + } + + function neighbourIsInStack(neighbour: RenderedConnection) { + return neighbour.getSourceBlock().getRootBlock() === root; + } + + for (const conn of this.getConnections_(false)) { + if (conn.isSuperior()) { + // Recurse down the block stack. + conn.targetBlock()?.bumpNeighbours(); + } + + for (const neighbour of conn.neighbours(config.snapRadius)) { + if (neighbourIsInStack(neighbour)) continue; + if (conn.isConnected() && neighbour.isConnected()) continue; + + if (conn.isSuperior()) { + neighbour.bumpAwayFrom(conn, /* initiatedByThis = */ false); + } else { + conn.bumpAwayFrom(neighbour, /* initiatedByThis = */ true); + } + } + } + } + + /** + * Snap to grid, and then bump neighbouring blocks away at the end of the next + * render. + */ + scheduleSnapAndBump() { + this.snapToGrid(); + this.bumpNeighbours(); + } + + /** + * Position a block so that it doesn't move the target block when connected. + * The block to position is usually either the first block in a dragged stack + * or an insertion marker. + * + * @param sourceConnection The connection on the moving block's stack. + * @param originalOffsetToTarget The connection original offset to the target connection + * @param originalOffsetInBlock The connection original offset in its block + * @internal + */ + positionNearConnection( + sourceConnection: RenderedConnection, + originalOffsetToTarget: {x: number; y: number}, + originalOffsetInBlock: Coordinate, + ) { + // We only need to position the new block if it's before the existing one, + // otherwise its position is set by the previous block. + if ( + sourceConnection.type === ConnectionType.NEXT_STATEMENT || + sourceConnection.type === ConnectionType.INPUT_VALUE + ) { + // First move the block to match the orginal target connection position + let dx = originalOffsetToTarget.x; + let dy = originalOffsetToTarget.y; + // Then adjust its position according to the connection resize + dx += originalOffsetInBlock.x - sourceConnection.getOffsetInBlock().x; + dy += originalOffsetInBlock.y - sourceConnection.getOffsetInBlock().y; + + this.moveBy(dx, dy); + } + } + + /** + * Find all the blocks that are directly nested inside this one. + * Includes value and statement inputs, as well as any following statement. + * Excludes any connection on an output tab or any preceding statement. + * Blocks are optionally sorted by position; top to bottom. + * + * @param ordered Sort the list if true. + * @returns Array of blocks. + */ + override getChildren(ordered: boolean): BlockSvg[] { + return super.getChildren(ordered) as BlockSvg[]; + } + + /** + * Triggers a rerender after a delay to allow for batching. + * + * @returns A promise that resolves after the currently queued renders have + * been completed. Used for triggering other behavior that relies on + * updated size/position location for the block. + * @internal + */ + queueRender(): Promise { + return renderManagement.queueRender(this); + } + + /** + * Immediately lays out and reflows a block based on its contents and + * settings. + */ + render() { + this.queueRender(); + renderManagement.triggerQueuedRenders(); + } + + /** + * Renders this block in a way that's compatible with the more efficient + * render management system. + * + * @internal + */ + renderEfficiently() { + dom.startTextWidthCache(); + + if (this.isCollapsed()) { + this.updateCollapsed(); + } + + if (!this.isEnabled()) { + this.updateDisabled(); + } + + this.workspace.getRenderer().render(this); + this.tightenChildrenEfficiently(); + + dom.stopTextWidthCache(); + this.updateMarkers_(); + } + + /** + * Tightens all children of this block so they are snuggly rendered against + * their parent connections. + * + * Does not update connection locations, so that they can be updated more + * efficiently by the render management system. + * + * @internal + */ + tightenChildrenEfficiently() { + for (const input of this.inputList) { + const conn = input.connection as RenderedConnection; + if (conn) conn.tightenEfficiently(); + } + if (this.nextConnection) this.nextConnection.tightenEfficiently(); + } + + /** Redraw any attached marker or cursor svgs if needed. */ + protected updateMarkers_() { + if (this.workspace.keyboardAccessibilityMode && this.pathObject.cursorSvg) { + this.workspace.getCursor()!.draw(); + } + if (this.workspace.keyboardAccessibilityMode && this.pathObject.markerSvg) { + // TODO(#4592): Update all markers on the block. + this.workspace.getMarker(MarkerManager.LOCAL_MARKER)!.draw(); + } + for (const input of this.inputList) { + for (const field of input.fieldRow) { + field.updateMarkers_(); + } + } + } + + /** + * Add the cursor SVG to this block's SVG group. + * + * @param cursorSvg The SVG root of the cursor to be added to the block SVG + * group. + * @internal + */ + setCursorSvg(cursorSvg: SVGElement) { + this.pathObject.setCursorSvg(cursorSvg); + } + + /** + * Add the marker SVG to this block's SVG group. + * + * @param markerSvg The SVG root of the marker to be added to the block SVG + * group. + * @internal + */ + setMarkerSvg(markerSvg: SVGElement) { + this.pathObject.setMarkerSvg(markerSvg); + } + + /** + * Returns a bounding box describing the dimensions of this block + * and any blocks stacked below it. + * + * @returns Object with height and width properties in workspace units. + * @internal + */ + getHeightWidth(): {height: number; width: number} { + let height = this.height; + let width = this.width; + // Recursively add size of subsequent blocks. + const nextBlock = this.getNextBlock(); + if (nextBlock) { + const nextHeightWidth = nextBlock.getHeightWidth(); + const tabHeight = this.workspace + .getRenderer() + .getConstants().NOTCH_HEIGHT; + height += nextHeightWidth.height - tabHeight; + width = Math.max(width, nextHeightWidth.width); + } + return {height, width}; + } + + /** + * Visual effect to show that if the dragging block is dropped, this block + * will be replaced. If a shadow block, it will disappear. Otherwise it will + * bump. + * + * @param add True if highlighting should be added. + * @internal + */ + fadeForReplacement(add: boolean) { + // TODO (7204): Remove these internal methods. + (this.pathObject as AnyDuringMigration).updateReplacementFade(add); + } + + /** + * Visual effect to show that if the dragging block is dropped it will connect + * to this input. + * + * @param conn The connection on the input to highlight. + * @param add True if highlighting should be added. + * @internal + */ + highlightShapeForInput(conn: RenderedConnection, add: boolean) { + // TODO (7204): Remove these internal methods. + (this.pathObject as AnyDuringMigration).updateShapeForInputHighlight( + conn, + add, + ); + } + + /** Sets the drag strategy for this block. */ + setDragStrategy(dragStrategy: IDragStrategy) { + this.dragStrategy = dragStrategy; + } + + /** Returns whether this block is movable or not. */ + override isMovable(): boolean { + return this.dragStrategy.isMovable(); + } + + /** Starts a drag on the block. */ + startDrag(e?: PointerEvent): void { + this.dragStrategy.startDrag(e); + } + + /** Drags the block to the given location. */ + drag(newLoc: Coordinate, e?: PointerEvent): void { + this.dragStrategy.drag(newLoc, e); + } + + /** Ends the drag on the block. */ + endDrag(e?: PointerEvent): void { + this.dragStrategy.endDrag(e); + } + + /** Moves the block back to where it was at the start of a drag. */ + revertDrag(): void { + this.dragStrategy.revertDrag(); + } + + /** + * Returns a representation of this block that can be displayed in a flyout. + */ + toFlyoutInfo(): FlyoutItemInfo[] { + const json: FlyoutItemInfo = { + kind: 'BLOCK', + ...blocks.save(this), + }; + + const toRemove = new Set(['id', 'height', 'width', 'pinned', 'enabled']); + + // Traverse the JSON recursively. + const traverseJson = function (json: {[key: string]: unknown}) { + for (const key in json) { + if (toRemove.has(key)) { + delete json[key]; + } else if (typeof json[key] === 'object') { + traverseJson(json[key] as {[key: string]: unknown}); + } + } + }; + + traverseJson(json as unknown as {[key: string]: unknown}); + return [json]; + } +} diff --git a/core/blockly.js b/core/blockly.js deleted file mode 100644 index 8b708340127..00000000000 --- a/core/blockly.js +++ /dev/null @@ -1,541 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2011 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Core JavaScript library for Blockly. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -/** - * The top level namespace used to access the Blockly library. - * @namespace Blockly - **/ -goog.provide('Blockly'); - -goog.require('Blockly.BlockSvg.render'); -goog.require('Blockly.Events'); -goog.require('Blockly.FieldAngle'); -goog.require('Blockly.FieldCheckbox'); -goog.require('Blockly.FieldColour'); -// Date picker commented out since it increases footprint by 60%. -// Add it only if you need it. -//goog.require('Blockly.FieldDate'); -goog.require('Blockly.FieldDropdown'); -goog.require('Blockly.FieldImage'); -goog.require('Blockly.FieldTextInput'); -goog.require('Blockly.FieldNumber'); -goog.require('Blockly.FieldVariable'); -goog.require('Blockly.Generator'); -goog.require('Blockly.Msg'); -goog.require('Blockly.Procedures'); -goog.require('Blockly.Toolbox'); -goog.require('Blockly.Touch'); -goog.require('Blockly.WidgetDiv'); -goog.require('Blockly.WorkspaceSvg'); -goog.require('Blockly.constants'); -goog.require('Blockly.inject'); -goog.require('Blockly.utils'); -goog.require('goog.color'); -goog.require('goog.userAgent'); - - -// Turn off debugging when compiled. -var CLOSURE_DEFINES = {'goog.DEBUG': false}; - -/** - * The main workspace most recently used. - * Set by Blockly.WorkspaceSvg.prototype.markFocused - * @type {Blockly.Workspace} - */ -Blockly.mainWorkspace = null; - -/** - * Currently selected block. - * @type {Blockly.Block} - */ -Blockly.selected = null; - -/** - * All of the connections on blocks that are currently being dragged. - * @type {!Array.} - * @private - */ -Blockly.draggingConnections_ = []; - -/** - * Contents of the local clipboard. - * @type {Element} - * @private - */ -Blockly.clipboardXml_ = null; - -/** - * Source of the local clipboard. - * @type {Blockly.WorkspaceSvg} - * @private - */ -Blockly.clipboardSource_ = null; - -/** - * Cached value for whether 3D is supported. - * @type {!boolean} - * @private - */ -Blockly.cache3dSupported_ = null; - -/** - * Convert a hue (HSV model) into an RGB hex triplet. - * @param {number} hue Hue on a colour wheel (0-360). - * @return {string} RGB code, e.g. '#5ba65b'. - */ -Blockly.hueToRgb = function(hue) { - return goog.color.hsvToHex(hue, Blockly.HSV_SATURATION, - Blockly.HSV_VALUE * 255); -}; - -/** - * Returns the dimensions of the specified SVG image. - * @param {!Element} svg SVG image. - * @return {!Object} Contains width and height properties. - */ -Blockly.svgSize = function(svg) { - return {width: svg.cachedWidth_, - height: svg.cachedHeight_}; -}; - -/** - * Size the workspace when the contents change. This also updates - * scrollbars accordingly. - * @param {!Blockly.WorkspaceSvg} workspace The workspace to resize. - */ -Blockly.resizeSvgContents = function(workspace) { - workspace.resizeContents(); -}; - -/** - * Size the SVG image to completely fill its container. Call this when the view - * actually changes sizes (e.g. on a window resize/device orientation change). - * See Blockly.resizeSvgContents to resize the workspace when the contents - * change (e.g. when a block is added or removed). - * Record the height/width of the SVG image. - * @param {!Blockly.WorkspaceSvg} workspace Any workspace in the SVG. - */ -Blockly.svgResize = function(workspace) { - var mainWorkspace = workspace; - while (mainWorkspace.options.parentWorkspace) { - mainWorkspace = mainWorkspace.options.parentWorkspace; - } - var svg = mainWorkspace.getParentSvg(); - var div = svg.parentNode; - if (!div) { - // Workspace deleted, or something. - return; - } - var width = div.offsetWidth; - var height = div.offsetHeight; - if (svg.cachedWidth_ != width) { - svg.setAttribute('width', width + 'px'); - svg.cachedWidth_ = width; - } - if (svg.cachedHeight_ != height) { - svg.setAttribute('height', height + 'px'); - svg.cachedHeight_ = height; - } - mainWorkspace.resize(); -}; - -/** - * Handle a key-down on SVG drawing surface. - * @param {!Event} e Key down event. - * @private - */ -Blockly.onKeyDown_ = function(e) { - if (Blockly.mainWorkspace.options.readOnly || Blockly.utils.isTargetInput(e)) { - // No key actions on readonly workspaces. - // When focused on an HTML text input widget, don't trap any keys. - return; - } - var deleteBlock = false; - if (e.keyCode == 27) { - // Pressing esc closes the context menu. - Blockly.hideChaff(); - } else if (e.keyCode == 8 || e.keyCode == 46) { - // Delete or backspace. - // Stop the browser from going back to the previous page. - // Do this first to prevent an error in the delete code from resulting in - // data loss. - e.preventDefault(); - // Don't delete while dragging. Jeez. - if (Blockly.mainWorkspace.isDragging()) { - return; - } - if (Blockly.selected && Blockly.selected.isDeletable()) { - deleteBlock = true; - } - } else if (e.altKey || e.ctrlKey || e.metaKey) { - // Don't use meta keys during drags. - if (Blockly.mainWorkspace.isDragging()) { - return; - } - if (Blockly.selected && - Blockly.selected.isDeletable() && Blockly.selected.isMovable()) { - if (e.keyCode == 67) { - // 'c' for copy. - Blockly.hideChaff(); - Blockly.copy_(Blockly.selected); - } else if (e.keyCode == 88) { - // 'x' for cut. - Blockly.copy_(Blockly.selected); - deleteBlock = true; - } - } - if (e.keyCode == 86) { - // 'v' for paste. - if (Blockly.clipboardXml_) { - Blockly.Events.setGroup(true); - Blockly.clipboardSource_.paste(Blockly.clipboardXml_); - Blockly.Events.setGroup(false); - } - } else if (e.keyCode == 90) { - // 'z' for undo 'Z' is for redo. - Blockly.hideChaff(); - Blockly.mainWorkspace.undo(e.shiftKey); - } - } - if (deleteBlock) { - // Common code for delete and cut. - Blockly.Events.setGroup(true); - Blockly.hideChaff(); - Blockly.selected.dispose(/* heal */ true, true); - Blockly.Events.setGroup(false); - } -}; - -/** - * Copy a block onto the local clipboard. - * @param {!Blockly.Block} block Block to be copied. - * @private - */ -Blockly.copy_ = function(block) { - var xmlBlock = Blockly.Xml.blockToDom(block); - // Copy only the selected block and internal blocks. - Blockly.Xml.deleteNext(xmlBlock); - // Encode start position in XML. - var xy = block.getRelativeToSurfaceXY(); - xmlBlock.setAttribute('x', block.RTL ? -xy.x : xy.x); - xmlBlock.setAttribute('y', xy.y); - Blockly.clipboardXml_ = xmlBlock; - Blockly.clipboardSource_ = block.workspace; -}; - -/** - * Duplicate this block and its children. - * @param {!Blockly.Block} block Block to be copied. - * @private - */ -Blockly.duplicate_ = function(block) { - // Save the clipboard. - var clipboardXml = Blockly.clipboardXml_; - var clipboardSource = Blockly.clipboardSource_; - - // Create a duplicate via a copy/paste operation. - Blockly.copy_(block); - block.workspace.paste(Blockly.clipboardXml_); - - // Restore the clipboard. - Blockly.clipboardXml_ = clipboardXml; - Blockly.clipboardSource_ = clipboardSource; -}; - -/** - * Cancel the native context menu, unless the focus is on an HTML input widget. - * @param {!Event} e Mouse down event. - * @private - */ -Blockly.onContextMenu_ = function(e) { - if (!Blockly.utils.isTargetInput(e)) { - // When focused on an HTML text input widget, don't cancel the context menu. - e.preventDefault(); - } -}; - -/** - * Close tooltips, context menus, dropdown selections, etc. - * @param {boolean=} opt_allowToolbox If true, don't close the toolbox. - */ -Blockly.hideChaff = function(opt_allowToolbox) { - Blockly.Tooltip.hide(); - Blockly.WidgetDiv.hide(); - if (!opt_allowToolbox) { - var workspace = Blockly.getMainWorkspace(); - if (workspace.toolbox_ && - workspace.toolbox_.flyout_ && - workspace.toolbox_.flyout_.autoClose) { - workspace.toolbox_.clearSelection(); - } - } -}; - -/** - * When something in Blockly's workspace changes, call a function. - * @param {!Function} func Function to call. - * @return {!Array.} Opaque data that can be passed to - * removeChangeListener. - * @deprecated April 2015 - */ -Blockly.addChangeListener = function(func) { - // Backwards compatibility from before there could be multiple workspaces. - console.warn('Deprecated call to Blockly.addChangeListener, ' + - 'use workspace.addChangeListener instead.'); - return Blockly.getMainWorkspace().addChangeListener(func); -}; - -/** - * Returns the main workspace. Returns the last used main workspace (based on - * focus). Try not to use this function, particularly if there are multiple - * Blockly instances on a page. - * @return {!Blockly.Workspace} The main workspace. - */ -Blockly.getMainWorkspace = function() { - return Blockly.mainWorkspace; -}; - -/** - * Wrapper to window.alert() that app developers may override to - * provide alternatives to the modal browser window. - * @param {string} message The message to display to the user. - * @param {function()=} opt_callback The callback when the alert is dismissed. - */ -Blockly.alert = function(message, opt_callback) { - window.alert(message); - if (opt_callback) { - opt_callback(); - } -}; - -/** - * Wrapper to window.confirm() that app developers may override to - * provide alternatives to the modal browser window. - * @param {string} message The message to display to the user. - * @param {!function(boolean)} callback The callback for handling user response. - */ -Blockly.confirm = function(message, callback) { - callback(window.confirm(message)); -}; - -/** - * Wrapper to window.prompt() that app developers may override to provide - * alternatives to the modal browser window. Built-in browser prompts are - * often used for better text input experience on mobile device. We strongly - * recommend testing mobile when overriding this. - * @param {string} message The message to display to the user. - * @param {string} defaultValue The value to initialize the prompt with. - * @param {!function(string)} callback The callback for handling user response. - */ -Blockly.prompt = function(message, defaultValue, callback) { - callback(window.prompt(message, defaultValue)); -}; - -/** - * Helper function for defining a block from JSON. The resulting function has - * the correct value of jsonDef at the point in code where jsonInit is called. - * @param {!Object} jsonDef The JSON definition of a block. - * @return {function()} A function that calls jsonInit with the correct value - * of jsonDef. - * @private - */ -Blockly.jsonInitFactory_ = function(jsonDef) { - return function() { - this.jsonInit(jsonDef); - }; -}; - -/** - * Define blocks from an array of JSON block definitions, as might be generated - * by the Blockly Developer Tools. - * @param {!Array.} jsonArray An array of JSON block definitions. - */ -Blockly.defineBlocksWithJsonArray = function(jsonArray) { - for (var i = 0, elem; elem = jsonArray[i]; i++) { - var typename = elem.type; - if (typename == null || typename === '') { - console.warn('Block definition #' + i + - ' in JSON array is missing a type attribute. Skipping.'); - } else { - if (Blockly.Blocks[typename]) { - console.warn('Block definition #' + i + - ' in JSON array overwrites prior definition of "' + typename + '".'); - } - Blockly.Blocks[typename] = { - init: Blockly.jsonInitFactory_(elem) - }; - } - } -}; - -/** - * Bind an event to a function call. When calling the function, verifies that - * it belongs to the touch stream that is currently being processed, and splits - * multitouch events into multiple events as needed. - * @param {!Node} node Node upon which to listen. - * @param {string} name Event name to listen to (e.g. 'mousedown'). - * @param {Object} thisObject The value of 'this' in the function. - * @param {!Function} func Function to call when event is triggered. - * @param {boolean} opt_noCaptureIdentifier True if triggering on this event - * should not block execution of other event handlers on this touch or other - * simultaneous touches. - * @return {!Array.} Opaque data that can be passed to unbindEvent_. - * @private - */ -Blockly.bindEventWithChecks_ = function(node, name, thisObject, func, - opt_noCaptureIdentifier) { - var handled = false; - var wrapFunc = function(e) { - var captureIdentifier = !opt_noCaptureIdentifier; - // Handle each touch point separately. If the event was a mouse event, this - // will hand back an array with one element, which we're fine handling. - var events = Blockly.Touch.splitEventByTouches(e); - for (var i = 0, event; event = events[i]; i++) { - if (captureIdentifier && !Blockly.Touch.shouldHandleEvent(event)) { - continue; - } - Blockly.Touch.setClientFromTouch(event); - if (thisObject) { - func.call(thisObject, event); - } else { - func(event); - } - handled = true; - } - }; - - node.addEventListener(name, wrapFunc, false); - var bindData = [[node, name, wrapFunc]]; - - // Add equivalent touch event. - if (name in Blockly.Touch.TOUCH_MAP) { - var touchWrapFunc = function(e) { - wrapFunc(e); - // Stop the browser from scrolling/zooming the page. - if (handled) { - e.preventDefault(); - } - }; - for (var i = 0, eventName; - eventName = Blockly.Touch.TOUCH_MAP[name][i]; i++) { - node.addEventListener(eventName, touchWrapFunc, false); - bindData.push([node, eventName, touchWrapFunc]); - } - } - return bindData; -}; - - -/** - * Bind an event to a function call. Handles multitouch events by using the - * coordinates of the first changed touch, and doesn't do any safety checks for - * simultaneous event processing. - * @deprecated in favor of bindEventWithChecks_, but preserved for external - * users. - * @param {!Node} node Node upon which to listen. - * @param {string} name Event name to listen to (e.g. 'mousedown'). - * @param {Object} thisObject The value of 'this' in the function. - * @param {!Function} func Function to call when event is triggered. - * @return {!Array.} Opaque data that can be passed to unbindEvent_. - * @private - */ -Blockly.bindEvent_ = function(node, name, thisObject, func) { - var wrapFunc = function(e) { - if (thisObject) { - func.call(thisObject, e); - } else { - func(e); - } - }; - - node.addEventListener(name, wrapFunc, false); - var bindData = [[node, name, wrapFunc]]; - - // Add equivalent touch event. - if (name in Blockly.Touch.TOUCH_MAP) { - var touchWrapFunc = function(e) { - // Punt on multitouch events. - if (e.changedTouches.length == 1) { - // Map the touch event's properties to the event. - var touchPoint = e.changedTouches[0]; - e.clientX = touchPoint.clientX; - e.clientY = touchPoint.clientY; - } - wrapFunc(e); - - // Stop the browser from scrolling/zooming the page. - e.preventDefault(); - }; - for (var i = 0, eventName; - eventName = Blockly.Touch.TOUCH_MAP[name][i]; i++) { - node.addEventListener(eventName, touchWrapFunc, false); - bindData.push([node, eventName, touchWrapFunc]); - } - } - return bindData; -}; - -/** - * Unbind one or more events event from a function call. - * @param {!Array.} bindData Opaque data from bindEvent_. - * This list is emptied during the course of calling this function. - * @return {!Function} The function call. - * @private - */ -Blockly.unbindEvent_ = function(bindData) { - while (bindData.length) { - var bindDatum = bindData.pop(); - var node = bindDatum[0]; - var name = bindDatum[1]; - var func = bindDatum[2]; - node.removeEventListener(name, func, false); - } - return func; -}; - -/** - * Is the given string a number (includes negative and decimals). - * @param {string} str Input string. - * @return {boolean} True if number, false otherwise. - */ -Blockly.isNumber = function(str) { - return !!str.match(/^\s*-?\d+(\.\d+)?\s*$/); -}; - -// IE9 does not have a console. Create a stub to stop errors. -if (!goog.global['console']) { - goog.global['console'] = { - 'log': function() {}, - 'warn': function() {} - }; -} - -// Export symbols that would otherwise be renamed by Closure compiler. -if (!goog.global['Blockly']) { - goog.global['Blockly'] = {}; -} -goog.global['Blockly']['getMainWorkspace'] = Blockly.getMainWorkspace; -goog.global['Blockly']['addChangeListener'] = Blockly.addChangeListener; diff --git a/core/blockly.ts b/core/blockly.ts new file mode 100644 index 00000000000..01490dbb694 --- /dev/null +++ b/core/blockly.ts @@ -0,0 +1,604 @@ +/** + * @license + * Copyright 2011 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly + +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_block_create.js'; +// Unused import preserved for side-effects. Remove if unneeded. +import './events/workspace_events.js'; +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_ui_base.js'; +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_var_create.js'; + +import {Block} from './block.js'; +import * as blockAnimations from './block_animations.js'; +import {BlockSvg} from './block_svg.js'; +import {BlocklyOptions} from './blockly_options.js'; +import {Blocks} from './blocks.js'; +import * as browserEvents from './browser_events.js'; +import * as bubbles from './bubbles.js'; +import {MiniWorkspaceBubble} from './bubbles/mini_workspace_bubble.js'; +import * as bumpObjects from './bump_objects.js'; +import * as clipboard from './clipboard.js'; +import * as comments from './comments.js'; +import * as common from './common.js'; +import {ComponentManager} from './component_manager.js'; +import {config} from './config.js'; +import {Connection} from './connection.js'; +import {ConnectionChecker} from './connection_checker.js'; +import {ConnectionDB} from './connection_db.js'; +import {ConnectionType} from './connection_type.js'; +import * as constants from './constants.js'; +import * as ContextMenu from './contextmenu.js'; +import * as ContextMenuItems from './contextmenu_items.js'; +import {ContextMenuRegistry} from './contextmenu_registry.js'; +import * as Css from './css.js'; +import {DeleteArea} from './delete_area.js'; +import * as dialog from './dialog.js'; +import {DragTarget} from './drag_target.js'; +import * as dragging from './dragging.js'; +import * as dropDownDiv from './dropdowndiv.js'; +import * as Events from './events/events.js'; +import * as Extensions from './extensions.js'; +import { + Field, + FieldConfig, + FieldValidator, + UnattachedFieldError, +} from './field.js'; +import { + FieldCheckbox, + FieldCheckboxConfig, + FieldCheckboxFromJsonConfig, + FieldCheckboxValidator, +} from './field_checkbox.js'; +import { + FieldDropdown, + FieldDropdownConfig, + FieldDropdownFromJsonConfig, + FieldDropdownValidator, + MenuGenerator, + MenuGeneratorFunction, + MenuOption, +} from './field_dropdown.js'; +import { + FieldImage, + FieldImageConfig, + FieldImageFromJsonConfig, +} from './field_image.js'; +import { + FieldLabel, + FieldLabelConfig, + FieldLabelFromJsonConfig, +} from './field_label.js'; +import {FieldLabelSerializable} from './field_label_serializable.js'; +import { + FieldNumber, + FieldNumberConfig, + FieldNumberFromJsonConfig, + FieldNumberValidator, +} from './field_number.js'; +import * as fieldRegistry from './field_registry.js'; +import { + FieldTextInput, + FieldTextInputConfig, + FieldTextInputFromJsonConfig, + FieldTextInputValidator, +} from './field_textinput.js'; +import { + FieldVariable, + FieldVariableConfig, + FieldVariableFromJsonConfig, + FieldVariableValidator, +} from './field_variable.js'; +import {Flyout} from './flyout_base.js'; +import {FlyoutButton} from './flyout_button.js'; +import {HorizontalFlyout} from './flyout_horizontal.js'; +import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; +import {VerticalFlyout} from './flyout_vertical.js'; +import {CodeGenerator} from './generator.js'; +import {Gesture} from './gesture.js'; +import {Grid} from './grid.js'; +import * as icons from './icons.js'; +import {inject} from './inject.js'; +import * as inputs from './inputs.js'; +import {Input} from './inputs/input.js'; +import {InsertionMarkerManager} from './insertion_marker_manager.js'; +import {InsertionMarkerPreviewer} from './insertion_marker_previewer.js'; +import {IASTNodeLocation} from './interfaces/i_ast_node_location.js'; +import {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; +import {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js'; +import {IAutoHideable} from './interfaces/i_autohideable.js'; +import {IBoundedElement} from './interfaces/i_bounded_element.js'; +import {IBubble} from './interfaces/i_bubble.js'; +import {ICollapsibleToolboxItem} from './interfaces/i_collapsible_toolbox_item.js'; +import {IComponent} from './interfaces/i_component.js'; +import {IConnectionChecker} from './interfaces/i_connection_checker.js'; +import {IConnectionPreviewer} from './interfaces/i_connection_previewer.js'; +import {IContextMenu} from './interfaces/i_contextmenu.js'; +import {ICopyData, ICopyable, isCopyable} from './interfaces/i_copyable.js'; +import {IDeletable, isDeletable} from './interfaces/i_deletable.js'; +import {IDeleteArea} from './interfaces/i_delete_area.js'; +import {IDragTarget} from './interfaces/i_drag_target.js'; +import { + IDragStrategy, + IDraggable, + isDraggable, +} from './interfaces/i_draggable.js'; +import {IDragger} from './interfaces/i_dragger.js'; +import {IFlyout} from './interfaces/i_flyout.js'; +import {IHasBubble, hasBubble} from './interfaces/i_has_bubble.js'; +import {IIcon, isIcon} from './interfaces/i_icon.js'; +import {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js'; +import {IMetricsManager} from './interfaces/i_metrics_manager.js'; +import {IMovable} from './interfaces/i_movable.js'; +import {IObservable, isObservable} from './interfaces/i_observable.js'; +import {IPaster, isPaster} from './interfaces/i_paster.js'; +import {IPositionable} from './interfaces/i_positionable.js'; +import {IRegistrable} from './interfaces/i_registrable.js'; +import { + IRenderedElement, + isRenderedElement, +} from './interfaces/i_rendered_element.js'; +import {ISelectable, isSelectable} from './interfaces/i_selectable.js'; +import {ISelectableToolboxItem} from './interfaces/i_selectable_toolbox_item.js'; +import {ISerializable, isSerializable} from './interfaces/i_serializable.js'; +import {IStyleable} from './interfaces/i_styleable.js'; +import {IToolbox} from './interfaces/i_toolbox.js'; +import {IToolboxItem} from './interfaces/i_toolbox_item.js'; +import { + IVariableBackedParameterModel, + isVariableBackedParameterModel, +} from './interfaces/i_variable_backed_parameter_model.js'; +import * as internalConstants from './internal_constants.js'; +import {ASTNode} from './keyboard_nav/ast_node.js'; +import {BasicCursor} from './keyboard_nav/basic_cursor.js'; +import {Cursor} from './keyboard_nav/cursor.js'; +import {Marker} from './keyboard_nav/marker.js'; +import {TabNavigateCursor} from './keyboard_nav/tab_navigate_cursor.js'; +import type {LayerManager} from './layer_manager.js'; +import * as layers from './layers.js'; +import {MarkerManager} from './marker_manager.js'; +import {Menu} from './menu.js'; +import {MenuItem} from './menuitem.js'; +import {MetricsManager} from './metrics_manager.js'; +import {Msg, setLocale} from './msg.js'; +import {Names} from './names.js'; +import {Options} from './options.js'; +import * as uiPosition from './positionable_helpers.js'; +import * as Procedures from './procedures.js'; +import * as registry from './registry.js'; +import * as renderManagement from './render_management.js'; +import {RenderedConnection} from './rendered_connection.js'; +import * as blockRendering from './renderers/common/block_rendering.js'; +import * as geras from './renderers/geras/geras.js'; +import * as thrasos from './renderers/thrasos/thrasos.js'; +import * as zelos from './renderers/zelos/zelos.js'; +import {Scrollbar} from './scrollbar.js'; +import {ScrollbarPair} from './scrollbar_pair.js'; +import * as serialization from './serialization.js'; +import * as ShortcutItems from './shortcut_items.js'; +import {ShortcutRegistry} from './shortcut_registry.js'; +import {Theme} from './theme.js'; +import * as Themes from './theme/themes.js'; +import {ThemeManager} from './theme_manager.js'; +import {ToolboxCategory} from './toolbox/category.js'; +import {CollapsibleToolboxCategory} from './toolbox/collapsible_category.js'; +import {ToolboxSeparator} from './toolbox/separator.js'; +import {Toolbox} from './toolbox/toolbox.js'; +import {ToolboxItem} from './toolbox/toolbox_item.js'; +import * as Tooltip from './tooltip.js'; +import * as Touch from './touch.js'; +import {Trashcan} from './trashcan.js'; +import * as utils from './utils.js'; +import * as toolbox from './utils/toolbox.js'; +import {VariableMap} from './variable_map.js'; +import {VariableModel} from './variable_model.js'; +import * as Variables from './variables.js'; +import * as VariablesDynamic from './variables_dynamic.js'; +import * as WidgetDiv from './widgetdiv.js'; +import {Workspace} from './workspace.js'; +import {WorkspaceAudio} from './workspace_audio.js'; +import {WorkspaceDragger} from './workspace_dragger.js'; +import {WorkspaceSvg} from './workspace_svg.js'; +import * as Xml from './xml.js'; +import {ZoomControls} from './zoom_controls.js'; + +/** + * Blockly core version. + * This constant is overridden by the build script (npm run build) to the value + * of the version in package.json. This is done by the Closure Compiler in the + * buildCompressed gulp task. + * For local builds, you can pass --define='Blockly.VERSION=X.Y.Z' to the + * compiler to override this constant. + * + * @define {string} + */ +export const VERSION = 'uncompiled'; + +/* + * Top-level functions and properties on the Blockly namespace. + * These are used only in external code. Do not reference these + * from internal code as importing from this file can cause circular + * dependencies. Do not add new functions here. There is probably a better + * namespace to put new functions on. + */ + +/* + * Aliases for constants used for connection and input types. + */ + +/** + * @see ConnectionType.INPUT_VALUE + */ +export const INPUT_VALUE = ConnectionType.INPUT_VALUE; + +/** + * @see ConnectionType.OUTPUT_VALUE + */ +export const OUTPUT_VALUE = ConnectionType.OUTPUT_VALUE; + +/** + * @see ConnectionType.NEXT_STATEMENT + */ +export const NEXT_STATEMENT = ConnectionType.NEXT_STATEMENT; + +/** + * @see ConnectionType.PREVIOUS_STATEMENT + */ +export const PREVIOUS_STATEMENT = ConnectionType.PREVIOUS_STATEMENT; + +/** Aliases for toolbox positions. */ + +/** + * @see toolbox.Position.TOP + */ +export const TOOLBOX_AT_TOP = toolbox.Position.TOP; + +/** + * @see toolbox.Position.BOTTOM + */ +export const TOOLBOX_AT_BOTTOM = toolbox.Position.BOTTOM; + +/** + * @see toolbox.Position.LEFT + */ +export const TOOLBOX_AT_LEFT = toolbox.Position.LEFT; + +/** + * @see toolbox.Position.RIGHT + */ +export const TOOLBOX_AT_RIGHT = toolbox.Position.RIGHT; + +/* + * Other aliased functions. + */ + +/** + * Size the SVG image to completely fill its container. Call this when the view + * actually changes sizes (e.g. on a window resize/device orientation change). + * See workspace.resizeContents to resize the workspace when the contents + * change (e.g. when a block is added or removed). + * Record the height/width of the SVG image. + * + * @param workspace Any workspace in the SVG. + * @see Blockly.common.svgResize + */ +export const svgResize = common.svgResize; + +/** + * Close tooltips, context menus, dropdown selections, etc. + * + * @param opt_onlyClosePopups Whether only popups should be closed. + * @see Blockly.WorkspaceSvg.hideChaff + */ +export function hideChaff(opt_onlyClosePopups?: boolean) { + (common.getMainWorkspace() as WorkspaceSvg).hideChaff(opt_onlyClosePopups); +} + +/** + * Returns the main workspace. Returns the last used main workspace (based on + * focus). Try not to use this function, particularly if there are multiple + * Blockly instances on a page. + * + * @see Blockly.common.getMainWorkspace + */ +export const getMainWorkspace = common.getMainWorkspace; + +/** + * Returns the currently selected copyable object. + */ +export const getSelected = common.getSelected; + +/** + * Define blocks from an array of JSON block definitions, as might be generated + * by the Blockly Developer Tools. + * + * @param jsonArray An array of JSON block definitions. + * @see Blockly.common.defineBlocksWithJsonArray + */ +export const defineBlocksWithJsonArray = common.defineBlocksWithJsonArray; + +/** + * Set the parent container. This is the container element that the WidgetDiv, + * dropDownDiv, and Tooltip are rendered into the first time `Blockly.inject` + * is called. + * This method is a NOP if called after the first `Blockly.inject`. + * + * @param container The container element. + * @see Blockly.common.setParentContainer + */ +export const setParentContainer = common.setParentContainer; + +// Aliases to allow external code to access these values for legacy reasons. +export const COLLAPSE_CHARS = internalConstants.COLLAPSE_CHARS; +export const OPPOSITE_TYPE = internalConstants.OPPOSITE_TYPE; +export const RENAME_VARIABLE_ID = internalConstants.RENAME_VARIABLE_ID; +export const DELETE_VARIABLE_ID = internalConstants.DELETE_VARIABLE_ID; +export const COLLAPSED_INPUT_NAME = constants.COLLAPSED_INPUT_NAME; +export const COLLAPSED_FIELD_NAME = constants.COLLAPSED_FIELD_NAME; + +/** + * String for use in the "custom" attribute of a category in toolbox XML. + * This string indicates that the category should be dynamically populated with + * variable blocks. + */ +export const VARIABLE_CATEGORY_NAME: string = Variables.CATEGORY_NAME; + +/** + * String for use in the "custom" attribute of a category in toolbox XML. + * This string indicates that the category should be dynamically populated with + * variable blocks. + */ +export const VARIABLE_DYNAMIC_CATEGORY_NAME: string = + VariablesDynamic.CATEGORY_NAME; +/** + * String for use in the "custom" attribute of a category in toolbox XML. + * This string indicates that the category should be dynamically populated with + * procedure blocks. + */ +export const PROCEDURE_CATEGORY_NAME: string = Procedures.CATEGORY_NAME; + +// Context for why we need to monkey-patch in these functions (internal): +// https://docs.google.com/document/d/1MbO0LEA-pAyx1ErGLJnyUqTLrcYTo-5zga9qplnxeXo/edit?usp=sharing&resourcekey=0-5h_32-i-dHwHjf_9KYEVKg + +// clang-format off +Workspace.prototype.newBlock = function ( + prototypeName: string, + opt_id?: string, +): Block { + return new Block(this, prototypeName, opt_id); +}; + +WorkspaceSvg.prototype.newBlock = function ( + prototypeName: string, + opt_id?: string, +): BlockSvg { + return new BlockSvg(this, prototypeName, opt_id); +}; + +Workspace.prototype.newComment = function ( + id?: string, +): comments.WorkspaceComment { + return new comments.WorkspaceComment(this, id); +}; + +WorkspaceSvg.prototype.newComment = function ( + id?: string, +): comments.RenderedWorkspaceComment { + return new comments.RenderedWorkspaceComment(this, id); +}; + +WorkspaceSvg.newTrashcan = function (workspace: WorkspaceSvg): Trashcan { + return new Trashcan(workspace); +}; + +MiniWorkspaceBubble.prototype.newWorkspaceSvg = function ( + options: Options, +): WorkspaceSvg { + return new WorkspaceSvg(options); +}; + +Names.prototype.populateProcedures = function ( + this: Names, + workspace: Workspace, +) { + const procedures = Procedures.allProcedures(workspace); + // Flatten the return vs no-return procedure lists. + const flattenedProcedures = procedures[0].concat(procedures[1]); + for (let i = 0; i < flattenedProcedures.length; i++) { + this.getName(flattenedProcedures[i][0], Names.NameType.PROCEDURE); + } +}; +// clang-format on + +// Re-export submodules that no longer declareLegacyNamespace. +export { + ASTNode, + BasicCursor, + Block, + BlockSvg, + BlocklyOptions, + Blocks, + CollapsibleToolboxCategory, + ComponentManager, + Connection, + ConnectionChecker, + ConnectionDB, + ConnectionType, + ContextMenu, + ContextMenuItems, + ContextMenuRegistry, + Css, + Cursor, + DeleteArea, + DragTarget, + Events, + Extensions, + Procedures, + ShortcutItems, + Themes, + Tooltip, + Touch, + Variables, + VariablesDynamic, + WidgetDiv, + Xml, + blockAnimations, + blockRendering, + browserEvents, + bubbles, + bumpObjects, + clipboard, + comments, + common, + constants, + dialog, + dragging, + fieldRegistry, + geras, + Procedures as procedures, + registry, + thrasos, + uiPosition, + utils, + zelos, +}; +export const DropDownDiv = dropDownDiv; +export { + CodeGenerator, + Field, + FieldCheckbox, + FieldCheckboxConfig, + FieldCheckboxFromJsonConfig, + FieldCheckboxValidator, + FieldConfig, + FieldDropdown, + FieldDropdownConfig, + FieldDropdownFromJsonConfig, + FieldDropdownValidator, + FieldImage, + FieldImageConfig, + FieldImageFromJsonConfig, + FieldLabel, + FieldLabelConfig, + FieldLabelFromJsonConfig, + FieldLabelSerializable, + FieldNumber, + FieldNumberConfig, + FieldNumberFromJsonConfig, + FieldNumberValidator, + FieldTextInput, + FieldTextInputConfig, + FieldTextInputFromJsonConfig, + FieldTextInputValidator, + FieldValidator, + FieldVariable, + FieldVariableConfig, + FieldVariableFromJsonConfig, + FieldVariableValidator, + Flyout, + FlyoutButton, + FlyoutMetricsManager, + CodeGenerator as Generator, + Gesture, + Grid, + HorizontalFlyout, + IASTNodeLocation, + IASTNodeLocationSvg, + IASTNodeLocationWithBlock, + IAutoHideable, + IBoundedElement, + IBubble, + ICollapsibleToolboxItem, + IComponent, + IConnectionChecker, + IConnectionPreviewer, + IContextMenu, + ICopyData, + ICopyable, + IDeletable, + IDeleteArea, + IDragStrategy, + IDragTarget, + IDraggable, + IDragger, + IFlyout, + IHasBubble, + IIcon, + IKeyboardAccessible, + IMetricsManager, + IMovable, + IObservable, + IPaster, + IPositionable, + IRegistrable, + IRenderedElement, + ISelectable, + ISelectableToolboxItem, + ISerializable, + IStyleable, + IToolbox, + IToolboxItem, + IVariableBackedParameterModel, + Input, + InsertionMarkerManager, + InsertionMarkerPreviewer, + LayerManager, + Marker, + MarkerManager, + Menu, + MenuGenerator, + MenuGeneratorFunction, + MenuItem, + MenuOption, + MetricsManager, + Msg, + Names, + Options, + RenderedConnection, + Scrollbar, + ScrollbarPair, + ShortcutRegistry, + TabNavigateCursor, + Theme, + ThemeManager, + Toolbox, + ToolboxCategory, + ToolboxItem, + ToolboxSeparator, + Trashcan, + UnattachedFieldError, + VariableMap, + VariableModel, + VerticalFlyout, + Workspace, + WorkspaceAudio, + WorkspaceDragger, + WorkspaceSvg, + ZoomControls, + config, + hasBubble, + icons, + inject, + inputs, + isCopyable, + isDeletable, + isDraggable, + isIcon, + isObservable, + isPaster, + isRenderedElement, + isSelectable, + isSerializable, + isVariableBackedParameterModel, + layers, + renderManagement, + serialization, + setLocale, +}; diff --git a/core/blockly_options.ts b/core/blockly_options.ts new file mode 100644 index 00000000000..dd18dbfee5d --- /dev/null +++ b/core/blockly_options.ts @@ -0,0 +1,71 @@ +/** + * @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.BlocklyOptions + +import type {ITheme, Theme} from './theme.js'; +import type {ToolboxDefinition} from './utils/toolbox.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Blockly options. + */ +export interface BlocklyOptions { + collapse?: boolean; + comments?: boolean; + css?: boolean; + disable?: boolean; + grid?: GridOptions; + horizontalLayout?: boolean; + maxBlocks?: number; + maxInstances?: {[blockType: string]: number}; + media?: string; + modalInputs?: boolean; + move?: MoveOptions; + oneBasedIndex?: boolean; + readOnly?: boolean; + renderer?: string; + rendererOverrides?: {[rendererConstant: string]: any}; + rtl?: boolean; + scrollbars?: ScrollbarOptions | boolean; + sounds?: boolean; + theme?: Theme | string | ITheme; + toolbox?: string | ToolboxDefinition | Element; + toolboxPosition?: string; + trashcan?: boolean; + maxTrashcanContents?: number; + plugins?: {[key: string]: (new (...p1: any[]) => any) | string}; + zoom?: ZoomOptions; + parentWorkspace?: WorkspaceSvg; +} + +export interface GridOptions { + colour?: string; + length?: number; + snap?: boolean; + spacing?: number; +} + +export interface MoveOptions { + drag?: boolean; + scrollbars?: boolean | ScrollbarOptions; + wheel?: boolean; +} + +export interface ScrollbarOptions { + horizontal?: boolean; + vertical?: boolean; +} + +export interface ZoomOptions { + controls?: boolean; + maxScale?: number; + minScale?: number; + pinch?: boolean; + scaleSpeed?: number; + startScale?: number; + wheel?: boolean; +} diff --git a/core/blocks.js b/core/blocks.js deleted file mode 100644 index 5b78050aa7f..00000000000 --- a/core/blocks.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2013 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 A mapping of block type names to block prototype objects. - * @author spertus@google.com (Ellen Spertus) - */ -'use strict'; - -/** - * A mapping of block type names to block prototype objects. - * @name Blockly.Blocks - */ -goog.provide('Blockly.Blocks'); - -/* - * A mapping of block type names to block prototype objects. - * @type {!Object} - */ -Blockly.Blocks = new Object(null); diff --git a/core/blocks.ts b/core/blocks.ts new file mode 100644 index 00000000000..33bac110b04 --- /dev/null +++ b/core/blocks.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2013 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.blocks + +/** + * A block definition. For now this very loose, but it can potentially + * be refined e.g. by replacing this typedef with a class definition. + */ +export type BlockDefinition = AnyDuringMigration; + +/** + * A mapping of block type names to block prototype objects. + */ +export const Blocks: {[key: string]: BlockDefinition} = Object.create(null); diff --git a/core/browser_events.ts b/core/browser_events.ts new file mode 100644 index 00000000000..8176fe10ff3 --- /dev/null +++ b/core/browser_events.ts @@ -0,0 +1,248 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.browserEvents + +// Theoretically we could figure out a way to type the event params correctly, +// but it's not high priority. +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ + +import * as Touch from './touch.js'; +import * as userAgent from './utils/useragent.js'; + +/** + * Blockly opaque event data used to unbind events when using + * `bind` and `conditionalBind`. + */ +export type Data = [EventTarget, string, (e: Event) => void][]; + +/** + * The multiplier for scroll wheel deltas using the line delta mode. + * See https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode + * for more information on deltaMode. + */ +const LINE_MODE_MULTIPLIER = 40; + +/** + * The multiplier for scroll wheel deltas using the page delta mode. + * See https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode + * for more information on deltaMode. + */ +const PAGE_MODE_MULTIPLIER = 125; + +/** + * Bind an event handler that can be ignored if it is not part of the active + * touch stream. + * Use this for events that either start or continue a multi-part gesture (e.g. + * mousedown or mousemove, which may be part of a drag or click). + * + * @param node Node upon which to listen. + * @param name Event name to listen to (e.g. 'mousedown'). + * @param thisObject The value of 'this' in the function. + * @param func Function to call when event is triggered. + * @param opt_noCaptureIdentifier True if triggering on this event should not + * block execution of other event handlers on this touch or other + * simultaneous touches. False by default. + * @returns Opaque data that can be passed to unbindEvent_. + */ +export function conditionalBind( + node: EventTarget, + name: string, + thisObject: object | null, + func: Function, + opt_noCaptureIdentifier?: boolean, +): Data { + /** + * + * @param e + */ + function wrapFunc(e: Event) { + const captureIdentifier = !opt_noCaptureIdentifier; + + if (!(captureIdentifier && !Touch.shouldHandleEvent(e))) { + if (thisObject) { + func.call(thisObject, e); + } else { + func(e); + } + } + } + + const bindData: Data = []; + if (name in Touch.TOUCH_MAP) { + for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) { + const type = Touch.TOUCH_MAP[name][i]; + node.addEventListener(type, wrapFunc, false); + bindData.push([node, type, wrapFunc]); + } + } else { + node.addEventListener(name, wrapFunc, false); + bindData.push([node, name, wrapFunc]); + } + return bindData; +} + +/** + * Bind an event handler that should be called regardless of whether it is part + * of the active touch stream. + * Use this for events that are not part of a multi-part gesture (e.g. + * mouseover for tooltips). + * + * @param node Node upon which to listen. + * @param name Event name to listen to (e.g. 'mousedown'). + * @param thisObject The value of 'this' in the function. + * @param func Function to call when event is triggered. + * @returns Opaque data that can be passed to unbindEvent_. + */ +export function bind( + node: EventTarget, + name: string, + thisObject: object | null, + func: Function, +): Data { + /** + * + * @param e + */ + function wrapFunc(e: Event) { + if (thisObject) { + func.call(thisObject, e); + } else { + func(e); + } + } + + const bindData: Data = []; + if (name in Touch.TOUCH_MAP) { + for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) { + const type = Touch.TOUCH_MAP[name][i]; + node.addEventListener(type, wrapFunc, false); + bindData.push([node, type, wrapFunc]); + } + } else { + node.addEventListener(name, wrapFunc, false); + bindData.push([node, name, wrapFunc]); + } + return bindData; +} + +/** + * Unbind one or more events event from a function call. + * + * @param bindData Opaque data from bindEvent_. + * This list is emptied during the course of calling this function. + * @returns The function call. + */ +export function unbind(bindData: Data): (e: Event) => void { + // Accessing an element of the last property of the array is unsafe if the + // bindData is an empty array. But that should never happen because developers + // should only pass Data from bind or conditionalBind. + const callback = bindData[bindData.length - 1][2]; + while (bindData.length) { + const [node, name, func] = bindData.pop()!; + node.removeEventListener(name, func, false); + } + return callback; +} + +/** + * Returns true if this event is targeting a text input widget? + * + * @param e An event. + * @returns True if text input. + */ +export function isTargetInput(e: Event): boolean { + if (e.target instanceof HTMLElement) { + if ( + e.target.isContentEditable || + e.target.getAttribute('data-is-text-input') === 'true' + ) { + return true; + } + + if (e.target instanceof HTMLInputElement) { + const target = e.target; + return ( + target.type === 'text' || + target.type === 'number' || + target.type === 'email' || + target.type === 'password' || + target.type === 'search' || + target.type === 'tel' || + target.type === 'url' + ); + } + + if (e.target instanceof HTMLTextAreaElement) { + return true; + } + } + + return false; +} + +/** + * Returns true this event is a right-click. + * + * @param e Mouse event. + * @returns True if right-click. + */ +export function isRightButton(e: MouseEvent): boolean { + if (e.ctrlKey && userAgent.MAC) { + // Control-clicking on Mac OS X is treated as a right-click. + // WebKit on Mac OS X fails to change button to 2 (but Gecko does). + return true; + } + return e.button === 2; +} + +/** + * Returns the converted coordinates of the given mouse event. + * The origin (0,0) is the top-left corner of the Blockly SVG. + * + * @param e Mouse event. + * @param svg SVG element. + * @param matrix Inverted screen CTM to use. + * @returns Object with .x and .y properties. + */ +export function mouseToSvg( + e: MouseEvent, + svg: SVGSVGElement, + matrix: SVGMatrix | null, +): SVGPoint { + const svgPoint = svg.createSVGPoint(); + svgPoint.x = e.clientX; + svgPoint.y = e.clientY; + + if (!matrix) { + matrix = svg.getScreenCTM()!.inverse(); + } + return svgPoint.matrixTransform(matrix); +} + +/** + * Returns the scroll delta of a mouse event in pixel units. + * + * @param e Mouse event. + * @returns Scroll delta object with .x and .y properties. + */ +export function getScrollDeltaPixels(e: WheelEvent): {x: number; y: number} { + switch (e.deltaMode) { + case 0x00: // Pixel mode. + default: + return {x: e.deltaX, y: e.deltaY}; + case 0x01: // Line mode. + return { + x: e.deltaX * LINE_MODE_MULTIPLIER, + y: e.deltaY * LINE_MODE_MULTIPLIER, + }; + case 0x02: // Page mode. + return { + x: e.deltaX * PAGE_MODE_MULTIPLIER, + y: e.deltaY * PAGE_MODE_MULTIPLIER, + }; + } +} diff --git a/core/bubble.js b/core/bubble.js deleted file mode 100644 index 1cd9443fd6a..00000000000 --- a/core/bubble.js +++ /dev/null @@ -1,586 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Object representing a UI bubble. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Bubble'); - -goog.require('Blockly.Touch'); -goog.require('Blockly.Workspace'); -goog.require('goog.dom'); -goog.require('goog.math'); -goog.require('goog.math.Coordinate'); -goog.require('goog.userAgent'); - - -/** - * Class for UI bubble. - * @param {!Blockly.WorkspaceSvg} workspace The workspace on which to draw the - * bubble. - * @param {!Element} content SVG content for the bubble. - * @param {Element} shape SVG element to avoid eclipsing. - * @param {!goog.math.Coodinate} anchorXY Absolute position of bubble's anchor - * point. - * @param {?number} bubbleWidth Width of bubble, or null if not resizable. - * @param {?number} bubbleHeight Height of bubble, or null if not resizable. - * @constructor - */ -Blockly.Bubble = function(workspace, content, shape, anchorXY, - bubbleWidth, bubbleHeight) { - this.workspace_ = workspace; - this.content_ = content; - this.shape_ = shape; - - var angle = Blockly.Bubble.ARROW_ANGLE; - if (this.workspace_.RTL) { - angle = -angle; - } - this.arrow_radians_ = goog.math.toRadians(angle); - - var canvas = workspace.getBubbleCanvas(); - canvas.appendChild(this.createDom_(content, !!(bubbleWidth && bubbleHeight))); - - this.setAnchorLocation(anchorXY); - if (!bubbleWidth || !bubbleHeight) { - var bBox = /** @type {SVGLocatable} */ (this.content_).getBBox(); - bubbleWidth = bBox.width + 2 * Blockly.Bubble.BORDER_WIDTH; - bubbleHeight = bBox.height + 2 * Blockly.Bubble.BORDER_WIDTH; - } - this.setBubbleSize(bubbleWidth, bubbleHeight); - - // Render the bubble. - this.positionBubble_(); - this.renderArrow_(); - this.rendered_ = true; - - if (!workspace.options.readOnly) { - Blockly.bindEventWithChecks_(this.bubbleBack_, 'mousedown', this, - this.bubbleMouseDown_); - if (this.resizeGroup_) { - Blockly.bindEventWithChecks_(this.resizeGroup_, 'mousedown', this, - this.resizeMouseDown_); - } - } -}; - -/** - * Width of the border around the bubble. - */ -Blockly.Bubble.BORDER_WIDTH = 6; - -/** - * Determines the thickness of the base of the arrow in relation to the size - * of the bubble. Higher numbers result in thinner arrows. - */ -Blockly.Bubble.ARROW_THICKNESS = 5; - -/** - * The number of degrees that the arrow bends counter-clockwise. - */ -Blockly.Bubble.ARROW_ANGLE = 20; - -/** - * The sharpness of the arrow's bend. Higher numbers result in smoother arrows. - */ -Blockly.Bubble.ARROW_BEND = 4; - -/** - * Distance between arrow point and anchor point. - */ -Blockly.Bubble.ANCHOR_RADIUS = 8; - -/** - * Wrapper function called when a mouseUp occurs during a drag operation. - * @type {Array.} - * @private - */ -Blockly.Bubble.onMouseUpWrapper_ = null; - -/** - * Wrapper function called when a mouseMove occurs during a drag operation. - * @type {Array.} - * @private - */ -Blockly.Bubble.onMouseMoveWrapper_ = null; - -/** - * Function to call on resize of bubble. - * @type {Function} - */ -Blockly.Bubble.prototype.resizeCallback_ = null; - -/** - * Stop binding to the global mouseup and mousemove events. - * @private - */ -Blockly.Bubble.unbindDragEvents_ = function() { - if (Blockly.Bubble.onMouseUpWrapper_) { - Blockly.unbindEvent_(Blockly.Bubble.onMouseUpWrapper_); - Blockly.Bubble.onMouseUpWrapper_ = null; - } - if (Blockly.Bubble.onMouseMoveWrapper_) { - Blockly.unbindEvent_(Blockly.Bubble.onMouseMoveWrapper_); - Blockly.Bubble.onMouseMoveWrapper_ = null; - } -}; - -/* - * Handle a mouse-up event while dragging a bubble's border or resize handle. - * @param {!Event} e Mouse up event. - * @private - */ -Blockly.Bubble.bubbleMouseUp_ = function(/*e*/) { - Blockly.Touch.clearTouchIdentifier(); - Blockly.Bubble.unbindDragEvents_(); -}; - -/** - * Flag to stop incremental rendering during construction. - * @private - */ -Blockly.Bubble.prototype.rendered_ = false; - -/** - * Absolute coordinate of anchor point. - * @type {goog.math.Coordinate} - * @private - */ -Blockly.Bubble.prototype.anchorXY_ = null; - -/** - * Relative X coordinate of bubble with respect to the anchor's centre. - * In RTL mode the initial value is negated. - * @private - */ -Blockly.Bubble.prototype.relativeLeft_ = 0; - -/** - * Relative Y coordinate of bubble with respect to the anchor's centre. - * @private - */ -Blockly.Bubble.prototype.relativeTop_ = 0; - -/** - * Width of bubble. - * @private - */ -Blockly.Bubble.prototype.width_ = 0; - -/** - * Height of bubble. - * @private - */ -Blockly.Bubble.prototype.height_ = 0; - -/** - * Automatically position and reposition the bubble. - * @private - */ -Blockly.Bubble.prototype.autoLayout_ = true; - -/** - * Create the bubble's DOM. - * @param {!Element} content SVG content for the bubble. - * @param {boolean} hasResize Add diagonal resize gripper if true. - * @return {!Element} The bubble's SVG group. - * @private - */ -Blockly.Bubble.prototype.createDom_ = function(content, hasResize) { - /* Create the bubble. Here's the markup that will be generated: - - - - - - - - - - - [...content goes here...] - - */ - this.bubbleGroup_ = Blockly.utils.createSvgElement('g', {}, null); - var filter = - {'filter': 'url(#' + this.workspace_.options.embossFilterId + ')'}; - if (goog.userAgent.getUserAgentString().indexOf('JavaFX') != -1) { - // Multiple reports that JavaFX can't handle filters. UserAgent: - // Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.44 - // (KHTML, like Gecko) JavaFX/8.0 Safari/537.44 - // https://github.com/google/blockly/issues/99 - filter = {}; - } - var bubbleEmboss = Blockly.utils.createSvgElement('g', - filter, this.bubbleGroup_); - this.bubbleArrow_ = Blockly.utils.createSvgElement('path', {}, bubbleEmboss); - this.bubbleBack_ = Blockly.utils.createSvgElement('rect', - {'class': 'blocklyDraggable', 'x': 0, 'y': 0, - 'rx': Blockly.Bubble.BORDER_WIDTH, 'ry': Blockly.Bubble.BORDER_WIDTH}, - bubbleEmboss); - if (hasResize) { - this.resizeGroup_ = Blockly.utils.createSvgElement('g', - {'class': this.workspace_.RTL ? - 'blocklyResizeSW' : 'blocklyResizeSE'}, - this.bubbleGroup_); - var resizeSize = 2 * Blockly.Bubble.BORDER_WIDTH; - Blockly.utils.createSvgElement('polygon', - {'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())}, - this.resizeGroup_); - Blockly.utils.createSvgElement('line', - {'class': 'blocklyResizeLine', - 'x1': resizeSize / 3, 'y1': resizeSize - 1, - 'x2': resizeSize - 1, 'y2': resizeSize / 3}, this.resizeGroup_); - Blockly.utils.createSvgElement('line', - {'class': 'blocklyResizeLine', - 'x1': resizeSize * 2 / 3, 'y1': resizeSize - 1, - 'x2': resizeSize - 1, 'y2': resizeSize * 2 / 3}, this.resizeGroup_); - } else { - this.resizeGroup_ = null; - } - this.bubbleGroup_.appendChild(content); - return this.bubbleGroup_; -}; - -/** - * Handle a mouse-down on bubble's border. - * @param {!Event} e Mouse down event. - * @private - */ -Blockly.Bubble.prototype.bubbleMouseDown_ = function(e) { - this.promote_(); - Blockly.Bubble.unbindDragEvents_(); - if (Blockly.utils.isRightButton(e)) { - // No right-click. - e.stopPropagation(); - return; - } else if (Blockly.utils.isTargetInput(e)) { - // When focused on an HTML text input widget, don't trap any events. - return; - } - // Left-click (or middle click) - this.workspace_.startDrag(e, new goog.math.Coordinate( - this.workspace_.RTL ? -this.relativeLeft_ : this.relativeLeft_, - this.relativeTop_)); - - Blockly.Bubble.onMouseUpWrapper_ = Blockly.bindEventWithChecks_(document, - 'mouseup', this, Blockly.Bubble.bubbleMouseUp_); - Blockly.Bubble.onMouseMoveWrapper_ = Blockly.bindEventWithChecks_(document, - 'mousemove', this, this.bubbleMouseMove_); - Blockly.hideChaff(); - // This event has been handled. No need to bubble up to the document. - e.stopPropagation(); -}; - -/** - * Drag this bubble to follow the mouse. - * @param {!Event} e Mouse move event. - * @private - */ -Blockly.Bubble.prototype.bubbleMouseMove_ = function(e) { - this.autoLayout_ = false; - var newXY = this.workspace_.moveDrag(e); - this.relativeLeft_ = this.workspace_.RTL ? -newXY.x : newXY.x; - this.relativeTop_ = newXY.y; - this.positionBubble_(); - this.renderArrow_(); -}; - -/** - * Handle a mouse-down on bubble's resize corner. - * @param {!Event} e Mouse down event. - * @private - */ -Blockly.Bubble.prototype.resizeMouseDown_ = function(e) { - this.promote_(); - Blockly.Bubble.unbindDragEvents_(); - if (Blockly.utils.isRightButton(e)) { - // No right-click. - e.stopPropagation(); - return; - } - // Left-click (or middle click) - this.workspace_.startDrag(e, new goog.math.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(); - // This event has been handled. No need to bubble up to the document. - e.stopPropagation(); -}; - -/** - * Resize this bubble to follow the mouse. - * @param {!Event} e Mouse move event. - * @private - */ -Blockly.Bubble.prototype.resizeMouseMove_ = function(e) { - this.autoLayout_ = false; - var newXY = this.workspace_.moveDrag(e); - this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y); - if (this.workspace_.RTL) { - // RTL requires the bubble to move its left edge. - this.positionBubble_(); - } -}; - -/** - * Register a function as a callback event for when the bubble is resized. - * @param {!Function} callback The function to call on resize. - */ -Blockly.Bubble.prototype.registerResizeEvent = function(callback) { - this.resizeCallback_ = callback; -}; - -/** - * Move this bubble to the top of the stack. - * @private - */ -Blockly.Bubble.prototype.promote_ = function() { - var svgGroup = this.bubbleGroup_.parentNode; - svgGroup.appendChild(this.bubbleGroup_); -}; - -/** - * Notification that the anchor has moved. - * Update the arrow and bubble accordingly. - * @param {!goog.math.Coordinate} xy Absolute location. - */ -Blockly.Bubble.prototype.setAnchorLocation = function(xy) { - this.anchorXY_ = xy; - if (this.rendered_) { - this.positionBubble_(); - } -}; - -/** - * Position the bubble so that it does not fall off-screen. - * @private - */ -Blockly.Bubble.prototype.layoutBubble_ = function() { - // Compute the preferred bubble location. - var relativeLeft = -this.width_ / 4; - var relativeTop = -this.height_ - Blockly.BlockSvg.MIN_BLOCK_Y; - // Prevent the bubble from being off-screen. - var metrics = this.workspace_.getMetrics(); - metrics.viewWidth /= this.workspace_.scale; - metrics.viewLeft /= this.workspace_.scale; - var anchorX = this.anchorXY_.x; - if (this.workspace_.RTL) { - if (anchorX - metrics.viewLeft - relativeLeft - this.width_ < - Blockly.Scrollbar.scrollbarThickness) { - // Slide the bubble right until it is onscreen. - relativeLeft = anchorX - metrics.viewLeft - this.width_ - - Blockly.Scrollbar.scrollbarThickness; - } else if (anchorX - metrics.viewLeft - relativeLeft > - metrics.viewWidth) { - // Slide the bubble left until it is onscreen. - relativeLeft = anchorX - metrics.viewLeft - metrics.viewWidth; - } - } else { - if (anchorX + relativeLeft < metrics.viewLeft) { - // Slide the bubble right until it is onscreen. - relativeLeft = metrics.viewLeft - anchorX; - } else if (metrics.viewLeft + metrics.viewWidth < - anchorX + relativeLeft + this.width_ + - Blockly.BlockSvg.SEP_SPACE_X + - Blockly.Scrollbar.scrollbarThickness) { - // Slide the bubble left until it is onscreen. - relativeLeft = metrics.viewLeft + metrics.viewWidth - anchorX - - this.width_ - Blockly.Scrollbar.scrollbarThickness; - } - } - if (this.anchorXY_.y + relativeTop < metrics.viewTop) { - // Slide the bubble below the block. - var bBox = /** @type {SVGLocatable} */ (this.shape_).getBBox(); - relativeTop = bBox.height; - } - this.relativeLeft_ = relativeLeft; - this.relativeTop_ = relativeTop; -}; - -/** - * Move the bubble to a location relative to the anchor's centre. - * @private - */ -Blockly.Bubble.prototype.positionBubble_ = function() { - var left = this.anchorXY_.x; - if (this.workspace_.RTL) { - left -= this.relativeLeft_ + this.width_; - } else { - left += this.relativeLeft_; - } - var top = this.relativeTop_ + this.anchorXY_.y; - this.bubbleGroup_.setAttribute('transform', - 'translate(' + left + ',' + top + ')'); -}; - -/** - * Get the dimensions of this bubble. - * @return {!Object} Object with width and height properties. - */ -Blockly.Bubble.prototype.getBubbleSize = function() { - return {width: this.width_, height: this.height_}; -}; - -/** - * Size this bubble. - * @param {number} width Width of the bubble. - * @param {number} height Height of the bubble. - */ -Blockly.Bubble.prototype.setBubbleSize = function(width, height) { - var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH; - // Minimum size of a bubble. - width = Math.max(width, doubleBorderWidth + 45); - height = Math.max(height, doubleBorderWidth + 20); - this.width_ = width; - this.height_ = height; - this.bubbleBack_.setAttribute('width', width); - this.bubbleBack_.setAttribute('height', height); - if (this.resizeGroup_) { - if (this.workspace_.RTL) { - // Mirror the resize group. - var resizeSize = 2 * Blockly.Bubble.BORDER_WIDTH; - this.resizeGroup_.setAttribute('transform', 'translate(' + - resizeSize + ',' + (height - doubleBorderWidth) + ') scale(-1 1)'); - } else { - this.resizeGroup_.setAttribute('transform', 'translate(' + - (width - doubleBorderWidth) + ',' + - (height - doubleBorderWidth) + ')'); - } - } - if (this.rendered_) { - if (this.autoLayout_) { - this.layoutBubble_(); - } - this.positionBubble_(); - this.renderArrow_(); - } - // Allow the contents to resize. - if (this.resizeCallback_) { - this.resizeCallback_(); - } -}; - -/** - * Draw the arrow between the bubble and the origin. - * @private - */ -Blockly.Bubble.prototype.renderArrow_ = function() { - var steps = []; - // Find the relative coordinates of the center of the bubble. - var relBubbleX = this.width_ / 2; - var relBubbleY = this.height_ / 2; - // Find the relative coordinates of the center of the anchor. - var relAnchorX = -this.relativeLeft_; - var relAnchorY = -this.relativeTop_; - if (relBubbleX == relAnchorX && relBubbleY == relAnchorY) { - // Null case. Bubble is directly on top of the anchor. - // Short circuit this rather than wade through divide by zeros. - steps.push('M ' + relBubbleX + ',' + relBubbleY); - } else { - // Compute the angle of the arrow's line. - var rise = relAnchorY - relBubbleY; - var run = relAnchorX - relBubbleX; - if (this.workspace_.RTL) { - run *= -1; - } - var hypotenuse = Math.sqrt(rise * rise + run * run); - var angle = Math.acos(run / hypotenuse); - if (rise < 0) { - angle = 2 * Math.PI - angle; - } - // Compute a line perpendicular to the arrow. - var rightAngle = angle + Math.PI / 2; - if (rightAngle > Math.PI * 2) { - rightAngle -= Math.PI * 2; - } - var rightRise = Math.sin(rightAngle); - var rightRun = Math.cos(rightAngle); - - // Calculate the thickness of the base of the arrow. - var bubbleSize = this.getBubbleSize(); - var thickness = (bubbleSize.width + bubbleSize.height) / - Blockly.Bubble.ARROW_THICKNESS; - thickness = Math.min(thickness, bubbleSize.width, bubbleSize.height) / 4; - - // Back the tip of the arrow off of the anchor. - var backoffRatio = 1 - Blockly.Bubble.ANCHOR_RADIUS / hypotenuse; - relAnchorX = relBubbleX + backoffRatio * run; - relAnchorY = relBubbleY + backoffRatio * rise; - - // Coordinates for the base of the arrow. - var baseX1 = relBubbleX + thickness * rightRun; - var baseY1 = relBubbleY + thickness * rightRise; - var baseX2 = relBubbleX - thickness * rightRun; - var baseY2 = relBubbleY - thickness * rightRise; - - // Distortion to curve the arrow. - var swirlAngle = angle + this.arrow_radians_; - if (swirlAngle > Math.PI * 2) { - swirlAngle -= Math.PI * 2; - } - var swirlRise = Math.sin(swirlAngle) * - hypotenuse / Blockly.Bubble.ARROW_BEND; - var swirlRun = Math.cos(swirlAngle) * - hypotenuse / Blockly.Bubble.ARROW_BEND; - - steps.push('M' + baseX1 + ',' + baseY1); - steps.push('C' + (baseX1 + swirlRun) + ',' + (baseY1 + swirlRise) + - ' ' + relAnchorX + ',' + relAnchorY + - ' ' + relAnchorX + ',' + relAnchorY); - steps.push('C' + relAnchorX + ',' + relAnchorY + - ' ' + (baseX2 + swirlRun) + ',' + (baseY2 + swirlRise) + - ' ' + baseX2 + ',' + baseY2); - } - steps.push('z'); - this.bubbleArrow_.setAttribute('d', steps.join(' ')); -}; - -/** - * Change the colour of a bubble. - * @param {string} hexColour Hex code of colour. - */ -Blockly.Bubble.prototype.setColour = function(hexColour) { - this.bubbleBack_.setAttribute('fill', hexColour); - this.bubbleArrow_.setAttribute('fill', hexColour); -}; - -/** - * Dispose of this bubble. - */ -Blockly.Bubble.prototype.dispose = function() { - Blockly.Bubble.unbindDragEvents_(); - // Dispose of and unlink the bubble. - goog.dom.removeNode(this.bubbleGroup_); - this.bubbleGroup_ = null; - this.bubbleArrow_ = null; - this.bubbleBack_ = null; - this.resizeGroup_ = null; - this.workspace_ = null; - this.content_ = null; - this.shape_ = null; -}; diff --git a/core/bubbles.ts b/core/bubbles.ts new file mode 100644 index 00000000000..a49c2ae3581 --- /dev/null +++ b/core/bubbles.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {Bubble} from './bubbles/bubble.js'; +import {MiniWorkspaceBubble} from './bubbles/mini_workspace_bubble.js'; +import {TextBubble} from './bubbles/text_bubble.js'; +import {TextInputBubble} from './bubbles/textinput_bubble.js'; + +export {Bubble, MiniWorkspaceBubble, TextBubble, TextInputBubble}; diff --git a/core/bubbles/bubble.ts b/core/bubbles/bubble.ts new file mode 100644 index 00000000000..bac94dbc8a0 --- /dev/null +++ b/core/bubbles/bubble.ts @@ -0,0 +1,659 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {ISelectable} from '../blockly.js'; +import * as browserEvents from '../browser_events.js'; +import * as common from '../common.js'; +import {BubbleDragStrategy} from '../dragging/bubble_drag_strategy.js'; +import {IBubble} from '../interfaces/i_bubble.js'; +import {ContainerRegion} from '../metrics_manager.js'; +import {Scrollbar} from '../scrollbar.js'; +import {Coordinate} from '../utils/coordinate.js'; +import * as dom from '../utils/dom.js'; +import * as idGenerator from '../utils/idgenerator.js'; +import * as math from '../utils/math.js'; +import {Rect} from '../utils/rect.js'; +import {Size} from '../utils/size.js'; +import {Svg} from '../utils/svg.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; + +/** + * The abstract pop-up bubble class. This creates a UI that looks like a speech + * bubble, where it has a "tail" that points to the block, and a "head" that + * displays arbitrary svg elements. + */ +export abstract class Bubble implements IBubble, ISelectable { + /** The width of the border around the bubble. */ + static readonly BORDER_WIDTH = 6; + + /** Double the width of the border around the bubble. */ + static readonly DOUBLE_BORDER = this.BORDER_WIDTH * 2; + + /** The minimum size the bubble can have. */ + static readonly MIN_SIZE = this.DOUBLE_BORDER; + + /** + * The thickness of the base of the tail in relation to the size of the + * bubble. Higher numbers result in thinner tails. + */ + static readonly TAIL_THICKNESS = 1; + + /** The number of degrees that the tail bends counter-clockwise. */ + static readonly TAIL_ANGLE = 20; + + /** + * The sharpness of the tail's bend. Higher numbers result in smoother + * tails. + */ + static readonly TAIL_BEND = 4; + + /** Distance between arrow point and anchor point. */ + static readonly ANCHOR_RADIUS = 8; + + public id: string; + + /** The SVG group containing all parts of the bubble. */ + protected svgRoot: SVGGElement; + + /** The SVG path for the arrow from the anchor to the bubble. */ + private tail: SVGPathElement; + + /** The SVG background rect for the main body of the bubble. */ + private background: SVGRectElement; + + /** The SVG group containing the contents of the bubble. */ + protected contentContainer: SVGGElement; + + /** + * The size of the bubble (including background and contents but not tail). + */ + private size = new Size(0, 0); + + /** The colour of the background of the bubble. */ + private colour = '#ffffff'; + + /** True if the bubble has been disposed, false otherwise. */ + public disposed = false; + + /** The position of the top of the bubble relative to its anchor. */ + private relativeTop = 0; + + /** The position of the left of the bubble realtive to its anchor. */ + private relativeLeft = 0; + + private dragStrategy = new BubbleDragStrategy(this, this.workspace); + + /** + * @param workspace The workspace this bubble belongs to. + * @param anchor The anchor location of the thing this bubble is attached to. + * The tail of the bubble will point to this location. + * @param ownerRect An optional rect we don't want the bubble to overlap with + * when automatically positioning. + */ + constructor( + public readonly workspace: WorkspaceSvg, + protected anchor: Coordinate, + protected ownerRect?: Rect, + ) { + this.id = idGenerator.getNextUniqueId(); + this.svgRoot = dom.createSvgElement( + Svg.G, + {'class': 'blocklyBubble'}, + workspace.getBubbleCanvas(), + ); + const embossGroup = dom.createSvgElement( + Svg.G, + { + 'filter': `url(#${ + this.workspace.getRenderer().getConstants().embossFilterId + })`, + }, + this.svgRoot, + ); + this.tail = dom.createSvgElement( + Svg.PATH, + {'class': 'blocklyBubbleTail'}, + embossGroup, + ); + this.background = dom.createSvgElement( + Svg.RECT, + { + 'class': 'blocklyDraggable', + 'x': 0, + 'y': 0, + 'rx': Bubble.BORDER_WIDTH, + 'ry': Bubble.BORDER_WIDTH, + }, + embossGroup, + ); + this.contentContainer = dom.createSvgElement(Svg.G, {}, this.svgRoot); + + browserEvents.conditionalBind( + this.background, + 'pointerdown', + this, + this.onMouseDown, + ); + } + + /** Dispose of this bubble. */ + dispose() { + dom.removeNode(this.svgRoot); + this.disposed = true; + } + + /** + * Set the location the tail of this bubble points to. + * + * @param anchor The location the tail of this bubble points to. + * @param relayout If true, reposition the bubble from scratch so that it is + * optimally visible. If false, reposition it so it maintains the same + * position relative to the anchor. + */ + setAnchorLocation(anchor: Coordinate, relayout = false) { + this.anchor = anchor; + if (relayout) { + this.positionByRect(this.ownerRect); + } else { + this.positionRelativeToAnchor(); + } + this.renderTail(); + } + + /** Sets the position of this bubble relative to its anchor. */ + setPositionRelativeToAnchor(left: number, top: number) { + this.relativeLeft = left; + this.relativeTop = top; + this.positionRelativeToAnchor(); + this.renderTail(); + } + + /** @returns the size of this bubble. */ + protected getSize() { + return this.size; + } + + /** + * Sets the size of this bubble, including the border. + * + * @param size Sets the size of this bubble, including the border. + * @param relayout If true, reposition the bubble from scratch so that it is + * optimally visible. If false, reposition it so it maintains the same + * position relative to the anchor. + */ + protected setSize(size: Size, relayout = false) { + size.width = Math.max(size.width, Bubble.MIN_SIZE); + size.height = Math.max(size.height, Bubble.MIN_SIZE); + this.size = size; + + this.background.setAttribute('width', `${size.width}`); + this.background.setAttribute('height', `${size.height}`); + + if (relayout) { + this.positionByRect(this.ownerRect); + } else { + this.positionRelativeToAnchor(); + } + this.renderTail(); + } + + /** Returns the colour of the background and tail of this bubble. */ + protected getColour(): string { + return this.colour; + } + + /** Sets the colour of the background and tail of this bubble. */ + public setColour(colour: string) { + this.colour = colour; + this.tail.setAttribute('fill', colour); + this.background.setAttribute('fill', colour); + } + + /** Brings the bubble to the front and passes the pointer event off to the gesture system. */ + private onMouseDown(e: PointerEvent) { + this.workspace.getGesture(e)?.handleBubbleStart(e, this); + this.bringToFront(); + common.setSelected(this); + } + + /** Positions the bubble relative to its anchor. Does not render its tail. */ + protected positionRelativeToAnchor() { + let left = this.anchor.x; + if (this.workspace.RTL) { + left -= this.relativeLeft + this.size.width; + } else { + left += this.relativeLeft; + } + const top = this.relativeTop + this.anchor.y; + this.moveTo(left, top); + } + + /** + * Moves the bubble to the given coordinates. + * + * @internal + */ + moveTo(x: number, y: number) { + this.svgRoot.setAttribute('transform', `translate(${x}, ${y})`); + } + + /** + * Positions the bubble "optimally" so that the most of it is visible and + * it does not overlap the rect (if provided). + */ + protected positionByRect(rect = new Rect(0, 0, 0, 0)) { + const viewMetrics = this.workspace.getMetricsManager().getViewMetrics(true); + + const optimalLeft = this.getOptimalRelativeLeft(viewMetrics); + const optimalTop = this.getOptimalRelativeTop(viewMetrics); + + const topPosition = { + x: optimalLeft, + y: (-this.size.height - + this.workspace.getRenderer().getConstants().MIN_BLOCK_HEIGHT) as number, + }; + const startPosition = {x: -this.size.width - 30, y: optimalTop}; + const endPosition = {x: rect.getWidth(), y: optimalTop}; + const bottomPosition = {x: optimalLeft, y: rect.getHeight()}; + + const closerPosition = + rect.getWidth() < rect.getHeight() ? endPosition : bottomPosition; + const fartherPosition = + rect.getWidth() < rect.getHeight() ? bottomPosition : endPosition; + + const topPositionOverlap = this.getOverlap(topPosition, viewMetrics); + const startPositionOverlap = this.getOverlap(startPosition, viewMetrics); + const closerPositionOverlap = this.getOverlap(closerPosition, viewMetrics); + const fartherPositionOverlap = this.getOverlap( + fartherPosition, + viewMetrics, + ); + + // Set the position to whichever position shows the most of the bubble, + // with tiebreaks going in the order: top > start > close > far. + const mostOverlap = Math.max( + topPositionOverlap, + startPositionOverlap, + closerPositionOverlap, + fartherPositionOverlap, + ); + if (topPositionOverlap === mostOverlap) { + this.relativeLeft = topPosition.x; + this.relativeTop = topPosition.y; + this.positionRelativeToAnchor(); + return; + } + if (startPositionOverlap === mostOverlap) { + this.relativeLeft = startPosition.x; + this.relativeTop = startPosition.y; + this.positionRelativeToAnchor(); + return; + } + if (closerPositionOverlap === mostOverlap) { + this.relativeLeft = closerPosition.x; + this.relativeTop = closerPosition.y; + this.positionRelativeToAnchor(); + return; + } + // TODO: I believe relativeLeft_ should actually be called relativeStart_ + // and then the math should be fixed to reflect this. (hopefully it'll + // make it look simpler) + this.relativeLeft = fartherPosition.x; + this.relativeTop = fartherPosition.y; + this.positionRelativeToAnchor(); + } + + /** + * Calculate the what percentage of the bubble overlaps with the visible + * workspace (what percentage of the bubble is visible). + * + * @param relativeMin The position of the top-left corner of the bubble + * relative to the anchor point. + * @param viewMetrics The view metrics of the workspace the bubble will appear + * in. + * @returns The percentage of the bubble that is visible. + */ + private getOverlap( + relativeMin: {x: number; y: number}, + viewMetrics: ContainerRegion, + ): number { + // The position of the top-left corner of the bubble in workspace units. + const bubbleMin = { + x: this.workspace.RTL + ? this.anchor.x - relativeMin.x - this.size.width + : relativeMin.x + this.anchor.x, + y: relativeMin.y + this.anchor.y, + }; + // The position of the bottom-right corner of the bubble in workspace units. + const bubbleMax = { + x: bubbleMin.x + this.size.width, + y: bubbleMin.y + this.size.height, + }; + + // We could adjust these values to account for the scrollbars, but the + // bubbles should have been adjusted to not collide with them anyway, so + // giving the workspace a slightly larger "bounding box" shouldn't affect + // the calculation. + + // The position of the top-left corner of the workspace. + const workspaceMin = {x: viewMetrics.left, y: viewMetrics.top}; + // The position of the bottom-right corner of the workspace. + const workspaceMax = { + x: viewMetrics.left + viewMetrics.width, + y: viewMetrics.top + viewMetrics.height, + }; + + const overlapWidth = + Math.min(bubbleMax.x, workspaceMax.x) - + Math.max(bubbleMin.x, workspaceMin.x); + const overlapHeight = + Math.min(bubbleMax.y, workspaceMax.y) - + Math.max(bubbleMin.y, workspaceMin.y); + return Math.max( + 0, + Math.min( + 1, + (overlapWidth * overlapHeight) / (this.size.width * this.size.height), + ), + ); + } + + /** + * Calculate what the optimal horizontal position of the top-left corner of + * the bubble is (relative to the anchor point) so that the most area of the + * bubble is shown. + * + * @param viewMetrics The view metrics of the workspace the bubble will appear + * in. + * @returns The optimal horizontal position of the top-left corner of the + * bubble. + */ + private getOptimalRelativeLeft(viewMetrics: ContainerRegion): number { + // By default, show the bubble just a bit to the left of the anchor. + let relativeLeft = -this.size.width / 4; + + // No amount of sliding left or right will give us better overlap. + if (this.size.width > viewMetrics.width) return relativeLeft; + + const workspaceRect = this.getWorkspaceViewRect(viewMetrics); + + if (this.workspace.RTL) { + // Bubble coordinates are flipped in RTL. + const bubbleRight = this.anchor.x - relativeLeft; + const bubbleLeft = bubbleRight - this.size.width; + + if (bubbleLeft < workspaceRect.left) { + // Slide the bubble right until it is onscreen. + relativeLeft = -(workspaceRect.left - this.anchor.x + this.size.width); + } else if (bubbleRight > workspaceRect.right) { + // Slide the bubble left until it is onscreen. + relativeLeft = -(workspaceRect.right - this.anchor.x); + } + } else { + const bubbleLeft = relativeLeft + this.anchor.x; + const bubbleRight = bubbleLeft + this.size.width; + + if (bubbleLeft < workspaceRect.left) { + // Slide the bubble right until it is onscreen. + relativeLeft = workspaceRect.left - this.anchor.x; + } else if (bubbleRight > workspaceRect.right) { + // Slide the bubble left until it is onscreen. + relativeLeft = workspaceRect.right - this.anchor.x - this.size.width; + } + } + + return relativeLeft; + } + + /** + * Calculate what the optimal vertical position of the top-left corner of + * the bubble is (relative to the anchor point) so that the most area of the + * bubble is shown. + * + * @param viewMetrics The view metrics of the workspace the bubble will appear + * in. + * @returns The optimal vertical position of the top-left corner of the + * bubble. + */ + private getOptimalRelativeTop(viewMetrics: ContainerRegion): number { + // By default, show the bubble just a bit higher than the anchor. + let relativeTop = -this.size.height / 4; + + // No amount of sliding up or down will give us better overlap. + if (this.size.height > viewMetrics.height) return relativeTop; + + const top = this.anchor.y + relativeTop; + const bottom = top + this.size.height; + const workspaceRect = this.getWorkspaceViewRect(viewMetrics); + + if (top < workspaceRect.top) { + // Slide the bubble down until it is onscreen. + relativeTop = workspaceRect.top - this.anchor.y; + } else if (bottom > workspaceRect.bottom) { + // Slide the bubble up until it is onscreen. + relativeTop = workspaceRect.bottom - this.anchor.y - this.size.height; + } + + return relativeTop; + } + + /** + * @returns a rect defining the bounds of the workspace's view in workspace + * coordinates. + */ + private getWorkspaceViewRect(viewMetrics: ContainerRegion): Rect { + const top = viewMetrics.top; + let bottom = viewMetrics.top + viewMetrics.height; + let left = viewMetrics.left; + let right = viewMetrics.left + viewMetrics.width; + + bottom -= this.getScrollbarThickness(); + if (this.workspace.RTL) { + left -= this.getScrollbarThickness(); + } else { + right -= this.getScrollbarThickness(); + } + + return new Rect(top, bottom, left, right); + } + + /** @returns the scrollbar thickness in workspace units. */ + private getScrollbarThickness() { + return Scrollbar.scrollbarThickness / this.workspace.scale; + } + + /** Draws the tail of the bubble. */ + private renderTail() { + const steps = []; + // Find the relative coordinates of the center of the bubble. + const relBubbleX = this.size.width / 2; + const relBubbleY = this.size.height / 2; + // Find the relative coordinates of the center of the anchor. + let relAnchorX = -this.relativeLeft; + let relAnchorY = -this.relativeTop; + if (relBubbleX === relAnchorX && relBubbleY === relAnchorY) { + // Null case. Bubble is directly on top of the anchor. + // Short circuit this rather than wade through divide by zeros. + steps.push('M ' + relBubbleX + ',' + relBubbleY); + } else { + // Compute the angle of the tail's line. + const rise = relAnchorY - relBubbleY; + let run = relAnchorX - relBubbleX; + if (this.workspace.RTL) { + run *= -1; + } + const hypotenuse = Math.sqrt(rise * rise + run * run); + let angle = Math.acos(run / hypotenuse); + if (rise < 0) { + angle = 2 * Math.PI - angle; + } + // Compute a line perpendicular to the tail. + let rightAngle = angle + Math.PI / 2; + if (rightAngle > Math.PI * 2) { + rightAngle -= Math.PI * 2; + } + const rightRise = Math.sin(rightAngle); + const rightRun = Math.cos(rightAngle); + + // Calculate the thickness of the base of the tail. + let thickness = + (this.size.width + this.size.height) / Bubble.TAIL_THICKNESS; + thickness = Math.min(thickness, this.size.width, this.size.height) / 4; + + // Back the tip of the tail off of the anchor. + const backoffRatio = 1 - Bubble.ANCHOR_RADIUS / hypotenuse; + relAnchorX = relBubbleX + backoffRatio * run; + relAnchorY = relBubbleY + backoffRatio * rise; + + // Coordinates for the base of the tail. + const baseX1 = relBubbleX + thickness * rightRun; + const baseY1 = relBubbleY + thickness * rightRise; + const baseX2 = relBubbleX - thickness * rightRun; + const baseY2 = relBubbleY - thickness * rightRise; + + // Distortion to curve the tail. + const radians = math.toRadians( + this.workspace.RTL ? -Bubble.TAIL_ANGLE : Bubble.TAIL_ANGLE, + ); + let swirlAngle = angle + radians; + if (swirlAngle > Math.PI * 2) { + swirlAngle -= Math.PI * 2; + } + const swirlRise = (Math.sin(swirlAngle) * hypotenuse) / Bubble.TAIL_BEND; + const swirlRun = (Math.cos(swirlAngle) * hypotenuse) / Bubble.TAIL_BEND; + + steps.push('M' + baseX1 + ',' + baseY1); + steps.push( + 'C' + + (baseX1 + swirlRun) + + ',' + + (baseY1 + swirlRise) + + ' ' + + relAnchorX + + ',' + + relAnchorY + + ' ' + + relAnchorX + + ',' + + relAnchorY, + ); + steps.push( + 'C' + + relAnchorX + + ',' + + relAnchorY + + ' ' + + (baseX2 + swirlRun) + + ',' + + (baseY2 + swirlRise) + + ' ' + + baseX2 + + ',' + + baseY2, + ); + } + steps.push('z'); + this.tail?.setAttribute('d', steps.join(' ')); + } + /** + * Move this bubble to the front of the visible workspace. + * + * @returns Whether or not the bubble has been moved. + * @internal + */ + bringToFront(): boolean { + const svgGroup = this.svgRoot?.parentNode; + if (this.svgRoot && svgGroup?.lastChild !== this.svgRoot) { + svgGroup?.appendChild(this.svgRoot); + return true; + } + return false; + } + + /** @internal */ + getRelativeToSurfaceXY(): Coordinate { + return new Coordinate( + this.workspace.RTL + ? -this.relativeLeft + this.anchor.x - this.size.width + : this.anchor.x + this.relativeLeft, + this.anchor.y + this.relativeTop, + ); + } + + /** @internal */ + getSvgRoot(): SVGElement { + return this.svgRoot; + } + + /** + * Move this bubble during a drag. + * + * @param newLoc The location to translate to, in workspace coordinates. + * @internal + */ + moveDuringDrag(newLoc: Coordinate) { + this.moveTo(newLoc.x, newLoc.y); + if (this.workspace.RTL) { + this.relativeLeft = this.anchor.x - newLoc.x - this.size.width; + } else { + this.relativeLeft = newLoc.x - this.anchor.x; + } + this.relativeTop = newLoc.y - this.anchor.y; + this.renderTail(); + } + + setDragging(_start: boolean) { + // NOOP in base class. + } + + /** @internal */ + setDeleteStyle(_enable: boolean) { + // NOOP in base class. + } + + /** @internal */ + isDeletable(): boolean { + return false; + } + + /** @internal */ + showContextMenu(_e: Event) { + // NOOP in base class. + } + + /** Returns whether this bubble is movable or not. */ + isMovable(): boolean { + return true; + } + + /** Starts a drag on the bubble. */ + startDrag(): void { + this.dragStrategy.startDrag(); + } + + /** Drags the bubble to the given location. */ + drag(newLoc: Coordinate): void { + this.dragStrategy.drag(newLoc); + } + + /** Ends the drag on the bubble. */ + endDrag(): void { + this.dragStrategy.endDrag(); + } + + /** Moves the bubble back to where it was at the start of a drag. */ + revertDrag(): void { + this.dragStrategy.revertDrag(); + } + + select(): void { + // Bubbles don't have any visual for being selected. + } + + unselect(): void { + // Bubbles don't have any visual for being selected. + } +} diff --git a/core/bubbles/mini_workspace_bubble.ts b/core/bubbles/mini_workspace_bubble.ts new file mode 100644 index 00000000000..f4ad96c8c00 --- /dev/null +++ b/core/bubbles/mini_workspace_bubble.ts @@ -0,0 +1,279 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {BlocklyOptions} from '../blockly_options.js'; +import {Abstract as AbstractEvent} from '../events/events_abstract.js'; +import {Options} from '../options.js'; +import {Coordinate} from '../utils/coordinate.js'; +import * as dom from '../utils/dom.js'; +import type {Rect} from '../utils/rect.js'; +import {Size} from '../utils/size.js'; +import {Svg} from '../utils/svg.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import {Bubble} from './bubble.js'; + +/** + * A bubble that contains a mini-workspace which can hold arbitrary blocks. + * Used by the mutator icon. + */ +export class MiniWorkspaceBubble extends Bubble { + /** + * The minimum amount of change to the mini workspace view to trigger + * resizing the bubble. + */ + private static readonly MINIMUM_VIEW_CHANGE = 10; + + /** + * An arbitrary margin of whitespace to put around the blocks in the + * workspace. + */ + private static readonly MARGIN = Bubble.DOUBLE_BORDER * 3; + + /** The root svg element containing the workspace. */ + private svgDialog: SVGElement; + + /** The workspace that gets shown within this bubble. */ + private miniWorkspace: WorkspaceSvg; + + /** + * Should this bubble automatically reposition itself when it resizes? + * Becomes false after this bubble is first dragged. + */ + private autoLayout = true; + + /** @internal */ + constructor( + workspaceOptions: BlocklyOptions, + public readonly workspace: WorkspaceSvg, + protected anchor: Coordinate, + protected ownerRect?: Rect, + ) { + super(workspace, anchor, ownerRect); + const options = new Options(workspaceOptions); + this.validateWorkspaceOptions(options); + + this.svgDialog = dom.createSvgElement( + Svg.SVG, + { + 'x': Bubble.BORDER_WIDTH, + 'y': Bubble.BORDER_WIDTH, + }, + this.contentContainer, + ); + workspaceOptions.parentWorkspace = this.workspace; + this.miniWorkspace = this.newWorkspaceSvg(new Options(workspaceOptions)); + // TODO (#7422): Change this to `internalIsMiniWorkspace` or something. Not + // all mini workspaces are necessarily mutators. + this.miniWorkspace.internalIsMutator = true; + const background = this.miniWorkspace.createDom('blocklyMutatorBackground'); + this.svgDialog.appendChild(background); + if (options.languageTree) { + background.insertBefore( + this.miniWorkspace.addFlyout(Svg.G), + this.miniWorkspace.getCanvas(), + ); + const flyout = this.miniWorkspace.getFlyout(); + flyout?.init(this.miniWorkspace); + flyout?.show(options.languageTree); + } + + this.miniWorkspace.addChangeListener(this.onWorkspaceChange.bind(this)); + this.miniWorkspace + .getFlyout() + ?.getWorkspace() + ?.addChangeListener(this.onWorkspaceChange.bind(this)); + this.updateBubbleSize(); + } + + dispose() { + this.miniWorkspace.dispose(); + super.dispose(); + } + + /** @internal */ + getWorkspace(): WorkspaceSvg { + return this.miniWorkspace; + } + + /** Adds a change listener to the mini workspace. */ + addWorkspaceChangeListener(listener: (e: AbstractEvent) => void) { + this.miniWorkspace.addChangeListener(listener); + } + + /** + * Validates the workspace options to make sure folks aren't trying to + * enable things the miniworkspace doesn't support. + */ + private validateWorkspaceOptions(options: Options) { + if (options.hasCategories) { + throw new Error( + 'The miniworkspace bubble does not support toolboxes with categories', + ); + } + if (options.hasTrashcan) { + throw new Error('The miniworkspace bubble does not support trashcans'); + } + if ( + options.zoomOptions.controls || + options.zoomOptions.wheel || + options.zoomOptions.pinch + ) { + throw new Error('The miniworkspace bubble does not support zooming'); + } + if ( + options.moveOptions.scrollbars || + options.moveOptions.wheel || + options.moveOptions.drag + ) { + throw new Error( + 'The miniworkspace bubble does not scrolling/moving the workspace', + ); + } + if (options.horizontalLayout) { + throw new Error( + 'The miniworkspace bubble does not support horizontal layouts', + ); + } + } + + private onWorkspaceChange() { + this.bumpBlocksIntoBounds(); + this.updateBubbleSize(); + } + + /** + * Bumps blocks that are above the top or outside the start-side of the + * workspace back within the workspace. + * + * Blocks that are below the bottom or outside the end-side of the workspace + * are dealt with by resizing the workspace to show them. + */ + private bumpBlocksIntoBounds() { + if (this.miniWorkspace.isDragging()) return; + + const MARGIN = 20; + + for (const block of this.miniWorkspace.getTopBlocks(false)) { + const blockXY = block.getRelativeToSurfaceXY(); + + // Bump any block that's above the top back inside. + if (blockXY.y < MARGIN) { + block.moveBy(0, MARGIN - blockXY.y); + } + // Bump any block overlapping the flyout back inside. + if (block.RTL) { + let right = -MARGIN; + const flyout = this.miniWorkspace.getFlyout(); + if (flyout) { + right -= flyout.getWidth(); + } + if (blockXY.x > right) { + block.moveBy(right - blockXY.x, 0); + } + } else if (blockXY.x < MARGIN) { + block.moveBy(MARGIN - blockXY.x, 0); + } + } + } + + /** + * Updates the size of this bubble to account for the size of the + * mini workspace. + */ + private updateBubbleSize() { + if (this.miniWorkspace.isDragging()) return; + + const currSize = this.getSize(); + const newSize = this.calculateWorkspaceSize(); + if ( + Math.abs(currSize.width - newSize.width) < + MiniWorkspaceBubble.MINIMUM_VIEW_CHANGE && + Math.abs(currSize.height - newSize.height) < + MiniWorkspaceBubble.MINIMUM_VIEW_CHANGE + ) { + // Only resize if the size has noticeably changed. + return; + } + this.svgDialog.setAttribute('width', `${newSize.width}px`); + this.svgDialog.setAttribute('height', `${newSize.height}px`); + this.miniWorkspace.setCachedParentSvgSize(newSize.width, newSize.height); + if (this.miniWorkspace.RTL) { + // Scroll the workspace to always left-align. + this.miniWorkspace + .getCanvas() + .setAttribute('transform', `translate(${newSize.width}, 0)`); + } + this.setSize( + new Size( + newSize.width + Bubble.DOUBLE_BORDER, + newSize.height + Bubble.DOUBLE_BORDER, + ), + this.autoLayout, + ); + this.miniWorkspace.resize(); + this.miniWorkspace.recordDragTargets(); + } + + /** + * Calculates the size of the mini workspace for use in resizing the bubble. + */ + private calculateWorkspaceSize(): Size { + const workspaceSize = this.miniWorkspace.getCanvas().getBBox(); + let width = workspaceSize.width + MiniWorkspaceBubble.MARGIN; + let height = workspaceSize.height + MiniWorkspaceBubble.MARGIN; + + const flyout = this.miniWorkspace.getFlyout(); + if (flyout) { + const flyoutScrollMetrics = flyout + .getWorkspace() + .getMetricsManager() + .getScrollMetrics(); + height = Math.max(height, flyoutScrollMetrics.height + 20); + width += flyout.getWidth(); + } + return new Size(width, height); + } + + /** Reapplies styles to all of the blocks in the mini workspace. */ + updateBlockStyles() { + for (const block of this.miniWorkspace.getAllBlocks(false)) { + block.setStyle(block.getStyleName()); + } + + const flyoutWs = this.miniWorkspace.getFlyout()?.getWorkspace(); + if (flyoutWs) { + for (const block of flyoutWs.getAllBlocks(false)) { + block.setStyle(block.getStyleName()); + } + } + } + + /** + * Move this bubble during a drag. + * + * @param newLoc The location to translate to, in workspace coordinates. + * @internal + */ + moveDuringDrag(newLoc: Coordinate): void { + super.moveDuringDrag(newLoc); + this.autoLayout = false; + } + + /** @internal */ + moveTo(x: number, y: number): void { + super.moveTo(x, y); + this.miniWorkspace.recordDragTargets(); + } + + /** @internal */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + newWorkspaceSvg(options: Options): WorkspaceSvg { + throw new Error( + 'The implementation of newWorkspaceSvg should be ' + + 'monkey-patched in by blockly.ts', + ); + } +} diff --git a/core/bubbles/text_bubble.ts b/core/bubbles/text_bubble.ts new file mode 100644 index 00000000000..7ac5fa02965 --- /dev/null +++ b/core/bubbles/text_bubble.ts @@ -0,0 +1,102 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {Coordinate} from '../utils/coordinate.js'; +import * as dom from '../utils/dom.js'; +import {Rect} from '../utils/rect.js'; +import {Size} from '../utils/size.js'; +import {Svg} from '../utils/svg.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; +import {Bubble} from './bubble.js'; + +/** + * A bubble that displays non-editable text. Used by the warning icon. + */ +export class TextBubble extends Bubble { + private paragraph: SVGTextElement; + + constructor( + private text: string, + public readonly workspace: WorkspaceSvg, + protected anchor: Coordinate, + protected ownerRect?: Rect, + ) { + super(workspace, anchor, ownerRect); + this.paragraph = this.stringToSvg(text, this.contentContainer); + this.updateBubbleSize(); + } + + /** @returns the current text of this text bubble. */ + getText(): string { + return this.text; + } + + /** Sets the current text of this text bubble, and updates the display. */ + setText(text: string) { + this.text = text; + dom.removeNode(this.paragraph); + this.paragraph = this.stringToSvg(text, this.contentContainer); + this.updateBubbleSize(); + } + + /** + * Converts the given string into an svg containing that string, + * broken up by newlines. + */ + private stringToSvg(text: string, container: SVGGElement) { + const paragraph = this.createParagraph(container); + const spans = this.createSpans(paragraph, text); + if (this.workspace.RTL) + this.rightAlignSpans(paragraph.getBBox().width, spans); + return paragraph; + } + + /** Creates the paragraph container for this bubble's view's spans. */ + private createParagraph(container: SVGGElement): SVGTextElement { + return dom.createSvgElement( + Svg.TEXT, + { + 'class': 'blocklyText blocklyBubbleText blocklyNoPointerEvents', + 'y': Bubble.BORDER_WIDTH, + }, + container, + ); + } + + /** Creates the spans visualizing the text of this bubble. */ + private createSpans(parent: SVGTextElement, text: string): SVGTSpanElement[] { + return text.split('\n').map((line) => { + const tspan = dom.createSvgElement( + Svg.TSPAN, + {'dy': '1em', 'x': Bubble.BORDER_WIDTH}, + parent, + ); + const textNode = document.createTextNode(line); + tspan.appendChild(textNode); + return tspan; + }); + } + + /** Right aligns the given spans. */ + private rightAlignSpans(maxWidth: number, spans: SVGTSpanElement[]) { + for (const span of spans) { + span.setAttribute('text-anchor', 'end'); + span.setAttribute('x', `${maxWidth + Bubble.BORDER_WIDTH}`); + } + } + + /** Updates the size of this bubble to account for the size of the text. */ + private updateBubbleSize() { + const bbox = this.paragraph.getBBox(); + this.setSize( + new Size( + bbox.width + Bubble.BORDER_WIDTH * 2, + bbox.height + Bubble.BORDER_WIDTH * 2, + ), + true, + ); + } +} diff --git a/core/bubbles/textinput_bubble.ts b/core/bubbles/textinput_bubble.ts new file mode 100644 index 00000000000..5b5278b91ff --- /dev/null +++ b/core/bubbles/textinput_bubble.ts @@ -0,0 +1,333 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Css from '../css.js'; +import * as touch from '../touch.js'; +import {browserEvents} from '../utils.js'; +import {Coordinate} from '../utils/coordinate.js'; +import * as dom from '../utils/dom.js'; +import * as drag from '../utils/drag.js'; +import {Rect} from '../utils/rect.js'; +import {Size} from '../utils/size.js'; +import {Svg} from '../utils/svg.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; +import {Bubble} from './bubble.js'; + +/** + * A bubble that displays editable text. It can also be resized by the user. + * Used by the comment icon. + */ +export class TextInputBubble extends Bubble { + /** The root of the elements specific to the text element. */ + private inputRoot: SVGForeignObjectElement; + + /** The text input area element. */ + private textArea: HTMLTextAreaElement; + + /** The group containing the lines indicating the bubble is resizable. */ + private resizeGroup: SVGGElement; + + /** + * Event data associated with the listener for pointer up events on the + * resize group. + */ + private resizePointerUpListener: browserEvents.Data | null = null; + + /** + * Event data associated with the listener for pointer move events on the + * resize group. + */ + private resizePointerMoveListener: browserEvents.Data | null = null; + + /** Functions listening for changes to the text of this bubble. */ + private textChangeListeners: (() => void)[] = []; + + /** Functions listening for changes to the size of this bubble. */ + private sizeChangeListeners: (() => void)[] = []; + + /** The text of this bubble. */ + private text = ''; + + /** The default size of this bubble, including borders. */ + private readonly DEFAULT_SIZE = new Size( + 160 + Bubble.DOUBLE_BORDER, + 80 + Bubble.DOUBLE_BORDER, + ); + + /** The minimum size of this bubble, including borders. */ + private readonly MIN_SIZE = new Size( + 45 + Bubble.DOUBLE_BORDER, + 20 + Bubble.DOUBLE_BORDER, + ); + + private editable = true; + + /** + * @param workspace The workspace this bubble belongs to. + * @param anchor The anchor location of the thing this bubble is attached to. + * The tail of the bubble will point to this location. + * @param ownerRect An optional rect we don't want the bubble to overlap with + * when automatically positioning. + */ + constructor( + public readonly workspace: WorkspaceSvg, + protected anchor: Coordinate, + protected ownerRect?: Rect, + ) { + super(workspace, anchor, ownerRect); + dom.addClass(this.svgRoot, 'blocklyTextInputBubble'); + ({inputRoot: this.inputRoot, textArea: this.textArea} = this.createEditor( + this.contentContainer, + )); + this.resizeGroup = this.createResizeHandle(this.svgRoot, workspace); + this.setSize(this.DEFAULT_SIZE, true); + } + + /** @returns the text of this bubble. */ + getText(): string { + return this.text; + } + + /** Sets the text of this bubble. Calls change listeners. */ + setText(text: string) { + this.text = text; + this.textArea.value = text; + this.onTextChange(); + } + + /** Sets whether or not the text in the bubble is editable. */ + setEditable(editable: boolean) { + this.editable = editable; + if (this.editable) { + this.textArea.removeAttribute('readonly'); + } else { + this.textArea.setAttribute('readonly', ''); + } + } + + /** Returns whether or not the text in the bubble is editable. */ + isEditable(): boolean { + return this.editable; + } + + /** Adds a change listener to be notified when this bubble's text changes. */ + addTextChangeListener(listener: () => void) { + this.textChangeListeners.push(listener); + } + + /** Adds a change listener to be notified when this bubble's size changes. */ + addSizeChangeListener(listener: () => void) { + this.sizeChangeListeners.push(listener); + } + + /** Creates the editor UI for this bubble. */ + private createEditor(container: SVGGElement): { + inputRoot: SVGForeignObjectElement; + textArea: HTMLTextAreaElement; + } { + const inputRoot = dom.createSvgElement( + Svg.FOREIGNOBJECT, + { + 'x': Bubble.BORDER_WIDTH, + 'y': Bubble.BORDER_WIDTH, + }, + container, + ); + + const body = document.createElementNS(dom.HTML_NS, 'body'); + body.setAttribute('xmlns', dom.HTML_NS); + body.className = 'blocklyMinimalBody'; + + const textArea = document.createElementNS( + dom.HTML_NS, + 'textarea', + ) as HTMLTextAreaElement; + textArea.className = 'blocklyTextarea blocklyText'; + textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR'); + + body.appendChild(textArea); + inputRoot.appendChild(body); + + this.bindTextAreaEvents(textArea); + setTimeout(() => { + textArea.focus(); + }, 0); + + return {inputRoot, textArea}; + } + + /** Binds events to the text area element. */ + private bindTextAreaEvents(textArea: HTMLTextAreaElement) { + // Don't zoom with mousewheel; let it scroll instead. + browserEvents.conditionalBind(textArea, 'wheel', this, (e: Event) => { + e.stopPropagation(); + }); + + browserEvents.conditionalBind( + textArea, + 'focus', + this, + this.onStartEdit, + true, + ); + browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange); + } + + /** Creates the resize handler elements and binds events to them. */ + private createResizeHandle( + container: SVGGElement, + workspace: WorkspaceSvg, + ): SVGGElement { + const resizeHandle = dom.createSvgElement( + Svg.IMAGE, + { + 'class': 'blocklyResizeHandle', + 'href': `${workspace.options.pathToMedia}resize-handle.svg`, + }, + container, + ); + + browserEvents.conditionalBind( + resizeHandle, + 'pointerdown', + this, + this.onResizePointerDown, + ); + + return resizeHandle; + } + + /** + * Sets the size of this bubble, including the border. + * + * @param size Sets the size of this bubble, including the border. + * @param relayout If true, reposition the bubble from scratch so that it is + * optimally visible. If false, reposition it so it maintains the same + * position relative to the anchor. + */ + setSize(size: Size, relayout = false) { + size.width = Math.max(size.width, this.MIN_SIZE.width); + size.height = Math.max(size.height, this.MIN_SIZE.height); + + const widthMinusBorder = size.width - Bubble.DOUBLE_BORDER; + const heightMinusBorder = size.height - Bubble.DOUBLE_BORDER; + this.inputRoot.setAttribute('width', `${widthMinusBorder}`); + this.inputRoot.setAttribute('height', `${heightMinusBorder}`); + + this.resizeGroup.setAttribute('y', `${heightMinusBorder}`); + if (this.workspace.RTL) { + this.resizeGroup.setAttribute('x', `${-Bubble.DOUBLE_BORDER}`); + } else { + this.resizeGroup.setAttribute('x', `${widthMinusBorder}`); + } + + super.setSize(size, relayout); + this.onSizeChange(); + } + + /** @returns the size of this bubble. */ + getSize(): Size { + // Overriden to be public. + return super.getSize(); + } + + /** Handles mouse down events on the resize target. */ + private onResizePointerDown(e: PointerEvent) { + this.bringToFront(); + if (browserEvents.isRightButton(e)) { + e.stopPropagation(); + return; + } + + drag.start( + this.workspace, + e, + new Coordinate( + this.workspace.RTL ? -this.getSize().width : this.getSize().width, + this.getSize().height, + ), + ); + + this.resizePointerUpListener = browserEvents.conditionalBind( + document, + 'pointerup', + this, + this.onResizePointerUp, + ); + this.resizePointerMoveListener = browserEvents.conditionalBind( + document, + 'pointermove', + this, + this.onResizePointerMove, + ); + this.workspace.hideChaff(); + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); + } + + /** Handles pointer up events on the resize target. */ + private onResizePointerUp(_e: PointerEvent) { + touch.clearTouchIdentifier(); + if (this.resizePointerUpListener) { + browserEvents.unbind(this.resizePointerUpListener); + this.resizePointerUpListener = null; + } + if (this.resizePointerMoveListener) { + browserEvents.unbind(this.resizePointerMoveListener); + this.resizePointerMoveListener = null; + } + } + + /** Handles pointer move events on the resize target. */ + private onResizePointerMove(e: PointerEvent) { + const delta = drag.move(this.workspace, e); + this.setSize( + new Size(this.workspace.RTL ? -delta.x : delta.x, delta.y), + false, + ); + this.onSizeChange(); + } + + /** + * Handles starting an edit of the text area. Brings the bubble to the front. + */ + private onStartEdit() { + if (this.bringToFront()) { + // Since the act of moving this node within the DOM causes a loss of + // focus, we need to reapply the focus. + this.textArea.focus(); + } + } + + /** Handles a text change event for the text area. Calls event listeners. */ + private onTextChange() { + this.text = this.textArea.value; + for (const listener of this.textChangeListeners) { + listener(); + } + } + + /** Handles a size change event for the text area. Calls event listeners. */ + private onSizeChange() { + for (const listener of this.sizeChangeListeners) { + listener(); + } + } +} + +Css.register(` +.blocklyTextInputBubble .blocklyTextarea { + background-color: var(--commentFillColour); + border: 0; + box-sizing: border-box; + display: block; + outline: 0; + padding: 5px; + resize: none; + width: 100%; + height: 100%; +} +`); diff --git a/core/bump_objects.ts b/core/bump_objects.ts new file mode 100644 index 00000000000..2aae257dde3 --- /dev/null +++ b/core/bump_objects.ts @@ -0,0 +1,188 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.bumpObjects + +import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; +import type {Abstract} from './events/events_abstract.js'; +import type {BlockCreate} from './events/events_block_create.js'; +import type {BlockMove} from './events/events_block_move.js'; +import type {CommentCreate} from './events/events_comment_create.js'; +import type {CommentMove} from './events/events_comment_move.js'; +import type {CommentResize} from './events/events_comment_resize.js'; +import {isViewportChange} from './events/predicates.js'; +import {BUMP_EVENTS, EventType} from './events/type.js'; +import * as eventUtils from './events/utils.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {ContainerRegion} from './metrics_manager.js'; +import * as mathUtils from './utils/math.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Bumps the given object that has passed out of bounds. + * + * @param workspace The workspace containing the object. + * @param bounds The region to bump an object into. For example, pass + * ScrollMetrics to bump a block into the scrollable region of the + * workspace, or pass ViewMetrics to bump a block into the visible region of + * the workspace. This should be specified in workspace coordinates. + * @param object The object to bump. + * @returns True if object was bumped. + */ +function bumpObjectIntoBounds( + workspace: WorkspaceSvg, + bounds: ContainerRegion, + object: IBoundedElement, +): boolean { + // Compute new top/left position for object. + const objectMetrics = object.getBoundingRectangle(); + const height = objectMetrics.bottom - objectMetrics.top; + const width = objectMetrics.right - objectMetrics.left; + + const topClamp = bounds.top; + const boundsBottom = bounds.top + bounds.height; + const bottomClamp = boundsBottom - height; + // If the object is taller than the workspace we want to + // top-align the block + const newYPosition = mathUtils.clamp( + topClamp, + objectMetrics.top, + bottomClamp, + ); + const deltaY = newYPosition - objectMetrics.top; + + // Note: Even in RTL mode the "anchor" of the object is the + // top-left corner of the object. + let leftClamp = bounds.left; + const boundsRight = bounds.left + bounds.width; + let rightClamp = boundsRight - width; + if (workspace.RTL) { + // If the object is wider than the workspace and we're in RTL + // mode we want to right-align the block, which means setting + // the left clamp to match. + leftClamp = Math.min(rightClamp, leftClamp); + } else { + // If the object is wider than the workspace and we're in LTR + // mode we want to left-align the block, which means setting + // the right clamp to match. + rightClamp = Math.max(leftClamp, rightClamp); + } + const newXPosition = mathUtils.clamp( + leftClamp, + objectMetrics.left, + rightClamp, + ); + const deltaX = newXPosition - objectMetrics.left; + + if (deltaX || deltaY) { + object.moveBy(deltaX, deltaY, ['inbounds']); + return true; + } + return false; +} +export const bumpIntoBounds = bumpObjectIntoBounds; + +/** + * Creates a handler for bumping objects when they cross fixed bounds. + * + * @param workspace The workspace to handle. + * @returns The event handler. + */ +export function bumpIntoBoundsHandler( + workspace: WorkspaceSvg, +): (p1: Abstract) => void { + return (e) => { + const metricsManager = workspace.getMetricsManager(); + if (!metricsManager.hasFixedEdges() || workspace.isDragging()) { + return; + } + + if (BUMP_EVENTS.includes(e.type ?? '')) { + const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true); + + // Triggered by move/create event + const object = extractObjectFromEvent( + workspace, + e as eventUtils.BumpEvent, + ); + if (!object) { + return; + } + // Handle undo. + const existingGroup = eventUtils.getGroup() || false; + eventUtils.setGroup(e.group); + + const wasBumped = bumpObjectIntoBounds( + workspace, + scrollMetricsInWsCoords, + object as IBoundedElement, + ); + + if (wasBumped && !e.group) { + console.warn( + 'Moved object in bounds but there was no' + + ' event group. This may break undo.', + ); + } + eventUtils.setGroup(existingGroup); + } else if (isViewportChange(e)) { + if (e.scale && e.oldScale && e.scale > e.oldScale) { + bumpTopObjectsIntoBounds(workspace); + } + } + }; +} + +/** + * Extracts the object from the given event. + * + * @param workspace The workspace the event originated + * from. + * @param e An event containing an object. + * @returns The extracted + * object. + */ +function extractObjectFromEvent( + workspace: WorkspaceSvg, + e: eventUtils.BumpEvent, +): IBoundedElement | null { + let object = null; + switch (e.type) { + case EventType.BLOCK_CREATE: + case EventType.BLOCK_MOVE: + object = workspace.getBlockById((e as BlockCreate | BlockMove).blockId!); + if (object) { + object = object.getRootBlock(); + } + break; + case EventType.COMMENT_CREATE: + case EventType.COMMENT_MOVE: + case EventType.COMMENT_RESIZE: + object = workspace.getCommentById( + (e as CommentCreate | CommentMove | CommentResize).commentId!, + ) as RenderedWorkspaceComment; + break; + } + return object; +} + +/** + * Bumps the top objects in the given workspace into bounds. + * + * @param workspace The workspace. + */ +export function bumpTopObjectsIntoBounds(workspace: WorkspaceSvg) { + const metricsManager = workspace.getMetricsManager(); + if (!metricsManager.hasFixedEdges() || workspace.isDragging()) { + return; + } + + const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true); + const topBlocks = workspace.getTopBoundedElements(); + for (let i = 0, block; (block = topBlocks[i]); i++) { + bumpObjectIntoBounds(workspace, scrollMetricsInWsCoords, block); + } +} diff --git a/core/clipboard.ts b/core/clipboard.ts new file mode 100644 index 00000000000..62e23fd24a3 --- /dev/null +++ b/core/clipboard.ts @@ -0,0 +1,113 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.clipboard + +import {BlockPaster} from './clipboard/block_paster.js'; +import * as registry from './clipboard/registry.js'; +import type {ICopyData, ICopyable} from './interfaces/i_copyable.js'; +import * as globalRegistry from './registry.js'; +import {Coordinate} from './utils/coordinate.js'; +import {WorkspaceSvg} from './workspace_svg.js'; + +/** Metadata about the object that is currently on the clipboard. */ +let stashedCopyData: ICopyData | null = null; + +let stashedWorkspace: WorkspaceSvg | null = null; + +/** + * Private version of copy for stubbing in tests. + */ +function copyInternal(toCopy: ICopyable): T | null { + const data = toCopy.toCopyData(); + stashedCopyData = data; + stashedWorkspace = (toCopy as any).workspace ?? null; + return data; +} + +/** + * Paste a pasteable element into the workspace. + * + * @param copyData The data to paste into the workspace. + * @param workspace The workspace to paste the data into. + * @param coordinate The location to paste the thing at. + * @returns The pasted thing if the paste was successful, null otherwise. + */ +export function paste( + copyData: T, + workspace: WorkspaceSvg, + coordinate?: Coordinate, +): ICopyable | null; + +/** + * Pastes the last copied ICopyable into the workspace. + * + * @returns the pasted thing if the paste was successful, null otherwise. + */ +export function paste(): ICopyable | null; + +/** + * Pastes the given data into the workspace, or the last copied ICopyable if + * no data is passed. + * + * @param copyData The data to paste into the workspace. + * @param workspace The workspace to paste the data into. + * @param coordinate The location to paste the thing at. + * @returns The pasted thing if the paste was successful, null otherwise. + */ +export function paste( + copyData?: T, + workspace?: WorkspaceSvg, + coordinate?: Coordinate, +): ICopyable | null { + if (!copyData || !workspace) { + if (!stashedCopyData || !stashedWorkspace) return null; + return pasteFromData(stashedCopyData, stashedWorkspace); + } + return pasteFromData(copyData, workspace, coordinate); +} + +/** + * Paste a pasteable element into the workspace. + * + * @param copyData The data to paste into the workspace. + * @param workspace The workspace to paste the data into. + * @param coordinate The location to paste the thing at. + * @returns The pasted thing if the paste was successful, null otherwise. + */ +function pasteFromData( + copyData: T, + workspace: WorkspaceSvg, + coordinate?: Coordinate, +): ICopyable | null { + workspace = workspace.getRootWorkspace() ?? workspace; + return (globalRegistry + .getObject(globalRegistry.Type.PASTER, copyData.paster, false) + ?.paste(copyData, workspace, coordinate) ?? null) as ICopyable | null; +} + +/** + * Private version of duplicate for stubbing in tests. + */ +function duplicateInternal< + U extends ICopyData, + T extends ICopyable & IHasWorkspace, +>(toDuplicate: T): T | null { + const data = toDuplicate.toCopyData(); + if (!data) return null; + return paste(data, toDuplicate.workspace) as T; +} + +interface IHasWorkspace { + workspace: WorkspaceSvg; +} + +export const TEST_ONLY = { + duplicateInternal, + copyInternal, +}; + +export {BlockPaster, registry}; diff --git a/core/clipboard/block_paster.ts b/core/clipboard/block_paster.ts new file mode 100644 index 00000000000..08ff220ee91 --- /dev/null +++ b/core/clipboard/block_paster.ts @@ -0,0 +1,143 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {BlockSvg} from '../block_svg.js'; +import * as common from '../common.js'; +import {config} from '../config.js'; +import {EventType} from '../events/type.js'; +import * as eventUtils from '../events/utils.js'; +import {ICopyData} from '../interfaces/i_copyable.js'; +import {IPaster} from '../interfaces/i_paster.js'; +import {State, append} from '../serialization/blocks.js'; +import {Coordinate} from '../utils/coordinate.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; +import * as registry from './registry.js'; + +export class BlockPaster implements IPaster { + static TYPE = 'block'; + + paste( + copyData: BlockCopyData, + workspace: WorkspaceSvg, + coordinate?: Coordinate, + ): BlockSvg | null { + if (!workspace.isCapacityAvailable(copyData.typeCounts!)) return null; + + if (coordinate) { + copyData.blockState['x'] = coordinate.x; + copyData.blockState['y'] = coordinate.y; + } + + // After appending the block to the workspace, it will be bumped from its neighbors + // However, the algorithm for deciding where to paste a block depends on + // the starting position of the copied block, so we'll pass those coordinates along + const initialCoordinates = + coordinate || + new Coordinate( + copyData.blockState['x'] || 0, + copyData.blockState['y'] || 0, + ); + + eventUtils.disable(); + let block; + try { + block = append(copyData.blockState, workspace) as BlockSvg; + moveBlockToNotConflict(block, initialCoordinates); + } finally { + eventUtils.enable(); + } + + if (!block) return block; + + if (eventUtils.isEnabled() && !block.isShadow()) { + eventUtils.fire(new (eventUtils.get(EventType.BLOCK_CREATE))(block)); + } + common.setSelected(block); + return block; + } +} + +/** + * Moves the given block to a location where it does not: (1) overlap exactly + * with any other blocks, or (2) look like it is connected to any other blocks. + * + * Exported for testing. + * + * @param block The block to move to an unambiguous location. + * @param originalPosition The initial coordinate to start searching from, + * likely the position of the copied block. + * @internal + */ +export function moveBlockToNotConflict( + block: BlockSvg, + originalPosition: Coordinate, +) { + const workspace = block.workspace; + const snapRadius = config.snapRadius; + const bumpOffset = Coordinate.difference( + originalPosition, + block.getRelativeToSurfaceXY(), + ); + const offset = new Coordinate(0, 0); + // getRelativeToSurfaceXY is really expensive, so we want to cache this. + const otherCoords = workspace + .getAllBlocks(false) + .filter((otherBlock) => otherBlock.id != block.id) + .map((b) => b.getRelativeToSurfaceXY()); + + while ( + blockOverlapsOtherExactly( + Coordinate.sum(originalPosition, offset), + otherCoords, + ) || + blockIsInSnapRadius(block, Coordinate.sum(bumpOffset, offset), snapRadius) + ) { + if (workspace.RTL) { + offset.translate(-snapRadius, snapRadius * 2); + } else { + offset.translate(snapRadius, snapRadius * 2); + } + } + + block!.moveTo(Coordinate.sum(originalPosition, offset)); +} + +/** + * @returns true if the given block coordinates are less than a delta of 1 from + * any of the other coordinates. + */ +function blockOverlapsOtherExactly( + coord: Coordinate, + otherCoords: Coordinate[], +): boolean { + return otherCoords.some( + (otherCoord) => + Math.abs(otherCoord.x - coord.x) <= 1 && + Math.abs(otherCoord.y - coord.y) <= 1, + ); +} + +/** + * @returns true if the given block (when offset by the given amount) is close + * enough to any other connections (within the snap radius) that it looks + * like they could connect. + */ +function blockIsInSnapRadius( + block: BlockSvg, + offset: Coordinate, + snapRadius: number, +): boolean { + return block + .getConnections_(false) + .some((connection) => !!connection.closest(snapRadius, offset).connection); +} + +export interface BlockCopyData extends ICopyData { + blockState: State; + typeCounts: {[key: string]: number}; +} + +registry.register(BlockPaster.TYPE, new BlockPaster()); diff --git a/core/clipboard/registry.ts b/core/clipboard/registry.ts new file mode 100644 index 00000000000..1257f5bdbb5 --- /dev/null +++ b/core/clipboard/registry.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {ICopyable, ICopyData} from '../interfaces/i_copyable.js'; +import type {IPaster} from '../interfaces/i_paster.js'; +import * as registry from '../registry.js'; + +/** + * Registers the given paster so that it cna be used for pasting. + * + * @param type The type of the paster to register, e.g. 'block', 'comment', etc. + * @param paster The paster to register. + */ +export function register>( + type: string, + paster: IPaster, +) { + registry.register(registry.Type.PASTER, type, paster); +} + +/** + * Unregisters the paster associated with the given type. + * + * @param type The type of the paster to unregister. + */ +export function unregister(type: string) { + registry.unregister(registry.Type.PASTER, type); +} diff --git a/core/clipboard/workspace_comment_paster.ts b/core/clipboard/workspace_comment_paster.ts new file mode 100644 index 00000000000..fdfbf0a8419 --- /dev/null +++ b/core/clipboard/workspace_comment_paster.ts @@ -0,0 +1,95 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js'; +import * as common from '../common.js'; +import {EventType} from '../events/type.js'; +import * as eventUtils from '../events/utils.js'; +import {ICopyData} from '../interfaces/i_copyable.js'; +import {IPaster} from '../interfaces/i_paster.js'; +import * as commentSerialiation from '../serialization/workspace_comments.js'; +import {Coordinate} from '../utils/coordinate.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; +import * as registry from './registry.js'; + +export class WorkspaceCommentPaster + implements IPaster +{ + static TYPE = 'workspace-comment'; + + paste( + copyData: WorkspaceCommentCopyData, + workspace: WorkspaceSvg, + coordinate?: Coordinate, + ): RenderedWorkspaceComment | null { + const state = copyData.commentState; + + if (coordinate) { + state['x'] = coordinate.x; + state['y'] = coordinate.y; + } + + eventUtils.disable(); + let comment; + try { + comment = commentSerialiation.append( + state, + workspace, + ) as RenderedWorkspaceComment; + moveCommentToNotConflict(comment); + } finally { + eventUtils.enable(); + } + + if (!comment) return null; + + if (eventUtils.isEnabled()) { + eventUtils.fire(new (eventUtils.get(EventType.COMMENT_CREATE))(comment)); + } + common.setSelected(comment); + return comment; + } +} + +function moveCommentToNotConflict(comment: RenderedWorkspaceComment) { + const workspace = comment.workspace; + const translateDistance = 30; + const coord = comment.getRelativeToSurfaceXY(); + const offset = new Coordinate(0, 0); + // getRelativeToSurfaceXY is really expensive, so we want to cache this. + const otherCoords = workspace + .getTopComments(false) + .filter((otherComment) => otherComment.id !== comment.id) + .map((c) => c.getRelativeToSurfaceXY()); + + while ( + commentOverlapsOtherExactly(Coordinate.sum(coord, offset), otherCoords) + ) { + offset.translate( + workspace.RTL ? -translateDistance : translateDistance, + translateDistance, + ); + } + + comment.moveTo(Coordinate.sum(coord, offset)); +} + +function commentOverlapsOtherExactly( + coord: Coordinate, + otherCoords: Coordinate[], +): boolean { + return otherCoords.some( + (otherCoord) => + Math.abs(otherCoord.x - coord.x) <= 1 && + Math.abs(otherCoord.y - coord.y) <= 1, + ); +} + +export interface WorkspaceCommentCopyData extends ICopyData { + commentState: commentSerialiation.State; +} + +registry.register(WorkspaceCommentPaster.TYPE, new WorkspaceCommentPaster()); diff --git a/core/comment.js b/core/comment.js deleted file mode 100644 index f0d5f35374d..00000000000 --- a/core/comment.js +++ /dev/null @@ -1,278 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2011 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Object representing a code comment. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Comment'); - -goog.require('Blockly.Bubble'); -goog.require('Blockly.Icon'); -goog.require('goog.userAgent'); - - -/** - * Class for a comment. - * @param {!Blockly.Block} block The block associated with this comment. - * @extends {Blockly.Icon} - * @constructor - */ -Blockly.Comment = function(block) { - Blockly.Comment.superClass_.constructor.call(this, block); - this.createIcon(); -}; -goog.inherits(Blockly.Comment, Blockly.Icon); - -/** - * Comment text (if bubble is not visible). - * @private - */ -Blockly.Comment.prototype.text_ = ''; - -/** - * Width of bubble. - * @private - */ -Blockly.Comment.prototype.width_ = 160; - -/** - * Height of bubble. - * @private - */ -Blockly.Comment.prototype.height_ = 80; - -/** - * Draw the comment icon. - * @param {!Element} group The icon group. - * @private - */ -Blockly.Comment.prototype.drawIcon_ = function(group) { - // Circle. - Blockly.utils.createSvgElement('circle', - {'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'}, - group); - // Can't use a real '?' text character since different browsers and operating - // systems render it differently. - // Body of question mark. - Blockly.utils.createSvgElement('path', - {'class': 'blocklyIconSymbol', - 'd': 'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405 0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25 -1.201,0.998 -1.201,1.528 -1.204,2.19z'}, - group); - // Dot of question mark. - Blockly.utils.createSvgElement('rect', - {'class': 'blocklyIconSymbol', - 'x': '6.8', 'y': '10.78', 'height': '2', 'width': '2'}, - group); -}; - -/** - * Create the editor for the comment's bubble. - * @return {!Element} The top-level node of the editor. - * @private - */ -Blockly.Comment.prototype.createEditor_ = function() { - /* Create the editor. Here's the markup that will be generated: - - - - - - */ - this.foreignObject_ = Blockly.utils.createSvgElement('foreignObject', - {'x': Blockly.Bubble.BORDER_WIDTH, 'y': Blockly.Bubble.BORDER_WIDTH}, - null); - var body = document.createElementNS(Blockly.HTML_NS, 'body'); - body.setAttribute('xmlns', Blockly.HTML_NS); - body.className = 'blocklyMinimalBody'; - var textarea = document.createElementNS(Blockly.HTML_NS, 'textarea'); - textarea.className = 'blocklyCommentTextarea'; - textarea.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR'); - body.appendChild(textarea); - this.textarea_ = textarea; - this.foreignObject_.appendChild(body); - Blockly.bindEventWithChecks_(textarea, 'mouseup', this, this.textareaFocus_); - // Don't zoom with mousewheel. - Blockly.bindEventWithChecks_(textarea, 'wheel', this, function(e) { - e.stopPropagation(); - }); - Blockly.bindEventWithChecks_(textarea, 'change', this, function(e) { - if (this.text_ != textarea.value) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.block_, 'comment', null, this.text_, textarea.value)); - this.text_ = textarea.value; - } - }); - setTimeout(function() { - textarea.focus(); - }, 0); - return this.foreignObject_; -}; - -/** - * Add or remove editability of the comment. - * @override - */ -Blockly.Comment.prototype.updateEditable = function() { - if (this.isVisible()) { - // Toggling visibility will force a rerendering. - this.setVisible(false); - this.setVisible(true); - } - // Allow the icon to update. - Blockly.Icon.prototype.updateEditable.call(this); -}; - -/** - * Callback function triggered when the bubble has resized. - * Resize the text area accordingly. - * @private - */ -Blockly.Comment.prototype.resizeBubble_ = function() { - if (this.isVisible()) { - var size = this.bubble_.getBubbleSize(); - var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH; - this.foreignObject_.setAttribute('width', size.width - doubleBorderWidth); - this.foreignObject_.setAttribute('height', size.height - doubleBorderWidth); - this.textarea_.style.width = (size.width - doubleBorderWidth - 4) + 'px'; - this.textarea_.style.height = (size.height - doubleBorderWidth - 4) + 'px'; - } -}; - -/** - * Show or hide the comment bubble. - * @param {boolean} visible True if the bubble should be visible. - */ -Blockly.Comment.prototype.setVisible = function(visible) { - if (visible == this.isVisible()) { - // No change. - return; - } - Blockly.Events.fire( - new Blockly.Events.Ui(this.block_, 'commentOpen', !visible, visible)); - if ((!this.block_.isEditable() && !this.textarea_) || goog.userAgent.IE) { - // Steal the code from warnings to make an uneditable text bubble. - // MSIE does not support foreignobject; textareas are impossible. - // http://msdn.microsoft.com/en-us/library/hh834675%28v=vs.85%29.aspx - // Always treat comments in IE as uneditable. - Blockly.Warning.prototype.setVisible.call(this, visible); - return; - } - // Save the bubble stats before the visibility switch. - var text = this.getText(); - var size = this.getBubbleSize(); - if (visible) { - // Create the bubble. - this.bubble_ = new Blockly.Bubble( - /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace), - this.createEditor_(), this.block_.svgPath_, - this.iconXY_, this.width_, this.height_); - this.bubble_.registerResizeEvent(this.resizeBubble_.bind(this)); - this.updateColour(); - } else { - // Dispose of the bubble. - this.bubble_.dispose(); - this.bubble_ = null; - this.textarea_ = null; - this.foreignObject_ = null; - } - // Restore the bubble stats after the visibility switch. - this.setText(text); - this.setBubbleSize(size.width, size.height); -}; - -/** - * Bring the comment to the top of the stack when clicked on. - * @param {!Event} e Mouse up event. - * @private - */ -Blockly.Comment.prototype.textareaFocus_ = function(e) { - // Ideally this would be hooked to the focus event for the comment. - // However doing so in Firefox swallows the cursor for unknown reasons. - // So this is hooked to mouseup instead. No big deal. - this.bubble_.promote_(); - // Since the act of moving this node within the DOM causes a loss of focus, - // we need to reapply the focus. - this.textarea_.focus(); -}; - -/** - * Get the dimensions of this comment's bubble. - * @return {!Object} Object with width and height properties. - */ -Blockly.Comment.prototype.getBubbleSize = function() { - if (this.isVisible()) { - return this.bubble_.getBubbleSize(); - } else { - return {width: this.width_, height: this.height_}; - } -}; - -/** - * Size this comment's bubble. - * @param {number} width Width of the bubble. - * @param {number} height Height of the bubble. - */ -Blockly.Comment.prototype.setBubbleSize = function(width, height) { - if (this.textarea_) { - this.bubble_.setBubbleSize(width, height); - } else { - this.width_ = width; - this.height_ = height; - } -}; - -/** - * Returns this comment's text. - * @return {string} Comment text. - */ -Blockly.Comment.prototype.getText = function() { - return this.textarea_ ? this.textarea_.value : this.text_; -}; - -/** - * Set this comment's text. - * @param {string} text Comment text. - */ -Blockly.Comment.prototype.setText = function(text) { - if (this.text_ != text) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.block_, 'comment', null, this.text_, text)); - this.text_ = text; - } - if (this.textarea_) { - this.textarea_.value = text; - } -}; - -/** - * Dispose of this comment. - */ -Blockly.Comment.prototype.dispose = function() { - if (Blockly.Events.isEnabled()) { - this.setText(''); // Fire event to delete comment. - } - this.block_.comment = null; - Blockly.Icon.prototype.dispose.call(this); -}; diff --git a/core/comments.ts b/core/comments.ts new file mode 100644 index 00000000000..ee85919873a --- /dev/null +++ b/core/comments.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export {CommentView} from './comments/comment_view.js'; +export {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; +export {WorkspaceComment} from './comments/workspace_comment.js'; diff --git a/core/comments/comment_view.ts b/core/comments/comment_view.ts new file mode 100644 index 00000000000..99c14aaa8f2 --- /dev/null +++ b/core/comments/comment_view.ts @@ -0,0 +1,891 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as browserEvents from '../browser_events.js'; +import * as css from '../css.js'; +import {IRenderedElement} from '../interfaces/i_rendered_element.js'; +import * as layers from '../layers.js'; +import * as touch from '../touch.js'; +import {Coordinate} from '../utils/coordinate.js'; +import * as dom from '../utils/dom.js'; +import * as drag from '../utils/drag.js'; +import {Size} from '../utils/size.js'; +import {Svg} from '../utils/svg.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; + +export class CommentView implements IRenderedElement { + /** The root group element of the comment view. */ + private svgRoot: SVGGElement; + + /** + * The svg rect element that we use to create a hightlight around the comment. + */ + private highlightRect: SVGRectElement; + + /** The group containing all of the top bar elements. */ + private topBarGroup: SVGGElement; + + /** The rect background for the top bar. */ + private topBarBackground: SVGRectElement; + + /** The delete icon that goes in the top bar. */ + private deleteIcon: SVGImageElement; + + /** The foldout icon that goes in the top bar. */ + private foldoutIcon: SVGImageElement; + + /** The text element that goes in the top bar. */ + private textPreview: SVGTextElement; + + /** The actual text node in the text preview. */ + private textPreviewNode: Text; + + /** The resize handle element. */ + private resizeHandle: SVGImageElement; + + /** The foreignObject containing the HTML text area. */ + private foreignObject: SVGForeignObjectElement; + + /** The text area where the user can type. */ + private textArea: HTMLTextAreaElement; + + /** The current size of the comment in workspace units. */ + private size: Size = new Size(120, 100); + + /** Whether the comment is collapsed or not. */ + private collapsed: boolean = false; + + /** Whether the comment is editable or not. */ + private editable: boolean = true; + + /** The current location of the comment in workspace coordinates. */ + private location: Coordinate = new Coordinate(0, 0); + + /** The current text of the comment. Updates on text area change. */ + private text: string = ''; + + /** Listeners for changes to text. */ + private textChangeListeners: Array< + (oldText: string, newText: string) => void + > = []; + + /** Listeners for changes to size. */ + private sizeChangeListeners: Array<(oldSize: Size, newSize: Size) => void> = + []; + + /** Listeners for disposal. */ + private disposeListeners: Array<() => void> = []; + + /** Listeners for collapsing. */ + private collapseChangeListeners: Array<(newCollapse: boolean) => void> = []; + + /** + * Event data for the pointer up event on the resize handle. Used to + * unregister the listener. + */ + private resizePointerUpListener: browserEvents.Data | null = null; + + /** + * Event data for the pointer move event on the resize handle. Used to + * unregister the listener. + */ + private resizePointerMoveListener: browserEvents.Data | null = null; + + /** Whether this comment view is currently being disposed or not. */ + private disposing = false; + + /** Whether this comment view has been disposed or not. */ + private disposed = false; + + /** Size of this comment when the resize drag was initiated. */ + private preResizeSize?: Size; + + constructor(private readonly workspace: WorkspaceSvg) { + this.svgRoot = dom.createSvgElement(Svg.G, { + 'class': 'blocklyComment blocklyEditable blocklyDraggable', + }); + + this.highlightRect = this.createHighlightRect(this.svgRoot); + + ({ + topBarGroup: this.topBarGroup, + topBarBackground: this.topBarBackground, + deleteIcon: this.deleteIcon, + foldoutIcon: this.foldoutIcon, + textPreview: this.textPreview, + textPreviewNode: this.textPreviewNode, + } = this.createTopBar(this.svgRoot, workspace)); + + ({foreignObject: this.foreignObject, textArea: this.textArea} = + this.createTextArea(this.svgRoot)); + + this.resizeHandle = this.createResizeHandle(this.svgRoot, workspace); + + // TODO: Remove this comment before merging. + // I think we want comments to exist on the same layer as blocks. + workspace.getLayerManager()?.append(this, layers.BLOCK); + + // Set size to the default size. + this.setSizeWithoutFiringEvents(this.size); + + // Set default transform (including inverted scale for RTL). + this.moveTo(new Coordinate(0, 0)); + } + + /** + * Creates the rect we use for highlighting the comment when it's selected. + */ + private createHighlightRect(svgRoot: SVGGElement): SVGRectElement { + return dom.createSvgElement( + Svg.RECT, + {'class': 'blocklyCommentHighlight'}, + svgRoot, + ); + } + + /** + * Creates the top bar and the elements visually within it. + * Registers event listeners. + */ + private createTopBar( + svgRoot: SVGGElement, + workspace: WorkspaceSvg, + ): { + topBarGroup: SVGGElement; + topBarBackground: SVGRectElement; + deleteIcon: SVGImageElement; + foldoutIcon: SVGImageElement; + textPreview: SVGTextElement; + textPreviewNode: Text; + } { + const topBarGroup = dom.createSvgElement( + Svg.G, + { + 'class': 'blocklyCommentTopbar', + }, + svgRoot, + ); + const topBarBackground = dom.createSvgElement( + Svg.RECT, + { + 'class': 'blocklyCommentTopbarBackground', + }, + topBarGroup, + ); + // TODO: Before merging, does this mean to override an individual image, + // folks need to replace the whole media folder? + const deleteIcon = dom.createSvgElement( + Svg.IMAGE, + { + 'class': 'blocklyDeleteIcon', + 'href': `${workspace.options.pathToMedia}delete-icon.svg`, + }, + topBarGroup, + ); + const foldoutIcon = dom.createSvgElement( + Svg.IMAGE, + { + 'class': 'blocklyFoldoutIcon', + 'href': `${workspace.options.pathToMedia}foldout-icon.svg`, + }, + topBarGroup, + ); + const textPreview = dom.createSvgElement( + Svg.TEXT, + { + 'class': 'blocklyCommentPreview blocklyCommentText blocklyText', + }, + topBarGroup, + ); + const textPreviewNode = document.createTextNode(''); + textPreview.appendChild(textPreviewNode); + + // TODO(toychest): Triggering this on pointerdown means that we can't start + // drags on the foldout icon. We need to open up the gesture system + // to fix this. + browserEvents.conditionalBind( + foldoutIcon, + 'pointerdown', + this, + this.onFoldoutDown, + ); + browserEvents.conditionalBind( + deleteIcon, + 'pointerdown', + this, + this.onDeleteDown, + ); + + return { + topBarGroup, + topBarBackground, + deleteIcon, + foldoutIcon, + textPreview, + textPreviewNode, + }; + } + + /** + * Creates the text area where users can type. Registers event listeners. + */ + private createTextArea(svgRoot: SVGGElement): { + foreignObject: SVGForeignObjectElement; + textArea: HTMLTextAreaElement; + } { + const foreignObject = dom.createSvgElement( + Svg.FOREIGNOBJECT, + { + 'class': 'blocklyCommentForeignObject', + }, + svgRoot, + ); + const body = document.createElementNS(dom.HTML_NS, 'body'); + body.setAttribute('xmlns', dom.HTML_NS); + body.className = 'blocklyMinimalBody'; + const textArea = document.createElementNS( + dom.HTML_NS, + 'textarea', + ) as HTMLTextAreaElement; + dom.addClass(textArea, 'blocklyCommentText'); + dom.addClass(textArea, 'blocklyTextarea'); + dom.addClass(textArea, 'blocklyText'); + body.appendChild(textArea); + foreignObject.appendChild(body); + + browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange); + + return {foreignObject, textArea}; + } + + /** Creates the DOM elements for the comment resize handle. */ + private createResizeHandle( + svgRoot: SVGGElement, + workspace: WorkspaceSvg, + ): SVGImageElement { + const resizeHandle = dom.createSvgElement( + Svg.IMAGE, + { + 'class': 'blocklyResizeHandle', + 'href': `${workspace.options.pathToMedia}resize-handle.svg`, + }, + svgRoot, + ); + + browserEvents.conditionalBind( + resizeHandle, + 'pointerdown', + this, + this.onResizePointerDown, + ); + + return resizeHandle; + } + + /** Returns the root SVG group element of the comment view. */ + getSvgRoot(): SVGGElement { + return this.svgRoot; + } + + /** + * Returns the current size of the comment in workspace units. + * Respects collapsing. + */ + getSize(): Size { + return this.collapsed ? this.topBarBackground.getBBox() : this.size; + } + + /** + * Sets the size of the comment in workspace units, and updates the view + * elements to reflect the new size. + */ + setSizeWithoutFiringEvents(size: Size) { + const topBarSize = this.topBarBackground.getBBox(); + const deleteSize = this.deleteIcon.getBBox(); + const foldoutSize = this.foldoutIcon.getBBox(); + const textPreviewSize = this.textPreview.getBBox(); + const resizeSize = this.resizeHandle.getBBox(); + + size = Size.max( + size, + this.calcMinSize(topBarSize, foldoutSize, deleteSize), + ); + this.size = size; + + this.svgRoot.setAttribute('height', `${size.height}`); + this.svgRoot.setAttribute('width', `${size.width}`); + + this.updateHighlightRect(size); + this.updateTopBarSize(size); + this.updateTextAreaSize(size, topBarSize); + this.updateDeleteIconPosition(size, topBarSize, deleteSize); + this.updateFoldoutIconPosition(topBarSize, foldoutSize); + this.updateTextPreviewSize( + size, + topBarSize, + textPreviewSize, + deleteSize, + resizeSize, + ); + this.updateResizeHandlePosition(size, resizeSize); + } + + /** + * Sets the size of the comment in workspace units, updates the view + * elements to reflect the new size, and triggers size change listeners. + */ + setSize(size: Size) { + const oldSize = this.preResizeSize || this.size; + this.setSizeWithoutFiringEvents(size); + this.onSizeChange(oldSize, this.size); + } + + /** + * Calculates the minimum size for the uncollapsed comment based on text + * size and visible icons. + * + * The minimum width is based on the width of the truncated preview text. + * + * The minimum height is based on the height of the top bar. + */ + private calcMinSize( + topBarSize: Size, + foldoutSize: Size, + deleteSize: Size, + ): Size { + this.updateTextPreview(this.textArea.value ?? ''); + const textPreviewWidth = dom.getTextWidth(this.textPreview); + + const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize); + const deleteMargin = this.calcDeleteMargin(topBarSize, deleteSize); + + let width = textPreviewWidth; + if (this.foldoutIcon.checkVisibility()) { + width += foldoutSize.width + foldoutMargin * 2; + } else if (textPreviewWidth) { + width += 4; // Arbitrary margin before text. + } + if (this.deleteIcon.checkVisibility()) { + width += deleteSize.width + deleteMargin * 2; + } else if (textPreviewWidth) { + width += 4; // Arbitrary margin after text. + } + + // Arbitrary additional height. + const height = topBarSize.height + 20; + + return new Size(width, height); + } + + /** Calculates the margin that should exist around the delete icon. */ + private calcDeleteMargin(topBarSize: Size, deleteSize: Size) { + return (topBarSize.height - deleteSize.height) / 2; + } + + /** Calculates the margin that should exist around the foldout icon. */ + private calcFoldoutMargin(topBarSize: Size, foldoutSize: Size) { + return (topBarSize.height - foldoutSize.height) / 2; + } + + /** Updates the size of the highlight rect to reflect the new size. */ + private updateHighlightRect(size: Size) { + this.highlightRect.setAttribute('height', `${size.height}`); + this.highlightRect.setAttribute('width', `${size.width}`); + if (this.workspace.RTL) { + this.highlightRect.setAttribute('x', `${-size.width}`); + } + } + + /** Updates the size of the top bar to reflect the new size. */ + private updateTopBarSize(size: Size) { + this.topBarBackground.setAttribute('width', `${size.width}`); + } + + /** Updates the size of the text area elements to reflect the new size. */ + private updateTextAreaSize(size: Size, topBarSize: Size) { + this.foreignObject.setAttribute( + 'height', + `${size.height - topBarSize.height}`, + ); + this.foreignObject.setAttribute('width', `${size.width}`); + this.foreignObject.setAttribute('y', `${topBarSize.height}`); + if (this.workspace.RTL) { + this.foreignObject.setAttribute('x', `${-size.width}`); + } + } + + /** + * Updates the position of the delete icon elements to reflect the new size. + */ + private updateDeleteIconPosition( + size: Size, + topBarSize: Size, + deleteSize: Size, + ) { + const deleteMargin = this.calcDeleteMargin(topBarSize, deleteSize); + this.deleteIcon.setAttribute('y', `${deleteMargin}`); + this.deleteIcon.setAttribute( + 'x', + `${size.width - deleteSize.width - deleteMargin}`, + ); + } + + /** + * Updates the position of the foldout icon elements to reflect the new size. + */ + private updateFoldoutIconPosition(topBarSize: Size, foldoutSize: Size) { + const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize); + this.foldoutIcon.setAttribute('y', `${foldoutMargin}`); + this.foldoutIcon.setAttribute('x', `${foldoutMargin}`); + } + + /** + * Updates the size and position of the text preview elements to reflect the new size. + */ + private updateTextPreviewSize( + size: Size, + topBarSize: Size, + textPreviewSize: Size, + deleteSize: Size, + foldoutSize: Size, + ) { + const textPreviewMargin = (topBarSize.height - textPreviewSize.height) / 2; + const deleteMargin = this.calcDeleteMargin(topBarSize, deleteSize); + const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize); + + const textPreviewWidth = + size.width - + foldoutSize.width - + foldoutMargin * 2 - + deleteSize.width - + deleteMargin * 2; + this.textPreview.setAttribute( + 'x', + `${ + foldoutSize.width + foldoutMargin * 2 * (this.workspace.RTL ? -1 : 1) + }`, + ); + this.textPreview.setAttribute( + 'y', + `${textPreviewMargin + textPreviewSize.height / 2}`, + ); + this.textPreview.setAttribute('width', `${textPreviewWidth}`); + } + + /** Updates the position of the resize handle to reflect the new size. */ + private updateResizeHandlePosition(size: Size, resizeSize: Size) { + this.resizeHandle.setAttribute('y', `${size.height - resizeSize.height}`); + this.resizeHandle.setAttribute('x', `${size.width - resizeSize.width}`); + } + + /** + * Triggers listeners when the size of the comment changes, either + * programmatically or manually by the user. + */ + private onSizeChange(oldSize: Size, newSize: Size) { + // Loop through listeners backwards in case they remove themselves. + for (let i = this.sizeChangeListeners.length - 1; i >= 0; i--) { + this.sizeChangeListeners[i](oldSize, newSize); + } + } + + /** + * Registers a callback that listens for size changes. + * + * @param listener Receives callbacks when the size of the comment changes. + * The new and old size are in workspace units. + */ + addSizeChangeListener(listener: (oldSize: Size, newSize: Size) => void) { + this.sizeChangeListeners.push(listener); + } + + /** Removes the given listener from the list of size change listeners. */ + removeSizeChangeListener(listener: () => void) { + this.sizeChangeListeners.splice( + this.sizeChangeListeners.indexOf(listener), + 1, + ); + } + + /** + * Handles starting an interaction with the resize handle to resize the + * comment. + */ + private onResizePointerDown(e: PointerEvent) { + if (!this.isEditable()) return; + + this.bringToFront(); + if (browserEvents.isRightButton(e)) { + e.stopPropagation(); + return; + } + + this.preResizeSize = this.getSize(); + + drag.start( + this.workspace, + e, + new Coordinate( + this.workspace.RTL ? -this.getSize().width : this.getSize().width, + this.getSize().height, + ), + ); + + this.resizePointerUpListener = browserEvents.conditionalBind( + document, + 'pointerup', + this, + this.onResizePointerUp, + ); + this.resizePointerMoveListener = browserEvents.conditionalBind( + document, + 'pointermove', + this, + this.onResizePointerMove, + ); + + this.workspace.hideChaff(); + + e.stopPropagation(); + } + + /** Ends an interaction with the resize handle. */ + private onResizePointerUp(_e: PointerEvent) { + touch.clearTouchIdentifier(); + if (this.resizePointerUpListener) { + browserEvents.unbind(this.resizePointerUpListener); + this.resizePointerUpListener = null; + } + if (this.resizePointerMoveListener) { + browserEvents.unbind(this.resizePointerMoveListener); + this.resizePointerMoveListener = null; + } + // When ending a resize drag, notify size change listeners to fire an event. + this.setSize(this.size); + this.preResizeSize = undefined; + } + + /** Resizes the comment in response to a drag on the resize handle. */ + private onResizePointerMove(e: PointerEvent) { + const size = drag.move(this.workspace, e); + this.setSizeWithoutFiringEvents( + new Size(this.workspace.RTL ? -size.x : size.x, size.y), + ); + } + + /** Returns true if the comment is currently collapsed. */ + isCollapsed(): boolean { + return this.collapsed; + } + + /** Sets whether the comment is currently collapsed or not. */ + setCollapsed(collapsed: boolean) { + this.collapsed = collapsed; + if (collapsed) { + dom.addClass(this.svgRoot, 'blocklyCollapsed'); + } else { + dom.removeClass(this.svgRoot, 'blocklyCollapsed'); + } + // Repositions resize handle and such. + this.setSizeWithoutFiringEvents(this.size); + this.onCollapse(); + } + + /** + * Triggers listeners when the collapsed-ness of the comment changes, either + * progrmatically or manually by the user. + */ + private onCollapse() { + // Loop through listeners backwards in case they remove themselves. + for (let i = this.collapseChangeListeners.length - 1; i >= 0; i--) { + this.collapseChangeListeners[i](this.collapsed); + } + } + + /** Registers a callback that listens for collapsed-ness changes. */ + addOnCollapseListener(listener: (newCollapse: boolean) => void) { + this.collapseChangeListeners.push(listener); + } + + /** Removes the given listener from the list of on collapse listeners. */ + removeOnCollapseListener(listener: () => void) { + this.collapseChangeListeners.splice( + this.collapseChangeListeners.indexOf(listener), + 1, + ); + } + + /** + * Toggles the collapsedness of the block when we receive a pointer down + * event on the foldout icon. + */ + private onFoldoutDown(e: PointerEvent) { + touch.clearTouchIdentifier(); + this.bringToFront(); + if (browserEvents.isRightButton(e)) { + e.stopPropagation(); + return; + } + + this.setCollapsed(!this.collapsed); + + this.workspace.hideChaff(); + + e.stopPropagation(); + } + + /** Returns true if the comment is currently editable. */ + isEditable(): boolean { + return this.editable; + } + + /** Sets the editability of the comment. */ + setEditable(editable: boolean) { + this.editable = editable; + if (this.editable) { + dom.addClass(this.svgRoot, 'blocklyEditable'); + dom.removeClass(this.svgRoot, 'blocklyReadonly'); + this.textArea.removeAttribute('readonly'); + } else { + dom.removeClass(this.svgRoot, 'blocklyEditable'); + dom.addClass(this.svgRoot, 'blocklyReadonly'); + this.textArea.setAttribute('readonly', 'true'); + } + } + + /** Returns the current location of the comment in workspace coordinates. */ + getRelativeToSurfaceXY(): Coordinate { + return this.location; + } + + /** + * Moves the comment view to the given location. + * + * @param location The location to move to in workspace coordinates. + */ + moveTo(location: Coordinate) { + this.location = location; + this.svgRoot.setAttribute( + 'transform', + `translate(${location.x}, ${location.y})`, + ); + } + + /** Retursn the current text of the comment. */ + getText() { + return this.text; + } + + /** Sets the current text of the comment. */ + setText(text: string) { + this.textArea.value = text; + this.onTextChange(); + } + + /** Registers a callback that listens for text changes. */ + addTextChangeListener(listener: (oldText: string, newText: string) => void) { + this.textChangeListeners.push(listener); + } + + /** Removes the given listener from the list of text change listeners. */ + removeTextChangeListener(listener: () => void) { + this.textChangeListeners.splice( + this.textChangeListeners.indexOf(listener), + 1, + ); + } + + /** + * Triggers listeners when the text of the comment changes, either + * programmatically or manually by the user. + */ + private onTextChange() { + const oldText = this.text; + this.text = this.textArea.value; + this.updateTextPreview(this.text); + // Update size in case our minimum size increased. + this.setSize(this.size); + // Loop through listeners backwards in case they remove themselves. + for (let i = this.textChangeListeners.length - 1; i >= 0; i--) { + this.textChangeListeners[i](oldText, this.text); + } + } + + /** Updates the preview text element to reflect the given text. */ + private updateTextPreview(text: string) { + this.textPreviewNode.textContent = this.truncateText(text); + } + + /** Truncates the text to fit within the top view. */ + private truncateText(text: string): string { + return text.length >= 12 ? `${text.substring(0, 9)}...` : text; + } + + /** Brings the workspace comment to the front of its layer. */ + private bringToFront() { + const parent = this.svgRoot.parentNode; + const childNodes = parent!.childNodes; + // Avoid moving the comment if it's already at the bottom. + if (childNodes[childNodes.length - 1] !== this.svgRoot) { + parent!.appendChild(this.svgRoot); + } + } + + /** + * Handles disposing of the comment when we get a pointer down event on the + * delete icon. + */ + private onDeleteDown(e: PointerEvent) { + touch.clearTouchIdentifier(); + if (browserEvents.isRightButton(e)) { + e.stopPropagation(); + return; + } + + this.dispose(); + e.stopPropagation(); + } + + /** Disposes of this comment view. */ + dispose() { + this.disposing = true; + dom.removeNode(this.svgRoot); + // Loop through listeners backwards in case they remove themselves. + for (let i = this.disposeListeners.length - 1; i >= 0; i--) { + this.disposeListeners[i](); + } + this.disposed = true; + } + + /** Returns whether this comment view has been disposed or not. */ + isDisposed(): boolean { + return this.disposed; + } + + /** + * Returns true if this comment view is currently being disposed or has + * already been disposed. + */ + isDeadOrDying(): boolean { + return this.disposing || this.disposed; + } + + /** Registers a callback that listens for disposal of this view. */ + addDisposeListener(listener: () => void) { + this.disposeListeners.push(listener); + } + + /** Removes the given listener from the list of disposal listeners. */ + removeDisposeListener(listener: () => void) { + this.disposeListeners.splice(this.disposeListeners.indexOf(listener), 1); + } +} + +css.register(` +.injectionDiv { + --commentFillColour: #FFFCC7; + --commentBorderColour: #F2E49B; +} + +.blocklyComment .blocklyTextarea { + background-color: var(--commentFillColour); + border: 1px solid var(--commentBorderColour); + box-sizing: border-box; + display: block; + outline: 0; + padding: 5px; + resize: none; + width: 100%; + height: 100%; +} + +.blocklyReadonly.blocklyComment .blocklyTextarea { + cursor: inherit; +} + +.blocklyDeleteIcon { + width: 20px; + height: 20px; + display: none; + cursor: pointer; +} + +.blocklyFoldoutIcon { + width: 20px; + height: 20px; + transform-origin: 12px 12px; + cursor: pointer; +} +.blocklyResizeHandle { + width: 12px; + height: 12px; + cursor: se-resize; +} +.blocklyReadonly.blocklyComment .blocklyResizeHandle { + cursor: inherit; +} + +.blocklyCommentTopbarBackground { + fill: var(--commentBorderColour); + height: 24px; +} + +.blocklyComment .blocklyCommentPreview.blocklyText { + fill: #000; + dominant-baseline: middle; + visibility: hidden; +} + +.blocklyCollapsed.blocklyComment .blocklyCommentPreview { + visibility: visible; +} + +.blocklyCollapsed.blocklyComment .blocklyCommentForeignObject, +.blocklyCollapsed.blocklyComment .blocklyResizeHandle { + display: none; +} + +.blocklyCollapsed.blocklyComment .blocklyFoldoutIcon { + transform: rotate(-90deg); +} + +.blocklyRTL .blocklyCommentTopbar { + transform: scale(-1, 1); +} + +.blocklyRTL .blocklyCommentForeignObject { + direction: rtl; +} + +.blocklyRTL .blocklyCommentPreview { + /* Revert the scale and control RTL using direction instead. */ + transform: scale(-1, 1); + direction: rtl; +} + +.blocklyRTL .blocklyResizeHandle { + transform: scale(-1, 1); + cursor: sw-resize; +} + +.blocklyCommentHighlight { + fill: none; +} + +.blocklySelected .blocklyCommentHighlight { + stroke: #fc3; + stroke-width: 3px; +} + +.blocklyCollapsed.blocklySelected .blocklyCommentHighlight { + stroke: none; +} + +.blocklyCollapsed.blocklySelected .blocklyCommentTopbarBackground { + stroke: #fc3; + stroke-width: 3px; +} +`); diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts new file mode 100644 index 00000000000..ff21335741b --- /dev/null +++ b/core/comments/rendered_workspace_comment.ts @@ -0,0 +1,294 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as browserEvents from '../browser_events.js'; +import { + WorkspaceCommentCopyData, + WorkspaceCommentPaster, +} from '../clipboard/workspace_comment_paster.js'; +import * as common from '../common.js'; +import * as contextMenu from '../contextmenu.js'; +import {ContextMenuRegistry} from '../contextmenu_registry.js'; +import {CommentDragStrategy} from '../dragging/comment_drag_strategy.js'; +import {IBoundedElement} from '../interfaces/i_bounded_element.js'; +import {IContextMenu} from '../interfaces/i_contextmenu.js'; +import {ICopyable} from '../interfaces/i_copyable.js'; +import {IDeletable} from '../interfaces/i_deletable.js'; +import {IDraggable} from '../interfaces/i_draggable.js'; +import {IRenderedElement} from '../interfaces/i_rendered_element.js'; +import {ISelectable} from '../interfaces/i_selectable.js'; +import * as layers from '../layers.js'; +import * as commentSerialization from '../serialization/workspace_comments.js'; +import {Coordinate} from '../utils/coordinate.js'; +import * as dom from '../utils/dom.js'; +import {Rect} from '../utils/rect.js'; +import {Size} from '../utils/size.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; +import {CommentView} from './comment_view.js'; +import {WorkspaceComment} from './workspace_comment.js'; + +export class RenderedWorkspaceComment + extends WorkspaceComment + implements + IBoundedElement, + IRenderedElement, + IDraggable, + ISelectable, + IDeletable, + ICopyable, + IContextMenu +{ + /** The class encompassing the svg elements making up the workspace comment. */ + private view: CommentView; + + public readonly workspace: WorkspaceSvg; + + private dragStrategy = new CommentDragStrategy(this); + + /** Constructs the workspace comment, including the view. */ + constructor(workspace: WorkspaceSvg, id?: string) { + super(workspace, id); + + this.workspace = workspace; + + this.view = new CommentView(workspace); + // Set the size to the default size as defined in the superclass. + this.view.setSize(this.getSize()); + this.view.setEditable(this.isEditable()); + this.view.getSvgRoot().setAttribute('data-id', this.id); + + this.addModelUpdateBindings(); + + browserEvents.conditionalBind( + this.view.getSvgRoot(), + 'pointerdown', + this, + this.startGesture, + ); + // Don't zoom with mousewheel; let it scroll instead. + browserEvents.conditionalBind( + this.view.getSvgRoot(), + 'wheel', + this, + (e: Event) => { + e.stopPropagation(); + }, + ); + } + + /** + * Adds listeners to the view that updates the model (i.e. the superclass) + * when changes are made to the view. + */ + private addModelUpdateBindings() { + this.view.addTextChangeListener( + (_, newText: string) => void super.setText(newText), + ); + this.view.addSizeChangeListener( + (_, newSize: Size) => void super.setSize(newSize), + ); + this.view.addOnCollapseListener( + () => void super.setCollapsed(this.view.isCollapsed()), + ); + this.view.addDisposeListener(() => { + if (!this.isDeadOrDying()) this.dispose(); + }); + } + + /** Sets the text of the comment. */ + override setText(text: string): void { + // setText will trigger the change listener that updates + // the model aka superclass. + this.view.setText(text); + } + + /** Sets the size of the comment. */ + override setSize(size: Size) { + // setSize will trigger the change listener that updates + // the model aka superclass. + this.view.setSize(size); + } + + /** Sets whether the comment is collapsed or not. */ + override setCollapsed(collapsed: boolean) { + // setCollapsed will trigger the change listener that updates + // the model aka superclass. + this.view.setCollapsed(collapsed); + } + + /** Sets whether the comment is editable or not. */ + override setEditable(editable: boolean): void { + super.setEditable(editable); + // Use isEditable rather than isOwnEditable to account for workspace state. + this.view.setEditable(this.isEditable()); + } + + /** Returns the root SVG element of this comment. */ + getSvgRoot(): SVGElement { + return this.view.getSvgRoot(); + } + + /** + * Returns the comment's size in workspace units. + * Does not respect collapsing. + */ + getSize(): Size { + return super.getSize(); + } + + /** + * Returns the bounding rectangle of this comment in workspace coordinates. + * Respects collapsing. + */ + getBoundingRectangle(): Rect { + const loc = this.getRelativeToSurfaceXY(); + const size = this.view?.getSize() ?? this.getSize(); + let left; + let right; + if (this.workspace.RTL) { + left = loc.x - size.width; + right = loc.x; + } else { + left = loc.x; + right = loc.x + size.width; + } + return new Rect(loc.y, loc.y + size.height, left, right); + } + + /** Move the comment by the given amounts in workspace coordinates. */ + moveBy(dx: number, dy: number, reason?: string[] | undefined): void { + const loc = this.getRelativeToSurfaceXY(); + const newLoc = new Coordinate(loc.x + dx, loc.y + dy); + this.moveTo(newLoc, reason); + } + + /** Moves the comment to the given location in workspace coordinates. */ + override moveTo(location: Coordinate, reason?: string[] | undefined): void { + super.moveTo(location, reason); + this.view.moveTo(location); + } + + /** + * Moves the comment during a drag. Doesn't fire move events. + * + * @internal + */ + moveDuringDrag(location: Coordinate): void { + this.location = location; + this.view.moveTo(location); + } + + /** + * Adds the dragging CSS class to this comment. + * + * @internal + */ + setDragging(dragging: boolean): void { + if (dragging) { + dom.addClass(this.getSvgRoot(), 'blocklyDragging'); + } else { + dom.removeClass(this.getSvgRoot(), 'blocklyDragging'); + } + } + + /** Disposes of the view. */ + override dispose() { + this.disposing = true; + if (!this.view.isDeadOrDying()) this.view.dispose(); + super.dispose(); + } + + /** + * Starts a gesture because we detected a pointer down on the comment + * (that wasn't otherwise gobbled up, e.g. by resizing). + */ + private startGesture(e: PointerEvent) { + const gesture = this.workspace.getGesture(e); + if (gesture) { + gesture.handleCommentStart(e, this); + this.workspace.getLayerManager()?.append(this, layers.BLOCK); + common.setSelected(this); + } + } + + /** Visually indicates that this comment would be deleted if dropped. */ + setDeleteStyle(wouldDelete: boolean): void { + if (wouldDelete) { + dom.addClass(this.getSvgRoot(), 'blocklyDraggingDelete'); + } else { + dom.removeClass(this.getSvgRoot(), 'blocklyDraggingDelete'); + } + } + + /** Returns whether this comment is movable or not. */ + isMovable(): boolean { + return this.dragStrategy.isMovable(); + } + + /** Starts a drag on the comment. */ + startDrag(): void { + this.dragStrategy.startDrag(); + } + + /** Drags the comment to the given location. */ + drag(newLoc: Coordinate): void { + this.dragStrategy.drag(newLoc); + } + + /** Ends the drag on the comment. */ + endDrag(): void { + this.dragStrategy.endDrag(); + } + + /** Moves the comment back to where it was at the start of a drag. */ + revertDrag(): void { + this.dragStrategy.revertDrag(); + } + + /** Visually highlights the comment. */ + select(): void { + dom.addClass(this.getSvgRoot(), 'blocklySelected'); + } + + /** Visually unhighlights the comment. */ + unselect(): void { + dom.removeClass(this.getSvgRoot(), 'blocklySelected'); + } + + /** + * Returns a JSON serializable representation of this comment's state that + * can be used for pasting. + */ + toCopyData(): WorkspaceCommentCopyData | null { + return { + paster: WorkspaceCommentPaster.TYPE, + commentState: commentSerialization.save(this, { + addCoordinates: true, + }), + }; + } + + /** Show a context menu for this comment. */ + showContextMenu(e: PointerEvent): void { + const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( + ContextMenuRegistry.ScopeType.COMMENT, + {comment: this}, + ); + contextMenu.show(e, menuOptions, this.workspace.RTL, this.workspace); + } + + /** Snap this comment to the nearest grid point. */ + snapToGrid(): void { + if (this.isDeadOrDying()) return; + const grid = this.workspace.getGrid(); + if (!grid?.shouldSnap()) return; + const currentXY = this.getRelativeToSurfaceXY(); + const alignedXY = grid.alignXY(currentXY); + if (alignedXY !== currentXY) { + this.moveTo(alignedXY, ['snap']); + } + } +} diff --git a/core/comments/workspace_comment.ts b/core/comments/workspace_comment.ts new file mode 100644 index 00000000000..2d59c715edd --- /dev/null +++ b/core/comments/workspace_comment.ts @@ -0,0 +1,240 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {CommentMove} from '../events/events_comment_move.js'; +import {CommentResize} from '../events/events_comment_resize.js'; +import {EventType} from '../events/type.js'; +import * as eventUtils from '../events/utils.js'; +import {Coordinate} from '../utils/coordinate.js'; +import * as idGenerator from '../utils/idgenerator.js'; +import {Size} from '../utils/size.js'; +import {Workspace} from '../workspace.js'; + +export class WorkspaceComment { + /** The unique identifier for this comment. */ + public readonly id: string; + + /** The text of the comment. */ + private text = ''; + + /** The size of the comment in workspace units. */ + private size = new Size(120, 100); + + /** Whether the comment is collapsed or not. */ + private collapsed = false; + + /** Whether the comment is editable or not. */ + private editable = true; + + /** Whether the comment is movable or not. */ + private movable = true; + + /** Whether the comment is deletable or not. */ + private deletable = true; + + /** The location of the comment in workspace coordinates. */ + protected location = new Coordinate(0, 0); + + /** Whether this comment has been disposed or not. */ + protected disposed = false; + + /** Whether this comment is being disposed or not. */ + protected disposing = false; + + /** + * Constructs the comment. + * + * @param workspace The workspace to construct the comment in. + * @param id An optional ID to give to the comment. If not provided, one will + * be generated. + */ + constructor( + public readonly workspace: Workspace, + id?: string, + ) { + this.id = id && !workspace.getCommentById(id) ? id : idGenerator.genUid(); + + workspace.addTopComment(this); + + this.fireCreateEvent(); + } + + private fireCreateEvent() { + if (eventUtils.isEnabled()) { + eventUtils.fire(new (eventUtils.get(EventType.COMMENT_CREATE))(this)); + } + } + + private fireDeleteEvent() { + if (eventUtils.isEnabled()) { + eventUtils.fire(new (eventUtils.get(EventType.COMMENT_DELETE))(this)); + } + } + + /** Fires a comment change event. */ + private fireChangeEvent(oldText: string, newText: string) { + if (eventUtils.isEnabled()) { + eventUtils.fire( + new (eventUtils.get(EventType.COMMENT_CHANGE))(this, oldText, newText), + ); + } + } + + /** Fires a comment collapse event. */ + private fireCollapseEvent(newCollapsed: boolean) { + if (eventUtils.isEnabled()) { + eventUtils.fire( + new (eventUtils.get(EventType.COMMENT_COLLAPSE))(this, newCollapsed), + ); + } + } + + /** Sets the text of the comment. */ + setText(text: string) { + const oldText = this.text; + this.text = text; + this.fireChangeEvent(oldText, text); + } + + /** Returns the text of the comment. */ + getText(): string { + return this.text; + } + + /** Sets the comment's size in workspace units. */ + setSize(size: Size) { + const event = new (eventUtils.get(EventType.COMMENT_RESIZE))( + this, + ) as CommentResize; + + this.size = size; + + event.recordCurrentSizeAsNewSize(); + eventUtils.fire(event); + } + + /** Returns the comment's size in workspace units. */ + getSize(): Size { + return this.size; + } + + /** Sets whether the comment is collapsed or not. */ + setCollapsed(collapsed: boolean) { + this.collapsed = collapsed; + this.fireCollapseEvent(collapsed); + } + + /** Returns whether the comment is collapsed or not. */ + isCollapsed(): boolean { + return this.collapsed; + } + + /** Sets whether the comment is editable or not. */ + setEditable(editable: boolean) { + this.editable = editable; + } + + /** + * Returns whether the comment is editable or not, respecting whether the + * workspace is read-only. + */ + isEditable(): boolean { + return this.isOwnEditable() && !this.workspace.options.readOnly; + } + + /** + * Returns whether the comment is editable or not, only examining its own + * state and ignoring the state of the workspace. + */ + isOwnEditable(): boolean { + return this.editable; + } + + /** Sets whether the comment is movable or not. */ + setMovable(movable: boolean) { + this.movable = movable; + } + + /** + * Returns whether the comment is movable or not, respecting whether the + * workspace is read-only. + */ + isMovable() { + return this.isOwnMovable() && !this.workspace.options.readOnly; + } + + /** + * Returns whether the comment is movable or not, only examining its own + * state and ignoring the state of the workspace. + */ + isOwnMovable() { + return this.movable; + } + + /** Sets whether the comment is deletable or not. */ + setDeletable(deletable: boolean) { + this.deletable = deletable; + } + + /** + * Returns whether the comment is deletable or not, respecting whether the + * workspace is read-only. + */ + isDeletable(): boolean { + return ( + this.isOwnDeletable() && + !this.isDeadOrDying() && + !this.workspace.options.readOnly + ); + } + + /** + * Returns whether the comment is deletable or not, only examining its own + * state and ignoring the state of the workspace. + */ + isOwnDeletable(): boolean { + return this.deletable; + } + + /** Moves the comment to the given location in workspace coordinates. */ + moveTo(location: Coordinate, reason?: string[] | undefined) { + const event = new (eventUtils.get(EventType.COMMENT_MOVE))( + this, + ) as CommentMove; + if (reason) event.setReason(reason); + + this.location = location; + + event.recordNew(); + eventUtils.fire(event); + } + + /** Returns the position of the comment in workspace coordinates. */ + getRelativeToSurfaceXY(): Coordinate { + return this.location; + } + + /** Disposes of this comment. */ + dispose() { + this.disposing = true; + this.fireDeleteEvent(); + this.workspace.removeTopComment(this); + this.disposed = true; + } + + /** Returns whether the comment has been disposed or not. */ + isDisposed() { + return this.disposed; + } + + /** + * Returns true if this comment view is currently being disposed or has + * already been disposed. + */ + isDeadOrDying(): boolean { + return this.disposing || this.disposed; + } +} diff --git a/core/common.ts b/core/common.ts new file mode 100644 index 00000000000..bc31bf17eea --- /dev/null +++ b/core/common.ts @@ -0,0 +1,305 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.common + +import type {Block} from './block.js'; +import {ISelectable} from './blockly.js'; +import {BlockDefinition, Blocks} from './blocks.js'; +import type {Connection} from './connection.js'; +import {EventType} from './events/type.js'; +import * as eventUtils from './events/utils.js'; +import type {Workspace} from './workspace.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** Database of all workspaces. */ +const WorkspaceDB_ = Object.create(null); + +/** + * Find the workspace with the specified ID. + * + * @param id ID of workspace to find. + * @returns The sought after workspace or null if not found. + */ +export function getWorkspaceById(id: string): Workspace | null { + return WorkspaceDB_[id] || null; +} + +/** + * Find all workspaces. + * + * @returns Array of workspaces. + */ +export function getAllWorkspaces(): Workspace[] { + const workspaces = []; + for (const workspaceId in WorkspaceDB_) { + workspaces.push(WorkspaceDB_[workspaceId]); + } + return workspaces; +} + +/** + * Register a workspace in the workspace db. + * + * @param workspace + */ +export function registerWorkspace(workspace: Workspace) { + WorkspaceDB_[workspace.id] = workspace; +} + +/** + * Unregister a workspace from the workspace db. + * + * @param workspace + */ +export function unregisterWorkpace(workspace: Workspace) { + delete WorkspaceDB_[workspace.id]; +} + +/** + * The main workspace most recently used. + * Set by Blockly.WorkspaceSvg.prototype.markFocused + */ +let mainWorkspace: Workspace; + +/** + * Returns the last used top level workspace (based on focus). Try not to use + * this function, particularly if there are multiple Blockly instances on a + * page. + * + * @returns The main workspace. + */ +export function getMainWorkspace(): Workspace { + return mainWorkspace; +} + +/** + * Sets last used main workspace. + * + * @param workspace The most recently used top level workspace. + */ +export function setMainWorkspace(workspace: Workspace) { + mainWorkspace = workspace; +} + +/** + * Currently selected copyable object. + */ +let selected: ISelectable | null = null; + +/** + * Returns the currently selected copyable object. + */ +export function getSelected(): ISelectable | null { + return selected; +} + +/** + * Sets the currently selected block. This function does not visually mark the + * block as selected or fire the required events. If you wish to + * programmatically select a block, use `BlockSvg#select`. + * + * @param newSelection The newly selected block. + * @internal + */ +export function setSelected(newSelection: ISelectable | null) { + if (selected === newSelection) return; + + const event = new (eventUtils.get(EventType.SELECTED))( + selected?.id ?? null, + newSelection?.id ?? null, + newSelection?.workspace.id ?? selected?.workspace.id ?? '', + ); + eventUtils.fire(event); + + selected?.unselect(); + selected = newSelection; + selected?.select(); +} + +/** + * Container element in which to render the WidgetDiv, DropDownDiv and Tooltip. + */ +let parentContainer: Element | null; + +/** + * Get the container element in which to render the WidgetDiv, DropDownDiv and + * Tooltip. + * + * @returns The parent container. + */ +export function getParentContainer(): Element | null { + return parentContainer; +} + +/** + * Set the parent container. This is the container element that the WidgetDiv, + * DropDownDiv, and Tooltip are rendered into the first time `Blockly.inject` + * is called. + * This method is a NOP if called after the first `Blockly.inject`. + * + * @param newParent The container element. + */ +export function setParentContainer(newParent: Element) { + parentContainer = newParent; +} + +/** + * Size the SVG image to completely fill its container. Call this when the view + * actually changes sizes (e.g. on a window resize/device orientation change). + * See workspace.resizeContents to resize the workspace when the contents + * change (e.g. when a block is added or removed). + * Record the height/width of the SVG image. + * + * @param workspace Any workspace in the SVG. + */ +export function svgResize(workspace: WorkspaceSvg) { + let mainWorkspace = workspace; + while (mainWorkspace.options.parentWorkspace) { + mainWorkspace = mainWorkspace.options.parentWorkspace; + } + const svg = mainWorkspace.getParentSvg(); + const cachedSize = mainWorkspace.getCachedParentSvgSize(); + const div = svg.parentElement; + if (!(div instanceof HTMLElement)) { + // Workspace deleted, or something. + return; + } + + const width = div.offsetWidth; + const height = div.offsetHeight; + if (cachedSize.width !== width) { + svg.setAttribute('width', width + 'px'); + mainWorkspace.setCachedParentSvgSize(width, null); + } + if (cachedSize.height !== height) { + svg.setAttribute('height', height + 'px'); + mainWorkspace.setCachedParentSvgSize(null, height); + } + mainWorkspace.resize(); +} + +/** + * All of the connections on blocks that are currently being dragged. + */ +export const draggingConnections: Connection[] = []; + +/** + * Get a map of all the block's descendants mapping their type to the number of + * children with that type. + * + * @param block The block to map. + * @param opt_stripFollowing Optionally ignore all following + * statements (blocks that are not inside a value or statement input + * of the block). + * @returns Map of types to type counts for descendants of the bock. + */ +export function getBlockTypeCounts( + block: Block, + opt_stripFollowing?: boolean, +): {[key: string]: number} { + const typeCountsMap = Object.create(null); + const descendants = block.getDescendants(true); + if (opt_stripFollowing) { + const nextBlock = block.getNextBlock(); + if (nextBlock) { + const index = descendants.indexOf(nextBlock); + descendants.splice(index, descendants.length - index); + } + } + for (let i = 0, checkBlock; (checkBlock = descendants[i]); i++) { + if (typeCountsMap[checkBlock.type]) { + typeCountsMap[checkBlock.type]++; + } else { + typeCountsMap[checkBlock.type] = 1; + } + } + return typeCountsMap; +} + +/** + * Helper function for defining a block from JSON. The resulting function has + * the correct value of jsonDef at the point in code where jsonInit is called. + * + * @param jsonDef The JSON definition of a block. + * @returns A function that calls jsonInit with the correct value + * of jsonDef. + */ +function jsonInitFactory(jsonDef: AnyDuringMigration): () => void { + return function (this: Block) { + this.jsonInit(jsonDef); + }; +} + +/** + * Define blocks from an array of JSON block definitions, as might be generated + * by the Blockly Developer Tools. + * + * @param jsonArray An array of JSON block definitions. + */ +export function defineBlocksWithJsonArray(jsonArray: AnyDuringMigration[]) { + TEST_ONLY.defineBlocksWithJsonArrayInternal(jsonArray); +} + +/** + * Private version of defineBlocksWithJsonArray for stubbing in tests. + */ +function defineBlocksWithJsonArrayInternal(jsonArray: AnyDuringMigration[]) { + defineBlocks(createBlockDefinitionsFromJsonArray(jsonArray)); +} + +/** + * Define blocks from an array of JSON block definitions, as might be generated + * by the Blockly Developer Tools. + * + * @param jsonArray An array of JSON block definitions. + * @returns A map of the block + * definitions created. + */ +export function createBlockDefinitionsFromJsonArray( + jsonArray: AnyDuringMigration[], +): {[key: string]: BlockDefinition} { + const blocks: {[key: string]: BlockDefinition} = {}; + for (let i = 0; i < jsonArray.length; i++) { + const elem = jsonArray[i]; + if (!elem) { + console.warn(`Block definition #${i} in JSON array is ${elem}. Skipping`); + continue; + } + const type = elem['type']; + if (!type) { + console.warn( + `Block definition #${i} in JSON array is missing a type attribute. ` + + 'Skipping.', + ); + continue; + } + blocks[type] = {init: jsonInitFactory(elem)}; + } + return blocks; +} + +/** + * Add the specified block definitions to the block definitions + * dictionary (Blockly.Blocks). + * + * @param blocks A map of block + * type names to block definitions. + */ +export function defineBlocks(blocks: {[key: string]: BlockDefinition}) { + // Iterate over own enumerable properties. + for (const type of Object.keys(blocks)) { + const definition = blocks[type]; + if (type in Blocks) { + console.warn( + `Block definition "${type}" overwrites previous definition.`, + ); + } + Blocks[type] = definition; + } +} + +export const TEST_ONLY = {defineBlocksWithJsonArrayInternal}; diff --git a/core/component_manager.ts b/core/component_manager.ts new file mode 100644 index 00000000000..8363d6fb4a0 --- /dev/null +++ b/core/component_manager.ts @@ -0,0 +1,247 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Manager for all items registered with the workspace. + * + * @class + */ +// Former goog.module ID: Blockly.ComponentManager + +import type {IAutoHideable} from './interfaces/i_autohideable.js'; +import type {IComponent} from './interfaces/i_component.js'; +import type {IDeleteArea} from './interfaces/i_delete_area.js'; +import type {IDragTarget} from './interfaces/i_drag_target.js'; +import type {IPositionable} from './interfaces/i_positionable.js'; +import * as arrayUtils from './utils/array.js'; + +class Capability<_T> { + static POSITIONABLE = new Capability('positionable'); + static DRAG_TARGET = new Capability('drag_target'); + static DELETE_AREA = new Capability('delete_area'); + static AUTOHIDEABLE = new Capability('autohideable'); + private readonly name: string; + /** @param name The name of the component capability. */ + constructor(name: string) { + this.name = name; + } + + /** + * Returns the name of the capability. + * + * @returns The name. + */ + toString(): string { + return this.name; + } +} + +/** + * Manager for all items registered with the workspace. + */ +export class ComponentManager { + static Capability = Capability; + + /** + * A map of the components registered with the workspace, mapped to id. + */ + private readonly componentData = new Map(); + + /** A map of capabilities to component IDs. */ + private readonly capabilityToComponentIds = new Map(); + + /** + * Adds a component. + * + * @param componentInfo The data for the component to register. + * @param opt_allowOverrides True to prevent an error when overriding an + * already registered item. + */ + addComponent(componentInfo: ComponentDatum, opt_allowOverrides?: boolean) { + // Don't throw an error if opt_allowOverrides is true. + const id = componentInfo.component.id; + if (!opt_allowOverrides && this.componentData.has(id)) { + throw Error( + 'Plugin "' + + id + + '" with capabilities "' + + this.componentData.get(id)?.capabilities + + '" already added.', + ); + } + this.componentData.set(id, componentInfo); + const stringCapabilities = []; + for (let i = 0; i < componentInfo.capabilities.length; i++) { + const capability = String(componentInfo.capabilities[i]).toLowerCase(); + stringCapabilities.push(capability); + if (!this.capabilityToComponentIds.has(capability)) { + this.capabilityToComponentIds.set(capability, [id]); + } else { + this.capabilityToComponentIds.get(capability)?.push(id); + } + } + this.componentData.get(id)!.capabilities = stringCapabilities; + } + + /** + * Removes a component. + * + * @param id The ID of the component to remove. + */ + removeComponent(id: string) { + const componentInfo = this.componentData.get(id); + if (!componentInfo) { + return; + } + for (let i = 0; i < componentInfo.capabilities.length; i++) { + const capability = String(componentInfo.capabilities[i]).toLowerCase(); + arrayUtils.removeElem(this.capabilityToComponentIds.get(capability)!, id); + } + this.componentData.delete(id); + } + + /** + * Adds a capability to a existing registered component. + * + * @param id The ID of the component to add the capability to. + * @param capability The capability to add. + */ + addCapability(id: string, capability: string | Capability) { + if (!this.getComponent(id)) { + throw Error( + 'Cannot add capability, "' + + capability + + '". Plugin "' + + id + + '" has not been added to the ComponentManager', + ); + } + if (this.hasCapability(id, capability)) { + console.warn( + 'Plugin "' + id + 'already has capability "' + capability + '"', + ); + return; + } + capability = `${capability}`.toLowerCase(); + this.componentData.get(id)?.capabilities.push(capability); + this.capabilityToComponentIds.get(capability)?.push(id); + } + + /** + * Removes a capability from an existing registered component. + * + * @param id The ID of the component to remove the capability from. + * @param capability The capability to remove. + */ + removeCapability(id: string, capability: string | Capability) { + if (!this.getComponent(id)) { + throw Error( + 'Cannot remove capability, "' + + capability + + '". Plugin "' + + id + + '" has not been added to the ComponentManager', + ); + } + if (!this.hasCapability(id, capability)) { + console.warn( + 'Plugin "' + + id + + 'doesn\'t have capability "' + + capability + + '" to remove', + ); + return; + } + capability = `${capability}`.toLowerCase(); + arrayUtils.removeElem(this.componentData.get(id)!.capabilities, capability); + arrayUtils.removeElem(this.capabilityToComponentIds.get(capability)!, id); + } + + /** + * Returns whether the component with this id has the specified capability. + * + * @param id The ID of the component to check. + * @param capability The capability to check for. + * @returns Whether the component has the capability. + */ + hasCapability(id: string, capability: string | Capability): boolean { + capability = `${capability}`.toLowerCase(); + return ( + this.componentData.has(id) && + this.componentData.get(id)!.capabilities.includes(capability) + ); + } + + /** + * Gets the component with the given ID. + * + * @param id The ID of the component to get. + * @returns The component with the given name or undefined if not found. + */ + getComponent(id: string): IComponent | undefined { + return this.componentData.get(id)?.component; + } + + /** + * Gets all the components with the specified capability. + * + * @param capability The capability of the component. + * @param sorted Whether to return list ordered by weights. + * @returns The components that match the specified capability. + */ + getComponents( + capability: string | Capability, + sorted: boolean, + ): T[] { + capability = `${capability}`.toLowerCase(); + const componentIds = this.capabilityToComponentIds.get(capability); + if (!componentIds) { + return []; + } + const components: T[] = []; + if (sorted) { + const componentDataList: ComponentDatum[] = []; + componentIds.forEach((id) => { + componentDataList.push(this.componentData.get(id)!); + }); + componentDataList.sort(function (a, b) { + return a.weight - b.weight; + }); + componentDataList.forEach(function (componentDatum) { + components.push(componentDatum.component as T); + }); + } else { + componentIds.forEach((id) => { + components.push(this.componentData.get(id)!.component as T); + }); + } + return components; + } +} + +export namespace ComponentManager { + export enum ComponentWeight { + // The toolbox weight is lower (higher precedence) than the flyout, so that + // if both are under the pointer, the toolbox takes precedence even though + // the flyout's drag target area is large enough to include the toolbox. + TOOLBOX_WEIGHT = 0, + FLYOUT_WEIGHT = 1, + TRASHCAN_WEIGHT = 2, + ZOOM_CONTROLS_WEIGHT = 3, + } + + /** An object storing component information. */ + export interface ComponentDatum { + component: IComponent; + capabilities: Array>; + weight: number; + } +} + +export type ComponentWeight = ComponentManager.ComponentWeight; +export const ComponentWeight = ComponentManager.ComponentWeight; +export type ComponentDatum = ComponentManager.ComponentDatum; diff --git a/core/config.ts b/core/config.ts new file mode 100644 index 00000000000..9def1dca4e9 --- /dev/null +++ b/core/config.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.config + +/** + * All the values that we expect developers to be able to change + * before injecting Blockly. + */ +interface Config { + dragRadius: number; + flyoutDragRadius: number; + snapRadius: number; + currentConnectionPreference: number; + bumpDelay: number; + connectingSnapRadius: number; +} + +/** Default snap radius. */ +const DEFAULT_SNAP_RADIUS = 28; + +/** + * Object holding all the values on Blockly that we expect developers to be + * able to change. + */ +export const config: Config = { + /** + * Number of pixels the mouse must move before a drag starts. + * + */ + dragRadius: 5, + /** + * Number of pixels the mouse must move before a drag/scroll starts from the + * flyout. Because the drag-intention is determined when this is reached, it + * is larger than dragRadius so that the drag-direction is clearer. + * + */ + flyoutDragRadius: 10, + /** + * Maximum misalignment between connections for them to snap together. + * + */ + snapRadius: DEFAULT_SNAP_RADIUS, + /** + * Maximum misalignment between connections for them to snap together. + * This should be the same as the snap radius. + */ + connectingSnapRadius: DEFAULT_SNAP_RADIUS, + /** + * How much to prefer staying connected to the current connection over moving + * to a new connection. The current previewed connection is considered to be + * this much closer to the matching connection on the block than it actually + * is. + * + */ + currentConnectionPreference: 8, + /** + * Delay in ms between trigger and bumping unconnected block out of alignment. + * + */ + bumpDelay: 250, +}; diff --git a/core/connection.js b/core/connection.js deleted file mode 100644 index fc13cfaf08a..00000000000 --- a/core/connection.js +++ /dev/null @@ -1,663 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2011 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Components for creating connections between blocks. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Connection'); - -goog.require('goog.asserts'); -goog.require('goog.dom'); - - -/** - * Class for a connection between blocks. - * @param {!Blockly.Block} source The block establishing this connection. - * @param {number} type The type of the connection. - * @constructor - */ -Blockly.Connection = function(source, type) { - /** - * @type {!Blockly.Block} - * @private - */ - this.sourceBlock_ = source; - /** @type {number} */ - this.type = type; - // Shortcut for the databases for this connection's workspace. - if (source.workspace.connectionDBList) { - this.db_ = source.workspace.connectionDBList[type]; - this.dbOpposite_ = - source.workspace.connectionDBList[Blockly.OPPOSITE_TYPE[type]]; - this.hidden_ = !this.db_; - } -}; - -/** - * Constants for checking whether two connections are compatible. - */ -Blockly.Connection.CAN_CONNECT = 0; -Blockly.Connection.REASON_SELF_CONNECTION = 1; -Blockly.Connection.REASON_WRONG_TYPE = 2; -Blockly.Connection.REASON_TARGET_NULL = 3; -Blockly.Connection.REASON_CHECKS_FAILED = 4; -Blockly.Connection.REASON_DIFFERENT_WORKSPACES = 5; -Blockly.Connection.REASON_SHADOW_PARENT = 6; - -/** - * Connection this connection connects to. Null if not connected. - * @type {Blockly.Connection} - */ -Blockly.Connection.prototype.targetConnection = null; - -/** - * List of compatible value types. Null if all types are compatible. - * @type {Array} - * @private - */ -Blockly.Connection.prototype.check_ = null; - -/** - * DOM representation of a shadow block, or null if none. - * @type {Element} - * @private - */ -Blockly.Connection.prototype.shadowDom_ = null; - -/** - * Horizontal location of this connection. - * @type {number} - * @private - */ -Blockly.Connection.prototype.x_ = 0; - -/** - * Vertical location of this connection. - * @type {number} - * @private - */ -Blockly.Connection.prototype.y_ = 0; - -/** - * Has this connection been added to the connection database? - * @type {boolean} - * @private - */ -Blockly.Connection.prototype.inDB_ = false; - -/** - * Connection database for connections of this type on the current workspace. - * @type {Blockly.ConnectionDB} - * @private - */ -Blockly.Connection.prototype.db_ = null; - -/** - * Connection database for connections compatible with this type on the - * current workspace. - * @type {Blockly.ConnectionDB} - * @private - */ -Blockly.Connection.prototype.dbOpposite_ = null; - -/** - * Whether this connections is hidden (not tracked in a database) or not. - * @type {boolean} - * @private - */ -Blockly.Connection.prototype.hidden_ = null; - -/** - * Connect two connections together. This is the connection on the superior - * block. - * @param {!Blockly.Connection} childConnection Connection on inferior block. - * @private - */ -Blockly.Connection.prototype.connect_ = function(childConnection) { - var parentConnection = this; - var parentBlock = parentConnection.getSourceBlock(); - var childBlock = childConnection.getSourceBlock(); - // Disconnect any existing parent on the child connection. - if (childConnection.isConnected()) { - childConnection.disconnect(); - } - if (parentConnection.isConnected()) { - // Other connection is already connected to something. - // Disconnect it and reattach it or bump it as needed. - var orphanBlock = parentConnection.targetBlock(); - var shadowDom = parentConnection.getShadowDom(); - // Temporarily set the shadow DOM to null so it does not respawn. - parentConnection.setShadowDom(null); - // Displaced shadow blocks dissolve rather than reattaching or bumping. - if (orphanBlock.isShadow()) { - // Save the shadow block so that field values are preserved. - shadowDom = Blockly.Xml.blockToDom(orphanBlock); - orphanBlock.dispose(); - orphanBlock = null; - } else if (parentConnection.type == Blockly.INPUT_VALUE) { - // Value connections. - // If female block is already connected, disconnect and bump the male. - if (!orphanBlock.outputConnection) { - throw 'Orphan block does not have an output connection.'; - } - // Attempt to reattach the orphan at the end of the newly inserted - // block. Since this block may be a row, walk down to the end - // or to the first (and only) shadow block. - var connection = Blockly.Connection.lastConnectionInRow_( - childBlock, orphanBlock); - if (connection) { - orphanBlock.outputConnection.connect(connection); - orphanBlock = null; - } - } else if (parentConnection.type == Blockly.NEXT_STATEMENT) { - // Statement connections. - // Statement blocks may be inserted into the middle of a stack. - // Split the stack. - if (!orphanBlock.previousConnection) { - throw 'Orphan block does not have a previous connection.'; - } - // Attempt to reattach the orphan at the bottom of the newly inserted - // block. Since this block may be a stack, walk down to the end. - var newBlock = childBlock; - while (newBlock.nextConnection) { - var nextBlock = newBlock.getNextBlock(); - if (nextBlock && !nextBlock.isShadow()) { - newBlock = nextBlock; - } else { - if (orphanBlock.previousConnection.checkType_( - newBlock.nextConnection)) { - newBlock.nextConnection.connect(orphanBlock.previousConnection); - orphanBlock = null; - } - break; - } - } - } - if (orphanBlock) { - // Unable to reattach orphan. - parentConnection.disconnect(); - if (Blockly.Events.recordUndo) { - // Bump it off to the side after a moment. - var group = Blockly.Events.getGroup(); - setTimeout(function() { - // Verify orphan hasn't been deleted or reconnected (user on meth). - if (orphanBlock.workspace && !orphanBlock.getParent()) { - Blockly.Events.setGroup(group); - if (orphanBlock.outputConnection) { - orphanBlock.outputConnection.bumpAwayFrom_(parentConnection); - } else if (orphanBlock.previousConnection) { - orphanBlock.previousConnection.bumpAwayFrom_(parentConnection); - } - Blockly.Events.setGroup(false); - } - }, Blockly.BUMP_DELAY); - } - } - // Restore the shadow DOM. - parentConnection.setShadowDom(shadowDom); - } - - var event; - if (Blockly.Events.isEnabled()) { - event = new Blockly.Events.BlockMove(childBlock); - } - // Establish the connections. - Blockly.Connection.connectReciprocally_(parentConnection, childConnection); - // Demote the inferior block so that one is a child of the superior one. - childBlock.setParent(parentBlock); - if (event) { - event.recordNew(); - Blockly.Events.fire(event); - } -}; - -/** - * Sever all links to this connection (not including from the source object). - */ -Blockly.Connection.prototype.dispose = function() { - if (this.isConnected()) { - throw 'Disconnect connection before disposing of it.'; - } - if (this.inDB_) { - this.db_.removeConnection_(this); - } - this.db_ = null; - this.dbOpposite_ = null; -}; - -/** - * Get the source block for this connection. - * @return {Blockly.Block} The source block, or null if there is none. - */ -Blockly.Connection.prototype.getSourceBlock = function() { - return this.sourceBlock_; -}; - -/** - * Does the connection belong to a superior block (higher in the source stack)? - * @return {boolean} True if connection faces down or right. - */ -Blockly.Connection.prototype.isSuperior = function() { - return this.type == Blockly.INPUT_VALUE || - this.type == Blockly.NEXT_STATEMENT; -}; - -/** - * Is the connection connected? - * @return {boolean} True if connection is connected to another connection. - */ -Blockly.Connection.prototype.isConnected = function() { - return !!this.targetConnection; -}; - -/** - * Checks whether the current connection can connect with the target - * connection. - * @param {Blockly.Connection} target Connection to check compatibility with. - * @return {number} Blockly.Connection.CAN_CONNECT if the connection is legal, - * an error code otherwise. - * @private - */ -Blockly.Connection.prototype.canConnectWithReason_ = function(target) { - if (!target) { - return Blockly.Connection.REASON_TARGET_NULL; - } - if (this.isSuperior()) { - var blockA = this.sourceBlock_; - var blockB = target.getSourceBlock(); - } else { - var blockB = this.sourceBlock_; - var blockA = target.getSourceBlock(); - } - if (blockA && blockA == blockB) { - return Blockly.Connection.REASON_SELF_CONNECTION; - } else if (target.type != Blockly.OPPOSITE_TYPE[this.type]) { - return Blockly.Connection.REASON_WRONG_TYPE; - } else if (blockA && blockB && blockA.workspace !== blockB.workspace) { - return Blockly.Connection.REASON_DIFFERENT_WORKSPACES; - } else if (!this.checkType_(target)) { - return Blockly.Connection.REASON_CHECKS_FAILED; - } else if (blockA.isShadow() && !blockB.isShadow()) { - return Blockly.Connection.REASON_SHADOW_PARENT; - } - return Blockly.Connection.CAN_CONNECT; -}; - -/** - * Checks whether the current connection and target connection are compatible - * and throws an exception if they are not. - * @param {Blockly.Connection} target The connection to check compatibility - * with. - * @private - */ -Blockly.Connection.prototype.checkConnection_ = function(target) { - switch (this.canConnectWithReason_(target)) { - case Blockly.Connection.CAN_CONNECT: - break; - case Blockly.Connection.REASON_SELF_CONNECTION: - throw 'Attempted to connect a block to itself.'; - case Blockly.Connection.REASON_DIFFERENT_WORKSPACES: - // Usually this means one block has been deleted. - throw 'Blocks not on same workspace.'; - case Blockly.Connection.REASON_WRONG_TYPE: - throw 'Attempt to connect incompatible types.'; - case Blockly.Connection.REASON_TARGET_NULL: - throw 'Target connection is null.'; - case Blockly.Connection.REASON_CHECKS_FAILED: - var msg = 'Connection checks failed. '; - msg += this + ' expected ' + this.check_ + ', found ' + target.check_; - throw msg; - case Blockly.Connection.REASON_SHADOW_PARENT: - throw 'Connecting non-shadow to shadow block.'; - default: - throw 'Unknown connection failure: this should never happen!'; - } -}; - -/** - * Check if the two connections can be dragged to connect to each other. - * @param {!Blockly.Connection} candidate A nearby connection to check. - * @return {boolean} True if the connection is allowed, false otherwise. - */ -Blockly.Connection.prototype.isConnectionAllowed = function(candidate) { - // Type checking. - var canConnect = this.canConnectWithReason_(candidate); - if (canConnect != Blockly.Connection.CAN_CONNECT) { - return false; - } - - // Don't offer to connect an already connected left (male) value plug to - // an available right (female) value plug. Don't offer to connect the - // bottom of a statement block to one that's already connected. - if (candidate.type == Blockly.OUTPUT_VALUE || - candidate.type == Blockly.PREVIOUS_STATEMENT) { - if (candidate.isConnected() || this.isConnected()) { - return false; - } - } - - // Offering to connect the left (male) of a value block to an already - // connected value pair is ok, we'll splice it in. - // However, don't offer to splice into an immovable block. - if (candidate.type == Blockly.INPUT_VALUE && candidate.isConnected() && - !candidate.targetBlock().isMovable() && - !candidate.targetBlock().isShadow()) { - return false; - } - - // Don't let a block with no next connection bump other blocks out of the - // stack. But covering up a shadow block or stack of shadow blocks is fine. - // Similarly, replacing a terminal statement with another terminal statement - // is allowed. - if (this.type == Blockly.PREVIOUS_STATEMENT && - candidate.isConnected() && - !this.sourceBlock_.nextConnection && - !candidate.targetBlock().isShadow() && - candidate.targetBlock().nextConnection) { - return false; - } - - // Don't let blocks try to connect to themselves or ones they nest. - if (Blockly.draggingConnections_.indexOf(candidate) != -1) { - return false; - } - - return true; -}; - -/** - * Connect this connection to another connection. - * @param {!Blockly.Connection} otherConnection Connection to connect to. - */ -Blockly.Connection.prototype.connect = function(otherConnection) { - if (this.targetConnection == otherConnection) { - // Already connected together. NOP. - return; - } - this.checkConnection_(otherConnection); - // Determine which block is superior (higher in the source stack). - if (this.isSuperior()) { - // Superior block. - this.connect_(otherConnection); - } else { - // Inferior block. - otherConnection.connect_(this); - } -}; - -/** - * Update two connections to target each other. - * @param {Blockly.Connection} first The first connection to update. - * @param {Blockly.Connection} second The second connection to update. - * @private - */ -Blockly.Connection.connectReciprocally_ = function(first, second) { - goog.asserts.assert(first && second, 'Cannot connect null connections.'); - first.targetConnection = second; - second.targetConnection = first; -}; - -/** - * Does the given block have one and only one connection point that will accept - * an orphaned block? - * @param {!Blockly.Block} block The superior block. - * @param {!Blockly.Block} orphanBlock The inferior block. - * @return {Blockly.Connection} The suitable connection point on 'block', - * or null. - * @private - */ -Blockly.Connection.singleConnection_ = function(block, orphanBlock) { - var connection = false; - for (var i = 0; i < block.inputList.length; i++) { - var thisConnection = block.inputList[i].connection; - if (thisConnection && thisConnection.type == Blockly.INPUT_VALUE && - orphanBlock.outputConnection.checkType_(thisConnection)) { - if (connection) { - return null; // More than one connection. - } - connection = thisConnection; - } - } - return connection; -}; - -/** - * Walks down a row a blocks, at each stage checking if there are any - * connections that will accept the orphaned block. If at any point there - * are zero or multiple eligible connections, returns null. Otherwise - * returns the only input on the last block in the chain. - * Terminates early for shadow blocks. - * @param {!Blockly.Block} startBlock The block on which to start the search. - * @param {!Blockly.Block} orphanBlock The block that is looking for a home. - * @return {Blockly.Connection} The suitable connection point on the chain - * of blocks, or null. - * @private - */ -Blockly.Connection.lastConnectionInRow_ = function(startBlock, orphanBlock) { - var newBlock = startBlock; - var connection; - while (connection = Blockly.Connection.singleConnection_( - /** @type {!Blockly.Block} */ (newBlock), orphanBlock)) { - // '=' is intentional in line above. - newBlock = connection.targetBlock(); - if (!newBlock || newBlock.isShadow()) { - return connection; - } - } - return null; -}; - -/** - * Disconnect this connection. - */ -Blockly.Connection.prototype.disconnect = function() { - var otherConnection = this.targetConnection; - goog.asserts.assert(otherConnection, 'Source connection not connected.'); - goog.asserts.assert(otherConnection.targetConnection == this, - 'Target connection not connected to source connection.'); - - var parentBlock, childBlock, parentConnection; - if (this.isSuperior()) { - // Superior block. - parentBlock = this.sourceBlock_; - childBlock = otherConnection.getSourceBlock(); - parentConnection = this; - } else { - // Inferior block. - parentBlock = otherConnection.getSourceBlock(); - childBlock = this.sourceBlock_; - parentConnection = otherConnection; - } - this.disconnectInternal_(parentBlock, childBlock); - parentConnection.respawnShadow_(); -}; - -/** - * Disconnect two blocks that are connected by this connection. - * @param {!Blockly.Block} parentBlock The superior block. - * @param {!Blockly.Block} childBlock The inferior block. - * @private - */ -Blockly.Connection.prototype.disconnectInternal_ = function(parentBlock, - childBlock) { - var event; - if (Blockly.Events.isEnabled()) { - event = new Blockly.Events.BlockMove(childBlock); - } - var otherConnection = this.targetConnection; - otherConnection.targetConnection = null; - this.targetConnection = null; - childBlock.setParent(null); - if (event) { - event.recordNew(); - Blockly.Events.fire(event); - } -}; - -/** - * Respawn the shadow block if there was one connected to the this connection. - * @private - */ -Blockly.Connection.prototype.respawnShadow_ = function() { - var parentBlock = this.getSourceBlock(); - var shadow = this.getShadowDom(); - if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) { - var blockShadow = - Blockly.Xml.domToBlock(shadow, parentBlock.workspace); - if (blockShadow.outputConnection) { - this.connect(blockShadow.outputConnection); - } else if (blockShadow.previousConnection) { - this.connect(blockShadow.previousConnection); - } else { - throw 'Child block does not have output or previous statement.'; - } - } -}; - -/** - * Returns the block that this connection connects to. - * @return {Blockly.Block} The connected block or null if none is connected. - */ -Blockly.Connection.prototype.targetBlock = function() { - if (this.isConnected()) { - return this.targetConnection.getSourceBlock(); - } - return null; -}; - -/** - * Is this connection compatible with another connection with respect to the - * value type system. E.g. square_root("Hello") is not compatible. - * @param {!Blockly.Connection} otherConnection Connection to compare against. - * @return {boolean} True if the connections share a type. - * @private - */ -Blockly.Connection.prototype.checkType_ = function(otherConnection) { - if (!this.check_ || !otherConnection.check_) { - // One or both sides are promiscuous enough that anything will fit. - return true; - } - // Find any intersection in the check lists. - for (var i = 0; i < this.check_.length; i++) { - if (otherConnection.check_.indexOf(this.check_[i]) != -1) { - return true; - } - } - // No intersection. - return false; -}; - -/** - * Function to be called when this connection's compatible types have changed. - * @private - */ -Blockly.Connection.prototype.onCheckChanged_ = function() { - // The new value type may not be compatible with the existing connection. - if (this.isConnected() && !this.checkType_(this.targetConnection)) { - var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_; - child.unplug(); - } -}; - -/** - * Change a connection's compatibility. - * @param {*} check Compatible value type or list of value types. - * Null if all types are compatible. - * @return {!Blockly.Connection} The connection being modified - * (to allow chaining). - */ -Blockly.Connection.prototype.setCheck = function(check) { - if (check) { - // Ensure that check is in an array. - if (!goog.isArray(check)) { - check = [check]; - } - this.check_ = check; - this.onCheckChanged_(); - } else { - this.check_ = null; - } - return this; -}; - -/** - * Change a connection's shadow block. - * @param {Element} shadow DOM representation of a block or null. - */ -Blockly.Connection.prototype.setShadowDom = function(shadow) { - this.shadowDom_ = shadow; -}; - -/** - * Return a connection's shadow block. - * @return {Element} shadow DOM representation of a block or null. - */ -Blockly.Connection.prototype.getShadowDom = function() { - return this.shadowDom_; -}; - -/** - * Find all nearby compatible connections to this connection. - * Type checking does not apply, since this function is used for bumping. - * - * Headless configurations (the default) do not have neighboring connection, - * and always return an empty list (the default). - * {@link Blockly.RenderedConnection} overrides this behavior with a list - * computed from the rendered positioning. - * @param {number} maxLimit The maximum radius to another connection. - * @return {!Array.} List of connections. - * @private - */ -Blockly.Connection.prototype.neighbours_ = function(/* maxLimit */) { - return []; -}; - -/** - * This method returns a string describing this Connection in developer terms - * (English only). Intended to on be used in console logs and errors. - * @return {string} The description. - */ -Blockly.Connection.prototype.toString = function() { - var msg; - var block = this.sourceBlock_; - if (!block) { - return 'Orphan Connection'; - } else if (block.outputConnection == this) { - msg = 'Output Connection of '; - } else if (block.previousConnection == this) { - msg = 'Previous Connection of '; - } else if (block.nextConnection == this) { - msg = 'Next Connection of '; - } else { - var parentInput = goog.array.find(block.inputList, function(input) { - return input.connection == this; - }, this); - if (parentInput) { - msg = 'Input "' + parentInput.name + '" connection on '; - } else { - console.warn('Connection not actually connected to sourceBlock_'); - return 'Orphan Connection'; - } - } - return msg + block.toDevString(); -}; diff --git a/core/connection.ts b/core/connection.ts new file mode 100644 index 00000000000..9cc2c28a923 --- /dev/null +++ b/core/connection.ts @@ -0,0 +1,794 @@ +/** + * @license + * Copyright 2011 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Components for creating connections between blocks. + * + * @class + */ +// Former goog.module ID: Blockly.Connection + +import type {Block} from './block.js'; +import {ConnectionType} from './connection_type.js'; +import type {BlockMove} from './events/events_block_move.js'; +import {EventType} from './events/type.js'; +import * as eventUtils from './events/utils.js'; +import type {Input} from './inputs/input.js'; +import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js'; +import type {IConnectionChecker} from './interfaces/i_connection_checker.js'; +import * as blocks from './serialization/blocks.js'; +import * as Xml from './xml.js'; + +/** + * Class for a connection between blocks. + */ +export class Connection implements IASTNodeLocationWithBlock { + /** Constants for checking whether two connections are compatible. */ + static CAN_CONNECT = 0; + static REASON_SELF_CONNECTION = 1; + static REASON_WRONG_TYPE = 2; + static REASON_TARGET_NULL = 3; + static REASON_CHECKS_FAILED = 4; + static REASON_DIFFERENT_WORKSPACES = 5; + static REASON_SHADOW_PARENT = 6; + static REASON_DRAG_CHECKS_FAILED = 7; + static REASON_PREVIOUS_AND_OUTPUT = 8; + + protected sourceBlock_: Block; + + /** Connection this connection connects to. Null if not connected. */ + targetConnection: Connection | null = null; + + /** + * Has this connection been disposed of? + * + * @internal + */ + disposed = false; + + /** List of compatible value types. Null if all types are compatible. */ + private check: string[] | null = null; + + /** DOM representation of a shadow block, or null if none. */ + private shadowDom: Element | null = null; + + /** + * Horizontal location of this connection. + * + * @internal + */ + x = 0; + + /** + * Vertical location of this connection. + * + * @internal + */ + y = 0; + + private shadowState: blocks.State | null = null; + + /** + * @param source The block establishing this connection. + * @param type The type of the connection. + */ + constructor( + source: Block, + public type: number, + ) { + this.sourceBlock_ = source; + } + + /** + * Connect two connections together. This is the connection on the superior + * block. + * + * @param childConnection Connection on inferior block. + */ + protected connect_(childConnection: Connection) { + const INPUT = ConnectionType.INPUT_VALUE; + const parentBlock = this.getSourceBlock(); + const childBlock = childConnection.getSourceBlock(); + + // Make sure the childConnection is available. + if (childConnection.isConnected()) { + childConnection.disconnectInternal(false); + } + + // Make sure the parentConnection is available. + let orphan; + if (this.isConnected()) { + const shadowState = this.stashShadowState(); + const target = this.targetBlock(); + if (target!.isShadow()) { + target!.dispose(false); + } else { + this.disconnectInternal(); + orphan = target; + } + this.applyShadowState(shadowState); + } + + // Connect the new connection to the parent. + let event; + if (eventUtils.isEnabled()) { + event = new (eventUtils.get(EventType.BLOCK_MOVE))( + childBlock, + ) as BlockMove; + event.setReason(['connect']); + } + connectReciprocally(this, childConnection); + childBlock.setParent(parentBlock); + if (event) { + event.recordNew(); + eventUtils.fire(event); + } + + // Deal with the orphan if it exists. + if (orphan) { + const orphanConnection = + this.type === INPUT + ? orphan.outputConnection + : orphan.previousConnection; + if (!orphanConnection) return; + const connection = Connection.getConnectionForOrphanedConnection( + childBlock, + orphanConnection, + ); + if (connection) { + orphanConnection.connect(connection); + } else { + orphanConnection.onFailedConnect(this); + } + } + } + + /** + * Dispose of this connection and deal with connected blocks. + * + * @internal + */ + dispose() { + // isConnected returns true for shadows and non-shadows. + if (this.isConnected()) { + if (this.isSuperior()) { + // Destroy the attached shadow block & its children (if it exists). + this.setShadowStateInternal(); + } + + const targetBlock = this.targetBlock(); + if (targetBlock && !targetBlock.isDeadOrDying()) { + // Disconnect the attached normal block. + targetBlock.unplug(); + } + } + + this.disposed = true; + } + + /** + * Get the source block for this connection. + * + * @returns The source block. + */ + getSourceBlock(): Block { + return this.sourceBlock_; + } + + /** + * Does the connection belong to a superior block (higher in the source + * stack)? + * + * @returns True if connection faces down or right. + */ + isSuperior(): boolean { + return ( + this.type === ConnectionType.INPUT_VALUE || + this.type === ConnectionType.NEXT_STATEMENT + ); + } + + /** + * Is the connection connected? + * + * @returns True if connection is connected to another connection. + */ + isConnected(): boolean { + return !!this.targetConnection; + } + + /** + * Get the workspace's connection type checker object. + * + * @returns The connection type checker for the source block's workspace. + * @internal + */ + getConnectionChecker(): IConnectionChecker { + return this.sourceBlock_.workspace.connectionChecker; + } + + /** + * Called when an attempted connection fails. NOP by default (i.e. for + * headless workspaces). + * + * @param _superiorConnection Connection that this connection failed to connect + * to. The provided connection should be the superior connection. + * @internal + */ + onFailedConnect(_superiorConnection: Connection) {} + // NOP + + /** + * Connect this connection to another connection. + * + * @param otherConnection Connection to connect to. + * @returns Whether the blocks are now connected or not. + */ + connect(otherConnection: Connection): boolean { + if (this.targetConnection === otherConnection) { + // Already connected together. NOP. + return true; + } + + const checker = this.getConnectionChecker(); + if (checker.canConnect(this, otherConnection, false)) { + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } + // Determine which block is superior (higher in the source stack). + if (this.isSuperior()) { + // Superior block. + this.connect_(otherConnection); + } else { + // Inferior block. + otherConnection.connect_(this); + } + eventUtils.setGroup(existingGroup); + } + + return this.isConnected(); + } + + /** + * Disconnect this connection. + */ + disconnect() { + this.disconnectInternal(); + } + + /** + * Disconnect two blocks that are connected by this connection. + * + * @param setParent Whether to set the parent of the disconnected block or + * not, defaults to true. + * If you do not set the parent, ensure that a subsequent action does, + * otherwise the view and model will be out of sync. + */ + protected disconnectInternal(setParent = true) { + const {parentConnection, childConnection} = + this.getParentAndChildConnections(); + if (!parentConnection || !childConnection) { + throw Error('Source connection not connected.'); + } + + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } + + let event; + if (eventUtils.isEnabled()) { + event = new (eventUtils.get(EventType.BLOCK_MOVE))( + childConnection.getSourceBlock(), + ) as BlockMove; + event.setReason(['disconnect']); + } + const otherConnection = this.targetConnection; + if (otherConnection) { + otherConnection.targetConnection = null; + } + this.targetConnection = null; + if (setParent) childConnection.getSourceBlock().setParent(null); + if (event) { + event.recordNew(); + eventUtils.fire(event); + } + + if (!childConnection.getSourceBlock().isShadow()) { + // If we were disconnecting a shadow, no need to spawn a new one. + parentConnection.respawnShadow_(); + } + + eventUtils.setGroup(existingGroup); + } + + /** + * Returns the parent connection (superior) and child connection (inferior) + * given this connection and the connection it is connected to. + * + * @returns The parent connection and child connection, given this connection + * and the connection it is connected to. + */ + protected getParentAndChildConnections(): { + parentConnection?: Connection; + childConnection?: Connection; + } { + if (!this.targetConnection) return {}; + if (this.isSuperior()) { + return { + parentConnection: this, + childConnection: this.targetConnection, + }; + } + return { + parentConnection: this.targetConnection, + childConnection: this, + }; + } + + /** + * Respawn the shadow block if there was one connected to the this connection. + */ + protected respawnShadow_() { + // Have to keep respawnShadow_ for backwards compatibility. + this.createShadowBlock(true); + } + + /** + * Reconnects this connection to the input with the given name on the given + * block. If there is already a connection connected to that input, that + * connection is disconnected. + * + * @param block The block to connect this connection to. + * @param inputName The name of the input to connect this connection to. + * @returns True if this connection was able to connect, false otherwise. + */ + reconnect(block: Block, inputName: string): boolean { + // No need to reconnect if this connection's block is deleted. + if (this.getSourceBlock().isDeadOrDying()) return false; + + const connectionParent = block.getInput(inputName)?.connection; + const currentParent = this.targetBlock(); + if ( + (!currentParent || currentParent === block) && + connectionParent && + connectionParent.targetConnection !== this + ) { + if (connectionParent.isConnected()) { + // There's already something connected here. Get rid of it. + connectionParent.disconnect(); + } + connectionParent.connect(this); + return true; + } + return false; + } + + /** + * Returns the block that this connection connects to. + * + * @returns The connected block or null if none is connected. + */ + targetBlock(): Block | null { + if (this.isConnected()) { + return this.targetConnection?.getSourceBlock() ?? null; + } + return null; + } + + /** + * Function to be called when this connection's compatible types have changed. + */ + protected onCheckChanged_() { + // The new value type may not be compatible with the existing connection. + if ( + this.isConnected() && + (!this.targetConnection || + !this.getConnectionChecker().canConnect( + this, + this.targetConnection, + false, + )) + ) { + const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_; + child!.unplug(); + } + } + + /** + * Change a connection's compatibility. + * + * @param check Compatible value type or list of value types. Null if all + * types are compatible. + * @returns The connection being modified (to allow chaining). + */ + setCheck(check: string | string[] | null): Connection { + if (check) { + if (!Array.isArray(check)) { + check = [check]; + } + this.check = check; + this.onCheckChanged_(); + } else { + this.check = null; + } + return this; + } + + /** + * Get a connection's compatibility. + * + * @returns List of compatible value types. + * Null if all types are compatible. + */ + getCheck(): string[] | null { + return this.check; + } + + /** + * Changes the connection's shadow block. + * + * @param shadowDom DOM representation of a block or null. + */ + setShadowDom(shadowDom: Element | null) { + this.setShadowStateInternal({shadowDom}); + } + + /** + * Returns the xml representation of the connection's shadow block. + * + * @param returnCurrent If true, and the shadow block is currently attached to + * this connection, this serializes the state of that block and returns it + * (so that field values are correct). Otherwise the saved shadowDom is + * just returned. + * @returns Shadow DOM representation of a block or null. + */ + getShadowDom(returnCurrent?: boolean): Element | null { + return returnCurrent && this.targetBlock()!.isShadow() + ? (Xml.blockToDom(this.targetBlock() as Block) as Element) + : this.shadowDom; + } + + /** + * Changes the connection's shadow block. + * + * @param shadowState An state represetation of the block or null. + */ + setShadowState(shadowState: blocks.State | null) { + this.setShadowStateInternal({shadowState}); + } + + /** + * Returns the serialized object representation of the connection's shadow + * block. + * + * @param returnCurrent If true, and the shadow block is currently attached to + * this connection, this serializes the state of that block and returns it + * (so that field values are correct). Otherwise the saved state is just + * returned. + * @returns Serialized object representation of the block, or null. + */ + getShadowState(returnCurrent?: boolean): blocks.State | null { + if (returnCurrent && this.targetBlock() && this.targetBlock()!.isShadow()) { + return blocks.save(this.targetBlock() as Block); + } + return this.shadowState; + } + + /** + * Find all nearby compatible connections to this connection. + * Type checking does not apply, since this function is used for bumping. + * + * Headless configurations (the default) do not have neighboring connection, + * and always return an empty list (the default). + * {@link RenderedConnection#neighbours} overrides this behavior with a list + * computed from the rendered positioning. + * + * @param _maxLimit The maximum radius to another connection. + * @returns List of connections. + * @internal + */ + neighbours(_maxLimit: number): Connection[] { + return []; + } + + /** + * Get the parent input of a connection. + * + * @returns The input that the connection belongs to or null if no parent + * exists. + * @internal + */ + getParentInput(): Input | null { + let parentInput = null; + const inputs = this.sourceBlock_.inputList; + for (let i = 0; i < inputs.length; i++) { + if (inputs[i].connection === this) { + parentInput = inputs[i]; + break; + } + } + return parentInput; + } + + /** + * This method returns a string describing this Connection in developer terms + * (English only). Intended to on be used in console logs and errors. + * + * @returns The description. + */ + toString(): string { + const block = this.sourceBlock_; + if (!block) { + return 'Orphan Connection'; + } + let msg; + if (block.outputConnection === this) { + msg = 'Output Connection of '; + } else if (block.previousConnection === this) { + msg = 'Previous Connection of '; + } else if (block.nextConnection === this) { + msg = 'Next Connection of '; + } else { + let parentInput = null; + for (let i = 0, input; (input = block.inputList[i]); i++) { + if (input.connection === this) { + parentInput = input; + break; + } + } + if (parentInput) { + msg = 'Input "' + parentInput.name + '" connection on '; + } else { + console.warn('Connection not actually connected to sourceBlock_'); + return 'Orphan Connection'; + } + } + return msg + block.toDevString(); + } + + /** + * Returns the state of the shadowDom_ and shadowState_ properties, then + * temporarily sets those properties to null so no shadow respawns. + * + * @returns The state of both the shadowDom_ and shadowState_ properties. + */ + private stashShadowState(): { + shadowDom: Element | null; + shadowState: blocks.State | null; + } { + const shadowDom = this.getShadowDom(true); + const shadowState = this.getShadowState(true); + // Set to null so it doesn't respawn. + this.shadowDom = null; + this.shadowState = null; + return {shadowDom, shadowState}; + } + + /** + * Reapplies the stashed state of the shadowDom_ and shadowState_ properties. + * + * @param param0 The state to reapply to the shadowDom_ and shadowState_ + * properties. + */ + private applyShadowState({ + shadowDom, + shadowState, + }: { + shadowDom: Element | null; + shadowState: blocks.State | null; + }) { + this.shadowDom = shadowDom; + this.shadowState = shadowState; + } + + /** + * Sets the state of the shadow of this connection. + * + * @param param0 The state to set the shadow of this connection to. + */ + private setShadowStateInternal({ + shadowDom = null, + shadowState = null, + }: { + shadowDom?: Element | null; + shadowState?: blocks.State | null; + } = {}) { + // One or both of these should always be null. + // If neither is null, the shadowState will get priority. + this.shadowDom = shadowDom; + this.shadowState = shadowState; + + if (this.getSourceBlock().isDeadOrDying()) return; + + const target = this.targetBlock(); + if (!target) { + this.respawnShadow_(); + if (this.targetBlock() && this.targetBlock()!.isShadow()) { + this.serializeShadow(this.targetBlock()); + } + } else if (target.isShadow()) { + target.dispose(false); + this.respawnShadow_(); + if (this.targetBlock() && this.targetBlock()!.isShadow()) { + this.serializeShadow(this.targetBlock()); + } + } else { + const shadow = this.createShadowBlock(false); + this.serializeShadow(shadow); + if (shadow) { + shadow.dispose(false); + } + } + } + + /** + * Creates a shadow block based on the current shadowState_ or shadowDom_. + * shadowState_ gets priority. + * + * @param attemptToConnect Whether to try to connect the shadow block to this + * connection or not. + * @returns The shadow block that was created, or null if both the + * shadowState_ and shadowDom_ are null. + */ + private createShadowBlock(attemptToConnect: boolean): Block | null { + const parentBlock = this.getSourceBlock(); + const shadowState = this.getShadowState(); + const shadowDom = this.getShadowDom(); + if (parentBlock.isDeadOrDying() || (!shadowState && !shadowDom)) { + return null; + } + + let blockShadow; + if (shadowState) { + blockShadow = blocks.appendInternal(shadowState, parentBlock.workspace, { + parentConnection: attemptToConnect ? this : undefined, + isShadow: true, + recordUndo: false, + }); + return blockShadow; + } + + if (shadowDom) { + blockShadow = Xml.domToBlockInternal(shadowDom, parentBlock.workspace); + if (attemptToConnect) { + if (this.type === ConnectionType.INPUT_VALUE) { + if (!blockShadow.outputConnection) { + throw new Error('Shadow block is missing an output connection'); + } + if (!this.connect(blockShadow.outputConnection)) { + throw new Error('Could not connect shadow block to connection'); + } + } else if (this.type === ConnectionType.NEXT_STATEMENT) { + if (!blockShadow.previousConnection) { + throw new Error('Shadow block is missing previous connection'); + } + if (!this.connect(blockShadow.previousConnection)) { + throw new Error('Could not connect shadow block to connection'); + } + } else { + throw new Error( + 'Cannot connect a shadow block to a previous/output connection', + ); + } + } + return blockShadow; + } + return null; + } + + /** + * Saves the given shadow block to both the shadowDom_ and shadowState_ + * properties, in their respective serialized forms. + * + * @param shadow The shadow to serialize, or null. + */ + private serializeShadow(shadow: Block | null) { + if (!shadow) { + return; + } + this.shadowDom = Xml.blockToDom(shadow) as Element; + this.shadowState = blocks.save(shadow); + } + + /** + * Returns the connection (starting at the startBlock) which will accept + * the given connection. This includes compatible connection types and + * connection checks. + * + * @param startBlock The block on which to start the search. + * @param orphanConnection The connection that is looking for a home. + * @returns The suitable connection point on the chain of blocks, or null. + */ + static getConnectionForOrphanedConnection( + startBlock: Block, + orphanConnection: Connection, + ): Connection | null { + if (orphanConnection.type === ConnectionType.OUTPUT_VALUE) { + return getConnectionForOrphanedOutput( + startBlock, + orphanConnection.getSourceBlock(), + ); + } + // Otherwise we're dealing with a stack. + const connection = startBlock.lastConnectionInStack(true); + const checker = orphanConnection.getConnectionChecker(); + if (connection && checker.canConnect(orphanConnection, connection, false)) { + return connection; + } + return null; + } +} + +/** + * Update two connections to target each other. + * + * @param first The first connection to update. + * @param second The second connection to update. + */ +function connectReciprocally(first: Connection, second: Connection) { + if (!first || !second) { + throw Error('Cannot connect null connections.'); + } + first.targetConnection = second; + second.targetConnection = first; +} +/** + * Returns the single connection on the block that will accept the orphaned + * block, if one can be found. If the block has multiple compatible connections + * (even if they are filled) this returns null. If the block has no compatible + * connections, this returns null. + * + * @param block The superior block. + * @param orphanBlock The inferior block. + * @returns The suitable connection point on 'block', or null. + */ +function getSingleConnection( + block: Block, + orphanBlock: Block, +): Connection | null { + let foundConnection = null; + const output = orphanBlock.outputConnection; + const typeChecker = output?.getConnectionChecker(); + + for (let i = 0, input; (input = block.inputList[i]); i++) { + const connection = input.connection; + if (connection && typeChecker?.canConnect(output, connection, false)) { + if (foundConnection) { + return null; // More than one connection. + } + foundConnection = connection; + } + } + return foundConnection; +} + +/** + * Walks down a row a blocks, at each stage checking if there are any + * connections that will accept the orphaned block. If at any point there + * are zero or multiple eligible connections, returns null. Otherwise + * returns the only input on the last block in the chain. + * Terminates early for shadow blocks. + * + * @param startBlock The block on which to start the search. + * @param orphanBlock The block that is looking for a home. + * @returns The suitable connection point on the chain of blocks, or null. + */ +function getConnectionForOrphanedOutput( + startBlock: Block, + orphanBlock: Block, +): Connection | null { + let newBlock: Block | null = startBlock; + let connection; + while ((connection = getSingleConnection(newBlock, orphanBlock))) { + newBlock = connection.targetBlock(); + if (!newBlock || newBlock.isShadow()) { + return connection; + } + } + return null; +} diff --git a/core/connection_checker.ts b/core/connection_checker.ts new file mode 100644 index 00000000000..6f5ecd5d5c1 --- /dev/null +++ b/core/connection_checker.ts @@ -0,0 +1,348 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * An object that encapsulates logic for checking whether a + * potential connection is safe and valid. + * + * @class + */ +// Former goog.module ID: Blockly.ConnectionChecker + +import * as common from './common.js'; +import {Connection} from './connection.js'; +import {ConnectionType} from './connection_type.js'; +import type {IConnectionChecker} from './interfaces/i_connection_checker.js'; +import * as internalConstants from './internal_constants.js'; +import * as registry from './registry.js'; +import type {RenderedConnection} from './rendered_connection.js'; + +/** + * Class for connection type checking logic. + */ +export class ConnectionChecker implements IConnectionChecker { + /** + * Check whether the current connection can connect with the target + * connection. + * + * @param a Connection to check compatibility with. + * @param b Connection to check compatibility with. + * @param isDragging True if the connection is being made by dragging a block. + * @param opt_distance The max allowable distance between the connections for + * drag checks. + * @returns Whether the connection is legal. + */ + canConnect( + a: Connection | null, + b: Connection | null, + isDragging: boolean, + opt_distance?: number, + ): boolean { + return ( + this.canConnectWithReason(a, b, isDragging, opt_distance) === + Connection.CAN_CONNECT + ); + } + + /** + * Checks whether the current connection can connect with the target + * connection, and return an error code if there are problems. + * + * @param a Connection to check compatibility with. + * @param b Connection to check compatibility with. + * @param isDragging True if the connection is being made by dragging a block. + * @param opt_distance The max allowable distance between the connections for + * drag checks. + * @returns Connection.CAN_CONNECT if the connection is legal, an error code + * otherwise. + */ + canConnectWithReason( + a: Connection | null, + b: Connection | null, + isDragging: boolean, + opt_distance?: number, + ): number { + const safety = this.doSafetyChecks(a, b); + if (safety !== Connection.CAN_CONNECT) { + return safety; + } + + // If the safety checks passed, both connections are non-null. + const connOne = a!; + const connTwo = b!; + if (!this.doTypeChecks(connOne, connTwo)) { + return Connection.REASON_CHECKS_FAILED; + } + + if ( + isDragging && + !this.doDragChecks( + a as RenderedConnection, + b as RenderedConnection, + opt_distance || 0, + ) + ) { + return Connection.REASON_DRAG_CHECKS_FAILED; + } + + return Connection.CAN_CONNECT; + } + + /** + * Helper method that translates a connection error code into a string. + * + * @param errorCode The error code. + * @param a One of the two connections being checked. + * @param b The second of the two connections being checked. + * @returns A developer-readable error string. + */ + getErrorMessage( + errorCode: number, + a: Connection | null, + b: Connection | null, + ): string { + switch (errorCode) { + case Connection.REASON_SELF_CONNECTION: + return 'Attempted to connect a block to itself.'; + case Connection.REASON_DIFFERENT_WORKSPACES: + // Usually this means one block has been deleted. + return 'Blocks not on same workspace.'; + case Connection.REASON_WRONG_TYPE: + return 'Attempt to connect incompatible types.'; + case Connection.REASON_TARGET_NULL: + return 'Target connection is null.'; + case Connection.REASON_CHECKS_FAILED: { + const connOne = a!; + const connTwo = b!; + let msg = 'Connection checks failed. '; + msg += + connOne + + ' expected ' + + connOne.getCheck() + + ', found ' + + connTwo.getCheck(); + return msg; + } + case Connection.REASON_SHADOW_PARENT: + return 'Connecting non-shadow to shadow block.'; + case Connection.REASON_DRAG_CHECKS_FAILED: + return 'Drag checks failed.'; + case Connection.REASON_PREVIOUS_AND_OUTPUT: + return 'Block would have an output and a previous connection.'; + default: + return 'Unknown connection failure: this should never happen!'; + } + } + + /** + * Check that connecting the given connections is safe, meaning that it would + * not break any of Blockly's basic assumptions (e.g. no self connections). + * + * @param a The first of the connections to check. + * @param b The second of the connections to check. + * @returns An enum with the reason this connection is safe or unsafe. + */ + doSafetyChecks(a: Connection | null, b: Connection | null): number { + if (!a || !b) { + return Connection.REASON_TARGET_NULL; + } + let superiorBlock; + let inferiorBlock; + let superiorConnection; + let inferiorConnection; + if (a.isSuperior()) { + superiorBlock = a.getSourceBlock(); + inferiorBlock = b.getSourceBlock(); + superiorConnection = a; + inferiorConnection = b; + } else { + inferiorBlock = a.getSourceBlock(); + superiorBlock = b.getSourceBlock(); + inferiorConnection = a; + superiorConnection = b; + } + if (superiorBlock === inferiorBlock) { + return Connection.REASON_SELF_CONNECTION; + } else if ( + inferiorConnection.type !== + internalConstants.OPPOSITE_TYPE[superiorConnection.type] + ) { + return Connection.REASON_WRONG_TYPE; + } else if (superiorBlock.workspace !== inferiorBlock.workspace) { + return Connection.REASON_DIFFERENT_WORKSPACES; + } else if (superiorBlock.isShadow() && !inferiorBlock.isShadow()) { + return Connection.REASON_SHADOW_PARENT; + } else if ( + inferiorConnection.type === ConnectionType.OUTPUT_VALUE && + inferiorBlock.previousConnection && + inferiorBlock.previousConnection.isConnected() + ) { + return Connection.REASON_PREVIOUS_AND_OUTPUT; + } else if ( + inferiorConnection.type === ConnectionType.PREVIOUS_STATEMENT && + inferiorBlock.outputConnection && + inferiorBlock.outputConnection.isConnected() + ) { + return Connection.REASON_PREVIOUS_AND_OUTPUT; + } + return Connection.CAN_CONNECT; + } + + /** + * Check whether this connection is compatible with another connection with + * respect to the value type system. E.g. square_root("Hello") is not + * compatible. + * + * @param a Connection to compare. + * @param b Connection to compare against. + * @returns True if the connections share a type. + */ + doTypeChecks(a: Connection, b: Connection): boolean { + const checkArrayOne = a.getCheck(); + const checkArrayTwo = b.getCheck(); + + if (!checkArrayOne || !checkArrayTwo) { + // One or both sides are promiscuous enough that anything will fit. + return true; + } + // Find any intersection in the check lists. + for (let i = 0; i < checkArrayOne.length; i++) { + if (checkArrayTwo.includes(checkArrayOne[i])) { + return true; + } + } + // No intersection. + return false; + } + + /** + * Check whether this connection can be made by dragging. + * + * @param a Connection to compare (on the block that's being dragged). + * @param b Connection to compare against. + * @param distance The maximum allowable distance between connections. + * @returns True if the connection is allowed during a drag. + */ + doDragChecks( + a: RenderedConnection, + b: RenderedConnection, + distance: number, + ): boolean { + if (a.distanceFrom(b) > distance) { + return false; + } + + // Don't consider insertion markers. + if (b.getSourceBlock().isInsertionMarker()) { + return false; + } + + switch (b.type) { + case ConnectionType.PREVIOUS_STATEMENT: + return this.canConnectToPrevious_(a, b); + case ConnectionType.OUTPUT_VALUE: { + // Don't offer to connect an already connected left (male) value plug to + // an available right (female) value plug. + if ( + (b.isConnected() && !b.targetBlock()!.isInsertionMarker()) || + a.isConnected() + ) { + return false; + } + break; + } + case ConnectionType.INPUT_VALUE: { + // Offering to connect the left (male) of a value block to an already + // connected value pair is ok, we'll splice it in. + // However, don't offer to splice into an immovable block. + if ( + b.isConnected() && + !b.targetBlock()!.isMovable() && + !b.targetBlock()!.isShadow() + ) { + return false; + } + break; + } + case ConnectionType.NEXT_STATEMENT: { + // Don't let a block with no next connection bump other blocks out of + // the stack. But covering up a shadow block or stack of shadow blocks + // is fine. Similarly, replacing a terminal statement with another + // terminal statement is allowed. + if ( + b.isConnected() && + !a.getSourceBlock().nextConnection && + !b.targetBlock()!.isShadow() && + b.targetBlock()!.nextConnection + ) { + return false; + } + + // Don't offer to splice into a stack where the connected block is + // immovable, unless the block is a shadow block. + if ( + b.targetBlock() && + !b.targetBlock()!.isMovable() && + !b.targetBlock()!.isShadow() + ) { + return false; + } + break; + } + default: + // Unexpected connection type. + return false; + } + + // Don't let blocks try to connect to themselves or ones they nest. + if (common.draggingConnections.includes(b)) { + return false; + } + + return true; + } + + /** + * Helper function for drag checking. + * + * @param a The connection to check, which must be a statement input or next + * connection. + * @param b A nearby connection to check, which must be a previous connection. + * @returns True if the connection is allowed, false otherwise. + */ + protected canConnectToPrevious_(a: Connection, b: Connection): boolean { + if (a.targetConnection) { + // This connection is already occupied. + // A next connection will never disconnect itself mid-drag. + return false; + } + + // Don't let blocks try to connect to themselves or ones they nest. + if (common.draggingConnections.includes(b)) { + return false; + } + + if (!b.targetConnection) { + return true; + } + + const targetBlock = b.targetBlock(); + // If it is connected to a real block, game over. + if (!targetBlock!.isInsertionMarker()) { + return false; + } + // If it's connected to an insertion marker but that insertion marker + // is the first block in a stack, it's still fine. If that insertion + // marker is in the middle of a stack, it won't work. + return !targetBlock!.getPreviousBlock(); + } +} + +registry.register( + registry.Type.CONNECTION_CHECKER, + registry.DEFAULT, + ConnectionChecker, +); diff --git a/core/connection_db.js b/core/connection_db.js deleted file mode 100644 index 8b3c3008e3c..00000000000 --- a/core/connection_db.js +++ /dev/null @@ -1,301 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2011 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Components for managing connections between blocks. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.ConnectionDB'); - -goog.require('Blockly.Connection'); - - -/** - * Database of connections. - * Connections are stored in order of their vertical component. This way - * connections in an area may be looked up quickly using a binary search. - * @constructor - */ -Blockly.ConnectionDB = function() { -}; - -Blockly.ConnectionDB.prototype = new Array(); -/** - * Don't inherit the constructor from Array. - * @type {!Function} - */ -Blockly.ConnectionDB.constructor = Blockly.ConnectionDB; - -/** - * Add a connection to the database. Must not already exist in DB. - * @param {!Blockly.Connection} connection The connection to be added. - */ -Blockly.ConnectionDB.prototype.addConnection = function(connection) { - if (connection.inDB_) { - throw 'Connection already in database.'; - } - if (connection.getSourceBlock().isInFlyout) { - // Don't bother maintaining a database of connections in a flyout. - return; - } - var position = this.findPositionForConnection_(connection); - this.splice(position, 0, connection); - connection.inDB_ = true; -}; - -/** - * Find the given connection. - * Starts by doing a binary search to find the approximate location, then - * linearly searches nearby for the exact connection. - * @param {!Blockly.Connection} conn The connection to find. - * @return {number} The index of the connection, or -1 if the connection was - * not found. - */ -Blockly.ConnectionDB.prototype.findConnection = function(conn) { - if (!this.length) { - return -1; - } - - var bestGuess = this.findPositionForConnection_(conn); - if (bestGuess >= this.length) { - // Not in list - return -1; - } - - var yPos = conn.y_; - // Walk forward and back on the y axis looking for the connection. - var pointerMin = bestGuess; - var pointerMax = bestGuess; - while (pointerMin >= 0 && this[pointerMin].y_ == yPos) { - if (this[pointerMin] == conn) { - return pointerMin; - } - pointerMin--; - } - - while (pointerMax < this.length && this[pointerMax].y_ == yPos) { - if (this[pointerMax] == conn) { - return pointerMax; - } - pointerMax++; - } - return -1; -}; - -/** - * Finds a candidate position for inserting this connection into the list. - * This will be in the correct y order but makes no guarantees about ordering in - * the x axis. - * @param {!Blockly.Connection} connection The connection to insert. - * @return {number} The candidate index. - * @private - */ -Blockly.ConnectionDB.prototype.findPositionForConnection_ = - function(connection) { - /* eslint-disable indent */ - if (!this.length) { - return 0; - } - var pointerMin = 0; - var pointerMax = this.length; - while (pointerMin < pointerMax) { - var pointerMid = Math.floor((pointerMin + pointerMax) / 2); - if (this[pointerMid].y_ < connection.y_) { - pointerMin = pointerMid + 1; - } else if (this[pointerMid].y_ > connection.y_) { - pointerMax = pointerMid; - } else { - pointerMin = pointerMid; - break; - } - } - return pointerMin; -}; /* eslint-enable indent */ - -/** - * Remove a connection from the database. Must already exist in DB. - * @param {!Blockly.Connection} connection The connection to be removed. - * @private - */ -Blockly.ConnectionDB.prototype.removeConnection_ = function(connection) { - if (!connection.inDB_) { - throw 'Connection not in database.'; - } - var removalIndex = this.findConnection(connection); - if (removalIndex == -1) { - throw 'Unable to find connection in connectionDB.'; - } - connection.inDB_ = false; - this.splice(removalIndex, 1); -}; - -/** - * Find all nearby connections to the given connection. - * Type checking does not apply, since this function is used for bumping. - * @param {!Blockly.Connection} connection The connection whose neighbours - * should be returned. - * @param {number} maxRadius The maximum radius to another connection. - * @return {!Array.} List of connections. - */ -Blockly.ConnectionDB.prototype.getNeighbours = function(connection, maxRadius) { - var db = this; - var currentX = connection.x_; - var currentY = connection.y_; - - // Binary search to find the closest y location. - var pointerMin = 0; - var pointerMax = db.length - 2; - var pointerMid = pointerMax; - while (pointerMin < pointerMid) { - if (db[pointerMid].y_ < currentY) { - pointerMin = pointerMid; - } else { - pointerMax = pointerMid; - } - pointerMid = Math.floor((pointerMin + pointerMax) / 2); - } - - var neighbours = []; - /** - * Computes if the current connection is within the allowed radius of another - * connection. - * This function is a closure and has access to outside variables. - * @param {number} yIndex The other connection's index in the database. - * @return {boolean} True if the current connection's vertical distance from - * the other connection is less than the allowed radius. - */ - function checkConnection_(yIndex) { - var dx = currentX - db[yIndex].x_; - var dy = currentY - db[yIndex].y_; - var r = Math.sqrt(dx * dx + dy * dy); - if (r <= maxRadius) { - neighbours.push(db[yIndex]); - } - return dy < maxRadius; - } - - // Walk forward and back on the y axis looking for the closest x,y point. - pointerMin = pointerMid; - pointerMax = pointerMid; - if (db.length) { - while (pointerMin >= 0 && checkConnection_(pointerMin)) { - pointerMin--; - } - do { - pointerMax++; - } while (pointerMax < db.length && checkConnection_(pointerMax)); - } - - return neighbours; -}; - - -/** - * Is the candidate connection close to the reference connection. - * Extremely fast; only looks at Y distance. - * @param {number} index Index in database of candidate connection. - * @param {number} baseY Reference connection's Y value. - * @param {number} maxRadius The maximum radius to another connection. - * @return {boolean} True if connection is in range. - * @private - */ -Blockly.ConnectionDB.prototype.isInYRange_ = function(index, baseY, maxRadius) { - return (Math.abs(this[index].y_ - baseY) <= maxRadius); -}; - -/** - * Find the closest compatible connection to this connection. - * @param {!Blockly.Connection} conn The connection searching for a compatible - * mate. - * @param {number} maxRadius The maximum radius to another connection. - * @param {!goog.math.Coordinate} dxy Offset between this connection's location - * in the database and the current location (as a result of dragging). - * @return {!{connection: ?Blockly.Connection, radius: number}} Contains two - * properties:' connection' which is either another connection or null, - * and 'radius' which is the distance. - */ -Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, - dxy) { - // Don't bother. - if (!this.length) { - return {connection: null, radius: maxRadius}; - } - - // Stash the values of x and y from before the drag. - var baseY = conn.y_; - var baseX = conn.x_; - - conn.x_ = baseX + dxy.x; - conn.y_ = baseY + dxy.y; - - // findPositionForConnection finds an index for insertion, which is always - // after any block with the same y index. We want to search both forward - // and back, so search on both sides of the index. - var closestIndex = this.findPositionForConnection_(conn); - - var bestConnection = null; - var bestRadius = maxRadius; - var temp; - - // Walk forward and back on the y axis looking for the closest x,y point. - var pointerMin = closestIndex - 1; - while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y_, maxRadius)) { - temp = this[pointerMin]; - if (conn.isConnectionAllowed(temp, bestRadius)) { - bestConnection = temp; - bestRadius = temp.distanceFrom(conn); - } - pointerMin--; - } - - var pointerMax = closestIndex; - while (pointerMax < this.length && this.isInYRange_(pointerMax, conn.y_, - maxRadius)) { - temp = this[pointerMax]; - if (conn.isConnectionAllowed(temp, bestRadius)) { - bestConnection = temp; - bestRadius = temp.distanceFrom(conn); - } - pointerMax++; - } - - // Reset the values of x and y. - conn.x_ = baseX; - conn.y_ = baseY; - - // If there were no valid connections, bestConnection will be null. - return {connection: bestConnection, radius: bestRadius}; -}; - -/** - * Initialize a set of connection DBs for a specified workspace. - * @param {!Blockly.Workspace} workspace The workspace this DB is for. - */ -Blockly.ConnectionDB.init = function(workspace) { - // Create four databases, one for each connection type. - var dbList = []; - dbList[Blockly.INPUT_VALUE] = new Blockly.ConnectionDB(); - dbList[Blockly.OUTPUT_VALUE] = new Blockly.ConnectionDB(); - dbList[Blockly.NEXT_STATEMENT] = new Blockly.ConnectionDB(); - dbList[Blockly.PREVIOUS_STATEMENT] = new Blockly.ConnectionDB(); - workspace.connectionDBList = dbList; -}; diff --git a/core/connection_db.ts b/core/connection_db.ts new file mode 100644 index 00000000000..8a83d154814 --- /dev/null +++ b/core/connection_db.ts @@ -0,0 +1,297 @@ +/** + * @license + * Copyright 2011 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * A database of all the rendered connections that could + * possibly be connected to (i.e. not collapsed, etc). + * Sorted by y coordinate. + * + * @class + */ +// Former goog.module ID: Blockly.ConnectionDB + +import {ConnectionType} from './connection_type.js'; +import type {IConnectionChecker} from './interfaces/i_connection_checker.js'; +import type {RenderedConnection} from './rendered_connection.js'; +import type {Coordinate} from './utils/coordinate.js'; + +/** + * Database of connections. + * Connections are stored in order of their vertical component. This way + * connections in an area may be looked up quickly using a binary search. + */ +export class ConnectionDB { + /** Array of connections sorted by y position in workspace units. */ + private readonly connections: RenderedConnection[] = []; + + /** + * @param connectionChecker The workspace's connection type checker, used to + * decide if connections are valid during a drag. + */ + constructor(private readonly connectionChecker: IConnectionChecker) {} + + /** + * Add a connection to the database. Should not already exist in the database. + * + * @param connection The connection to be added. + * @param yPos The y position used to decide where to insert the connection. + * @internal + */ + addConnection(connection: RenderedConnection, yPos: number) { + const index = this.calculateIndexForYPos(yPos); + this.connections.splice(index, 0, connection); + } + + /** + * Finds the index of the given connection. + * + * Starts by doing a binary search to find the approximate location, then + * linearly searches nearby for the exact connection. + * + * @param conn The connection to find. + * @param yPos The y position used to find the index of the connection. + * @returns The index of the connection, or -1 if the connection was not + * found. + */ + private findIndexOfConnection( + conn: RenderedConnection, + yPos: number, + ): number { + if (!this.connections.length) { + return -1; + } + + const bestGuess = this.calculateIndexForYPos(yPos); + if (bestGuess >= this.connections.length) { + // Not in list + return -1; + } + + yPos = conn.y; + // Walk forward and back on the y axis looking for the connection. + let pointer = bestGuess; + while (pointer >= 0 && this.connections[pointer].y === yPos) { + if (this.connections[pointer] === conn) { + return pointer; + } + pointer--; + } + + pointer = bestGuess; + while ( + pointer < this.connections.length && + this.connections[pointer].y === yPos + ) { + if (this.connections[pointer] === conn) { + return pointer; + } + pointer++; + } + return -1; + } + + /** + * Finds the correct index for the given y position. + * + * @param yPos The y position used to decide where to insert the connection. + * @returns The candidate index. + */ + private calculateIndexForYPos(yPos: number): number { + if (!this.connections.length) { + return 0; + } + let pointerMin = 0; + let pointerMax = this.connections.length; + while (pointerMin < pointerMax) { + const pointerMid = Math.floor((pointerMin + pointerMax) / 2); + if (this.connections[pointerMid].y < yPos) { + pointerMin = pointerMid + 1; + } else if (this.connections[pointerMid].y > yPos) { + pointerMax = pointerMid; + } else { + pointerMin = pointerMid; + break; + } + } + return pointerMin; + } + + /** + * Remove a connection from the database. Must already exist in DB. + * + * @param connection The connection to be removed. + * @param yPos The y position used to find the index of the connection. + * @throws {Error} If the connection cannot be found in the database. + */ + removeConnection(connection: RenderedConnection, yPos: number) { + const index = this.findIndexOfConnection(connection, yPos); + if (index === -1) { + throw Error('Unable to find connection in connectionDB.'); + } + this.connections.splice(index, 1); + } + + /** + * Find all nearby connections to the given connection. + * Type checking does not apply, since this function is used for bumping. + * + * @param connection The connection whose neighbours should be returned. + * @param maxRadius The maximum radius to another connection. + * @returns List of connections. + */ + getNeighbours( + connection: RenderedConnection, + maxRadius: number, + ): RenderedConnection[] { + const db = this.connections; + const currentX = connection.x; + const currentY = connection.y; + + // Binary search to find the closest y location. + let pointerMin = 0; + let pointerMax = db.length - 2; + let pointerMid = pointerMax; + while (pointerMin < pointerMid) { + if (db[pointerMid].y < currentY) { + pointerMin = pointerMid; + } else { + pointerMax = pointerMid; + } + pointerMid = Math.floor((pointerMin + pointerMax) / 2); + } + + const neighbours: RenderedConnection[] = []; + /** + * Computes if the current connection is within the allowed radius of + * another connection. This function is a closure and has access to outside + * variables. + * + * @param yIndex The other connection's index in the database. + * @returns True if the current connection's vertical distance from the + * other connection is less than the allowed radius. + */ + function checkConnection(yIndex: number): boolean { + const dx = currentX - db[yIndex].x; + const dy = currentY - db[yIndex].y; + const r = Math.sqrt(dx * dx + dy * dy); + if (r <= maxRadius) { + neighbours.push(db[yIndex]); + } + return dy < maxRadius; + } + + // Walk forward and back on the y axis looking for the closest x,y point. + pointerMin = pointerMid; + pointerMax = pointerMid; + if (db.length) { + while (pointerMin >= 0 && checkConnection(pointerMin)) { + pointerMin--; + } + do { + pointerMax++; + } while (pointerMax < db.length && checkConnection(pointerMax)); + } + + return neighbours; + } + + /** + * Is the candidate connection close to the reference connection. + * Extremely fast; only looks at Y distance. + * + * @param index Index in database of candidate connection. + * @param baseY Reference connection's Y value. + * @param maxRadius The maximum radius to another connection. + * @returns True if connection is in range. + */ + private isInYRange(index: number, baseY: number, maxRadius: number): boolean { + return Math.abs(this.connections[index].y - baseY) <= maxRadius; + } + + /** + * Find the closest compatible connection to this connection. + * + * @param conn The connection searching for a compatible mate. + * @param maxRadius The maximum radius to another connection. + * @param dxy Offset between this connection's location in the database and + * the current location (as a result of dragging). + * @returns Contains two properties: 'connection' which is either another + * connection or null, and 'radius' which is the distance. + */ + searchForClosest( + conn: RenderedConnection, + maxRadius: number, + dxy: Coordinate, + ): {connection: RenderedConnection | null; radius: number} { + if (!this.connections.length) { + // Don't bother. + return {connection: null, radius: maxRadius}; + } + + // Stash the values of x and y from before the drag. + const baseY = conn.y; + const baseX = conn.x; + + conn.x = baseX + dxy.x; + conn.y = baseY + dxy.y; + + // calculateIndexForYPos_ finds an index for insertion, which is always + // after any block with the same y index. We want to search both forward + // and back, so search on both sides of the index. + const closestIndex = this.calculateIndexForYPos(conn.y); + + let bestConnection = null; + let bestRadius = maxRadius; + let temp; + + // Walk forward and back on the y axis looking for the closest x,y point. + let pointerMin = closestIndex - 1; + while (pointerMin >= 0 && this.isInYRange(pointerMin, conn.y, maxRadius)) { + temp = this.connections[pointerMin]; + if (this.connectionChecker.canConnect(conn, temp, true, bestRadius)) { + bestConnection = temp; + bestRadius = temp.distanceFrom(conn); + } + pointerMin--; + } + + let pointerMax = closestIndex; + while ( + pointerMax < this.connections.length && + this.isInYRange(pointerMax, conn.y, maxRadius) + ) { + temp = this.connections[pointerMax]; + if (this.connectionChecker.canConnect(conn, temp, true, bestRadius)) { + bestConnection = temp; + bestRadius = temp.distanceFrom(conn); + } + pointerMax++; + } + + // Reset the values of x and y. + conn.x = baseX; + conn.y = baseY; + // If there were no valid connections, bestConnection will be null. + return {connection: bestConnection, radius: bestRadius}; + } + + /** + * Initialize a set of connection DBs for a workspace. + * + * @param checker The workspace's connection checker, used to decide if + * connections are valid during a drag. + * @returns Array of databases. + */ + static init(checker: IConnectionChecker): ConnectionDB[] { + // Create four databases, one for each connection type. + const dbList = []; + dbList[ConnectionType.INPUT_VALUE] = new ConnectionDB(checker); + dbList[ConnectionType.OUTPUT_VALUE] = new ConnectionDB(checker); + dbList[ConnectionType.NEXT_STATEMENT] = new ConnectionDB(checker); + dbList[ConnectionType.PREVIOUS_STATEMENT] = new ConnectionDB(checker); + return dbList; + } +} diff --git a/core/connection_type.ts b/core/connection_type.ts new file mode 100644 index 00000000000..ca18d00cbd2 --- /dev/null +++ b/core/connection_type.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.ConnectionType + +/** + * Enum for the type of a connection or input. + */ +export enum ConnectionType { + // A right-facing value input. E.g. 'set item to' or 'return'. + INPUT_VALUE = 1, + // A left-facing value output. E.g. 'random fraction'. + OUTPUT_VALUE, + // A down-facing block stack. E.g. 'if-do' or 'else'. + NEXT_STATEMENT, + // An up-facing block stack. E.g. 'break out of loop'. + PREVIOUS_STATEMENT, +} diff --git a/core/constants.js b/core/constants.js deleted file mode 100644 index f5428ff741f..00000000000 --- a/core/constants.js +++ /dev/null @@ -1,267 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Blockly constants. - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -goog.provide('Blockly.constants'); - - -/** - * Number of pixels the mouse must move before a drag starts. - */ -Blockly.DRAG_RADIUS = 5; - -/** - * Number of pixels the mouse must move before a drag/scroll starts from the - * flyout. Because the drag-intention is determined when this is reached, it is - * larger than Blockly.DRAG_RADIUS so that the drag-direction is clearer. - */ -Blockly.FLYOUT_DRAG_RADIUS = 10; - -/** - * Maximum misalignment between connections for them to snap together. - */ -Blockly.SNAP_RADIUS = 20; - -/** - * Delay in ms between trigger and bumping unconnected block out of alignment. - */ -Blockly.BUMP_DELAY = 250; - -/** - * Number of characters to truncate a collapsed block to. - */ -Blockly.COLLAPSE_CHARS = 30; - -/** - * Length in ms for a touch to become a long press. - */ -Blockly.LONGPRESS = 750; - -/** - * Prevent a sound from playing if another sound preceded it within this many - * milliseconds. - */ -Blockly.SOUND_LIMIT = 100; - -/** - * When dragging a block out of a stack, split the stack in two (true), or drag - * out the block healing the stack (false). - */ -Blockly.DRAG_STACK = true; - -/** - * The richness of block colours, regardless of the hue. - * Must be in the range of 0 (inclusive) to 1 (exclusive). - */ -Blockly.HSV_SATURATION = 0.45; - -/** - * The intensity of block colours, regardless of the hue. - * Must be in the range of 0 (inclusive) to 1 (exclusive). - */ -Blockly.HSV_VALUE = 0.65; - -/** - * Sprited icons and images. - */ -Blockly.SPRITE = { - width: 96, - height: 124, - url: 'sprites.png' -}; - -// Constants below this point are not intended to be changed. - -/** - * Required name space for SVG elements. - * @const - */ -Blockly.SVG_NS = 'http://www.w3.org/2000/svg'; - -/** - * Required name space for HTML elements. - * @const - */ -Blockly.HTML_NS = 'http://www.w3.org/1999/xhtml'; - -/** - * ENUM for a right-facing value input. E.g. 'set item to' or 'return'. - * @const - */ -Blockly.INPUT_VALUE = 1; - -/** - * ENUM for a left-facing value output. E.g. 'random fraction'. - * @const - */ -Blockly.OUTPUT_VALUE = 2; - -/** - * ENUM for a down-facing block stack. E.g. 'if-do' or 'else'. - * @const - */ -Blockly.NEXT_STATEMENT = 3; - -/** - * ENUM for an up-facing block stack. E.g. 'break out of loop'. - * @const - */ -Blockly.PREVIOUS_STATEMENT = 4; - -/** - * ENUM for an dummy input. Used to add field(s) with no input. - * @const - */ -Blockly.DUMMY_INPUT = 5; - -/** - * ENUM for left alignment. - * @const - */ -Blockly.ALIGN_LEFT = -1; - -/** - * ENUM for centre alignment. - * @const - */ -Blockly.ALIGN_CENTRE = 0; - -/** - * ENUM for right alignment. - * @const - */ -Blockly.ALIGN_RIGHT = 1; - -/** - * ENUM for no drag operation. - * @const - */ -Blockly.DRAG_NONE = 0; - -/** - * ENUM for inside the sticky DRAG_RADIUS. - * @const - */ -Blockly.DRAG_STICKY = 1; - -/** - * ENUM for inside the non-sticky DRAG_RADIUS, for differentiating between - * clicks and drags. - * @const - */ -Blockly.DRAG_BEGIN = 1; - -/** - * ENUM for freely draggable (outside the DRAG_RADIUS, if one applies). - * @const - */ -Blockly.DRAG_FREE = 2; - -/** - * Lookup table for determining the opposite type of a connection. - * @const - */ -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; - - -/** - * ENUM for toolbox and flyout at top of screen. - * @const - */ -Blockly.TOOLBOX_AT_TOP = 0; - -/** - * ENUM for toolbox and flyout at bottom of screen. - * @const - */ -Blockly.TOOLBOX_AT_BOTTOM = 1; - -/** - * ENUM for toolbox and flyout at left of screen. - * @const - */ -Blockly.TOOLBOX_AT_LEFT = 2; - -/** - * ENUM for toolbox and flyout at right of screen. - * @const - */ -Blockly.TOOLBOX_AT_RIGHT = 3; - -/** - * ENUM representing that an event is not in any delete areas. - * Null for backwards compatibility reasons. - * @const - */ -Blockly.DELETE_AREA_NONE = null; - -/** - * ENUM representing that an event is in the delete area of the trash can. - * @const - */ -Blockly.DELETE_AREA_TRASH = 1; - -/** - * ENUM representing that an event is in the delete area of the toolbox or - * flyout. - * @const - */ -Blockly.DELETE_AREA_TOOLBOX = 2; - -/** - * String for use in the "custom" attribute of a category in toolbox xml. - * This string indicates that the category should be dynamically populated with - * variable blocks. - * @const {string} - */ -Blockly.VARIABLE_CATEGORY_NAME = 'VARIABLE'; - -/** - * String for use in the "custom" attribute of a category in toolbox xml. - * This string indicates that the category should be dynamically populated with - * procedure blocks. - * @const {string} - */ -Blockly.PROCEDURE_CATEGORY_NAME = 'PROCEDURE'; - -/** - * String for use in the dropdown created in field_variable. - * This string indicates that this option in the dropdown is 'Rename - * variable...' and if selected, should trigger the prompt to rename a variable. - * @const {string} - */ -Blockly.RENAME_VARIABLE_ID = 'RENAME_VARIABLE_ID'; - -/** - * String for use in the dropdown created in field_variable. - * This string indicates that this option in the dropdown is 'Delete the "%1" - * variable' and if selected, should trigger the prompt to delete a variable. - * @const {string} - */ -Blockly.DELETE_VARIABLE_ID = 'DELETE_VARIABLE_ID'; diff --git a/core/constants.ts b/core/constants.ts new file mode 100644 index 00000000000..538bd378300 --- /dev/null +++ b/core/constants.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.constants + +/** + * The language-neutral ID given to the collapsed input. + */ +export const COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT'; + +/** + * The language-neutral ID given to the collapsed field. + */ +export const COLLAPSED_FIELD_NAME = '_TEMP_COLLAPSED_FIELD'; + +/** + * The language-neutral ID for when the reason why a block is disabled is + * because the user manually disabled it, such as via the context menu. + */ +export const MANUALLY_DISABLED = 'MANUALLY_DISABLED'; diff --git a/core/contextmenu.js b/core/contextmenu.js deleted file mode 100644 index ebb8caeb6e7..00000000000 --- a/core/contextmenu.js +++ /dev/null @@ -1,191 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2011 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Functionality for the right-click context menus. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -/** - * @name Blockly.ContextMenu - * @namespace - */ -goog.provide('Blockly.ContextMenu'); - -goog.require('goog.dom'); -goog.require('goog.events'); -goog.require('goog.style'); -goog.require('goog.ui.Menu'); -goog.require('goog.ui.MenuItem'); - - -/** - * Which block is the context menu attached to? - * @type {Blockly.Block} - */ -Blockly.ContextMenu.currentBlock = null; - -/** - * @type {Array.} Opaque data that can be passed to unbindEvent_. - * @private - */ -Blockly.ContextMenu.eventWrapper_ = null; - -/** - * Construct the menu based on the list of options and show the menu. - * @param {!Event} e Mouse event. - * @param {!Array.} options Array of menu options. - * @param {boolean} rtl True if RTL, false if LTR. - */ -Blockly.ContextMenu.show = function(e, options, rtl) { - Blockly.WidgetDiv.show(Blockly.ContextMenu, rtl, null); - if (!options.length) { - Blockly.ContextMenu.hide(); - return; - } - var menu = Blockly.ContextMenu.populate_(options, rtl); - - goog.events.listen(menu, goog.ui.Component.EventType.ACTION, - Blockly.ContextMenu.hide); - - Blockly.ContextMenu.position_(menu, e, rtl); - // 1ms delay is required for focusing on context menus because some other - // mouse event is still waiting in the queue and clears focus. - setTimeout(function() {menu.getElement().focus();}, 1); - Blockly.ContextMenu.currentBlock = null; // May be set by Blockly.Block. -}; - -/** - * Create the context menu object and populate it with the given options. - * @param {!Array.} options Array of menu options. - * @param {boolean} rtl True if RTL, false if LTR. - * @return {!goog.ui.Menu} The menu that will be shown on right click. - * @private - */ -Blockly.ContextMenu.populate_ = function(options, rtl) { - /* Here's what one option object looks like: - {text: 'Make It So', - enabled: true, - callback: Blockly.MakeItSo} - */ - var menu = new goog.ui.Menu(); - menu.setAllowAutoFocus(true); - menu.setRightToLeft(rtl); - for (var i = 0, option; option = options[i]; i++) { - var menuItem = new goog.ui.MenuItem(option.text); - menuItem.setRightToLeft(rtl); - menu.addChild(menuItem, true); - menuItem.setEnabled(option.enabled); - if (option.enabled) { - goog.events.listen(menuItem, goog.ui.Component.EventType.ACTION, - option.callback); - menuItem.handleContextMenu = function(/* e */) { - // Right-clicking on menu option should count as a click. - goog.events.dispatchEvent(this, goog.ui.Component.EventType.ACTION); - }; - } - } - return menu; -}; - -/** - * Add the menu to the page and position it correctly. - * @param {!goog.ui.Menu} menu The menu to add and position. - * @param {!Event} e Mouse event for the right click that is making the context - * menu appear. - * @param {boolean} rtl True if RTL, false if LTR. - * @private - */ -Blockly.ContextMenu.position_ = function(menu, e, rtl) { - // Record windowSize and scrollOffset before adding menu. - var windowSize = goog.dom.getViewportSize(); - var scrollOffset = goog.style.getViewportPageOffset(document); - var div = Blockly.WidgetDiv.DIV; - menu.render(div); - var menuDom = menu.getElement(); - Blockly.utils.addClass(menuDom, 'blocklyContextMenu'); - // Prevent system context menu when right-clicking a Blockly context menu. - Blockly.bindEventWithChecks_(menuDom, 'contextmenu', null, - Blockly.utils.noEvent); - // Record menuSize after adding menu. - var menuSize = goog.style.getSize(menuDom); - - // Position the menu. - var x = e.clientX + scrollOffset.x; - var y = e.clientY + scrollOffset.y; - // Flip menu vertically if off the bottom. - if (e.clientY + menuSize.height >= windowSize.height) { - y -= menuSize.height; - } - // Flip menu horizontally if off the edge. - if (rtl) { - if (menuSize.width >= e.clientX) { - x += menuSize.width; - } - } else { - if (e.clientX + menuSize.width >= windowSize.width) { - x -= menuSize.width; - } - } - Blockly.WidgetDiv.position(x, y, windowSize, scrollOffset, rtl); -}; - -/** - * Hide the context menu. - */ -Blockly.ContextMenu.hide = function() { - Blockly.WidgetDiv.hideIfOwner(Blockly.ContextMenu); - Blockly.ContextMenu.currentBlock = null; - if (Blockly.ContextMenu.eventWrapper_) { - Blockly.unbindEvent_(Blockly.ContextMenu.eventWrapper_); - } -}; - -/** - * Create a callback function that creates and configures a block, - * then places the new block next to the original. - * @param {!Blockly.Block} block Original block. - * @param {!Element} xml XML representation of new block. - * @return {!Function} Function that creates a block. - */ -Blockly.ContextMenu.callbackFactory = function(block, xml) { - return function() { - Blockly.Events.disable(); - try { - var newBlock = Blockly.Xml.domToBlock(xml, block.workspace); - // Move the new block next to the old block. - var xy = block.getRelativeToSurfaceXY(); - if (block.RTL) { - xy.x -= Blockly.SNAP_RADIUS; - } else { - xy.x += Blockly.SNAP_RADIUS; - } - xy.y += Blockly.SNAP_RADIUS * 2; - newBlock.moveBy(xy.x, xy.y); - } finally { - Blockly.Events.enable(); - } - if (Blockly.Events.isEnabled() && !newBlock.isShadow()) { - Blockly.Events.fire(new Blockly.Events.BlockCreate(newBlock)); - } - newBlock.select(); - }; -}; diff --git a/core/contextmenu.ts b/core/contextmenu.ts new file mode 100644 index 00000000000..b49dcba51c0 --- /dev/null +++ b/core/contextmenu.ts @@ -0,0 +1,269 @@ +/** + * @license + * Copyright 2011 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.ContextMenu + +import type {Block} from './block.js'; +import type {BlockSvg} from './block_svg.js'; +import * as browserEvents from './browser_events.js'; +import * as common from './common.js'; +import {config} from './config.js'; +import type { + ContextMenuOption, + LegacyContextMenuOption, +} from './contextmenu_registry.js'; +import {EventType} from './events/type.js'; +import * as eventUtils from './events/utils.js'; +import {Menu} from './menu.js'; +import {MenuItem} from './menuitem.js'; +import * as serializationBlocks from './serialization/blocks.js'; +import * as aria from './utils/aria.js'; +import * as dom from './utils/dom.js'; +import {Rect} from './utils/rect.js'; +import * as svgMath from './utils/svg_math.js'; +import * as WidgetDiv from './widgetdiv.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; +import * as Xml from './xml.js'; + +/** + * Which block is the context menu attached to? + */ +let currentBlock: Block | null = null; + +const dummyOwner = {}; + +/** + * Gets the block the context menu is currently attached to. + * + * @returns The block the context menu is attached to. + */ +export function getCurrentBlock(): Block | null { + return currentBlock; +} + +/** + * Sets the block the context menu is currently attached to. + * + * @param block The block the context menu is attached to. + */ +export function setCurrentBlock(block: Block | null) { + currentBlock = block; +} + +/** + * Menu object. + */ +let menu_: Menu | null = null; + +/** + * Construct the menu based on the list of options and show the menu. + * + * @param e Mouse event. + * @param options Array of menu options. + * @param rtl True if RTL, false if LTR. + * @param workspace The workspace associated with the context menu, if any. + */ +export function show( + e: PointerEvent, + options: (ContextMenuOption | LegacyContextMenuOption)[], + rtl: boolean, + workspace?: WorkspaceSvg, +) { + WidgetDiv.show(dummyOwner, rtl, dispose, workspace); + if (!options.length) { + hide(); + return; + } + const menu = populate_(options, rtl, e); + menu_ = menu; + + position_(menu, e, rtl); + // 1ms delay is required for focusing on context menus because some other + // mouse event is still waiting in the queue and clears focus. + setTimeout(function () { + menu.focus(); + }, 1); + currentBlock = null; // May be set by Blockly.Block. +} + +/** + * Create the context menu object and populate it with the given options. + * + * @param options Array of menu options. + * @param rtl True if RTL, false if LTR. + * @param e The event that triggered the context menu to open. + * @returns The menu that will be shown on right click. + */ +function populate_( + options: (ContextMenuOption | LegacyContextMenuOption)[], + rtl: boolean, + e: PointerEvent, +): Menu { + /* Here's what one option object looks like: + {text: 'Make It So', + enabled: true, + callback: Blockly.MakeItSo} + */ + const menu = new Menu(); + menu.setRole(aria.Role.MENU); + for (let i = 0; i < options.length; i++) { + const option = options[i]; + const menuItem = new MenuItem(option.text); + menuItem.setRightToLeft(rtl); + menuItem.setRole(aria.Role.MENUITEM); + menu.addChild(menuItem); + menuItem.setEnabled(option.enabled); + if (option.enabled) { + const actionHandler = function () { + hide(); + requestAnimationFrame(() => { + setTimeout(() => { + // If .scope does not exist on the option, then the callback + // will not be expecting a scope parameter, so there should be + // no problems. Just assume it is a ContextMenuOption and we'll + // pass undefined if it's not. + option.callback((option as ContextMenuOption).scope, e); + }, 0); + }); + }; + menuItem.onAction(actionHandler, {}); + } + } + return menu; +} + +/** + * Add the menu to the page and position it correctly. + * + * @param menu The menu to add and position. + * @param e Mouse event for the right click that is making the context + * menu appear. + * @param rtl True if RTL, false if LTR. + */ +function position_(menu: Menu, e: Event, rtl: boolean) { + // Record windowSize and scrollOffset before adding menu. + const viewportBBox = svgMath.getViewportBBox(); + const mouseEvent = e as MouseEvent; + // This one is just a point, but we'll pretend that it's a rect so we can use + // some helper functions. + const anchorBBox = new Rect( + mouseEvent.clientY + viewportBBox.top, + mouseEvent.clientY + viewportBBox.top, + mouseEvent.clientX + viewportBBox.left, + mouseEvent.clientX + viewportBBox.left, + ); + + createWidget_(menu); + const menuSize = menu.getSize(); + + if (rtl) { + anchorBBox.left += menuSize.width; + anchorBBox.right += menuSize.width; + viewportBBox.left += menuSize.width; + viewportBBox.right += menuSize.width; + } + + WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, menuSize, rtl); + // Calling menuDom.focus() has to wait until after the menu has been placed + // correctly. Otherwise it will cause a page scroll to get the misplaced menu + // in view. See issue #1329. + menu.focus(); +} + +/** + * Create and render the menu widget inside Blockly's widget div. + * + * @param menu The menu to add to the widget div. + */ +function createWidget_(menu: Menu) { + const div = WidgetDiv.getDiv(); + if (!div) { + throw Error('Attempting to create a context menu when widget div is null'); + } + const menuDom = menu.render(div); + dom.addClass(menuDom, 'blocklyContextMenu'); + // Prevent system context menu when right-clicking a Blockly context menu. + browserEvents.conditionalBind( + menuDom as EventTarget, + 'contextmenu', + null, + haltPropagation, + ); + // Focus only after the initial render to avoid issue #1329. + menu.focus(); +} +/** + * Halts the propagation of the event without doing anything else. + * + * @param e An event. + */ +function haltPropagation(e: Event) { + // This event has been handled. No need to bubble up to the document. + e.preventDefault(); + e.stopPropagation(); +} + +/** + * Hide the context menu. + */ +export function hide() { + WidgetDiv.hideIfOwner(dummyOwner); + currentBlock = null; +} + +/** + * Dispose of the menu. + */ +export function dispose() { + if (menu_) { + menu_.dispose(); + menu_ = null; + } +} + +/** + * Create a callback function that creates and configures a block, + * then places the new block next to the original and returns it. + * + * @param block Original block. + * @param state XML or JSON object representation of the new block. + * @returns Function that creates a block. + */ +export function callbackFactory( + block: Block, + state: Element | serializationBlocks.State, +): () => BlockSvg { + return () => { + eventUtils.disable(); + let newBlock: BlockSvg; + try { + if (state instanceof Element) { + newBlock = Xml.domToBlockInternal(state, block.workspace!) as BlockSvg; + } else { + newBlock = serializationBlocks.appendInternal( + state, + block.workspace, + ) as BlockSvg; + } + // Move the new block next to the old block. + const xy = block.getRelativeToSurfaceXY(); + if (block.RTL) { + xy.x -= config.snapRadius; + } else { + xy.x += config.snapRadius; + } + xy.y += config.snapRadius * 2; + newBlock.moveBy(xy.x, xy.y); + } finally { + eventUtils.enable(); + } + if (eventUtils.isEnabled() && !newBlock.isShadow()) { + eventUtils.fire(new (eventUtils.get(EventType.BLOCK_CREATE))(newBlock)); + } + common.setSelected(newBlock); + return newBlock; + }; +} diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts new file mode 100644 index 00000000000..25ffab59b8b --- /dev/null +++ b/core/contextmenu_items.ts @@ -0,0 +1,700 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.ContextMenuItems + +import type {BlockSvg} from './block_svg.js'; +import * as clipboard from './clipboard.js'; +import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; +import * as common from './common.js'; +import {MANUALLY_DISABLED} from './constants.js'; +import { + ContextMenuRegistry, + RegistryItem, + Scope, +} from './contextmenu_registry.js'; +import * as dialog from './dialog.js'; +import * as Events from './events/events.js'; +import * as eventUtils from './events/utils.js'; +import {CommentIcon} from './icons/comment_icon.js'; +import {Msg} from './msg.js'; +import {StatementInput} from './renderers/zelos/zelos.js'; +import {Coordinate} from './utils/coordinate.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Option to undo previous action. + */ +export function registerUndo() { + const undoOption: RegistryItem = { + displayText() { + return Msg['UNDO']; + }, + preconditionFn(scope: Scope) { + if (scope.workspace!.getUndoStack().length > 0) { + return 'enabled'; + } + return 'disabled'; + }, + callback(scope: Scope) { + scope.workspace!.undo(false); + }, + scopeType: ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'undoWorkspace', + weight: 1, + }; + ContextMenuRegistry.registry.register(undoOption); +} + +/** + * Option to redo previous action. + */ +export function registerRedo() { + const redoOption: RegistryItem = { + displayText() { + return Msg['REDO']; + }, + preconditionFn(scope: Scope) { + if (scope.workspace!.getRedoStack().length > 0) { + return 'enabled'; + } + return 'disabled'; + }, + callback(scope: Scope) { + scope.workspace!.undo(true); + }, + scopeType: ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'redoWorkspace', + weight: 2, + }; + ContextMenuRegistry.registry.register(redoOption); +} + +/** + * Option to clean up blocks. + */ +export function registerCleanup() { + const cleanOption: RegistryItem = { + displayText() { + return Msg['CLEAN_UP']; + }, + preconditionFn(scope: Scope) { + if (scope.workspace!.isMovable()) { + if (scope.workspace!.getTopBlocks(false).length > 1) { + return 'enabled'; + } + return 'disabled'; + } + return 'hidden'; + }, + callback(scope: Scope) { + scope.workspace!.cleanUp(); + }, + scopeType: ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'cleanWorkspace', + weight: 3, + }; + ContextMenuRegistry.registry.register(cleanOption); +} +/** + * Creates a callback to collapse or expand top blocks. + * + * @param shouldCollapse Whether a block should collapse. + * @param topBlocks Top blocks in the workspace. + */ +function toggleOption_(shouldCollapse: boolean, topBlocks: BlockSvg[]) { + const DELAY = 10; + let ms = 0; + let timeoutCounter = 0; + function timeoutFn(block: BlockSvg) { + timeoutCounter--; + block.setCollapsed(shouldCollapse); + if (timeoutCounter === 0) { + Events.setGroup(false); + } + } + Events.setGroup(true); + for (let i = 0; i < topBlocks.length; i++) { + let block: BlockSvg | null = topBlocks[i]; + while (block) { + timeoutCounter++; + setTimeout(timeoutFn.bind(null, block), ms); + block = block.getNextBlock(); + ms += DELAY; + } + } +} + +/** + * Option to collapse all blocks. + */ +export function registerCollapse() { + const collapseOption: RegistryItem = { + displayText() { + return Msg['COLLAPSE_ALL']; + }, + preconditionFn(scope: Scope) { + if (scope.workspace!.options.collapse) { + const topBlocks = scope.workspace!.getTopBlocks(false); + for (let i = 0; i < topBlocks.length; i++) { + let block: BlockSvg | null = topBlocks[i]; + while (block) { + if (!block.isCollapsed()) { + return 'enabled'; + } + block = block.getNextBlock(); + } + } + return 'disabled'; + } + return 'hidden'; + }, + callback(scope: Scope) { + toggleOption_(true, scope.workspace!.getTopBlocks(true)); + }, + scopeType: ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'collapseWorkspace', + weight: 4, + }; + ContextMenuRegistry.registry.register(collapseOption); +} + +/** + * Option to expand all blocks. + */ +export function registerExpand() { + const expandOption: RegistryItem = { + displayText() { + return Msg['EXPAND_ALL']; + }, + preconditionFn(scope: Scope) { + if (scope.workspace!.options.collapse) { + const topBlocks = scope.workspace!.getTopBlocks(false); + for (let i = 0; i < topBlocks.length; i++) { + let block: BlockSvg | null = topBlocks[i]; + while (block) { + if (block.isCollapsed()) { + return 'enabled'; + } + block = block.getNextBlock(); + } + } + return 'disabled'; + } + return 'hidden'; + }, + callback(scope: Scope) { + toggleOption_(false, scope.workspace!.getTopBlocks(true)); + }, + scopeType: ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'expandWorkspace', + weight: 5, + }; + ContextMenuRegistry.registry.register(expandOption); +} +/** + * Adds a block and its children to a list of deletable blocks. + * + * @param block to delete. + * @param deleteList list of blocks that can be deleted. + * This will be modified in place with the given block and its descendants. + */ +function addDeletableBlocks_(block: BlockSvg, deleteList: BlockSvg[]) { + if (block.isDeletable()) { + Array.prototype.push.apply(deleteList, block.getDescendants(false)); + } else { + const children = block.getChildren(false); + for (let i = 0; i < children.length; i++) { + addDeletableBlocks_(children[i], deleteList); + } + } +} + +/** + * Constructs a list of blocks that can be deleted in the given workspace. + * + * @param workspace to delete all blocks from. + * @returns list of blocks to delete. + */ +function getDeletableBlocks_(workspace: WorkspaceSvg): BlockSvg[] { + const deleteList: BlockSvg[] = []; + const topBlocks = workspace.getTopBlocks(true); + for (let i = 0; i < topBlocks.length; i++) { + addDeletableBlocks_(topBlocks[i], deleteList); + } + return deleteList; +} + +/** + * Deletes the given blocks. Used to delete all blocks in the workspace. + * + * @param deleteList List of blocks to delete. + * @param eventGroup Event group ID with which all delete events should be + * associated. If not specified, create a new group. + */ +function deleteNext_(deleteList: BlockSvg[], eventGroup?: string) { + const DELAY = 10; + if (eventGroup) { + eventUtils.setGroup(eventGroup); + } else { + eventUtils.setGroup(true); + eventGroup = eventUtils.getGroup(); + } + const block = deleteList.shift(); + if (block) { + if (!block.isDeadOrDying()) { + block.dispose(false, true); + setTimeout(deleteNext_, DELAY, deleteList, eventGroup); + } else { + deleteNext_(deleteList, eventGroup); + } + } + eventUtils.setGroup(false); +} + +/** + * Option to delete all blocks. + */ +export function registerDeleteAll() { + const deleteOption: RegistryItem = { + displayText(scope: Scope) { + if (!scope.workspace) { + return ''; + } + const deletableBlocksLength = getDeletableBlocks_(scope.workspace).length; + if (deletableBlocksLength === 1) { + return Msg['DELETE_BLOCK']; + } + return Msg['DELETE_X_BLOCKS'].replace('%1', `${deletableBlocksLength}`); + }, + preconditionFn(scope: Scope) { + if (!scope.workspace) { + return 'disabled'; + } + const deletableBlocksLength = getDeletableBlocks_(scope.workspace).length; + return deletableBlocksLength > 0 ? 'enabled' : 'disabled'; + }, + callback(scope: Scope) { + if (!scope.workspace) { + return; + } + scope.workspace.cancelCurrentGesture(); + const deletableBlocks = getDeletableBlocks_(scope.workspace); + if (deletableBlocks.length < 2) { + deleteNext_(deletableBlocks); + } else { + dialog.confirm( + Msg['DELETE_ALL_BLOCKS'].replace( + '%1', + String(deletableBlocks.length), + ), + function (ok) { + if (ok) { + deleteNext_(deletableBlocks); + } + }, + ); + } + }, + scopeType: ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'workspaceDelete', + weight: 6, + }; + ContextMenuRegistry.registry.register(deleteOption); +} +/** Registers all workspace-scoped context menu items. */ +function registerWorkspaceOptions_() { + registerUndo(); + registerRedo(); + registerCleanup(); + registerCollapse(); + registerExpand(); + registerDeleteAll(); +} + +/** + * Option to duplicate a block. + */ +export function registerDuplicate() { + const duplicateOption: RegistryItem = { + displayText() { + return Msg['DUPLICATE_BLOCK']; + }, + preconditionFn(scope: Scope) { + const block = scope.block; + if (!block!.isInFlyout && block!.isDeletable() && block!.isMovable()) { + if (block!.isDuplicatable()) { + return 'enabled'; + } + return 'disabled'; + } + return 'hidden'; + }, + callback(scope: Scope) { + if (!scope.block) return; + const data = scope.block.toCopyData(); + if (!data) return; + clipboard.paste(data, scope.block.workspace); + }, + scopeType: ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockDuplicate', + weight: 1, + }; + ContextMenuRegistry.registry.register(duplicateOption); +} + +/** + * Option to add or remove block-level comment. + */ +export function registerComment() { + const commentOption: RegistryItem = { + displayText(scope: Scope) { + if (scope.block!.hasIcon(CommentIcon.TYPE)) { + // If there's already a comment, option is to remove. + return Msg['REMOVE_COMMENT']; + } + // If there's no comment yet, option is to add. + return Msg['ADD_COMMENT']; + }, + preconditionFn(scope: Scope) { + const block = scope.block; + if ( + !block!.isInFlyout && + block!.workspace.options.comments && + !block!.isCollapsed() && + block!.isEditable() + ) { + return 'enabled'; + } + return 'hidden'; + }, + callback(scope: Scope) { + const block = scope.block; + if (block!.hasIcon(CommentIcon.TYPE)) { + block!.setCommentText(null); + } else { + block!.setCommentText(''); + } + }, + scopeType: ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockComment', + weight: 2, + }; + ContextMenuRegistry.registry.register(commentOption); +} + +/** + * Option to inline variables. + */ +export function registerInline() { + const inlineOption: RegistryItem = { + displayText(scope: Scope) { + return scope.block!.getInputsInline() + ? Msg['EXTERNAL_INPUTS'] + : Msg['INLINE_INPUTS']; + }, + preconditionFn(scope: Scope) { + const block = scope.block; + if (!block!.isInFlyout && block!.isMovable() && !block!.isCollapsed()) { + for (let i = 1; i < block!.inputList.length; i++) { + // Only display this option if there are two value or dummy inputs + // next to each other. + if ( + !(block!.inputList[i - 1] instanceof StatementInput) && + !(block!.inputList[i] instanceof StatementInput) + ) { + return 'enabled'; + } + } + } + return 'hidden'; + }, + callback(scope: Scope) { + scope.block!.setInputsInline(!scope.block!.getInputsInline()); + }, + scopeType: ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockInline', + weight: 3, + }; + ContextMenuRegistry.registry.register(inlineOption); +} + +/** + * Option to collapse or expand a block. + */ +export function registerCollapseExpandBlock() { + const collapseExpandOption: RegistryItem = { + displayText(scope: Scope) { + return scope.block!.isCollapsed() + ? Msg['EXPAND_BLOCK'] + : Msg['COLLAPSE_BLOCK']; + }, + preconditionFn(scope: Scope) { + const block = scope.block; + if ( + !block!.isInFlyout && + block!.isMovable() && + block!.workspace.options.collapse + ) { + return 'enabled'; + } + return 'hidden'; + }, + callback(scope: Scope) { + scope.block!.setCollapsed(!scope.block!.isCollapsed()); + }, + scopeType: ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockCollapseExpand', + weight: 4, + }; + ContextMenuRegistry.registry.register(collapseExpandOption); +} + +/** + * Option to disable or enable a block. + */ +export function registerDisable() { + const disableOption: RegistryItem = { + displayText(scope: Scope) { + return scope.block!.hasDisabledReason(MANUALLY_DISABLED) + ? Msg['ENABLE_BLOCK'] + : Msg['DISABLE_BLOCK']; + }, + preconditionFn(scope: Scope) { + const block = scope.block; + if ( + !block!.isInFlyout && + block!.workspace.options.disable && + block!.isEditable() + ) { + // Determine whether this block is currently disabled for any reason + // other than the manual reason that this context menu item controls. + const disabledReasons = block!.getDisabledReasons(); + const isDisabledForOtherReason = + disabledReasons.size > + (disabledReasons.has(MANUALLY_DISABLED) ? 1 : 0); + + if (block!.getInheritedDisabled() || isDisabledForOtherReason) { + return 'disabled'; + } + return 'enabled'; + } + return 'hidden'; + }, + callback(scope: Scope) { + const block = scope.block; + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } + block!.setDisabledReason( + !block!.hasDisabledReason(MANUALLY_DISABLED), + MANUALLY_DISABLED, + ); + eventUtils.setGroup(existingGroup); + }, + scopeType: ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockDisable', + weight: 5, + }; + ContextMenuRegistry.registry.register(disableOption); +} + +/** + * Option to delete a block. + */ +export function registerDelete() { + const deleteOption: RegistryItem = { + displayText(scope: Scope) { + const block = scope.block; + // Count the number of blocks that are nested in this block. + let descendantCount = block!.getDescendants(false).length; + const nextBlock = block!.getNextBlock(); + if (nextBlock) { + // Blocks in the current stack would survive this block's deletion. + descendantCount -= nextBlock.getDescendants(false).length; + } + return descendantCount === 1 + ? Msg['DELETE_BLOCK'] + : Msg['DELETE_X_BLOCKS'].replace('%1', `${descendantCount}`); + }, + preconditionFn(scope: Scope) { + if (!scope.block!.isInFlyout && scope.block!.isDeletable()) { + return 'enabled'; + } + return 'hidden'; + }, + callback(scope: Scope) { + if (scope.block) { + scope.block.checkAndDelete(); + } + }, + scopeType: ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockDelete', + weight: 6, + }; + ContextMenuRegistry.registry.register(deleteOption); +} + +/** + * Option to open help for a block. + */ +export function registerHelp() { + const helpOption: RegistryItem = { + displayText() { + return Msg['HELP']; + }, + preconditionFn(scope: Scope) { + const block = scope.block; + const url = + typeof block!.helpUrl === 'function' + ? block!.helpUrl() + : block!.helpUrl; + if (url) { + return 'enabled'; + } + return 'hidden'; + }, + callback(scope: Scope) { + scope.block!.showHelp(); + }, + scopeType: ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockHelp', + weight: 7, + }; + ContextMenuRegistry.registry.register(helpOption); +} + +/** Registers an option for deleting a workspace comment. */ +export function registerCommentDelete() { + const deleteOption: RegistryItem = { + displayText: () => Msg['REMOVE_COMMENT'], + preconditionFn(scope: Scope) { + return scope.comment?.isDeletable() ? 'enabled' : 'hidden'; + }, + callback(scope: Scope) { + eventUtils.setGroup(true); + scope.comment?.dispose(); + eventUtils.setGroup(false); + }, + scopeType: ContextMenuRegistry.ScopeType.COMMENT, + id: 'commentDelete', + weight: 6, + }; + ContextMenuRegistry.registry.register(deleteOption); +} + +/** Registers an option for duplicating a workspace comment. */ +export function registerCommentDuplicate() { + const duplicateOption: RegistryItem = { + displayText: () => Msg['DUPLICATE_COMMENT'], + preconditionFn(scope: Scope) { + return scope.comment?.isMovable() ? 'enabled' : 'hidden'; + }, + callback(scope: Scope) { + if (!scope.comment) return; + const data = scope.comment.toCopyData(); + if (!data) return; + clipboard.paste(data, scope.comment.workspace); + }, + scopeType: ContextMenuRegistry.ScopeType.COMMENT, + id: 'commentDuplicate', + weight: 1, + }; + ContextMenuRegistry.registry.register(duplicateOption); +} + +/** Registers an option for adding a workspace comment to the workspace. */ +export function registerCommentCreate() { + const createOption: RegistryItem = { + displayText: () => Msg['ADD_COMMENT'], + preconditionFn: () => 'enabled', + callback: (scope: Scope, e: PointerEvent) => { + const workspace = scope.workspace; + if (!workspace) return; + eventUtils.setGroup(true); + const comment = new RenderedWorkspaceComment(workspace); + comment.setText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']); + comment.moveTo( + pixelsToWorkspaceCoords( + new Coordinate(e.clientX, e.clientY), + workspace, + ), + ); + common.setSelected(comment); + eventUtils.setGroup(false); + }, + scopeType: ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'commentCreate', + weight: 8, + }; + ContextMenuRegistry.registry.register(createOption); +} + +/** + * Converts pixel coordinates (relative to the window) to workspace coordinates. + */ +function pixelsToWorkspaceCoords( + pixelCoord: Coordinate, + workspace: WorkspaceSvg, +): Coordinate { + const injectionDiv = workspace.getInjectionDiv(); + // Bounding rect coordinates are in client coordinates, meaning that they + // are in pixels relative to the upper left corner of the visible browser + // window. These coordinates change when you scroll the browser window. + const boundingRect = injectionDiv.getBoundingClientRect(); + + // The client coordinates offset by the injection div's upper left corner. + const clientOffsetPixels = new Coordinate( + pixelCoord.x - boundingRect.left, + pixelCoord.y - boundingRect.top, + ); + + // The offset in pixels between the main workspace's origin and the upper + // left corner of the injection div. + const mainOffsetPixels = workspace.getOriginOffsetInPixels(); + + // The position of the new comment in pixels relative to the origin of the + // main workspace. + const finalOffset = Coordinate.difference( + clientOffsetPixels, + mainOffsetPixels, + ); + // The position of the new comment in main workspace coordinates. + finalOffset.scale(1 / workspace.scale); + return finalOffset; +} + +/** Registers all block-scoped context menu items. */ +function registerBlockOptions_() { + registerDuplicate(); + registerComment(); + registerInline(); + registerCollapseExpandBlock(); + registerDisable(); + registerDelete(); + registerHelp(); +} + +/** Registers all workspace comment related menu items. */ +export function registerCommentOptions() { + registerCommentDuplicate(); + registerCommentDelete(); + registerCommentCreate(); +} + +/** + * Registers all default context menu items. This should be called once per + * instance of ContextMenuRegistry. + * + * @internal + */ +export function registerDefaultOptions() { + registerWorkspaceOptions_(); + registerBlockOptions_(); +} + +registerDefaultOptions(); diff --git a/core/contextmenu_registry.ts b/core/contextmenu_registry.ts new file mode 100644 index 00000000000..fb0d899d141 --- /dev/null +++ b/core/contextmenu_registry.ts @@ -0,0 +1,194 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Registry for context menu option items. + * + * @class + */ +// Former goog.module ID: Blockly.ContextMenuRegistry + +import type {BlockSvg} from './block_svg.js'; +import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Class for the registry of context menu items. This is intended to be a + * singleton. You should not create a new instance, and only access this class + * from ContextMenuRegistry.registry. + */ +export class ContextMenuRegistry { + static registry: ContextMenuRegistry; + /** Registry of all registered RegistryItems, keyed by ID. */ + private registeredItems = new Map(); + + /** Resets the existing singleton instance of ContextMenuRegistry. */ + constructor() { + this.reset(); + } + + /** Clear and recreate the registry. */ + reset() { + this.registeredItems.clear(); + } + + /** + * Registers a RegistryItem. + * + * @param item Context menu item to register. + * @throws {Error} if an item with the given ID already exists. + */ + register(item: RegistryItem) { + if (this.registeredItems.has(item.id)) { + throw Error('Menu item with ID "' + item.id + '" is already registered.'); + } + this.registeredItems.set(item.id, item); + } + + /** + * Unregisters a RegistryItem with the given ID. + * + * @param id The ID of the RegistryItem to remove. + * @throws {Error} if an item with the given ID does not exist. + */ + unregister(id: string) { + if (!this.registeredItems.has(id)) { + throw new Error('Menu item with ID "' + id + '" not found.'); + } + this.registeredItems.delete(id); + } + + /** + * @param id The ID of the RegistryItem to get. + * @returns RegistryItem or null if not found + */ + getItem(id: string): RegistryItem | null { + return this.registeredItems.get(id) ?? null; + } + + /** + * Gets the valid context menu options for the given scope type (e.g. block or + * workspace) and scope. Blocks are only shown if the preconditionFn shows + * they should not be hidden. + * + * @param scopeType Type of scope where menu should be shown (e.g. on a block + * or on a workspace) + * @param scope Current scope of context menu (i.e., the exact workspace or + * block being clicked on) + * @returns the list of ContextMenuOptions + */ + getContextMenuOptions( + scopeType: ScopeType, + scope: Scope, + ): ContextMenuOption[] { + const menuOptions: ContextMenuOption[] = []; + for (const item of this.registeredItems.values()) { + if (scopeType === item.scopeType) { + const precondition = item.preconditionFn(scope); + if (precondition !== 'hidden') { + const displayText = + typeof item.displayText === 'function' + ? item.displayText(scope) + : item.displayText; + const menuOption: ContextMenuOption = { + text: displayText, + enabled: precondition === 'enabled', + callback: item.callback, + scope, + weight: item.weight, + }; + menuOptions.push(menuOption); + } + } + } + menuOptions.sort(function (a, b) { + return a.weight - b.weight; + }); + return menuOptions; + } +} + +export namespace ContextMenuRegistry { + /** + * Where this menu item should be rendered. If the menu item should be + * rendered in multiple scopes, e.g. on both a block and a workspace, it + * should be registered for each scope. + */ + export enum ScopeType { + BLOCK = 'block', + WORKSPACE = 'workspace', + COMMENT = 'comment', + } + + /** + * The actual workspace/block where the menu is being rendered. This is passed + * to callback and displayText functions that depend on this information. + */ + export interface Scope { + block?: BlockSvg; + workspace?: WorkspaceSvg; + comment?: RenderedWorkspaceComment; + } + + /** + * A menu item as entered in the registry. + */ + export interface RegistryItem { + /** + * @param scope Object that provides a reference to the thing that had its + * context menu opened. + * @param e The original event that triggered the context menu to open. Not + * the event that triggered the click on the option. + */ + callback: (scope: Scope, e: PointerEvent) => void; + scopeType: ScopeType; + displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement; + preconditionFn: (p1: Scope) => string; + weight: number; + id: string; + } + + /** + * A menu item as presented to contextmenu.js. + */ + export interface ContextMenuOption { + text: string | HTMLElement; + enabled: boolean; + /** + * @param scope Object that provides a reference to the thing that had its + * context menu opened. + * @param e The original event that triggered the context menu to open. Not + * the event that triggered the click on the option. + */ + callback: (scope: Scope, e: PointerEvent) => void; + scope: Scope; + weight: number; + } + + /** + * A subset of ContextMenuOption corresponding to what was publicly + * documented. ContextMenuOption should be preferred for new code. + */ + export interface LegacyContextMenuOption { + text: string; + enabled: boolean; + callback: (p1: Scope) => void; + } + + /** + * Singleton instance of this class. All interactions with this class should + * be done on this object. + */ + ContextMenuRegistry.registry = new ContextMenuRegistry(); +} + +export type ScopeType = ContextMenuRegistry.ScopeType; +export const ScopeType = ContextMenuRegistry.ScopeType; +export type Scope = ContextMenuRegistry.Scope; +export type RegistryItem = ContextMenuRegistry.RegistryItem; +export type ContextMenuOption = ContextMenuRegistry.ContextMenuOption; +export type LegacyContextMenuOption = + ContextMenuRegistry.LegacyContextMenuOption; diff --git a/core/css.js b/core/css.js deleted file mode 100644 index a4d766c36f2..00000000000 --- a/core/css.js +++ /dev/null @@ -1,871 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2013 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Inject Blockly's CSS synchronously. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -/** - * @name Blockly.Css - * @namespace - */ -goog.provide('Blockly.Css'); - - -/** - * List of cursors. - * @enum {string} - */ -Blockly.Css.Cursor = { - OPEN: 'handopen', - CLOSED: 'handclosed', - DELETE: 'handdelete' -}; - -/** - * Current cursor (cached value). - * @type {string} - * @private - */ -Blockly.Css.currentCursor_ = ''; - -/** - * Large stylesheet added by Blockly.Css.inject. - * @type {Element} - * @private - */ -Blockly.Css.styleSheet_ = null; - -/** - * Path to media directory, with any trailing slash removed. - * @type {string} - * @private - */ -Blockly.Css.mediaPath_ = ''; - -/** - * Inject the CSS into the DOM. This is preferable over using a regular CSS - * file since: - * a) It loads synchronously and doesn't force a redraw later. - * b) It speeds up loading by not blocking on a separate HTTP transfer. - * c) The CSS content may be made dynamic depending on init options. - * @param {boolean} hasCss If false, don't inject CSS - * (providing CSS becomes the document's responsibility). - * @param {string} pathToMedia Path from page to the Blockly media directory. - */ -Blockly.Css.inject = function(hasCss, pathToMedia) { - // Only inject the CSS once. - if (Blockly.Css.styleSheet_) { - return; - } - // Placeholder for cursor rule. Must be first rule (index 0). - var text = '.blocklyDraggable {}\n'; - if (hasCss) { - text += Blockly.Css.CONTENT.join('\n'); - if (Blockly.FieldDate) { - text += Blockly.FieldDate.CSS.join('\n'); - } - } - // Strip off any trailing slash (either Unix or Windows). - Blockly.Css.mediaPath_ = pathToMedia.replace(/[\\\/]$/, ''); - text = text.replace(/<<>>/g, Blockly.Css.mediaPath_); - // Inject CSS tag at start of head. - var cssNode = document.createElement('style'); - document.head.insertBefore(cssNode, document.head.firstChild); - - var cssTextNode = document.createTextNode(text); - cssNode.appendChild(cssTextNode); - Blockly.Css.styleSheet_ = cssNode.sheet; -}; - -/** - * Set the cursor to be displayed when over something draggable. - * See See https://github.com/google/blockly/issues/981 for context. - * @param {Blockly.Css.Cursor} cursor Enum. - * @deprecated April 2017. - */ -Blockly.Css.setCursor = function(cursor) { - console.warn('Deprecated call to Blockly.Css.setCursor.' + - 'See https://github.com/google/blockly/issues/981 for context'); -}; - -/** - * Array making up the CSS content for Blockly. - */ -Blockly.Css.CONTENT = [ - '.blocklySvg {', - 'background-color: #fff;', - 'outline: none;', - 'overflow: hidden;', /* IE overflows by default. */ - 'position: absolute;', - 'display: block;', - '}', - - '.blocklyWidgetDiv {', - 'display: none;', - 'position: absolute;', - 'z-index: 99999;', /* big value for bootstrap3 compatibility */ - '}', - - '.injectionDiv {', - 'height: 100%;', - 'position: relative;', - 'overflow: hidden;', /* So blocks in drag surface disappear at edges */ - 'touch-action: none', - '}', - - '.blocklyNonSelectable {', - 'user-select: none;', - '-moz-user-select: none;', - '-webkit-user-select: none;', - '-ms-user-select: none;', - '}', - - '.blocklyWsDragSurface {', - 'display: none;', - 'position: absolute;', - 'overflow: visible;', - 'top: 0;', - 'left: 0;', - '}', - - '.blocklyBlockDragSurface {', - 'display: none;', - 'position: absolute;', - 'top: 0;', - 'left: 0;', - 'right: 0;', - 'bottom: 0;', - 'overflow: visible !important;', - 'z-index: 50;', /* Display below toolbox, but above everything else. */ - '}', - - '.blocklyTooltipDiv {', - 'background-color: #ffffc7;', - 'border: 1px solid #ddc;', - 'box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);', - 'color: #000;', - 'display: none;', - 'font-family: sans-serif;', - 'font-size: 9pt;', - 'opacity: 0.9;', - 'padding: 2px;', - 'position: absolute;', - 'z-index: 100000;', /* big value for bootstrap3 compatibility */ - '}', - - '.blocklyResizeSE {', - 'cursor: se-resize;', - 'fill: #aaa;', - '}', - - '.blocklyResizeSW {', - 'cursor: sw-resize;', - 'fill: #aaa;', - '}', - - '.blocklyResizeLine {', - 'stroke: #888;', - 'stroke-width: 1;', - '}', - - '.blocklyHighlightedConnectionPath {', - 'fill: none;', - 'stroke: #fc3;', - 'stroke-width: 4px;', - '}', - - '.blocklyPathLight {', - 'fill: none;', - 'stroke-linecap: round;', - 'stroke-width: 1;', - '}', - - '.blocklySelected>.blocklyPath {', - 'stroke: #fc3;', - 'stroke-width: 3px;', - '}', - - '.blocklySelected>.blocklyPathLight {', - 'display: none;', - '}', - - '.blocklyDraggable {', - /* backup for browsers (e.g. IE11) that don't support grab */ - 'cursor: url("<<>>/handopen.cur"), auto;', - 'cursor: grab;', - 'cursor: -webkit-grab;', - 'cursor: -moz-grab;', - '}', - - '.blocklyDragging {', - /* backup for browsers (e.g. IE11) that don't support grabbing */ - 'cursor: url("<<>>/handclosed.cur"), auto;', - 'cursor: grabbing;', - 'cursor: -webkit-grabbing;', - 'cursor: -moz-grabbing;', - '}', - /* Changes cursor on mouse down. Not effective in Firefox because of - https://bugzilla.mozilla.org/show_bug.cgi?id=771241 */ - '.blocklyDraggable:active {', - /* backup for browsers (e.g. IE11) that don't support grabbing */ - 'cursor: url("<<>>/handclosed.cur"), auto;', - 'cursor: grabbing;', - 'cursor: -webkit-grabbing;', - 'cursor: -moz-grabbing;', - '}', - /* Change the cursor on the whole drag surface in case the mouse gets - ahead of block during a drag. This way the cursor is still a closed hand. - */ - '.blocklyBlockDragSurface .blocklyDraggable {', - /* backup for browsers (e.g. IE11) that don't support grabbing */ - 'cursor: url("<<>>/handclosed.cur"), auto;', - 'cursor: grabbing;', - 'cursor: -webkit-grabbing;', - 'cursor: -moz-grabbing;', - '}', - - '.blocklyDragging.blocklyDraggingDelete {', - 'cursor: url("<<>>/handdelete.cur"), auto;', - '}', - - '.blocklyToolboxDelete {', - '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;', - '}', - - '.blocklyText {', - 'cursor: default;', - 'fill: #fff;', - 'font-family: sans-serif;', - 'font-size: 11pt;', - '}', - - '.blocklyNonEditableText>text {', - 'pointer-events: none;', - '}', - - '.blocklyNonEditableText>rect,', - '.blocklyEditableText>rect {', - 'fill: #fff;', - 'fill-opacity: .6;', - '}', - - '.blocklyNonEditableText>text,', - '.blocklyEditableText>text {', - 'fill: #000;', - '}', - - '.blocklyEditableText:hover>rect {', - 'stroke: #fff;', - 'stroke-width: 2;', - '}', - - '.blocklyBubbleText {', - 'fill: #000;', - '}', - - '.blocklyFlyout {', - 'position: absolute;', - 'z-index: 20;', - '}', - '.blocklyFlyoutButton {', - 'fill: #888;', - 'cursor: default;', - '}', - - '.blocklyFlyoutButtonShadow {', - 'fill: #666;', - '}', - - '.blocklyFlyoutButton:hover {', - 'fill: #aaa;', - '}', - - '.blocklyFlyoutLabel {', - 'cursor: default;', - '}', - - '.blocklyFlyoutLabelBackground {', - 'opacity: 0;', - '}', - - '.blocklyFlyoutLabelText {', - 'fill: #000;', - '}', - - /* - Don't allow users to select text. It gets annoying when trying to - drag a block and selected text moves instead. - */ - '.blocklySvg text, .blocklyBlockDragSurface text {', - 'user-select: none;', - '-moz-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;', - '}', - - '.blocklyCommentTextarea {', - 'background-color: #ffc;', - 'border: 0;', - 'margin: 0;', - 'padding: 2px;', - 'resize: none;', - '}', - - '.blocklyHtmlInput {', - 'border: none;', - 'border-radius: 4px;', - 'font-family: sans-serif;', - 'height: 100%;', - 'margin: 0;', - 'outline: none;', - 'padding: 0 1px;', - 'width: 100%', - '}', - - '.blocklyMainBackground {', - 'stroke-width: 1;', - 'stroke: #c6c6c6;', /* Equates to #ddd due to border being off-pixel. */ - '}', - - '.blocklyMutatorBackground {', - 'fill: #fff;', - 'stroke: #ddd;', - 'stroke-width: 1;', - '}', - - '.blocklyFlyoutBackground {', - 'fill: #ddd;', - 'fill-opacity: .8;', - '}', - - '.blocklyTransparentBackground {', - 'opacity: 0;', - '}', - - '.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;', - '}', - - '.blocklyZoom>image {', - 'opacity: .4;', - '}', - - '.blocklyZoom>image:hover {', - 'opacity: .6;', - '}', - - '.blocklyZoom>image:active {', - 'opacity: .8;', - '}', - - /* Darken flyout scrollbars due to being on a grey background. */ - /* By contrast, workspace scrollbars are on a white background. */ - '.blocklyFlyout .blocklyScrollbarHandle {', - 'fill: #bbb;', - '}', - - '.blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,', - '.blocklyFlyout .blocklyScrollbarHandle:hover {', - 'fill: #aaa;', - '}', - - '.blocklyInvalidInput {', - 'background: #faa;', - '}', - - '.blocklyAngleCircle {', - 'stroke: #444;', - 'stroke-width: 1;', - 'fill: #ddd;', - 'fill-opacity: .8;', - '}', - - '.blocklyAngleMarks {', - 'stroke: #444;', - 'stroke-width: 1;', - '}', - - '.blocklyAngleGauge {', - 'fill: #f88;', - 'fill-opacity: .8;', - '}', - - '.blocklyAngleLine {', - 'stroke: #f00;', - 'stroke-width: 2;', - 'stroke-linecap: round;', - 'pointer-events: none;', - '}', - - '.blocklyContextMenu {', - 'border-radius: 4px;', - '}', - - '.blocklyDropdownMenu {', - 'padding: 0 !important;', - '}', - - /* Override the default Closure URL. */ - '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon {', - 'background: url(<<>>/sprites.png) no-repeat -48px -16px !important;', - '}', - - /* Category tree in Toolbox. */ - '.blocklyToolboxDiv {', - 'background-color: #ddd;', - 'overflow-x: visible;', - 'overflow-y: auto;', - 'position: absolute;', - 'z-index: 70;', /* so blocks go under toolbox when dragging */ - '}', - - '.blocklyTreeRoot {', - 'padding: 4px 0;', - '}', - - '.blocklyTreeRoot:focus {', - 'outline: none;', - '}', - - '.blocklyTreeRow {', - 'height: 22px;', - 'line-height: 22px;', - 'margin-bottom: 3px;', - 'padding-right: 8px;', - 'white-space: nowrap;', - '}', - - '.blocklyHorizontalTree {', - 'float: left;', - 'margin: 1px 5px 8px 0;', - '}', - - '.blocklyHorizontalTreeRtl {', - 'float: right;', - 'margin: 1px 0 8px 5px;', - '}', - - '.blocklyToolboxDiv[dir="RTL"] .blocklyTreeRow {', - 'margin-left: 8px;', - '}', - - '.blocklyTreeRow:not(.blocklyTreeSelected):hover {', - 'background-color: #e4e4e4;', - '}', - - '.blocklyTreeSeparator {', - 'border-bottom: solid #e5e5e5 1px;', - 'height: 0;', - 'margin: 5px 0;', - '}', - - '.blocklyTreeSeparatorHorizontal {', - 'border-right: solid #e5e5e5 1px;', - 'width: 0;', - 'padding: 5px 0;', - 'margin: 0 5px;', - '}', - - - '.blocklyTreeIcon {', - 'background-image: url(<<>>/sprites.png);', - 'height: 16px;', - 'vertical-align: middle;', - 'width: 16px;', - '}', - - '.blocklyTreeIconClosedLtr {', - 'background-position: -32px -1px;', - '}', - - '.blocklyTreeIconClosedRtl {', - 'background-position: 0px -1px;', - '}', - - '.blocklyTreeIconOpen {', - 'background-position: -16px -1px;', - '}', - - '.blocklyTreeSelected>.blocklyTreeIconClosedLtr {', - 'background-position: -32px -17px;', - '}', - - '.blocklyTreeSelected>.blocklyTreeIconClosedRtl {', - 'background-position: 0px -17px;', - '}', - - '.blocklyTreeSelected>.blocklyTreeIconOpen {', - 'background-position: -16px -17px;', - '}', - - '.blocklyTreeIconNone,', - '.blocklyTreeSelected>.blocklyTreeIconNone {', - 'background-position: -48px -1px;', - '}', - - '.blocklyTreeLabel {', - 'cursor: default;', - 'font-family: sans-serif;', - 'font-size: 16px;', - 'padding: 0 3px;', - 'vertical-align: middle;', - '}', - - '.blocklyToolboxDelete .blocklyTreeLabel {', - 'cursor: url("<<>>/handdelete.cur"), auto;', - '}', - - '.blocklyTreeSelected .blocklyTreeLabel {', - 'color: #fff;', - '}', - - /* Copied from: goog/css/colorpicker-simplegrid.css */ - /* - * Copyright 2007 The Closure Library Authors. All Rights Reserved. - * - * Use of this source code is governed by the Apache License, Version 2.0. - * See the COPYING file for details. - */ - - /* Author: pupius@google.com (Daniel Pupius) */ - - /* - Styles to make the colorpicker look like the old gmail color picker - NOTE: without CSS scoping this will override styles defined in palette.css - */ - '.blocklyWidgetDiv .goog-palette {', - 'outline: none;', - 'cursor: default;', - '}', - - '.blocklyWidgetDiv .goog-palette-table {', - 'border: 1px solid #666;', - 'border-collapse: collapse;', - '}', - - '.blocklyWidgetDiv .goog-palette-cell {', - 'height: 13px;', - 'width: 15px;', - 'margin: 0;', - 'border: 0;', - 'text-align: center;', - 'vertical-align: middle;', - 'border-right: 1px solid #666;', - 'font-size: 1px;', - '}', - - '.blocklyWidgetDiv .goog-palette-colorswatch {', - 'position: relative;', - 'height: 13px;', - 'width: 15px;', - 'border: 1px solid #666;', - '}', - - '.blocklyWidgetDiv .goog-palette-cell-hover .goog-palette-colorswatch {', - 'border: 1px solid #FFF;', - '}', - - '.blocklyWidgetDiv .goog-palette-cell-selected .goog-palette-colorswatch {', - 'border: 1px solid #000;', - 'color: #fff;', - '}', - - /* Copied from: goog/css/menu.css */ - /* - * Copyright 2009 The Closure Library Authors. All Rights Reserved. - * - * Use of this source code is governed by the Apache License, Version 2.0. - * See the COPYING file for details. - */ - - /** - * Standard styling for menus created by goog.ui.MenuRenderer. - * - * @author attila@google.com (Attila Bodis) - */ - - '.blocklyWidgetDiv .goog-menu {', - 'background: #fff;', - 'border-color: #ccc #666 #666 #ccc;', - 'border-style: solid;', - 'border-width: 1px;', - 'cursor: default;', - '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;', /* Arbitrary, but some apps depend on it... */ - '}', - - /* Copied from: goog/css/menuitem.css */ - /* - * Copyright 2009 The Closure Library Authors. All Rights Reserved. - * - * Use of this source code is governed by the Apache License, Version 2.0. - * See the COPYING file for details. - */ - - /** - * Standard styling for menus created by goog.ui.MenuItemRenderer. - * - * @author attila@google.com (Attila Bodis) - */ - - /** - * State: resting. - * - * NOTE(mleibman,chrishenry): - * The RTL support in Closure is provided via two mechanisms -- "rtl" CSS - * classes and BiDi flipping done by the CSS compiler. Closure supports RTL - * with or without the use of the CSS compiler. In order for them not - * to conflict with each other, the "rtl" CSS classes need to have the #noflip - * annotation. The non-rtl counterparts should ideally have them as well, but, - * since .goog-menuitem existed without .goog-menuitem-rtl for so long before - * being added, there is a risk of people having templates where they are not - * rendering the .goog-menuitem-rtl class when in RTL and instead rely solely - * on the BiDi flipping by the CSS compiler. That's why we're not adding the - * #noflip to .goog-menuitem. - */ - '.blocklyWidgetDiv .goog-menuitem {', - 'color: #000;', - 'font: normal 13px Arial, sans-serif;', - 'list-style: none;', - 'margin: 0;', - /* 28px on the left for icon or checkbox; 7em on the right for shortcut. */ - 'padding: 4px 7em 4px 28px;', - 'white-space: nowrap;', - '}', - - /* BiDi override for the resting state. */ - /* #noflip */ - '.blocklyWidgetDiv .goog-menuitem.goog-menuitem-rtl {', - /* Flip left/right padding for BiDi. */ - 'padding-left: 7em;', - 'padding-right: 28px;', - '}', - - /* If a menu doesn't have checkable items or items with icons, remove padding. */ - '.blocklyWidgetDiv .goog-menu-nocheckbox .goog-menuitem,', - '.blocklyWidgetDiv .goog-menu-noicon .goog-menuitem {', - 'padding-left: 12px;', - '}', - - /* - * If a menu doesn't have items with shortcuts, leave just enough room for - * submenu arrows, if they are rendered. - */ - '.blocklyWidgetDiv .goog-menu-noaccel .goog-menuitem {', - 'padding-right: 20px;', - '}', - - '.blocklyWidgetDiv .goog-menuitem-content {', - 'color: #000;', - 'font: normal 13px Arial, sans-serif;', - '}', - - /* State: disabled. */ - '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-accel,', - '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-content {', - 'color: #ccc !important;', - '}', - - '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-icon {', - 'opacity: 0.3;', - '-moz-opacity: 0.3;', - 'filter: alpha(opacity=30);', - '}', - - /* State: hover. */ - '.blocklyWidgetDiv .goog-menuitem-highlight,', - '.blocklyWidgetDiv .goog-menuitem-hover {', - 'background-color: #d6e9f8;', - /* Use an explicit top and bottom border so that the selection is visible', - * in high contrast mode. */ - 'border-color: #d6e9f8;', - 'border-style: dotted;', - 'border-width: 1px 0;', - 'padding-bottom: 3px;', - 'padding-top: 3px;', - '}', - - /* State: selected/checked. */ - '.blocklyWidgetDiv .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-menuitem-icon {', - 'background-repeat: no-repeat;', - 'height: 16px;', - 'left: 6px;', - 'position: absolute;', - 'right: auto;', - 'vertical-align: middle;', - 'width: 16px;', - '}', - - /* BiDi override for the selected/checked state. */ - /* #noflip */ - '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-icon {', - /* Flip left/right positioning. */ - 'left: auto;', - 'right: 6px;', - '}', - - '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon {', - /* Client apps may override the URL at which they serve the sprite. */ - 'background: url(//ssl.gstatic.com/editor/editortoolbar.png) no-repeat -512px 0;', - '}', - - /* Keyboard shortcut ("accelerator") style. */ - '.blocklyWidgetDiv .goog-menuitem-accel {', - 'color: #999;', - /* Keyboard shortcuts are untranslated; always left-to-right. */ - /* #noflip */ - 'direction: ltr;', - 'left: auto;', - 'padding: 0 6px;', - 'position: absolute;', - 'right: 0;', - 'text-align: right;', - '}', - - /* BiDi override for shortcut style. */ - /* #noflip */ - '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-accel {', - /* Flip left/right positioning and text alignment. */ - 'left: 0;', - 'right: auto;', - 'text-align: left;', - '}', - - /* Mnemonic styles. */ - '.blocklyWidgetDiv .goog-menuitem-mnemonic-hint {', - 'text-decoration: underline;', - '}', - - '.blocklyWidgetDiv .goog-menuitem-mnemonic-separator {', - 'color: #999;', - 'font-size: 12px;', - 'padding-left: 4px;', - '}', - - /* Copied from: goog/css/menuseparator.css */ - /* - * Copyright 2009 The Closure Library Authors. All Rights Reserved. - * - * Use of this source code is governed by the Apache License, Version 2.0. - * See the COPYING file for details. - */ - - /** - * Standard styling for menus created by goog.ui.MenuSeparatorRenderer. - * - * @author attila@google.com (Attila Bodis) - */ - - '.blocklyWidgetDiv .goog-menuseparator {', - 'border-top: 1px solid #ccc;', - 'margin: 4px 0;', - 'padding: 0;', - '}', - - '' -]; diff --git a/core/css.ts b/core/css.ts new file mode 100644 index 00000000000..c7443e5f06d --- /dev/null +++ b/core/css.ts @@ -0,0 +1,501 @@ +/** + * @license + * Copyright 2013 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.Css + +/** Has CSS already been injected? */ +let injected = false; + +/** + * Add some CSS to the blob that will be injected later. Allows optional + * components such as fields and the toolbox to store separate CSS. + * + * @param cssContent Multiline CSS string or an array of single lines of CSS. + */ +export function register(cssContent: string) { + if (injected) { + throw Error('CSS already injected'); + } + content += '\n' + cssContent; +} + +/** + * Inject the CSS into the DOM. This is preferable over using a regular CSS + * file since: + * a) It loads synchronously and doesn't force a redraw later. + * b) It speeds up loading by not blocking on a separate HTTP transfer. + * c) The CSS content may be made dynamic depending on init options. + * + * @param hasCss If false, don't inject CSS (providing CSS becomes the + * document's responsibility). + * @param pathToMedia Path from page to the Blockly media directory. + */ +export function inject(hasCss: boolean, pathToMedia: string) { + // Only inject the CSS once. + if (injected) { + return; + } + injected = true; + if (!hasCss) { + return; + } + // Strip off any trailing slash (either Unix or Windows). + const mediaPath = pathToMedia.replace(/[\\/]$/, ''); + const cssContent = content.replace(/<<>>/g, mediaPath); + // Cleanup the collected css content after injecting it to the DOM. + content = ''; + + // Inject CSS tag at start of head. + const cssNode = document.createElement('style'); + cssNode.id = 'blockly-common-style'; + const cssTextNode = document.createTextNode(cssContent); + cssNode.appendChild(cssTextNode); + document.head.insertBefore(cssNode, document.head.firstChild); +} + +/** + * The CSS content for Blockly. + */ +let content = ` +.blocklySvg { + background-color: #fff; + outline: none; + overflow: hidden; /* IE overflows by default. */ + position: absolute; + display: block; +} + +.blocklyWidgetDiv { + display: none; + position: absolute; + z-index: 99999; /* big value for bootstrap3 compatibility */ +} + +.injectionDiv { + height: 100%; + position: relative; + overflow: hidden; /* So blocks in drag surface disappear at edges */ + touch-action: none; + user-select: none; + -webkit-user-select: none; +} + +.blocklyNonSelectable { + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; +} + +.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; /* big value for bootstrap3 compatibility */ +} + +.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; /* @todo: spec for maximum height. */ + 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: grab; + cursor: -webkit-grab; +} + +.blocklyDragging { + cursor: grabbing; + cursor: -webkit-grabbing; + /* Drag surface disables events to not block the toolbox, so we have to + * reenable them here for the cursor values to work. */ + pointer-events: auto; +} + + /* Changes cursor on mouse down. Not effective in Firefox because of + https://bugzilla.mozilla.org/show_bug.cgi?id=771241 */ +.blocklyDraggable:active { + 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; +} + +.blocklyNonEditableText>text { + pointer-events: none; +} + +.blocklyFlyout { + position: absolute; + z-index: 20; +} + +.blocklyText text { + cursor: default; +} + +.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; + height: 100%; +} + +.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; +} + +/* Remove the increase and decrease arrows on the field number editor */ +input.blocklyHtmlInput[type=number]::-webkit-inner-spin-button, +input.blocklyHtmlInput[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type=number] { + -moz-appearance: textfield; +} + +.blocklyMainBackground { + stroke-width: 1; + stroke: #c6c6c6; /* Equates to #ddd due to border being off-pixel. */ +} + +.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; +} + +/* Darken flyout scrollbars due to being on a grey background. */ +/* By contrast, workspace scrollbars are on a white background. */ +.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 { + /* 28px on the left for icon or checkbox. */ + padding-left: 28px; +} + +/* BiDi override for the resting state. */ +.blocklyDropdownMenu .blocklyMenuItemRtl { + /* Flip left/right padding for BiDi. */ + 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; /* Arbitrary, but some apps depend on it... */ +} + +.blocklyWidgetDiv .blocklyMenu.blocklyFocused { + box-shadow: 0 0 6px 1px rgba(0,0,0,.3); +} + +.blocklyDropDownDiv .blocklyMenu { + background: inherit; /* Compatibility with gapi, reset from goog-menu */ + border: inherit; /* Compatibility with gapi, reset from goog-menu */ + font: normal 13px "Helvetica Neue", Helvetica, sans-serif; + outline: none; + position: relative; /* Compatibility with gapi, reset from goog-menu */ + z-index: 20000; /* Arbitrary, but some apps depend on it... */ +} + +/* State: resting. */ +.blocklyMenuItem { + border: none; + color: #000; + cursor: pointer; + list-style: none; + margin: 0; + /* 7em on the right for shortcut. */ + min-width: 7em; + padding: 6px 15px; + white-space: nowrap; +} + +/* State: disabled. */ +.blocklyMenuItemDisabled { + color: #ccc; + cursor: inherit; +} + +/* State: hover. */ +.blocklyMenuItemHighlight { + background-color: rgba(0,0,0,.1); +} + +/* State: selected/checked. */ +.blocklyMenuItemCheckbox { + height: 16px; + position: absolute; + width: 16px; +} + +.blocklyMenuItemSelected .blocklyMenuItemCheckbox { + background: url(<<>>/sprites.png) no-repeat -48px -16px; + float: left; + margin-left: -24px; + position: static; /* Scroll with the menu. */ +} + +.blocklyMenuItemRtl .blocklyMenuItemCheckbox { + float: right; + margin-right: -24px; +} + +.blocklyBlockDragSurface, .blocklyAnimationLayer { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: visible !important; + z-index: 80; + pointer-events: none; +} +`; diff --git a/core/delete_area.ts b/core/delete_area.ts new file mode 100644 index 00000000000..405084db9b1 --- /dev/null +++ b/core/delete_area.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The abstract class for a component that can delete a block or + * bubble that is dropped on top of it. + * + * @class + */ +// Former goog.module ID: Blockly.DeleteArea + +import {BlockSvg} from './block_svg.js'; +import {DragTarget} from './drag_target.js'; +import {isDeletable} from './interfaces/i_deletable.js'; +import type {IDeleteArea} from './interfaces/i_delete_area.js'; +import type {IDraggable} from './interfaces/i_draggable.js'; + +/** + * Abstract class for a component that can delete a block or bubble that is + * dropped on top of it. + */ +export class DeleteArea extends DragTarget implements IDeleteArea { + /** + * Whether the last block or bubble dragged over this delete area would be + * deleted if dropped on this component. + * This property is not updated after the block or bubble is deleted. + */ + protected wouldDelete_ = false; + + /** + * The unique id for this component that is used to register with the + * ComponentManager. + */ + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + override id!: string; + + /** + * Constructor for DeleteArea. Should not be called directly, only by a + * subclass. + */ + constructor() { + super(); + } + + /** + * Returns whether the provided block or bubble would be deleted if dropped on + * this area. + * This method should check if the element is deletable and is always called + * before onDragEnter/onDragOver/onDragExit. + * + * @param element The block or bubble currently being dragged. + * @returns Whether the element provided would be deleted if dropped on this + * area. + */ + wouldDelete(element: IDraggable): boolean { + if (element instanceof BlockSvg) { + const block = element; + const couldDeleteBlock = !block.getParent() && block.isDeletable(); + this.updateWouldDelete_(couldDeleteBlock); + } else { + this.updateWouldDelete_(isDeletable(element) && element.isDeletable()); + } + return this.wouldDelete_; + } + + /** + * Updates the internal wouldDelete_ state. + * + * @param wouldDelete The new value for the wouldDelete state. + */ + protected updateWouldDelete_(wouldDelete: boolean) { + this.wouldDelete_ = wouldDelete; + } +} diff --git a/core/dialog.ts b/core/dialog.ts new file mode 100644 index 00000000000..7e21129855c --- /dev/null +++ b/core/dialog.ts @@ -0,0 +1,121 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.dialog + +let alertImplementation = function ( + message: string, + opt_callback?: () => void, +) { + window.alert(message); + if (opt_callback) { + opt_callback(); + } +}; + +let confirmImplementation = function ( + message: string, + callback: (result: boolean) => void, +) { + callback(window.confirm(message)); +}; + +let promptImplementation = function ( + message: string, + defaultValue: string, + callback: (result: string | null) => void, +) { + callback(window.prompt(message, defaultValue)); +}; + +/** + * Wrapper to window.alert() that app developers may override via setAlert to + * provide alternatives to the modal browser window. + * + * @param message The message to display to the user. + * @param opt_callback The callback when the alert is dismissed. + */ +export function alert(message: string, opt_callback?: () => void) { + alertImplementation(message, opt_callback); +} + +/** + * Sets the function to be run when Blockly.dialog.alert() is called. + * + * @param alertFunction The function to be run. + * @see Blockly.dialog.alert + */ +export function setAlert(alertFunction: (p1: string, p2?: () => void) => void) { + alertImplementation = alertFunction; +} + +/** + * Wrapper to window.confirm() that app developers may override via setConfirm + * to provide alternatives to the modal browser window. + * + * @param message The message to display to the user. + * @param callback The callback for handling user response. + */ +export function confirm(message: string, callback: (p1: boolean) => void) { + TEST_ONLY.confirmInternal(message, callback); +} + +/** + * Private version of confirm for stubbing in tests. + */ +function confirmInternal(message: string, callback: (p1: boolean) => void) { + confirmImplementation(message, callback); +} + +/** + * Sets the function to be run when Blockly.dialog.confirm() is called. + * + * @param confirmFunction The function to be run. + * @see Blockly.dialog.confirm + */ +export function setConfirm( + confirmFunction: (p1: string, p2: (p1: boolean) => void) => void, +) { + confirmImplementation = confirmFunction; +} + +/** + * Wrapper to window.prompt() that app developers may override via setPrompt to + * provide alternatives to the modal browser window. Built-in browser prompts + * are often used for better text input experience on mobile device. We strongly + * recommend testing mobile when overriding this. + * + * @param message The message to display to the user. + * @param defaultValue The value to initialize the prompt with. + * @param callback The callback for handling user response. + */ +export function prompt( + message: string, + defaultValue: string, + callback: (p1: string | null) => void, +) { + promptImplementation(message, defaultValue, callback); +} + +/** + * Sets the function to be run when Blockly.dialog.prompt() is called. + * + * @param promptFunction The function to be run. + * @see Blockly.dialog.prompt + */ +export function setPrompt( + promptFunction: ( + p1: string, + p2: string, + p3: (p1: string | null) => void, + ) => void, +) { + promptImplementation = promptFunction; +} + +export const TEST_ONLY = { + confirmInternal, +}; diff --git a/core/drag_target.ts b/core/drag_target.ts new file mode 100644 index 00000000000..e973f2dd1c3 --- /dev/null +++ b/core/drag_target.ts @@ -0,0 +1,97 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The abstract class for a component with custom behaviour when a + * block or bubble is dragged over or dropped on top of it. + * + * @class + */ +// Former goog.module ID: Blockly.DragTarget + +import type {IDragTarget} from './interfaces/i_drag_target.js'; +import type {IDraggable} from './interfaces/i_draggable.js'; +import type {Rect} from './utils/rect.js'; + +/** + * Abstract class for a component with custom behaviour when a block or bubble + * is dragged over or dropped on top of it. + */ +export class DragTarget implements IDragTarget { + /** + * The unique id for this component that is used to register with the + * ComponentManager. + */ + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + id!: string; + + /** + * Constructor for DragTarget. It exists to add the id property and should not + * be called directly, only by a subclass. + */ + constructor() {} + + /** + * Handles when a cursor with a block or bubble enters this drag target. + * + * @param _dragElement The block or bubble currently being dragged. + */ + onDragEnter(_dragElement: IDraggable) { + // no-op + } + + /** + * Handles when a cursor with a block or bubble is dragged over this drag + * target. + * + * @param _dragElement The block or bubble currently being dragged. + */ + onDragOver(_dragElement: IDraggable) { + // no-op + } + + /** + * Handles when a cursor with a block or bubble exits this drag target. + * + * @param _dragElement The block or bubble currently being dragged. + */ + onDragExit(_dragElement: IDraggable) { + // no-op + } + /** + * Handles when a block or bubble is dropped on this component. + * Should not handle delete here. + * + * @param _dragElement The block or bubble currently being dragged. + */ + onDrop(_dragElement: IDraggable) { + // no-op + } + + /** + * Returns the bounding rectangle of the drag target area in pixel units + * relative to the Blockly injection div. + * + * @returns The component's bounding box. Null if drag target area should be + * ignored. + */ + getClientRect(): Rect | null { + return null; + } + + /** + * Returns whether the provided block or bubble should not be moved after + * being dropped on this component. If true, the element will return to where + * it was when the drag started. + * + * @param _dragElement The block or bubble currently being dragged. + * @returns Whether the block or bubble provided should be returned to drag + * start. + */ + shouldPreventMove(_dragElement: IDraggable): boolean { + return false; + } +} diff --git a/core/dragged_connection_manager.js b/core/dragged_connection_manager.js deleted file mode 100644 index 1ebe2ef667f..00000000000 --- a/core/dragged_connection_manager.js +++ /dev/null @@ -1,237 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Class that controls updates to connections during drags. - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -goog.provide('Blockly.DraggedConnectionManager'); - -goog.require('Blockly.RenderedConnection'); - -goog.require('goog.math.Coordinate'); - - -/** - * Class that controls updates to connections during drags. It is primarily - * responsible for finding the closest eligible connection and highlighting or - * unhiglighting it as needed during a drag. - * @param {!Blockly.BlockSvg} block The top block in the stack being dragged. - * @constructor - */ -Blockly.DraggedConnectionManager = function(block) { - Blockly.selected = block; - - /** - * The top block in the stack being dragged. - * Does not change during a drag. - * @type {!Blockly.Block} - * @private - */ - this.topBlock_ = block; - - /** - * The workspace on which these connections are being dragged. - * Does not change during a drag. - * @type {!Blockly.WorkspaceSvg} - * @private - */ - this.workspace_ = block.workspace; - - /** - * The connections on the dragging blocks that are available to connect to - * other blocks. This includes all open connections on the top block, as well - * as the last connection on the block stack. - * Does not change during a drag. - * @type {!Array.} - * @private - */ - this.availableConnections_ = this.initAvailableConnections_(); - - /** - * The connection that this block would connect to if released immediately. - * Updated on every mouse move. - * @type {Blockly.RenderedConnection} - * @private - */ - this.closestConnection_ = null; - - /** - * The connection that would connect to this.closestConnection_ if this block - * were released immediately. - * Updated on every mouse move. - * @type {Blockly.RenderedConnection} - * @private - */ - this.localConnection_ = null; - - /** - * The distance between this.closestConnection_ and this.localConnection_, - * in workspace units. - * Updated on every mouse move. - * @type {number} - * @private - */ - this.radiusConnection_ = 0; - - /** - * Whether the block would be deleted if it were dropped immediately. - * Updated on every mouse move. - * @type {boolean} - * @private - */ - this.wouldDeleteBlock_ = false; -}; - -/** - * Sever all links from this object. - * @package - */ -Blockly.DraggedConnectionManager.prototype.dispose = function() { - this.topBlock_ = null; - this.workspace_ = null; - this.availableConnections_.length = 0; - this.closestConnection_ = null; - this.localConnection_ = null; -}; - -/** - * Return whether the block would be deleted if dropped immediately, based on - * information from the most recent move event. - * @return {boolean} true if the block would be deleted if dropped immediately. - * @package - */ -Blockly.DraggedConnectionManager.prototype.wouldDeleteBlock = function() { - return this.wouldDeleteBlock_; -}; - -/** - * Connect to the closest connection and render the results. - * This should be called at the end of a drag. - * @package - */ -Blockly.DraggedConnectionManager.prototype.applyConnections = function() { - if (this.closestConnection_) { - // Connect two blocks together. - this.localConnection_.connect(this.closestConnection_); - if (this.topBlock_.rendered) { - // Trigger a connection animation. - // Determine which connection is inferior (lower in the source stack). - var inferiorConnection = this.localConnection_.isSuperior() ? - this.closestConnection_ : this.localConnection_; - inferiorConnection.getSourceBlock().connectionUiEffect(); - } - this.removeHighlighting_(); - } -}; - -/** - * Update highlighted connections based on the most recent move location. - * @param {!goog.math.Coordinate} dxy Position relative to drag start, - * in workspace units. - * @param {?number} deleteArea One of {@link Blockly.DELETE_AREA_TRASH}, - * {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}. - * @package - */ -Blockly.DraggedConnectionManager.prototype.update = function(dxy, deleteArea) { - var oldClosestConnection = this.closestConnection_; - var closestConnectionChanged = this.updateClosest_(dxy); - - if (closestConnectionChanged && oldClosestConnection) { - oldClosestConnection.unhighlight(); - } - - // Prefer connecting over dropping into the trash can, but prefer dragging to - // the toolbox over connecting to other blocks. - var wouldConnect = !!this.closestConnection_ && - deleteArea != Blockly.DELETE_AREA_TOOLBOX; - var wouldDelete = !!deleteArea && !this.topBlock_.getParent() && - this.topBlock_.isDeletable(); - this.wouldDeleteBlock_ = wouldDelete && !wouldConnect; - - if (!this.wouldDeleteBlock_ && closestConnectionChanged && - this.closestConnection_) { - this.addHighlighting_(); - } -}; - -/** - * Remove highlighting from the currently highlighted connection, if it exists. - * @private - */ -Blockly.DraggedConnectionManager.prototype.removeHighlighting_ = function() { - if (this.closestConnection_) { - this.closestConnection_.unhighlight(); - } -}; - -/** - * Add highlighting to the closest connection, if it exists. - * @private - */ -Blockly.DraggedConnectionManager.prototype.addHighlighting_ = function() { - if (this.closestConnection_) { - this.closestConnection_.highlight(); - } -}; - -/** - * Populate the list of available connections on this block stack. This should - * only be called once, at the beginning of a drag. - * @return {!Array.} a list of available - * connections. - * @private - */ -Blockly.DraggedConnectionManager.prototype.initAvailableConnections_ = function() { - var available = this.topBlock_.getConnections_(false); - // Also check the last connection on this stack - var lastOnStack = this.topBlock_.lastConnectionInStack_(); - if (lastOnStack && lastOnStack != this.topBlock_.nextConnection) { - available.push(lastOnStack); - } - return available; -}; - -/** - * Find the new closest connection, and update internal state in response. - * @param {!goog.math.Coordinate} dxy Position relative to the drag start, - * in workspace units. - * @return {boolean} Whether the closest connection has changed. - * @private - */ -Blockly.DraggedConnectionManager.prototype.updateClosest_ = function(dxy) { - var oldClosestConnection = this.closestConnection_; - - this.closestConnection_ = null; - this.localConnection_ = null; - this.radiusConnection_ = Blockly.SNAP_RADIUS; - for (var i = 0; i < this.availableConnections_.length; i++) { - var myConnection = this.availableConnections_[i]; - var neighbour = myConnection.closest(this.radiusConnection_, dxy); - if (neighbour.connection) { - this.closestConnection_ = neighbour.connection; - this.localConnection_ = myConnection; - this.radiusConnection_ = neighbour.radius; - } - } - return oldClosestConnection != this.closestConnection_; -}; diff --git a/core/dragging.ts b/core/dragging.ts new file mode 100644 index 00000000000..4ba85c49f7d --- /dev/null +++ b/core/dragging.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {BlockDragStrategy} from './dragging/block_drag_strategy.js'; +import {BubbleDragStrategy} from './dragging/bubble_drag_strategy.js'; +import {CommentDragStrategy} from './dragging/comment_drag_strategy.js'; +import {Dragger} from './dragging/dragger.js'; + +export {BlockDragStrategy, BubbleDragStrategy, CommentDragStrategy, Dragger}; diff --git a/core/dragging/block_drag_strategy.ts b/core/dragging/block_drag_strategy.ts new file mode 100644 index 00000000000..c9a1ea0abf7 --- /dev/null +++ b/core/dragging/block_drag_strategy.ts @@ -0,0 +1,466 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Block} from '../block.js'; +import * as blockAnimation from '../block_animations.js'; +import {BlockSvg} from '../block_svg.js'; +import * as bumpObjects from '../bump_objects.js'; +import {config} from '../config.js'; +import {Connection} from '../connection.js'; +import {ConnectionType} from '../connection_type.js'; +import type {BlockMove} from '../events/events_block_move.js'; +import {EventType} from '../events/type.js'; +import * as eventUtils from '../events/utils.js'; +import {IConnectionPreviewer} from '../interfaces/i_connection_previewer.js'; +import {IDragStrategy} from '../interfaces/i_draggable.js'; +import * as layers from '../layers.js'; +import * as registry from '../registry.js'; +import {finishQueuedRenders} from '../render_management.js'; +import {RenderedConnection} from '../rendered_connection.js'; +import {Coordinate} from '../utils.js'; +import * as dom from '../utils/dom.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; + +/** Represents a nearby valid connection. */ +interface ConnectionCandidate { + /** A connection on the dragging stack that is compatible with neighbour. */ + local: RenderedConnection; + + /** A nearby connection that is compatible with local. */ + neighbour: RenderedConnection; + + /** The distance between the local connection and the neighbour connection. */ + distance: number; +} + +export class BlockDragStrategy implements IDragStrategy { + private workspace: WorkspaceSvg; + + /** The parent block at the start of the drag. */ + private startParentConn: RenderedConnection | null = null; + + /** + * The child block at the start of the drag. Only gets set if + * `healStack` is true. + */ + private startChildConn: RenderedConnection | null = null; + + private startLoc: Coordinate | null = null; + + private connectionCandidate: ConnectionCandidate | null = null; + + private connectionPreviewer: IConnectionPreviewer | null = null; + + private dragging = false; + + /** + * If this is a shadow block, the offset between this block and the parent + * block, to add to the drag location. In workspace units. + */ + private dragOffset = new Coordinate(0, 0); + + /** Was there already an event group in progress when the drag started? */ + private inGroup: boolean = false; + + constructor(private block: BlockSvg) { + this.workspace = block.workspace; + } + + /** Returns true if the block is currently movable. False otherwise. */ + isMovable(): boolean { + if (this.block.isShadow()) { + return this.block.getParent()?.isMovable() ?? false; + } + + return ( + this.block.isOwnMovable() && + !this.block.isDeadOrDying() && + !this.workspace.options.readOnly && + // We never drag blocks in the flyout, only create new blocks that are + // dragged. + !this.block.isInFlyout + ); + } + + /** + * Handles any setup for starting the drag, including disconnecting the block + * from any parent blocks. + */ + startDrag(e?: PointerEvent): void { + if (this.block.isShadow()) { + this.startDraggingShadow(e); + return; + } + + this.dragging = true; + this.inGroup = !!eventUtils.getGroup(); + if (!this.inGroup) { + eventUtils.setGroup(true); + } + this.fireDragStartEvent(); + + this.startLoc = this.block.getRelativeToSurfaceXY(); + + this.connectionCandidate = null; + const previewerConstructor = registry.getClassFromOptions( + registry.Type.CONNECTION_PREVIEWER, + this.workspace.options, + ); + this.connectionPreviewer = new previewerConstructor!(this.block); + + // During a drag there may be a lot of rerenders, but not field changes. + // Turn the cache on so we don't do spurious remeasures during the drag. + dom.startTextWidthCache(); + this.workspace.setResizesEnabled(false); + blockAnimation.disconnectUiStop(); + + const healStack = !!e && (e.altKey || e.ctrlKey || e.metaKey); + + if (this.shouldDisconnect(healStack)) { + this.disconnectBlock(healStack); + } + this.block.setDragging(true); + this.workspace.getLayerManager()?.moveToDragLayer(this.block); + } + + /** Starts a drag on a shadow, recording the drag offset. */ + private startDraggingShadow(e?: PointerEvent) { + const parent = this.block.getParent(); + if (!parent) { + throw new Error( + 'Tried to drag a shadow block with no parent. ' + + 'Shadow blocks should always have parents.', + ); + } + this.dragOffset = Coordinate.difference( + parent.getRelativeToSurfaceXY(), + this.block.getRelativeToSurfaceXY(), + ); + parent.startDrag(e); + } + + /** + * Whether or not we should disconnect the block when a drag is started. + * + * @param healStack Whether or not to heal the stack after disconnecting. + * @returns True to disconnect the block, false otherwise. + */ + private shouldDisconnect(healStack: boolean): boolean { + return !!( + this.block.getParent() || + (healStack && + this.block.nextConnection && + this.block.nextConnection.targetBlock()) + ); + } + + /** + * Disconnects the block from any parents. If `healStack` is true and this is + * a stack block, we also disconnect from any next blocks and attempt to + * attach them to any parent. + * + * @param healStack Whether or not to heal the stack after disconnecting. + */ + private disconnectBlock(healStack: boolean) { + this.startParentConn = + this.block.outputConnection?.targetConnection ?? + this.block.previousConnection?.targetConnection; + if (healStack) { + this.startChildConn = this.block.nextConnection?.targetConnection; + } + + this.block.unplug(healStack); + blockAnimation.disconnectUiEffect(this.block); + } + + /** Fire a UI event at the start of a block drag. */ + private fireDragStartEvent() { + const event = new (eventUtils.get(EventType.BLOCK_DRAG))( + this.block, + true, + this.block.getDescendants(false), + ); + eventUtils.fire(event); + } + + /** Fire a UI event at the end of a block drag. */ + private fireDragEndEvent() { + const event = new (eventUtils.get(EventType.BLOCK_DRAG))( + this.block, + false, + this.block.getDescendants(false), + ); + eventUtils.fire(event); + } + + /** Fire a move event at the end of a block drag. */ + private fireMoveEvent() { + if (this.block.isDeadOrDying()) return; + const event = new (eventUtils.get(EventType.BLOCK_MOVE))( + this.block, + ) as BlockMove; + event.setReason(['drag']); + event.oldCoordinate = this.startLoc!; + event.recordNew(); + eventUtils.fire(event); + } + + /** Moves the block and updates any connection previews. */ + drag(newLoc: Coordinate): void { + if (this.block.isShadow()) { + this.block.getParent()?.drag(Coordinate.sum(newLoc, this.dragOffset)); + return; + } + + this.block.moveDuringDrag(newLoc); + this.updateConnectionPreview( + this.block, + Coordinate.difference(newLoc, this.startLoc!), + ); + } + + /** + * @param draggingBlock The block being dragged. + * @param delta How far the pointer has moved from the position + * at the start of the drag, in workspace units. + */ + private updateConnectionPreview(draggingBlock: BlockSvg, delta: Coordinate) { + const currCandidate = this.connectionCandidate; + const newCandidate = this.getConnectionCandidate(draggingBlock, delta); + if (!newCandidate) { + this.connectionPreviewer!.hidePreview(); + this.connectionCandidate = null; + return; + } + const candidate = + currCandidate && + this.currCandidateIsBetter(currCandidate, delta, newCandidate) + ? currCandidate + : newCandidate; + this.connectionCandidate = candidate; + + const {local, neighbour} = candidate; + const localIsOutputOrPrevious = + local.type === ConnectionType.OUTPUT_VALUE || + local.type === ConnectionType.PREVIOUS_STATEMENT; + const neighbourIsConnectedToRealBlock = + neighbour.isConnected() && !neighbour.targetBlock()!.isInsertionMarker(); + if ( + localIsOutputOrPrevious && + neighbourIsConnectedToRealBlock && + !this.orphanCanConnectAtEnd( + draggingBlock, + neighbour.targetBlock()!, + local.type, + ) + ) { + this.connectionPreviewer!.previewReplacement( + local, + neighbour, + neighbour.targetBlock()!, + ); + return; + } + this.connectionPreviewer!.previewConnection(local, neighbour); + } + + /** + * Returns true if the given orphan block can connect at the end of the + * top block's stack or row, false otherwise. + */ + private orphanCanConnectAtEnd( + topBlock: BlockSvg, + orphanBlock: BlockSvg, + localType: number, + ): boolean { + const orphanConnection = + localType === ConnectionType.OUTPUT_VALUE + ? orphanBlock.outputConnection + : orphanBlock.previousConnection; + return !!Connection.getConnectionForOrphanedConnection( + topBlock as Block, + orphanConnection as Connection, + ); + } + + /** + * Returns true if the current candidate is better than the new candidate. + * + * We slightly prefer the current candidate even if it is farther away. + */ + private currCandidateIsBetter( + currCandiate: ConnectionCandidate, + delta: Coordinate, + newCandidate: ConnectionCandidate, + ): boolean { + const {local: currLocal, neighbour: currNeighbour} = currCandiate; + const localPos = new Coordinate(currLocal.x, currLocal.y); + const neighbourPos = new Coordinate(currNeighbour.x, currNeighbour.y); + const currDistance = Coordinate.distance( + Coordinate.sum(localPos, delta), + neighbourPos, + ); + return ( + newCandidate.distance > currDistance - config.currentConnectionPreference + ); + } + + /** + * Returns the closest valid candidate connection, if one can be found. + * + * Valid neighbour connections are within the configured start radius, with a + * compatible type (input, output, etc) and connection check. + */ + private getConnectionCandidate( + draggingBlock: BlockSvg, + delta: Coordinate, + ): ConnectionCandidate | null { + const localConns = this.getLocalConnections(draggingBlock); + let radius = this.connectionCandidate + ? config.connectingSnapRadius + : config.snapRadius; + let candidate = null; + + for (const conn of localConns) { + const {connection: neighbour, radius: rad} = conn.closest(radius, delta); + if (neighbour) { + candidate = { + local: conn, + neighbour: neighbour, + distance: rad, + }; + radius = rad; + } + } + + return candidate; + } + + /** + * Returns all of the connections we might connect to blocks on the workspace. + * + * Includes any connections on the dragging block, and any last next + * connection on the stack (if one exists). + */ + private getLocalConnections(draggingBlock: BlockSvg): RenderedConnection[] { + const available = draggingBlock.getConnections_(false); + const lastOnStack = draggingBlock.lastConnectionInStack(true); + if (lastOnStack && lastOnStack !== draggingBlock.nextConnection) { + available.push(lastOnStack); + } + return available; + } + + /** + * Cleans up any state at the end of the drag. Applies any pending + * connections. + */ + endDrag(e?: PointerEvent): void { + if (this.block.isShadow()) { + this.block.getParent()?.endDrag(e); + return; + } + + this.fireDragEndEvent(); + this.fireMoveEvent(); + + dom.stopTextWidthCache(); + + blockAnimation.disconnectUiStop(); + this.connectionPreviewer!.hidePreview(); + + if (!this.block.isDeadOrDying() && this.dragging) { + // These are expensive and don't need to be done if we're deleting, or + // if we've already stopped dragging because we moved back to the start. + this.workspace + .getLayerManager() + ?.moveOffDragLayer(this.block, layers.BLOCK); + this.block.setDragging(false); + } + + if (this.connectionCandidate) { + // Applying connections also rerenders the relevant blocks. + this.applyConnections(this.connectionCandidate); + this.disposeStep(); + } else { + this.block.queueRender().then(() => this.disposeStep()); + } + + if (!this.inGroup) { + eventUtils.setGroup(false); + } + } + + /** Disposes of any state at the end of the drag. */ + private disposeStep() { + this.block.snapToGrid(); + + // Must dispose after connections are applied to not break the dynamic + // connections plugin. See #7859 + this.connectionPreviewer!.dispose(); + this.workspace.setResizesEnabled(true); + } + + /** Connects the given candidate connections. */ + private applyConnections(candidate: ConnectionCandidate) { + const {local, neighbour} = candidate; + local.connect(neighbour); + + const inferiorConnection = local.isSuperior() ? neighbour : local; + const rootBlock = this.block.getRootBlock(); + + finishQueuedRenders().then(() => { + blockAnimation.connectionUiEffect(inferiorConnection.getSourceBlock()); + // bringToFront is incredibly expensive. Delay until the next frame. + setTimeout(() => { + rootBlock.bringToFront(); + }, 0); + }); + } + + /** + * Moves the block back to where it was at the beginning of the drag, + * including reconnecting connections. + */ + revertDrag(): void { + if (this.block.isShadow()) { + this.block.getParent()?.revertDrag(); + return; + } + + this.startChildConn?.connect(this.block.nextConnection); + if (this.startParentConn) { + switch (this.startParentConn.type) { + case ConnectionType.INPUT_VALUE: + this.startParentConn.connect(this.block.outputConnection); + break; + case ConnectionType.NEXT_STATEMENT: + this.startParentConn.connect(this.block.previousConnection); + } + } else { + this.block.moveTo(this.startLoc!, ['drag']); + this.workspace + .getLayerManager() + ?.moveOffDragLayer(this.block, layers.BLOCK); + // Blocks dragged directly from a flyout may need to be bumped into + // bounds. + bumpObjects.bumpIntoBounds( + this.workspace, + this.workspace.getMetricsManager().getScrollMetrics(true), + this.block, + ); + } + + this.startChildConn = null; + this.startParentConn = null; + + this.connectionPreviewer!.hidePreview(); + this.connectionCandidate = null; + + this.block.setDragging(false); + this.dragging = false; + } +} diff --git a/core/dragging/bubble_drag_strategy.ts b/core/dragging/bubble_drag_strategy.ts new file mode 100644 index 00000000000..c2a5c58f4a2 --- /dev/null +++ b/core/dragging/bubble_drag_strategy.ts @@ -0,0 +1,60 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {IBubble, WorkspaceSvg} from '../blockly.js'; +import * as eventUtils from '../events/utils.js'; +import {IDragStrategy} from '../interfaces/i_draggable.js'; +import * as layers from '../layers.js'; +import {Coordinate} from '../utils.js'; + +export class BubbleDragStrategy implements IDragStrategy { + private startLoc: Coordinate | null = null; + + /** Was there already an event group in progress when the drag started? */ + private inGroup: boolean = false; + + constructor( + private bubble: IBubble, + private workspace: WorkspaceSvg, + ) {} + + isMovable(): boolean { + return true; + } + + startDrag(): void { + this.inGroup = !!eventUtils.getGroup(); + if (!this.inGroup) { + eventUtils.setGroup(true); + } + this.startLoc = this.bubble.getRelativeToSurfaceXY(); + this.workspace.setResizesEnabled(false); + this.workspace.getLayerManager()?.moveToDragLayer(this.bubble); + if (this.bubble.setDragging) { + this.bubble.setDragging(true); + } + } + + drag(newLoc: Coordinate): void { + this.bubble.moveDuringDrag(newLoc); + } + + endDrag(): void { + this.workspace.setResizesEnabled(true); + if (!this.inGroup) { + eventUtils.setGroup(false); + } + + this.workspace + .getLayerManager() + ?.moveOffDragLayer(this.bubble, layers.BUBBLE); + this.bubble.setDragging(false); + } + + revertDrag(): void { + if (this.startLoc) this.bubble.moveDuringDrag(this.startLoc); + } +} diff --git a/core/dragging/comment_drag_strategy.ts b/core/dragging/comment_drag_strategy.ts new file mode 100644 index 00000000000..dd8b10fc2f9 --- /dev/null +++ b/core/dragging/comment_drag_strategy.ts @@ -0,0 +1,102 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {RenderedWorkspaceComment} from '../comments.js'; +import {CommentMove} from '../events/events_comment_move.js'; +import {EventType} from '../events/type.js'; +import * as eventUtils from '../events/utils.js'; +import {IDragStrategy} from '../interfaces/i_draggable.js'; +import * as layers from '../layers.js'; +import {Coordinate} from '../utils.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; + +export class CommentDragStrategy implements IDragStrategy { + private startLoc: Coordinate | null = null; + + private workspace: WorkspaceSvg; + + /** Was there already an event group in progress when the drag started? */ + private inGroup: boolean = false; + + constructor(private comment: RenderedWorkspaceComment) { + this.workspace = comment.workspace; + } + + isMovable(): boolean { + return ( + this.comment.isOwnMovable() && + !this.comment.isDeadOrDying() && + !this.workspace.options.readOnly + ); + } + + startDrag(): void { + this.inGroup = !!eventUtils.getGroup(); + if (!this.inGroup) { + eventUtils.setGroup(true); + } + this.fireDragStartEvent(); + this.startLoc = this.comment.getRelativeToSurfaceXY(); + this.workspace.setResizesEnabled(false); + this.workspace.getLayerManager()?.moveToDragLayer(this.comment); + this.comment.setDragging(true); + } + + drag(newLoc: Coordinate): void { + this.comment.moveDuringDrag(newLoc); + } + + endDrag(): void { + this.fireDragEndEvent(); + this.fireMoveEvent(); + + this.workspace + .getLayerManager() + ?.moveOffDragLayer(this.comment, layers.BLOCK); + this.comment.setDragging(false); + + this.comment.snapToGrid(); + + this.workspace.setResizesEnabled(true); + if (!this.inGroup) { + eventUtils.setGroup(false); + } + } + + /** Fire a UI event at the start of a comment drag. */ + private fireDragStartEvent() { + const event = new (eventUtils.get(EventType.COMMENT_DRAG))( + this.comment, + true, + ); + eventUtils.fire(event); + } + + /** Fire a UI event at the end of a comment drag. */ + private fireDragEndEvent() { + const event = new (eventUtils.get(EventType.COMMENT_DRAG))( + this.comment, + false, + ); + eventUtils.fire(event); + } + + /** Fire a move event at the end of a comment drag. */ + private fireMoveEvent() { + if (this.comment.isDeadOrDying()) return; + const event = new (eventUtils.get(EventType.COMMENT_MOVE))( + this.comment, + ) as CommentMove; + event.setReason(['drag']); + event.oldCoordinate_ = this.startLoc!; + event.recordNew(); + eventUtils.fire(event); + } + + revertDrag(): void { + if (this.startLoc) this.comment.moveDuringDrag(this.startLoc); + } +} diff --git a/core/dragging/dragger.ts b/core/dragging/dragger.ts new file mode 100644 index 00000000000..8a9ac87c6a9 --- /dev/null +++ b/core/dragging/dragger.ts @@ -0,0 +1,163 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as blockAnimations from '../block_animations.js'; +import {BlockSvg} from '../block_svg.js'; +import {ComponentManager} from '../component_manager.js'; +import * as eventUtils from '../events/utils.js'; +import {IDeletable, isDeletable} from '../interfaces/i_deletable.js'; +import {IDeleteArea} from '../interfaces/i_delete_area.js'; +import {IDragTarget} from '../interfaces/i_drag_target.js'; +import {IDraggable} from '../interfaces/i_draggable.js'; +import {IDragger} from '../interfaces/i_dragger.js'; +import * as registry from '../registry.js'; +import {Coordinate} from '../utils/coordinate.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; + +export class Dragger implements IDragger { + protected startLoc: Coordinate; + + protected dragTarget: IDragTarget | null = null; + + constructor( + protected draggable: IDraggable, + protected workspace: WorkspaceSvg, + ) { + this.startLoc = draggable.getRelativeToSurfaceXY(); + } + + /** Handles any drag startup. */ + onDragStart(e: PointerEvent) { + this.draggable.startDrag(e); + } + + /** + * Handles calculating where the element should actually be moved to. + * + * @param totalDelta The total amount in pixel coordinates the mouse has moved + * since the start of the drag. + */ + onDrag(e: PointerEvent, totalDelta: Coordinate) { + this.moveDraggable(e, totalDelta); + const root = this.getRoot(this.draggable); + + // Must check `wouldDelete` before calling other hooks on drag targets + // since we have documented that we would do so. + if (isDeletable(root)) { + root.setDeleteStyle(this.wouldDeleteDraggable(e, root)); + } + this.updateDragTarget(e); + } + + /** Updates the drag target under the pointer (if there is one). */ + protected updateDragTarget(e: PointerEvent) { + const newDragTarget = this.workspace.getDragTarget(e); + const root = this.getRoot(this.draggable); + if (this.dragTarget !== newDragTarget) { + this.dragTarget?.onDragExit(root); + newDragTarget?.onDragEnter(root); + } + newDragTarget?.onDragOver(root); + this.dragTarget = newDragTarget; + } + + /** + * Calculates the correct workspace coordinate for the movable and tells + * the draggable to go to that location. + */ + private moveDraggable(e: PointerEvent, totalDelta: Coordinate) { + const delta = this.pixelsToWorkspaceUnits(totalDelta); + const newLoc = Coordinate.sum(this.startLoc, delta); + this.draggable.drag(newLoc, e); + } + + /** + * Returns true if we would delete the draggable if it was dropped + * at the current location. + */ + protected wouldDeleteDraggable( + e: PointerEvent, + rootDraggable: IDraggable & IDeletable, + ) { + const dragTarget = this.workspace.getDragTarget(e); + if (!dragTarget) return false; + + const componentManager = this.workspace.getComponentManager(); + const isDeleteArea = componentManager.hasCapability( + dragTarget.id, + ComponentManager.Capability.DELETE_AREA, + ); + if (!isDeleteArea) return false; + + return (dragTarget as IDeleteArea).wouldDelete(rootDraggable); + } + + /** Handles any drag cleanup. */ + onDragEnd(e: PointerEvent) { + const origGroup = eventUtils.getGroup(); + const dragTarget = this.workspace.getDragTarget(e); + const root = this.getRoot(this.draggable); + + if (dragTarget) { + this.dragTarget?.onDrop(root); + } + + if (this.shouldReturnToStart(e, root)) { + this.draggable.revertDrag(); + } + + const wouldDelete = isDeletable(root) && this.wouldDeleteDraggable(e, root); + + // TODO(#8148): use a generalized API instead of an instanceof check. + if (wouldDelete && this.draggable instanceof BlockSvg) { + blockAnimations.disposeUiEffect(this.draggable.getRootBlock()); + } + + this.draggable.endDrag(e); + + if (wouldDelete && isDeletable(root)) { + // We want to make sure the delete gets grouped with any possible + // move event. + const newGroup = eventUtils.getGroup(); + eventUtils.setGroup(origGroup); + root.dispose(); + eventUtils.setGroup(newGroup); + } + } + + // We need to special case blocks for now so that we look at the root block + // instead of the one actually being dragged in most cases. + private getRoot(draggable: IDraggable): IDraggable { + return draggable instanceof BlockSvg ? draggable.getRootBlock() : draggable; + } + + /** + * Returns true if we should return the draggable to its original location + * at the end of the drag. + */ + protected shouldReturnToStart(e: PointerEvent, rootDraggable: IDraggable) { + const dragTarget = this.workspace.getDragTarget(e); + if (!dragTarget) return false; + return dragTarget.shouldPreventMove(rootDraggable); + } + + protected pixelsToWorkspaceUnits(pixelCoord: Coordinate): Coordinate { + const result = new Coordinate( + pixelCoord.x / this.workspace.scale, + pixelCoord.y / this.workspace.scale, + ); + if (this.workspace.isMutator) { + // If we're in a mutator, its scale is always 1, purely because of some + // oddities in our rendering optimizations. The actual scale is the same + // as the scale on the parent workspace. Fix that for dragging. + const mainScale = this.workspace.options.parentWorkspace!.scale; + result.scale(1 / mainScale); + } + return result; + } +} + +registry.register(registry.Type.BLOCK_DRAGGER, registry.DEFAULT, Dragger); diff --git a/core/dropdowndiv.ts b/core/dropdowndiv.ts new file mode 100644 index 00000000000..f9af02ac9f7 --- /dev/null +++ b/core/dropdowndiv.ts @@ -0,0 +1,774 @@ +/** + * @license + * Copyright 2016 Massachusetts Institute of Technology + * All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * A div that floats on top of the workspace, for drop-down menus. + * + * @class + */ +// Former goog.module ID: Blockly.dropDownDiv + +import type {BlockSvg} from './block_svg.js'; +import * as common from './common.js'; +import type {Field} from './field.js'; +import * as dom from './utils/dom.js'; +import * as math from './utils/math.js'; +import {Rect} from './utils/rect.js'; +import type {Size} from './utils/size.js'; +import * as style from './utils/style.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Arrow size in px. Should match the value in CSS + * (need to position pre-render). + */ +export const ARROW_SIZE = 16; + +/** + * Drop-down border size in px. Should match the value in CSS (need to position + * the arrow). + */ +export const BORDER_SIZE = 1; + +/** + * Amount the arrow must be kept away from the edges of the main drop-down div, + * in px. + */ +export const ARROW_HORIZONTAL_PADDING = 12; + +/** Amount drop-downs should be padded away from the source, in px. */ +export const PADDING_Y = 16; + +/** Length of animations in seconds. */ +export const ANIMATION_TIME = 0.25; + +/** + * Timer for animation out, to be cleared if we need to immediately hide + * without disrupting new shows. + */ +let animateOutTimer: ReturnType | null = null; + +/** Callback for when the drop-down is hidden. */ +let onHide: (() => void) | null = null; + +/** A class name representing the current owner's workspace renderer. */ +let renderedClassName = ''; + +/** A class name representing the current owner's workspace theme. */ +let themeClassName = ''; + +/** The content element. */ +let div: HTMLDivElement; + +/** The content element. */ +let content: HTMLDivElement; + +/** The arrow element. */ +let arrow: HTMLDivElement; + +/** + * Drop-downs will appear within the bounds of this element if possible. + * Set in setBoundsElement. + */ +let boundsElement: Element | null = null; + +/** The object currently using the drop-down. */ +let owner: Field | null = null; + +/** Whether the dropdown was positioned to a field or the source block. */ +let positionToField: boolean | null = null; + +/** + * Dropdown bounds info object used to encapsulate sizing information about a + * bounding element (bounding box and width/height). + */ +export interface BoundsInfo { + top: number; + left: number; + bottom: number; + right: number; + width: number; + height: number; +} + +/** Dropdown position metrics. */ +export interface PositionMetrics { + initialX: number; + initialY: number; + finalX: number; + finalY: number; + arrowX: number | null; + arrowY: number | null; + arrowAtTop: boolean | null; + arrowVisible: boolean; +} + +/** + * Create and insert the DOM element for this div. + * + * @internal + */ +export function createDom() { + if (document.querySelector('.blocklyDropDownDiv')) { + return; // Already created. + } + div = document.createElement('div'); + div.className = 'blocklyDropDownDiv'; + const parentDiv = common.getParentContainer() || document.body; + parentDiv.appendChild(div); + + content = document.createElement('div'); + content.className = 'blocklyDropDownContent'; + div.appendChild(content); + + arrow = document.createElement('div'); + arrow.className = 'blocklyDropDownArrow'; + div.appendChild(arrow); + + div.style.opacity = '0'; + // Transition animation for transform: translate() and opacity. + div.style.transition = + 'transform ' + ANIMATION_TIME + 's, ' + 'opacity ' + ANIMATION_TIME + 's'; + + // Handle focusin/out events to add a visual indicator when + // a child is focused or blurred. + div.addEventListener('focusin', function () { + dom.addClass(div, 'blocklyFocused'); + }); + div.addEventListener('focusout', function () { + dom.removeClass(div, 'blocklyFocused'); + }); +} + +/** + * Set an element to maintain bounds within. Drop-downs will appear + * within the box of this element if possible. + * + * @param boundsElem Element to bind drop-down to. + */ +export function setBoundsElement(boundsElem: Element | null) { + boundsElement = boundsElem; +} + +/** + * @returns The field that currently owns this, or null. + */ +export function getOwner(): Field | null { + return owner; +} + +/** + * Provide the div for inserting content into the drop-down. + * + * @returns Div to populate with content. + */ +export function getContentDiv(): Element { + return content; +} + +/** Clear the content of the drop-down. */ +export function clearContent() { + content.textContent = ''; + content.style.width = ''; +} + +/** + * Set the colour for the drop-down. + * + * @param backgroundColour Any CSS colour for the background. + * @param borderColour Any CSS colour for the border. + */ +export function setColour(backgroundColour: string, borderColour: string) { + div.style.backgroundColor = backgroundColour; + div.style.borderColor = borderColour; +} + +/** + * Shortcut to show and place the drop-down with positioning determined + * by a particular block. The primary position will be below the block, + * and the secondary position above the block. Drop-down will be + * constrained to the block's workspace. + * + * @param field The field showing the drop-down. + * @param block Block to position the drop-down around. + * @param opt_onHide Optional callback for when the drop-down is hidden. + * @param opt_secondaryYOffset Optional Y offset for above-block positioning. + * @returns True if the menu rendered below block; false if above. + */ +export function showPositionedByBlock( + field: Field, + block: BlockSvg, + opt_onHide?: () => void, + opt_secondaryYOffset?: number, +): boolean { + return showPositionedByRect( + getScaledBboxOfBlock(block), + field as Field, + opt_onHide, + opt_secondaryYOffset, + ); +} + +/** + * Shortcut to show and place the drop-down with positioning determined + * by a particular field. The primary position will be below the field, + * and the secondary position above the field. Drop-down will be + * constrained to the block's workspace. + * + * @param field The field to position the dropdown against. + * @param opt_onHide Optional callback for when the drop-down is hidden. + * @param opt_secondaryYOffset Optional Y offset for above-block positioning. + * @returns True if the menu rendered below block; false if above. + */ +export function showPositionedByField( + field: Field, + opt_onHide?: () => void, + opt_secondaryYOffset?: number, +): boolean { + positionToField = true; + return showPositionedByRect( + getScaledBboxOfField(field as Field), + field as Field, + opt_onHide, + opt_secondaryYOffset, + ); +} +/** + * Get the scaled bounding box of a block. + * + * @param block The block. + * @returns The scaled bounding box of the block. + */ +function getScaledBboxOfBlock(block: BlockSvg): Rect { + const blockSvg = block.getSvgRoot(); + const scale = block.workspace.scale; + const scaledHeight = block.height * scale; + const scaledWidth = block.width * scale; + const xy = style.getPageOffset(blockSvg); + return new Rect(xy.y, xy.y + scaledHeight, xy.x, xy.x + scaledWidth); +} + +/** + * Get the scaled bounding box of a field. + * + * @param field The field. + * @returns The scaled bounding box of the field. + */ +function getScaledBboxOfField(field: Field): Rect { + const bBox = field.getScaledBBox(); + return new Rect(bBox.top, bBox.bottom, bBox.left, bBox.right); +} + +/** + * Helper method to show and place the drop-down with positioning determined + * by a scaled bounding box. The primary position will be below the rect, + * and the secondary position above the rect. Drop-down will be constrained to + * the block's workspace. + * + * @param bBox The scaled bounding box. + * @param field The field to position the dropdown against. + * @param opt_onHide Optional callback for when the drop-down is hidden. + * @param opt_secondaryYOffset Optional Y offset for above-block positioning. + * @returns True if the menu rendered below block; false if above. + */ +function showPositionedByRect( + bBox: Rect, + field: Field, + opt_onHide?: () => void, + opt_secondaryYOffset?: number, +): boolean { + // If we can fit it, render below the block. + const primaryX = bBox.left + (bBox.right - bBox.left) / 2; + const primaryY = bBox.bottom; + // If we can't fit it, render above the entire parent block. + const secondaryX = primaryX; + let secondaryY = bBox.top; + if (opt_secondaryYOffset) { + secondaryY += opt_secondaryYOffset; + } + const sourceBlock = field.getSourceBlock() as BlockSvg; + // Set bounds to main workspace; show the drop-down. + let workspace = sourceBlock.workspace; + while (workspace.options.parentWorkspace) { + workspace = workspace.options.parentWorkspace; + } + setBoundsElement(workspace.getParentSvg().parentNode as Element | null); + return show( + field, + sourceBlock.RTL, + primaryX, + primaryY, + secondaryX, + secondaryY, + opt_onHide, + ); +} + +/** + * Show and place the drop-down. + * The drop-down is placed with an absolute "origin point" (x, y) - i.e., + * the arrow will point at this origin and box will positioned below or above + * it. If we can maintain the container bounds at the primary point, the arrow + * will point there, and the container will be positioned below it. + * If we can't maintain the container bounds at the primary point, fall-back to + * the secondary point and position above. + * + * @param newOwner The object showing the drop-down + * @param rtl Right-to-left (true) or left-to-right (false). + * @param primaryX Desired origin point x, in absolute px. + * @param primaryY Desired origin point y, in absolute px. + * @param secondaryX Secondary/alternative origin point x, in absolute px. + * @param secondaryY Secondary/alternative origin point y, in absolute px. + * @param opt_onHide Optional callback for when the drop-down is hidden. + * @returns True if the menu rendered at the primary origin point. + * @internal + */ +export function show( + newOwner: Field, + rtl: boolean, + primaryX: number, + primaryY: number, + secondaryX: number, + secondaryY: number, + opt_onHide?: () => void, +): boolean { + owner = newOwner as Field; + onHide = opt_onHide || null; + // Set direction. + div.style.direction = rtl ? 'rtl' : 'ltr'; + + const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg; + renderedClassName = mainWorkspace.getRenderer().getClassName(); + themeClassName = mainWorkspace.getTheme().getClassName(); + if (renderedClassName) { + dom.addClass(div, renderedClassName); + } + if (themeClassName) { + dom.addClass(div, themeClassName); + } + + // When we change `translate` multiple times in close succession, + // Chrome may choose to wait and apply them all at once. + // Since we want the translation to initial X, Y to be immediate, + // and the translation to final X, Y to be animated, + // we saw problems where both would be applied after animation was turned on, + // making the dropdown appear to fly in from (0, 0). + // Using both `left`, `top` for the initial translation and then `translate` + // for the animated transition to final X, Y is a workaround. + return positionInternal(primaryX, primaryY, secondaryX, secondaryY); +} + +const internal = { + /** + * Get sizing info about the bounding element. + * + * @returns An object containing size information about the bounding element + * (bounding box and width/height). + */ + getBoundsInfo: function (): BoundsInfo { + const boundPosition = style.getPageOffset(boundsElement as Element); + const boundSize = style.getSize(boundsElement as Element); + + return { + left: boundPosition.x, + right: boundPosition.x + boundSize.width, + top: boundPosition.y, + bottom: boundPosition.y + boundSize.height, + width: boundSize.width, + height: boundSize.height, + }; + }, + + /** + * Helper to position the drop-down and the arrow, maintaining bounds. + * See explanation of origin points in show. + * + * @param primaryX Desired origin point x, in absolute px. + * @param primaryY Desired origin point y, in absolute px. + * @param secondaryX Secondary/alternative origin point x, in absolute px. + * @param secondaryY Secondary/alternative origin point y, in absolute px. + * @returns Various final metrics, including rendered positions for drop-down + * and arrow. + */ + getPositionMetrics: function ( + primaryX: number, + primaryY: number, + secondaryX: number, + secondaryY: number, + ): PositionMetrics { + const boundsInfo = internal.getBoundsInfo(); + const divSize = style.getSize(div as Element); + + // Can we fit in-bounds below the target? + if (primaryY + divSize.height < boundsInfo.bottom) { + return getPositionBelowMetrics(primaryX, primaryY, boundsInfo, divSize); + } + // Can we fit in-bounds above the target? + if (secondaryY - divSize.height > boundsInfo.top) { + return getPositionAboveMetrics( + secondaryX, + secondaryY, + boundsInfo, + divSize, + ); + } + // Can we fit outside the workspace bounds (but inside the window) + // below? + if (primaryY + divSize.height < document.documentElement.clientHeight) { + return getPositionBelowMetrics(primaryX, primaryY, boundsInfo, divSize); + } + // Can we fit outside the workspace bounds (but inside the window) + // above? + if (secondaryY - divSize.height > document.documentElement.clientTop) { + return getPositionAboveMetrics( + secondaryX, + secondaryY, + boundsInfo, + divSize, + ); + } + + // Last resort, render at top of page. + return getPositionTopOfPageMetrics(primaryX, boundsInfo, divSize); + }, +}; + +/** + * Get the metrics for positioning the div below the source. + * + * @param primaryX Desired origin point x, in absolute px. + * @param primaryY Desired origin point y, in absolute px. + * @param boundsInfo An object containing size information about the bounding + * element (bounding box and width/height). + * @param divSize An object containing information about the size of the + * DropDownDiv (width & height). + * @returns Various final metrics, including rendered positions for drop-down + * and arrow. + */ +function getPositionBelowMetrics( + primaryX: number, + primaryY: number, + boundsInfo: BoundsInfo, + divSize: Size, +): PositionMetrics { + const xCoords = getPositionX( + primaryX, + boundsInfo.left, + boundsInfo.right, + divSize.width, + ); + + const arrowY = -(ARROW_SIZE / 2 + BORDER_SIZE); + const finalY = primaryY + PADDING_Y; + + return { + initialX: xCoords.divX, + initialY: primaryY, + finalX: xCoords.divX, // X position remains constant during animation. + finalY, + arrowX: xCoords.arrowX, + arrowY, + arrowAtTop: true, + arrowVisible: true, + }; +} + +/** + * Get the metrics for positioning the div above the source. + * + * @param secondaryX Secondary/alternative origin point x, in absolute px. + * @param secondaryY Secondary/alternative origin point y, in absolute px. + * @param boundsInfo An object containing size information about the bounding + * element (bounding box and width/height). + * @param divSize An object containing information about the size of the + * DropDownDiv (width & height). + * @returns Various final metrics, including rendered positions for drop-down + * and arrow. + */ +function getPositionAboveMetrics( + secondaryX: number, + secondaryY: number, + boundsInfo: BoundsInfo, + divSize: Size, +): PositionMetrics { + const xCoords = getPositionX( + secondaryX, + boundsInfo.left, + boundsInfo.right, + divSize.width, + ); + + const arrowY = divSize.height - BORDER_SIZE * 2 - ARROW_SIZE / 2; + const finalY = secondaryY - divSize.height - PADDING_Y; + const initialY = secondaryY - divSize.height; // No padding on Y. + + return { + initialX: xCoords.divX, + initialY, + finalX: xCoords.divX, // X position remains constant during animation. + finalY, + arrowX: xCoords.arrowX, + arrowY, + arrowAtTop: false, + arrowVisible: true, + }; +} + +/** + * Get the metrics for positioning the div at the top of the page. + * + * @param sourceX Desired origin point x, in absolute px. + * @param boundsInfo An object containing size information about the bounding + * element (bounding box and width/height). + * @param divSize An object containing information about the size of the + * DropDownDiv (width & height). + * @returns Various final metrics, including rendered positions for drop-down + * and arrow. + */ +function getPositionTopOfPageMetrics( + sourceX: number, + boundsInfo: BoundsInfo, + divSize: Size, +): PositionMetrics { + const xCoords = getPositionX( + sourceX, + boundsInfo.left, + boundsInfo.right, + divSize.width, + ); + + // No need to provide arrow-specific information because it won't be visible. + return { + initialX: xCoords.divX, + initialY: 0, + finalX: xCoords.divX, // X position remains constant during animation. + finalY: 0, // Y position remains constant during animation. + arrowAtTop: null, + arrowX: null, + arrowY: null, + arrowVisible: false, + }; +} + +/** + * Get the x positions for the left side of the DropDownDiv and the arrow, + * accounting for the bounds of the workspace. + * + * @param sourceX Desired origin point x, in absolute px. + * @param boundsLeft The left edge of the bounding element, in absolute px. + * @param boundsRight The right edge of the bounding element, in absolute px. + * @param divWidth The width of the div in px. + * @returns An object containing metrics for the x positions of the left side of + * the DropDownDiv and the arrow. + * @internal + */ +export function getPositionX( + sourceX: number, + boundsLeft: number, + boundsRight: number, + divWidth: number, +): {divX: number; arrowX: number} { + let divX = sourceX; + // Offset the topLeft coord so that the dropdowndiv is centered. + divX -= divWidth / 2; + // Fit the dropdowndiv within the bounds of the workspace. + divX = math.clamp(boundsLeft, divX, boundsRight - divWidth); + + let arrowX = sourceX; + // Offset the arrow coord so that the arrow is centered. + arrowX -= ARROW_SIZE / 2; + // Convert the arrow position to be relative to the top left of the div. + let relativeArrowX = arrowX - divX; + const horizPadding = ARROW_HORIZONTAL_PADDING; + // Clamp the arrow position so that it stays attached to the dropdowndiv. + relativeArrowX = math.clamp( + horizPadding, + relativeArrowX, + divWidth - horizPadding - ARROW_SIZE, + ); + + return {arrowX: relativeArrowX, divX}; +} + +/** + * Is the container visible? + * + * @returns True if visible. + */ +export function isVisible(): boolean { + return !!owner; +} + +/** + * Hide the menu only if it is owned by the provided object. + * + * @param divOwner Object which must be owning the drop-down to hide. + * @param opt_withoutAnimation True if we should hide the dropdown without + * animating. + * @returns True if hidden. + */ +export function hideIfOwner( + divOwner: Field, + opt_withoutAnimation?: boolean, +): boolean { + if (owner === divOwner) { + if (opt_withoutAnimation) { + hideWithoutAnimation(); + } else { + hide(); + } + return true; + } + return false; +} + +/** Hide the menu, triggering animation. */ +export function hide() { + // Start the animation by setting the translation and fading out. + // Reset to (initialX, initialY) - i.e., no translation. + div.style.transform = 'translate(0, 0)'; + div.style.opacity = '0'; + // Finish animation - reset all values to default. + animateOutTimer = setTimeout(function () { + hideWithoutAnimation(); + }, ANIMATION_TIME * 1000); + if (onHide) { + onHide(); + onHide = null; + } +} + +/** Hide the menu, without animation. */ +export function hideWithoutAnimation() { + if (!isVisible()) { + return; + } + if (animateOutTimer) { + clearTimeout(animateOutTimer); + } + + // Reset style properties in case this gets called directly + // instead of hide() - see discussion on #2551. + div.style.transform = ''; + div.style.left = ''; + div.style.top = ''; + div.style.opacity = '0'; + div.style.display = 'none'; + div.style.backgroundColor = ''; + div.style.borderColor = ''; + + if (onHide) { + onHide(); + onHide = null; + } + clearContent(); + owner = null; + + if (renderedClassName) { + dom.removeClass(div, renderedClassName); + renderedClassName = ''; + } + if (themeClassName) { + dom.removeClass(div, themeClassName); + themeClassName = ''; + } + (common.getMainWorkspace() as WorkspaceSvg).markFocused(); +} + +/** + * Set the dropdown div's position. + * + * @param primaryX Desired origin point x, in absolute px. + * @param primaryY Desired origin point y, in absolute px. + * @param secondaryX Secondary/alternative origin point x, in absolute px. + * @param secondaryY Secondary/alternative origin point y, in absolute px. + * @returns True if the menu rendered at the primary origin point. + */ +function positionInternal( + primaryX: number, + primaryY: number, + secondaryX: number, + secondaryY: number, +): boolean { + const metrics = internal.getPositionMetrics( + primaryX, + primaryY, + secondaryX, + secondaryY, + ); + + // Update arrow CSS. + if (metrics.arrowVisible) { + arrow.style.display = ''; + arrow.style.transform = + 'translate(' + + metrics.arrowX + + 'px,' + + metrics.arrowY + + 'px) rotate(45deg)'; + arrow.setAttribute( + 'class', + metrics.arrowAtTop + ? 'blocklyDropDownArrow blocklyArrowTop' + : 'blocklyDropDownArrow blocklyArrowBottom', + ); + } else { + arrow.style.display = 'none'; + } + + const initialX = Math.floor(metrics.initialX); + const initialY = Math.floor(metrics.initialY); + const finalX = Math.floor(metrics.finalX); + const finalY = Math.floor(metrics.finalY); + + // First apply initial translation. + div.style.left = initialX + 'px'; + div.style.top = initialY + 'px'; + + // Show the div. + div.style.display = 'block'; + div.style.opacity = '1'; + // Add final translate, animated through `transition`. + // Coordinates are relative to (initialX, initialY), + // where the drop-down is absolutely positioned. + const dx = finalX - initialX; + const dy = finalY - initialY; + div.style.transform = 'translate(' + dx + 'px,' + dy + 'px)'; + + return !!metrics.arrowAtTop; +} + +/** + * Repositions the dropdownDiv on window resize. If it doesn't know how to + * calculate the new position, it will just hide it instead. + * + * @internal + */ +export function repositionForWindowResize() { + // This condition mainly catches the dropdown div when it is being used as a + // dropdown. It is important not to close it in this case because on Android, + // when a field is focused, the soft keyboard opens triggering a window resize + // event and we want the dropdown div to stick around so users can type into + // it. + if (owner) { + const block = owner.getSourceBlock() as BlockSvg; + const bBox = positionToField + ? getScaledBboxOfField(owner) + : getScaledBboxOfBlock(block); + // If we can fit it, render below the block. + const primaryX = bBox.left + (bBox.right - bBox.left) / 2; + const primaryY = bBox.bottom; + // If we can't fit it, render above the entire parent block. + const secondaryX = primaryX; + const secondaryY = bBox.top; + positionInternal(primaryX, primaryY, secondaryX, secondaryY); + } else { + hide(); + } +} + +export const TEST_ONLY = internal; diff --git a/core/events.js b/core/events.js deleted file mode 100644 index 3e196dc567d..00000000000 --- a/core/events.js +++ /dev/null @@ -1,1116 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Events fired as a result of actions in Blockly's editor. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -/** - * Events fired as a result of actions in Blockly's editor. - * @namespace Blockly.Events - */ -goog.provide('Blockly.Events'); - -goog.require('goog.array'); -goog.require('goog.math.Coordinate'); - - -/** - * Group ID for new events. Grouped events are indivisible. - * @type {string} - * @private - */ -Blockly.Events.group_ = ''; - -/** - * Sets whether events should be added to the undo stack. - * @type {boolean} - */ -Blockly.Events.recordUndo = true; - -/** - * Allow change events to be created and fired. - * @type {number} - * @private - */ -Blockly.Events.disabled_ = 0; - -/** - * Name of event that creates a block. Will be deprecated for BLOCK_CREATE. - * @const - */ -Blockly.Events.CREATE = 'create'; - -/** - * Name of event that creates a block. - * @const - */ -Blockly.Events.BLOCK_CREATE = Blockly.Events.CREATE; - -/** - * Name of event that deletes a block. Will be deprecated for BLOCK_DELETE. - * @const - */ -Blockly.Events.DELETE = 'delete'; - -/** - * Name of event that deletes a block. - * @const - */ -Blockly.Events.BLOCK_DELETE = Blockly.Events.DELETE; - -/** - * Name of event that changes a block. Will be deprecated for BLOCK_CHANGE. - * @const - */ -Blockly.Events.CHANGE = 'change'; - -/** - * Name of event that changes a block. - * @const - */ -Blockly.Events.BLOCK_CHANGE = Blockly.Events.CHANGE; - -/** - * Name of event that moves a block. Will be deprecated for BLOCK_MOVE. - * @const - */ -Blockly.Events.MOVE = 'move'; - -/** - * Name of event that moves a block. - * @const - */ -Blockly.Events.BLOCK_MOVE = Blockly.Events.MOVE; - -/** - * Name of event that creates a variable. - * @const - */ -Blockly.Events.VAR_CREATE = 'var_create'; - -/** - * Name of event that deletes a variable. - * @const - */ -Blockly.Events.VAR_DELETE = 'var_delete'; - -/** - * Name of event that renames a variable. - * @const - */ -Blockly.Events.VAR_RENAME = 'var_rename'; - -/** - * Name of event that records a UI change. - * @const - */ -Blockly.Events.UI = 'ui'; - -/** - * List of events queued for firing. - * @private - */ -Blockly.Events.FIRE_QUEUE_ = []; - -/** - * Create a custom event and fire it. - * @param {!Blockly.Events.Abstract} event Custom data for event. - */ -Blockly.Events.fire = function(event) { - if (!Blockly.Events.isEnabled()) { - return; - } - if (!Blockly.Events.FIRE_QUEUE_.length) { - // First event added; schedule a firing of the event queue. - setTimeout(Blockly.Events.fireNow_, 0); - } - Blockly.Events.FIRE_QUEUE_.push(event); -}; - -/** - * Fire all queued events. - * @private - */ -Blockly.Events.fireNow_ = function() { - var queue = Blockly.Events.filter(Blockly.Events.FIRE_QUEUE_, true); - Blockly.Events.FIRE_QUEUE_.length = 0; - for (var i = 0, event; event = queue[i]; i++) { - var workspace = Blockly.Workspace.getById(event.workspaceId); - if (workspace) { - workspace.fireChangeListener(event); - } - } -}; - -/** - * Filter the queued events and merge duplicates. - * @param {!Array.} queueIn Array of events. - * @param {boolean} forward True if forward (redo), false if backward (undo). - * @return {!Array.} Array of filtered events. - */ -Blockly.Events.filter = function(queueIn, forward) { - var queue = goog.array.clone(queueIn); - if (!forward) { - // Undo is merged in reverse order. - queue.reverse(); - } - // Merge duplicates. O(n^2), but n should be very small. - for (var i = 0, event1; event1 = queue[i]; i++) { - for (var j = i + 1, event2; event2 = queue[j]; j++) { - if (event1.type == event2.type && - event1.blockId == event2.blockId && - event1.workspaceId == event2.workspaceId) { - if (event1.type == Blockly.Events.MOVE) { - // Merge move events. - event1.newParentId = event2.newParentId; - event1.newInputName = event2.newInputName; - event1.newCoordinate = event2.newCoordinate; - queue.splice(j, 1); - j--; - } else if (event1.type == Blockly.Events.CHANGE && - event1.element == event2.element && - event1.name == event2.name) { - // Merge change events. - event1.newValue = event2.newValue; - queue.splice(j, 1); - j--; - } else if (event1.type == Blockly.Events.UI && - event2.element == 'click' && - (event1.element == 'commentOpen' || - event1.element == 'mutatorOpen' || - event1.element == 'warningOpen')) { - // Merge change events. - event1.newValue = event2.newValue; - queue.splice(j, 1); - j--; - } - } - } - } - // Remove null events. - for (var i = queue.length - 1; i >= 0; i--) { - if (queue[i].isNull()) { - queue.splice(i, 1); - } - } - if (!forward) { - // Restore undo order. - queue.reverse(); - } - // Move mutation events to the top of the queue. - // Intentionally skip first event. - for (var i = 1, event; event = queue[i]; i++) { - if (event.type == Blockly.Events.CHANGE && - event.element == 'mutation') { - queue.unshift(queue.splice(i, 1)[0]); - } - } - return queue; -}; - -/** - * Modify pending undo events so that when they are fired they don't land - * in the undo stack. Called by Blockly.Workspace.clearUndo. - */ -Blockly.Events.clearPendingUndo = function() { - for (var i = 0, event; event = Blockly.Events.FIRE_QUEUE_[i]; i++) { - event.recordUndo = false; - } -}; - -/** - * Stop sending events. Every call to this function MUST also call enable. - */ -Blockly.Events.disable = function() { - Blockly.Events.disabled_++; -}; - -/** - * Start sending events. Unless events were already disabled when the - * corresponding call to disable was made. - */ -Blockly.Events.enable = function() { - Blockly.Events.disabled_--; -}; - -/** - * Returns whether events may be fired or not. - * @return {boolean} True if enabled. - */ -Blockly.Events.isEnabled = function() { - return Blockly.Events.disabled_ == 0; -}; - -/** - * Current group. - * @return {string} ID string. - */ -Blockly.Events.getGroup = function() { - return Blockly.Events.group_; -}; - -/** - * Start or stop a group. - * @param {boolean|string} state True to start new group, false to end group. - * String to set group explicitly. - */ -Blockly.Events.setGroup = function(state) { - if (typeof state == 'boolean') { - Blockly.Events.group_ = state ? Blockly.utils.genUid() : ''; - } else { - Blockly.Events.group_ = state; - } -}; - -/** - * Compute a list of the IDs of the specified block and all its descendants. - * @param {!Blockly.Block} block The root block. - * @return {!Array.} List of block IDs. - * @private - */ -Blockly.Events.getDescendantIds_ = function(block) { - var ids = []; - var descendants = block.getDescendants(); - for (var i = 0, descendant; descendant = descendants[i]; i++) { - ids[i] = descendant.id; - } - return ids; -}; - -/** - * Decode the JSON into an event. - * @param {!Object} json JSON representation. - * @param {!Blockly.Workspace} workspace Target workspace for event. - * @return {!Blockly.Events.Abstract} The event represented by the JSON. - */ -Blockly.Events.fromJson = function(json, workspace) { - var event; - switch (json.type) { - case Blockly.Events.CREATE: - event = new Blockly.Events.Create(null); - break; - case Blockly.Events.DELETE: - event = new Blockly.Events.Delete(null); - break; - case Blockly.Events.CHANGE: - event = new Blockly.Events.Change(null); - break; - case Blockly.Events.MOVE: - event = new Blockly.Events.Move(null); - break; - case Blockly.Events.VAR_CREATE: - event = new Blockly.Events.VarCreate(null); - break; - case Blockly.Events.VAR_DELETE: - event = new Blockly.Events.VarDelete(null); - break; - case Blockly.Events.VAR_RENAME: - event = new Blockly.Events.VarRename(null); - break; - case Blockly.Events.UI: - event = new Blockly.Events.Ui(null); - break; - default: - throw 'Unknown event type.'; - } - event.fromJson(json); - event.workspaceId = workspace.id; - return event; -}; - -/** - * Abstract class for an event. - * @param {Blockly.Block|Blockly.VariableModel} elem The block or variable. - * @constructor - */ -Blockly.Events.Abstract = function(elem) { - if (elem instanceof Blockly.Block) { - this.blockId = elem.id; - this.workspaceId = elem.workspace.id; - } - else if (elem instanceof Blockly.VariableModel){ - this.workspaceId = elem.workspace.id; - this.varId = elem.getId(); - } - this.group = Blockly.Events.group_; - this.recordUndo = Blockly.Events.recordUndo; -}; - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -Blockly.Events.Abstract.prototype.toJson = function() { - var json = { - 'type': this.type - }; - if (this.blockId) { - json['blockId'] = this.blockId; - } - if (this.varId) { - json['varId'] = this.varId; - } - if (this.group) { - json['group'] = this.group; - } - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -Blockly.Events.Abstract.prototype.fromJson = function(json) { - this.blockId = json['blockId']; - this.varId = json['varId']; - this.group = json['group']; -}; - -/** - * Does this event record any change of state? - * @return {boolean} True if null, false if something changed. - */ -Blockly.Events.Abstract.prototype.isNull = function() { - return false; -}; - -/** - * Run an event. - * @param {boolean} forward True if run forward, false if run backward (undo). - */ -Blockly.Events.Abstract.prototype.run = function(forward) { - // Defined by subclasses. -}; - -/** - * Get workspace the event belongs to. - * @return {Blockly.Workspace} The workspace the event belongs to. - * @throws {Error} if workspace is null. - * @private - */ -Blockly.Events.Abstract.prototype.getEventWorkspace_ = function() { - var workspace = Blockly.Workspace.getById(this.workspaceId); - if (!workspace) { - throw Error('Workspace is null. Event must have been generated from real' + - ' Blockly events.'); - } - return workspace; -}; - -/** - * Class for a block creation event. - * @param {Blockly.Block} block The created block. Null for a blank event. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.Create = function(block) { - if (!block) { - return; // Blank event to be populated by fromJson. - } - Blockly.Events.Create.superClass_.constructor.call(this, block); - - if (block.workspace.rendered) { - this.xml = Blockly.Xml.blockToDomWithXY(block); - } else { - this.xml = Blockly.Xml.blockToDom(block); - } - this.ids = Blockly.Events.getDescendantIds_(block); -}; -goog.inherits(Blockly.Events.Create, Blockly.Events.Abstract); - -/** - * Class for a block creation event. - * @param {Blockly.Block} block The created block. Null for a blank event. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.BlockCreate = Blockly.Events.Create; - -/** - * Type of this event. - * @type {string} - */ -Blockly.Events.Create.prototype.type = Blockly.Events.CREATE; - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -Blockly.Events.Create.prototype.toJson = function() { - var json = Blockly.Events.Create.superClass_.toJson.call(this); - json['xml'] = Blockly.Xml.domToText(this.xml); - json['ids'] = this.ids; - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -Blockly.Events.Create.prototype.fromJson = function(json) { - Blockly.Events.Create.superClass_.fromJson.call(this, json); - this.xml = Blockly.Xml.textToDom('' + json['xml'] + '').firstChild; - this.ids = json['ids']; -}; - -/** - * Run a creation event. - * @param {boolean} forward True if run forward, false if run backward (undo). - */ -Blockly.Events.Create.prototype.run = function(forward) { - var workspace = this.getEventWorkspace_(); - if (forward) { - var xml = goog.dom.createDom('xml'); - xml.appendChild(this.xml); - Blockly.Xml.domToWorkspace(xml, workspace); - } else { - for (var i = 0, id; id = this.ids[i]; i++) { - var block = workspace.getBlockById(id); - if (block) { - block.dispose(false, false); - } else if (id == this.blockId) { - // Only complain about root-level block. - console.warn("Can't uncreate non-existant block: " + id); - } - } - } -}; - -/** - * Class for a block deletion event. - * @param {Blockly.Block} block The deleted block. Null for a blank event. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.Delete = function(block) { - if (!block) { - return; // Blank event to be populated by fromJson. - } - if (block.getParent()) { - throw 'Connected blocks cannot be deleted.'; - } - Blockly.Events.Delete.superClass_.constructor.call(this, block); - - if (block.workspace.rendered) { - this.oldXml = Blockly.Xml.blockToDomWithXY(block); - } else { - this.oldXml = Blockly.Xml.blockToDom(block); - } - this.ids = Blockly.Events.getDescendantIds_(block); -}; -goog.inherits(Blockly.Events.Delete, Blockly.Events.Abstract); - -/** - * Class for a block deletion event. - * @param {Blockly.Block} block The deleted block. Null for a blank event. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.BlockDelete = Blockly.Events.Delete; - -/** - * Type of this event. - * @type {string} - */ -Blockly.Events.Delete.prototype.type = Blockly.Events.DELETE; - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -Blockly.Events.Delete.prototype.toJson = function() { - var json = Blockly.Events.Delete.superClass_.toJson.call(this); - json['ids'] = this.ids; - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -Blockly.Events.Delete.prototype.fromJson = function(json) { - Blockly.Events.Delete.superClass_.fromJson.call(this, json); - this.ids = json['ids']; -}; - -/** - * Run a deletion event. - * @param {boolean} forward True if run forward, false if run backward (undo). - */ -Blockly.Events.Delete.prototype.run = function(forward) { - var workspace = this.getEventWorkspace_(); - if (forward) { - for (var i = 0, id; id = this.ids[i]; i++) { - var block = workspace.getBlockById(id); - if (block) { - block.dispose(false, false); - } else if (id == this.blockId) { - // Only complain about root-level block. - console.warn("Can't delete non-existant block: " + id); - } - } - } else { - var xml = goog.dom.createDom('xml'); - xml.appendChild(this.oldXml); - Blockly.Xml.domToWorkspace(xml, workspace); - } -}; - -/** - * Class for a block change event. - * @param {Blockly.Block} block The changed block. Null for a blank event. - * @param {string} element One of 'field', 'comment', 'disabled', etc. - * @param {?string} name Name of input or field affected, or null. - * @param {string} oldValue Previous value of element. - * @param {string} newValue New value of element. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.Change = function(block, element, name, oldValue, newValue) { - if (!block) { - return; // Blank event to be populated by fromJson. - } - Blockly.Events.Change.superClass_.constructor.call(this, block); - this.element = element; - this.name = name; - this.oldValue = oldValue; - this.newValue = newValue; -}; -goog.inherits(Blockly.Events.Change, Blockly.Events.Abstract); - -/** - * Class for a block change event. - * @param {Blockly.Block} block The changed block. Null for a blank event. - * @param {string} element One of 'field', 'comment', 'disabled', etc. - * @param {?string} name Name of input or field affected, or null. - * @param {string} oldValue Previous value of element. - * @param {string} newValue New value of element. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.BlockChange = Blockly.Events.Change; - -/** - * Type of this event. - * @type {string} - */ -Blockly.Events.Change.prototype.type = Blockly.Events.CHANGE; - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -Blockly.Events.Change.prototype.toJson = function() { - var json = Blockly.Events.Change.superClass_.toJson.call(this); - json['element'] = this.element; - if (this.name) { - json['name'] = this.name; - } - json['newValue'] = this.newValue; - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -Blockly.Events.Change.prototype.fromJson = function(json) { - Blockly.Events.Change.superClass_.fromJson.call(this, json); - this.element = json['element']; - this.name = json['name']; - this.newValue = json['newValue']; -}; - -/** - * Does this event record any change of state? - * @return {boolean} True if something changed. - */ -Blockly.Events.Change.prototype.isNull = function() { - return this.oldValue == this.newValue; -}; - -/** - * Run a change event. - * @param {boolean} forward True if run forward, false if run backward (undo). - */ -Blockly.Events.Change.prototype.run = function(forward) { - var workspace = this.getEventWorkspace_(); - var block = workspace.getBlockById(this.blockId); - if (!block) { - console.warn("Can't change non-existant block: " + this.blockId); - return; - } - if (block.mutator) { - // Close the mutator (if open) since we don't want to update it. - block.mutator.setVisible(false); - } - var value = forward ? this.newValue : this.oldValue; - switch (this.element) { - case 'field': - var field = block.getField(this.name); - if (field) { - // Run the validator for any side-effects it may have. - // The validator's opinion on validity is ignored. - field.callValidator(value); - field.setValue(value); - } else { - console.warn("Can't set non-existant field: " + this.name); - } - break; - case 'comment': - block.setCommentText(value || null); - break; - case 'collapsed': - block.setCollapsed(value); - break; - case 'disabled': - block.setDisabled(value); - break; - case 'inline': - block.setInputsInline(value); - break; - case 'mutation': - var oldMutation = ''; - if (block.mutationToDom) { - var oldMutationDom = block.mutationToDom(); - oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom); - } - if (block.domToMutation) { - value = value || ''; - var dom = Blockly.Xml.textToDom('' + value + ''); - block.domToMutation(dom.firstChild); - } - Blockly.Events.fire(new Blockly.Events.Change( - block, 'mutation', null, oldMutation, value)); - break; - default: - console.warn('Unknown change type: ' + this.element); - } -}; - -/** - * Class for a block move event. Created before the move. - * @param {Blockly.Block} block The moved block. Null for a blank event. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.Move = function(block) { - if (!block) { - return; // Blank event to be populated by fromJson. - } - Blockly.Events.Move.superClass_.constructor.call(this, block); - var location = this.currentLocation_(); - this.oldParentId = location.parentId; - this.oldInputName = location.inputName; - this.oldCoordinate = location.coordinate; -}; -goog.inherits(Blockly.Events.Move, Blockly.Events.Abstract); - - -/** - * Class for a block move event. Created before the move. - * @param {Blockly.Block} block The moved block. Null for a blank event. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.BlockMove = Blockly.Events.Move; - -/** - * Type of this event. - * @type {string} - */ -Blockly.Events.Move.prototype.type = Blockly.Events.MOVE; - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -Blockly.Events.Move.prototype.toJson = function() { - var json = Blockly.Events.Move.superClass_.toJson.call(this); - if (this.newParentId) { - json['newParentId'] = this.newParentId; - } - if (this.newInputName) { - json['newInputName'] = this.newInputName; - } - if (this.newCoordinate) { - json['newCoordinate'] = Math.round(this.newCoordinate.x) + ',' + - Math.round(this.newCoordinate.y); - } - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -Blockly.Events.Move.prototype.fromJson = function(json) { - Blockly.Events.Move.superClass_.fromJson.call(this, json); - this.newParentId = json['newParentId']; - this.newInputName = json['newInputName']; - if (json['newCoordinate']) { - var xy = json['newCoordinate'].split(','); - this.newCoordinate = - new goog.math.Coordinate(parseFloat(xy[0]), parseFloat(xy[1])); - } -}; - -/** - * Record the block's new location. Called after the move. - */ -Blockly.Events.Move.prototype.recordNew = function() { - var location = this.currentLocation_(); - this.newParentId = location.parentId; - this.newInputName = location.inputName; - this.newCoordinate = location.coordinate; -}; - -/** - * Returns the parentId and input if the block is connected, - * or the XY location if disconnected. - * @return {!Object} Collection of location info. - * @private - */ -Blockly.Events.Move.prototype.currentLocation_ = function() { - var workspace = Blockly.Workspace.getById(this.workspaceId); - var block = workspace.getBlockById(this.blockId); - var location = {}; - var parent = block.getParent(); - if (parent) { - location.parentId = parent.id; - var input = parent.getInputWithBlock(block); - if (input) { - location.inputName = input.name; - } - } else { - location.coordinate = block.getRelativeToSurfaceXY(); - } - return location; -}; - -/** - * Does this event record any change of state? - * @return {boolean} True if something changed. - */ -Blockly.Events.Move.prototype.isNull = function() { - return this.oldParentId == this.newParentId && - this.oldInputName == this.newInputName && - goog.math.Coordinate.equals(this.oldCoordinate, this.newCoordinate); -}; - -/** - * Run a move event. - * @param {boolean} forward True if run forward, false if run backward (undo). - */ -Blockly.Events.Move.prototype.run = function(forward) { - var workspace = this.getEventWorkspace_(); - var block = workspace.getBlockById(this.blockId); - if (!block) { - console.warn("Can't move non-existant block: " + this.blockId); - return; - } - var parentId = forward ? this.newParentId : this.oldParentId; - var inputName = forward ? this.newInputName : this.oldInputName; - var coordinate = forward ? this.newCoordinate : this.oldCoordinate; - var parentBlock = null; - if (parentId) { - parentBlock = workspace.getBlockById(parentId); - if (!parentBlock) { - console.warn("Can't connect to non-existant block: " + parentId); - return; - } - } - if (block.getParent()) { - block.unplug(); - } - if (coordinate) { - var xy = block.getRelativeToSurfaceXY(); - block.moveBy(coordinate.x - xy.x, coordinate.y - xy.y); - } else { - var blockConnection = block.outputConnection || block.previousConnection; - var parentConnection; - if (inputName) { - var input = parentBlock.getInput(inputName); - if (input) { - parentConnection = input.connection; - } - } else if (blockConnection.type == Blockly.PREVIOUS_STATEMENT) { - parentConnection = parentBlock.nextConnection; - } - if (parentConnection) { - blockConnection.connect(parentConnection); - } else { - console.warn("Can't connect to non-existant input: " + inputName); - } - } -}; - -/** - * Class for a UI event. - * @param {Blockly.Block} block The affected block. - * @param {string} element One of 'selected', 'comment', 'mutator', etc. - * @param {string} oldValue Previous value of element. - * @param {string} newValue New value of element. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.Ui = function(block, element, oldValue, newValue) { - Blockly.Events.Ui.superClass_.constructor.call(this, block); - this.element = element; - this.oldValue = oldValue; - this.newValue = newValue; - this.recordUndo = false; -}; -goog.inherits(Blockly.Events.Ui, Blockly.Events.Abstract); - -/** - * Type of this event. - * @type {string} - */ -Blockly.Events.Ui.prototype.type = Blockly.Events.UI; - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -Blockly.Events.Ui.prototype.toJson = function() { - var json = Blockly.Events.Ui.superClass_.toJson.call(this); - json['element'] = this.element; - if (this.newValue !== undefined) { - json['newValue'] = this.newValue; - } - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -Blockly.Events.Ui.prototype.fromJson = function(json) { - Blockly.Events.Ui.superClass_.fromJson.call(this, json); - this.element = json['element']; - this.newValue = json['newValue']; -}; - -/** - * Class for a variable creation event. - * @param {Blockly.VariableModel} variable The created variable. - * Null for a blank event. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.VarCreate = function(variable) { - if (!variable) { - return; // Blank event to be populated by fromJson. - } - Blockly.Events.VarCreate.superClass_.constructor.call(this, variable); - this.varType = variable.type; - this.varName = variable.name; -}; -goog.inherits(Blockly.Events.VarCreate, Blockly.Events.Abstract); - -/** - * Type of this event. - * @type {string} - */ -Blockly.Events.VarCreate.prototype.type = Blockly.Events.VAR_CREATE; - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -Blockly.Events.VarCreate.prototype.toJson = function() { - var json = Blockly.Events.VarCreate.superClass_.toJson.call(this); - json['varType'] = this.varType; - json['varName'] = this.varName; - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -Blockly.Events.VarCreate.prototype.fromJson = function(json) { - Blockly.Events.VarCreate.superClass_.fromJson.call(this, json); - this.varType = json['varType']; - this.varName = json['varName']; -}; - -/** - * Run a variable creation event. - * @param {boolean} forward True if run forward, false if run backward (undo). - */ -Blockly.Events.VarCreate.prototype.run = function(forward) { - var workspace = this.getEventWorkspace_(); - if (forward) { - workspace.createVariable(this.varName, this.varType, this.varId); - } else { - workspace.deleteVariableById(this.varId); - } -}; - -/** - * Class for a variable deletion event. - * @param {Blockly.VariableModel} variable The deleted variable. - * Null for a blank event. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.VarDelete = function(variable) { - if (!variable) { - return; // Blank event to be populated by fromJson. - } - Blockly.Events.VarDelete.superClass_.constructor.call(this, variable); - this.varType = variable.type; - this.varName = variable.name; -}; -goog.inherits(Blockly.Events.VarDelete, Blockly.Events.Abstract); - -/** - * Type of this event. - * @type {string} - */ -Blockly.Events.VarDelete.prototype.type = Blockly.Events.VAR_DELETE; - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -Blockly.Events.VarDelete.prototype.toJson = function() { - var json = Blockly.Events.VarDelete.superClass_.toJson.call(this); - json['varType'] = this.varType; - json['varName'] = this.varName; - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -Blockly.Events.VarDelete.prototype.fromJson = function(json) { - Blockly.Events.VarDelete.superClass_.fromJson.call(this, json); - this.varType = json['varType']; - this.varName = json['varName']; -}; - -/** - * Run a variable deletion event. - * @param {boolean} forward True if run forward, false if run backward (undo). - */ -Blockly.Events.VarDelete.prototype.run = function(forward) { - var workspace = this.getEventWorkspace_(); - if (forward) { - workspace.deleteVariableById(this.varId); - } else { - workspace.createVariable(this.varName, this.varType, this.varId); - } -}; - -/** - * Class for a variable rename event. - * @param {Blockly.VariableModel} variable The renamed variable. - * Null for a blank event. - * @param {string} newName The new name the variable will be changed to. - * @extends {Blockly.Events.Abstract} - * @constructor - */ -Blockly.Events.VarRename = function(variable, newName) { - if (!variable) { - return; // Blank event to be populated by fromJson. - } - Blockly.Events.VarRename.superClass_.constructor.call(this, variable); - this.oldName = variable.name; - this.newName = newName; -}; -goog.inherits(Blockly.Events.VarRename, Blockly.Events.Abstract); - -/** - * Type of this event. - * @type {string} - */ -Blockly.Events.VarRename.prototype.type = Blockly.Events.VAR_RENAME; - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -Blockly.Events.VarRename.prototype.toJson = function() { - var json = Blockly.Events.VarRename.superClass_.toJson.call(this); - json['oldName'] = this.oldName; - json['newName'] = this.newName; - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -Blockly.Events.VarRename.prototype.fromJson = function(json) { - Blockly.Events.VarRename.superClass_.fromJson.call(this, json); - this.oldName = json['oldName']; - this.newName = json['newName']; -}; - -/** - * Run a variable rename event. - * @param {boolean} forward True if run forward, false if run backward (undo). - */ -Blockly.Events.VarRename.prototype.run = function(forward) { - var workspace = this.getEventWorkspace_(); - if (forward) { - workspace.renameVariableById(this.varId, this.newName); - } else { - workspace.renameVariableById(this.varId, this.oldName); - } -}; - -/** - * Enable/disable a block depending on whether it is properly connected. - * Use this on applications where all blocks should be connected to a top block. - * Recommend setting the 'disable' option to 'false' in the config so that - * users don't try to reenable disabled orphan blocks. - * @param {!Blockly.Events.Abstract} event Custom data for event. - */ -Blockly.Events.disableOrphans = function(event) { - if (event.type == Blockly.Events.MOVE || - event.type == Blockly.Events.CREATE) { - Blockly.Events.disable(); - var workspace = Blockly.Workspace.getById(event.workspaceId); - var block = workspace.getBlockById(event.blockId); - if (block) { - if (block.getParent() && !block.getParent().disabled) { - var children = block.getDescendants(); - for (var i = 0, child; child = children[i]; i++) { - child.setDisabled(false); - } - } else if ((block.outputConnection || block.previousConnection) && - !workspace.isDragging()) { - do { - block.setDisabled(true); - block = block.getNextBlock(); - } while (block); - } - } - Blockly.Events.enable(); - } -}; diff --git a/core/events/events.ts b/core/events/events.ts new file mode 100644 index 00000000000..86899565381 --- /dev/null +++ b/core/events/events.ts @@ -0,0 +1,108 @@ +/** + * @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.Events + +import {EventType} from './type.js'; + +// Events. +export {Abstract, AbstractEventJson} from './events_abstract.js'; +export {BlockBase, BlockBaseJson} from './events_block_base.js'; +export {BlockChange, BlockChangeJson} from './events_block_change.js'; +export {BlockCreate, BlockCreateJson} from './events_block_create.js'; +export {BlockDelete, BlockDeleteJson} from './events_block_delete.js'; +export {BlockDrag, BlockDragJson} from './events_block_drag.js'; +export { + BlockFieldIntermediateChange, + BlockFieldIntermediateChangeJson, +} from './events_block_field_intermediate_change.js'; +export {BlockMove, BlockMoveJson} from './events_block_move.js'; +export {BubbleOpen, BubbleOpenJson, BubbleType} from './events_bubble_open.js'; +export {Click, ClickJson, ClickTarget} from './events_click.js'; +export {CommentBase, CommentBaseJson} from './events_comment_base.js'; +export {CommentChange, CommentChangeJson} from './events_comment_change.js'; +export { + CommentCollapse, + CommentCollapseJson, +} from './events_comment_collapse.js'; +export {CommentCreate, CommentCreateJson} from './events_comment_create.js'; +export {CommentDelete} from './events_comment_delete.js'; +export {CommentDrag, CommentDragJson} from './events_comment_drag.js'; +export {CommentMove, CommentMoveJson} from './events_comment_move.js'; +export {CommentResize, CommentResizeJson} from './events_comment_resize.js'; +export {MarkerMove, MarkerMoveJson} from './events_marker_move.js'; +export {Selected, SelectedJson} from './events_selected.js'; +export {ThemeChange, ThemeChangeJson} from './events_theme_change.js'; +export { + ToolboxItemSelect, + ToolboxItemSelectJson, +} from './events_toolbox_item_select.js'; +export {TrashcanOpen, TrashcanOpenJson} from './events_trashcan_open.js'; +export {UiBase} from './events_ui_base.js'; +export {VarBase, VarBaseJson} from './events_var_base.js'; +export {VarCreate, VarCreateJson} from './events_var_create.js'; +export {VarDelete, VarDeleteJson} from './events_var_delete.js'; +export {VarRename, VarRenameJson} from './events_var_rename.js'; +export {ViewportChange, ViewportChangeJson} from './events_viewport.js'; +export {FinishedLoading} from './workspace_events.js'; + +export type {BumpEvent} from './utils.js'; + +// Event types. +export const BLOCK_CHANGE = EventType.BLOCK_CHANGE; +export const BLOCK_CREATE = EventType.BLOCK_CREATE; +export const BLOCK_DELETE = EventType.BLOCK_DELETE; +export const BLOCK_DRAG = EventType.BLOCK_DRAG; +export const BLOCK_MOVE = EventType.BLOCK_MOVE; +export const BLOCK_FIELD_INTERMEDIATE_CHANGE = + EventType.BLOCK_FIELD_INTERMEDIATE_CHANGE; +export const BUBBLE_OPEN = EventType.BUBBLE_OPEN; +/** @deprecated Use BLOCK_CHANGE instead */ +export const CHANGE = EventType.BLOCK_CHANGE; +export const CLICK = EventType.CLICK; +export const COMMENT_CHANGE = EventType.COMMENT_CHANGE; +export const COMMENT_CREATE = EventType.COMMENT_CREATE; +export const COMMENT_DELETE = EventType.COMMENT_DELETE; +export const COMMENT_MOVE = EventType.COMMENT_MOVE; +export const COMMENT_RESIZE = EventType.COMMENT_RESIZE; +export const COMMENT_DRAG = EventType.COMMENT_DRAG; +/** @deprecated Use BLOCK_CREATE instead */ +export const CREATE = EventType.BLOCK_CREATE; +/** @deprecated Use BLOCK_DELETE instead */ +export const DELETE = EventType.BLOCK_DELETE; +export const FINISHED_LOADING = EventType.FINISHED_LOADING; +export const MARKER_MOVE = EventType.MARKER_MOVE; +/** @deprecated Use BLOCK_MOVE instead */ +export const MOVE = EventType.BLOCK_MOVE; +export const SELECTED = EventType.SELECTED; +export const THEME_CHANGE = EventType.THEME_CHANGE; +export const TOOLBOX_ITEM_SELECT = EventType.TOOLBOX_ITEM_SELECT; +export const TRASHCAN_OPEN = EventType.TRASHCAN_OPEN; +export const UI = EventType.UI; +export const VAR_CREATE = EventType.VAR_CREATE; +export const VAR_DELETE = EventType.VAR_DELETE; +export const VAR_RENAME = EventType.VAR_RENAME; +export const VIEWPORT_CHANGE = EventType.VIEWPORT_CHANGE; + +export {BUMP_EVENTS} from './type.js'; + +// Event utils. +export { + clearPendingUndo, + disable, + disableOrphans, + enable, + filter, + fire, + fromJson, + get, + getDescendantIds, + getGroup, + getRecordUndo, + isEnabled, + setGroup, + setRecordUndo, +} from './utils.js'; diff --git a/core/events/events_abstract.ts b/core/events/events_abstract.ts new file mode 100644 index 00000000000..e5a77dc7d6b --- /dev/null +++ b/core/events/events_abstract.ts @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Abstract class for events fired as a result of actions in + * Blockly's editor. + * + * @class + */ +// Former goog.module ID: Blockly.Events.Abstract + +import * as common from '../common.js'; +import type {Workspace} from '../workspace.js'; +import {getGroup, getRecordUndo} from './utils.js'; + +/** + * Abstract class for an event. + */ +export abstract class Abstract { + /** + * Whether or not the event was constructed without necessary parameters + * (to be populated by fromJson). + */ + abstract isBlank: boolean; + + /** The workspace identifier for this event. */ + workspaceId?: string = undefined; + + /** + * An ID for the group of events this block is associated with. + * + * Groups define events that should be treated as an single action from the + * user's perspective, and should be undone together. + */ + group: string; + + /** Whether this event is undoable or not. */ + recordUndo: boolean; + + /** Whether or not the event is a UI event. */ + isUiEvent = false; + + /** Type of this event. */ + type = ''; + + constructor() { + this.group = getGroup(); + this.recordUndo = getRecordUndo(); + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + toJson(): AbstractEventJson { + return { + 'type': this.type, + 'group': this.group, + }; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of Abstract (like all events), but we can't specify that due to the + * fact that parameters to static methods in subclasses must be + * supertypes of parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: AbstractEventJson, + workspace: Workspace, + event: any, + ): Abstract { + event.isBlank = false; + event.group = json['group'] || ''; + event.workspaceId = workspace.id; + return event; + } + + /** + * Does this event record any change of state? + * + * @returns True if null, false if something changed. + */ + isNull(): boolean { + return false; + } + + /** + * Run an event. + * + * @param _forward True if run forward, false if run backward (undo). + */ + run(_forward: boolean) { + // Defined by subclasses. Cannot be abstract b/c UI events do /not/ define + // this. + } + + /** + * Get workspace the event belongs to. + * + * @returns The workspace the event belongs to. + * @throws {Error} if workspace is null. + */ + getEventWorkspace_(): Workspace { + let workspace; + if (this.workspaceId) { + workspace = common.getWorkspaceById(this.workspaceId); + } + if (!workspace) { + throw Error( + 'Workspace is null. Event must have been generated from real' + + ' Blockly events.', + ); + } + return workspace; + } +} + +export interface AbstractEventJson { + type: string; + group: string; +} diff --git a/core/events/events_block_base.ts b/core/events/events_block_base.ts new file mode 100644 index 00000000000..d15b8e439ed --- /dev/null +++ b/core/events/events_block_base.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Base class for all types of block events. + * + * @class + */ +// Former goog.module ID: Blockly.Events.BlockBase + +import type {Block} from '../block.js'; +import type {Workspace} from '../workspace.js'; +import { + Abstract as AbstractEvent, + AbstractEventJson, +} from './events_abstract.js'; + +/** + * Abstract class for any event related to blocks. + */ +export class BlockBase extends AbstractEvent { + override isBlank = true; + + /** The ID of the block associated with this event. */ + blockId?: string; + + /** + * @param opt_block The block this event corresponds to. + * Undefined for a blank event. + */ + constructor(opt_block?: Block) { + super(); + this.isBlank = !opt_block; + + if (!opt_block) return; + + this.blockId = opt_block.id; + this.workspaceId = opt_block.workspace.id; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): BlockBaseJson { + const json = super.toJson() as BlockBaseJson; + if (!this.blockId) { + throw new Error( + 'The block ID is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + json['blockId'] = this.blockId; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of BlockBase, but we can't specify that due to the fact that parameters + * to static methods in subclasses must be supertypes of parameters to + * static methods in superclasses. + * @internal + */ + static fromJson( + json: BlockBaseJson, + workspace: Workspace, + event?: any, + ): BlockBase { + const newEvent = super.fromJson( + json, + workspace, + event ?? new BlockBase(), + ) as BlockBase; + newEvent.blockId = json['blockId']; + return newEvent; + } +} + +export interface BlockBaseJson extends AbstractEventJson { + blockId: string; +} diff --git a/core/events/events_block_change.ts b/core/events/events_block_change.ts new file mode 100644 index 00000000000..e71eabb1747 --- /dev/null +++ b/core/events/events_block_change.ts @@ -0,0 +1,259 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for a block change event. + * + * @class + */ +// Former goog.module ID: Blockly.Events.BlockChange + +import type {Block} from '../block.js'; +import type {BlockSvg} from '../block_svg.js'; +import {MANUALLY_DISABLED} from '../constants.js'; +import {IconType} from '../icons/icon_types.js'; +import {hasBubble} from '../interfaces/i_has_bubble.js'; +import * as registry from '../registry.js'; +import * as utilsXml from '../utils/xml.js'; +import {Workspace} from '../workspace.js'; +import * as Xml from '../xml.js'; +import {BlockBase, BlockBaseJson} from './events_block_base.js'; +import {EventType} from './type.js'; +import * as eventUtils from './utils.js'; + +/** + * Notifies listeners when some element of a block has changed (e.g. + * field values, comments, etc). + */ +export class BlockChange extends BlockBase { + override type = EventType.BLOCK_CHANGE; + /** + * The element that changed; one of 'field', 'comment', 'collapsed', + * 'disabled', 'inline', or 'mutation' + */ + element?: string; + + /** The name of the field that changed, if this is a change to a field. */ + name?: string; + + /** The original value of the element. */ + oldValue: unknown; + + /** The new value of the element. */ + newValue: unknown; + + /** + * If element is 'disabled', this is the language-neutral identifier of the + * reason why the block was or was not disabled. + */ + private disabledReason?: string; + + /** + * @param opt_block The changed block. Undefined for a blank event. + * @param opt_element One of 'field', 'comment', 'disabled', etc. + * @param opt_name Name of input or field affected, or null. + * @param opt_oldValue Previous value of element. + * @param opt_newValue New value of element. + */ + constructor( + opt_block?: Block, + opt_element?: string, + opt_name?: string | null, + opt_oldValue?: unknown, + opt_newValue?: unknown, + ) { + super(opt_block); + + if (!opt_block) { + return; // Blank event to be populated by fromJson. + } + this.element = opt_element; + this.name = opt_name || undefined; + this.oldValue = opt_oldValue; + this.newValue = opt_newValue; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): BlockChangeJson { + const json = super.toJson() as BlockChangeJson; + if (!this.element) { + throw new Error( + 'The changed element is undefined. Either pass an ' + + 'element to the constructor, or call fromJson', + ); + } + json['element'] = this.element; + json['name'] = this.name; + json['oldValue'] = this.oldValue; + json['newValue'] = this.newValue; + if (this.disabledReason) { + json['disabledReason'] = this.disabledReason; + } + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of BlockChange, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: BlockChangeJson, + workspace: Workspace, + event?: any, + ): BlockChange { + const newEvent = super.fromJson( + json, + workspace, + event ?? new BlockChange(), + ) as BlockChange; + newEvent.element = json['element']; + newEvent.name = json['name']; + newEvent.oldValue = json['oldValue']; + newEvent.newValue = json['newValue']; + if (json['disabledReason'] !== undefined) { + newEvent.disabledReason = json['disabledReason']; + } + return newEvent; + } + + /** + * Set the language-neutral identifier for the reason why the block was or was + * not disabled. This is only valid for events where element is 'disabled'. + * Defaults to 'MANUALLY_DISABLED'. + * + * @param disabledReason The identifier of the reason why the block was or was + * not disabled. + */ + setDisabledReason(disabledReason: string) { + if (this.element !== 'disabled') { + throw new Error( + 'Cannot set the disabled reason for a BlockChange event if the ' + + 'element is not "disabled".', + ); + } + this.disabledReason = disabledReason; + } + + /** + * Does this event record any change of state? + * + * @returns False if something changed. + */ + override isNull(): boolean { + return this.oldValue === this.newValue; + } + + /** + * Run a change event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.blockId) { + throw new Error( + 'The block ID is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + const block = workspace.getBlockById(this.blockId); + if (!block) { + throw new Error( + 'The associated block is undefined. Either pass a ' + + 'block to the constructor, or call fromJson', + ); + } + // Assume the block is rendered so that then we can check. + const icon = block.getIcon(IconType.MUTATOR); + if (icon && hasBubble(icon) && icon.bubbleIsVisible()) { + // Close the mutator (if open) since we don't want to update it. + icon.setBubbleVisible(false); + } + const value = forward ? this.newValue : this.oldValue; + switch (this.element) { + case 'field': { + const field = block.getField(this.name!); + if (field) { + field.setValue(value); + } else { + console.warn("Can't set non-existent field: " + this.name); + } + break; + } + case 'comment': + block.setCommentText((value as string) || null); + break; + case 'collapsed': + block.setCollapsed(!!value); + break; + case 'disabled': + block.setDisabledReason( + !!value, + this.disabledReason ?? MANUALLY_DISABLED, + ); + break; + case 'inline': + block.setInputsInline(!!value); + break; + case 'mutation': { + const oldState = BlockChange.getExtraBlockState_(block as BlockSvg); + if (block.loadExtraState) { + block.loadExtraState(JSON.parse((value as string) || '{}')); + } else if (block.domToMutation) { + block.domToMutation( + utilsXml.textToDom((value as string) || ''), + ); + } + eventUtils.fire( + new BlockChange(block, 'mutation', null, oldState, value), + ); + break; + } + default: + console.warn('Unknown change type: ' + this.element); + } + } + + // TODO (#5397): Encapsulate this in the BlocklyMutationChange event when + // refactoring change events. + /** + * Returns the extra state of the given block (either as XML or a JSO, + * depending on the block's definition). + * + * @param block The block to get the extra state of. + * @returns A stringified version of the extra state of the given block. + * @internal + */ + static getExtraBlockState_(block: BlockSvg): string { + if (block.saveExtraState) { + const state = block.saveExtraState(true); + return state ? JSON.stringify(state) : ''; + } else if (block.mutationToDom) { + const state = block.mutationToDom(); + return state ? Xml.domToText(state) : ''; + } + return ''; + } +} + +export interface BlockChangeJson extends BlockBaseJson { + element: string; + name?: string; + newValue: unknown; + oldValue: unknown; + disabledReason?: string; +} + +registry.register(registry.Type.EVENT, EventType.BLOCK_CHANGE, BlockChange); diff --git a/core/events/events_block_create.ts b/core/events/events_block_create.ts new file mode 100644 index 00000000000..ca697945488 --- /dev/null +++ b/core/events/events_block_create.ts @@ -0,0 +1,185 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for a block creation event. + * + * @class + */ +// Former goog.module ID: Blockly.Events.BlockCreate + +import type {Block} from '../block.js'; +import * as registry from '../registry.js'; +import * as blocks from '../serialization/blocks.js'; +import * as utilsXml from '../utils/xml.js'; +import {Workspace} from '../workspace.js'; +import * as Xml from '../xml.js'; +import {BlockBase, BlockBaseJson} from './events_block_base.js'; +import {EventType} from './type.js'; +import * as eventUtils from './utils.js'; + +/** + * Notifies listeners when a block (or connected stack of blocks) is + * created. + */ +export class BlockCreate extends BlockBase { + override type = EventType.BLOCK_CREATE; + + /** The XML representation of the created block(s). */ + xml?: Element | DocumentFragment; + + /** The JSON respresentation of the created block(s). */ + json?: blocks.State; + + /** All of the IDs of created blocks. */ + ids?: string[]; + + /** @param opt_block The created block. Undefined for a blank event. */ + constructor(opt_block?: Block) { + super(opt_block); + + if (!opt_block) { + return; // Blank event to be populated by fromJson. + } + + if (opt_block.isShadow()) { + // Moving shadow blocks is handled via disconnection. + this.recordUndo = false; + } + + this.xml = Xml.blockToDomWithXY(opt_block); + this.ids = eventUtils.getDescendantIds(opt_block); + + this.json = blocks.save(opt_block, {addCoordinates: true}) as blocks.State; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): BlockCreateJson { + const json = super.toJson() as BlockCreateJson; + if (!this.xml) { + throw new Error( + 'The block XML is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.ids) { + throw new Error( + 'The block IDs are undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.json) { + throw new Error( + 'The block JSON is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + json['xml'] = Xml.domToText(this.xml); + json['ids'] = this.ids; + json['json'] = this.json; + if (!this.recordUndo) { + json['recordUndo'] = this.recordUndo; + } + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of BlockCreate, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: BlockCreateJson, + workspace: Workspace, + event?: any, + ): BlockCreate { + const newEvent = super.fromJson( + json, + workspace, + event ?? new BlockCreate(), + ) as BlockCreate; + newEvent.xml = utilsXml.textToDom(json['xml']); + newEvent.ids = json['ids']; + newEvent.json = json['json'] as blocks.State; + if (json['recordUndo'] !== undefined) { + newEvent.recordUndo = json['recordUndo']; + } + return newEvent; + } + + /** + * Run a creation event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.json) { + throw new Error( + 'The block JSON is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.ids) { + throw new Error( + 'The block IDs are undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + if (allShadowBlocks(workspace, this.ids)) return; + if (forward) { + blocks.append(this.json, workspace); + } else { + for (let i = 0; i < this.ids.length; i++) { + const id = this.ids[i]; + const block = workspace.getBlockById(id); + if (block) { + block.dispose(false); + } else if (id === this.blockId) { + // Only complain about root-level block. + console.warn("Can't uncreate non-existent block: " + id); + } + } + } + } +} +/** + * Returns true if all blocks in the list are shadow blocks. If so, that means + * the top-level block being created is a shadow block. This only happens when a + * block that was covering up a shadow block is removed. We don't need to create + * an additional block in that case because the original block still has its + * shadow block. + * + * @param workspace Workspace to check for blocks + * @param ids A list of block ids that were created in this event + * @returns True if all block ids in the list are shadow blocks + */ +const allShadowBlocks = function ( + workspace: Workspace, + ids: string[], +): boolean { + const shadows = ids + .map((id) => workspace.getBlockById(id)) + .filter((block) => block && block.isShadow()); + return shadows.length === ids.length; +}; + +export interface BlockCreateJson extends BlockBaseJson { + xml: string; + ids: string[]; + json: object; + recordUndo?: boolean; +} + +registry.register(registry.Type.EVENT, EventType.BLOCK_CREATE, BlockCreate); diff --git a/core/events/events_block_delete.ts b/core/events/events_block_delete.ts new file mode 100644 index 00000000000..5dd23160642 --- /dev/null +++ b/core/events/events_block_delete.ts @@ -0,0 +1,182 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for a block delete event. + * + * @class + */ +// Former goog.module ID: Blockly.Events.BlockDelete + +import type {Block} from '../block.js'; +import * as registry from '../registry.js'; +import * as blocks from '../serialization/blocks.js'; +import * as utilsXml from '../utils/xml.js'; +import {Workspace} from '../workspace.js'; +import * as Xml from '../xml.js'; +import {BlockBase, BlockBaseJson} from './events_block_base.js'; +import {EventType} from './type.js'; +import * as eventUtils from './utils.js'; + +/** + * Notifies listeners when a block (or connected stack of blocks) is + * deleted. + */ +export class BlockDelete extends BlockBase { + /** The XML representation of the deleted block(s). */ + oldXml?: Element | DocumentFragment; + + /** The JSON respresentation of the deleted block(s). */ + oldJson?: blocks.State; + + /** All of the IDs of deleted blocks. */ + ids?: string[]; + + /** True if the deleted block was a shadow block, false otherwise. */ + wasShadow?: boolean; + + override type = EventType.BLOCK_DELETE; + + /** @param opt_block The deleted block. Undefined for a blank event. */ + constructor(opt_block?: Block) { + super(opt_block); + + if (!opt_block) { + return; // Blank event to be populated by fromJson. + } + + if (opt_block.getParent()) { + throw Error('Connected blocks cannot be deleted.'); + } + if (opt_block.isShadow()) { + // Respawning shadow blocks is handled via disconnection. + this.recordUndo = false; + } + + this.oldXml = Xml.blockToDomWithXY(opt_block); + this.ids = eventUtils.getDescendantIds(opt_block); + this.wasShadow = opt_block.isShadow(); + this.oldJson = blocks.save(opt_block, { + addCoordinates: true, + }) as blocks.State; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): BlockDeleteJson { + const json = super.toJson() as BlockDeleteJson; + if (!this.oldXml) { + throw new Error( + 'The old block XML is undefined. Either pass a block ' + + 'to the constructor, or call fromJson', + ); + } + if (!this.ids) { + throw new Error( + 'The block IDs are undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + if (this.wasShadow === undefined) { + throw new Error( + 'Whether the block was a shadow is undefined. Either ' + + 'pass a block to the constructor, or call fromJson', + ); + } + if (!this.oldJson) { + throw new Error( + 'The old block JSON is undefined. Either pass a block ' + + 'to the constructor, or call fromJson', + ); + } + json['oldXml'] = Xml.domToText(this.oldXml); + json['ids'] = this.ids; + json['wasShadow'] = this.wasShadow; + json['oldJson'] = this.oldJson; + if (!this.recordUndo) { + json['recordUndo'] = this.recordUndo; + } + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of BlockDelete, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: BlockDeleteJson, + workspace: Workspace, + event?: any, + ): BlockDelete { + const newEvent = super.fromJson( + json, + workspace, + event ?? new BlockDelete(), + ) as BlockDelete; + newEvent.oldXml = utilsXml.textToDom(json['oldXml']); + newEvent.ids = json['ids']; + newEvent.wasShadow = + json['wasShadow'] || newEvent.oldXml.tagName.toLowerCase() === 'shadow'; + newEvent.oldJson = json['oldJson']; + if (json['recordUndo'] !== undefined) { + newEvent.recordUndo = json['recordUndo']; + } + return newEvent; + } + + /** + * Run a deletion event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.ids) { + throw new Error( + 'The block IDs are undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.oldJson) { + throw new Error( + 'The old block JSON is undefined. Either pass a block ' + + 'to the constructor, or call fromJson', + ); + } + if (forward) { + for (let i = 0; i < this.ids.length; i++) { + const id = this.ids[i]; + const block = workspace.getBlockById(id); + if (block) { + block.dispose(false); + } else if (id === this.blockId) { + // Only complain about root-level block. + console.warn("Can't delete non-existent block: " + id); + } + } + } else { + blocks.append(this.oldJson, workspace); + } + } +} + +export interface BlockDeleteJson extends BlockBaseJson { + oldXml: string; + ids: string[]; + wasShadow: boolean; + oldJson: blocks.State; + recordUndo?: boolean; +} + +registry.register(registry.Type.EVENT, EventType.BLOCK_DELETE, BlockDelete); diff --git a/core/events/events_block_drag.ts b/core/events/events_block_drag.ts new file mode 100644 index 00000000000..4a91c4d112d --- /dev/null +++ b/core/events/events_block_drag.ts @@ -0,0 +1,116 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Events fired as a block drag. + * + * @class + */ +// Former goog.module ID: Blockly.Events.BlockDrag + +import type {Block} from '../block.js'; +import * as registry from '../registry.js'; +import {Workspace} from '../workspace.js'; +import {AbstractEventJson} from './events_abstract.js'; +import {UiBase} from './events_ui_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners when a block is being manually dragged/dropped. + */ +export class BlockDrag extends UiBase { + /** The ID of the top-level block being dragged. */ + blockId?: string; + + /** True if this is the start of a drag, false if this is the end of one. */ + isStart?: boolean; + + /** + * A list of all of the blocks (i.e. all descendants of the block associated + * with the block ID) being dragged. + */ + blocks?: Block[]; + + override type = EventType.BLOCK_DRAG; + + /** + * @param opt_block The top block in the stack that is being dragged. + * Undefined for a blank event. + * @param opt_isStart Whether this is the start of a block drag. + * Undefined for a blank event. + * @param opt_blocks The blocks affected by this drag. Undefined for a blank + * event. + */ + constructor(opt_block?: Block, opt_isStart?: boolean, opt_blocks?: Block[]) { + const workspaceId = opt_block ? opt_block.workspace.id : undefined; + super(workspaceId); + if (!opt_block) return; + + this.blockId = opt_block.id; + this.isStart = opt_isStart; + this.blocks = opt_blocks; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): BlockDragJson { + const json = super.toJson() as BlockDragJson; + if (this.isStart === undefined) { + throw new Error( + 'Whether this event is the start of a drag is undefined. ' + + 'Either pass the value to the constructor, or call fromJson', + ); + } + if (this.blockId === undefined) { + throw new Error( + 'The block ID is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + json['isStart'] = this.isStart; + json['blockId'] = this.blockId; + // TODO: I don't think we should actually apply the blocks array to the JSON + // object b/c they have functions and aren't actually serializable. + json['blocks'] = this.blocks; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of BlockDrag, but we can't specify that due to the fact that parameters + * to static methods in subclasses must be supertypes of parameters to + * static methods in superclasses.. + * @internal + */ + static fromJson( + json: BlockDragJson, + workspace: Workspace, + event?: any, + ): BlockDrag { + const newEvent = super.fromJson( + json, + workspace, + event ?? new BlockDrag(), + ) as BlockDrag; + newEvent.isStart = json['isStart']; + newEvent.blockId = json['blockId']; + newEvent.blocks = json['blocks']; + return newEvent; + } +} + +export interface BlockDragJson extends AbstractEventJson { + isStart: boolean; + blockId: string; + blocks?: Block[]; +} + +registry.register(registry.Type.EVENT, EventType.BLOCK_DRAG, BlockDrag); diff --git a/core/events/events_block_field_intermediate_change.ts b/core/events/events_block_field_intermediate_change.ts new file mode 100644 index 00000000000..49280cf2b4a --- /dev/null +++ b/core/events/events_block_field_intermediate_change.ts @@ -0,0 +1,166 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for an event representing an intermediate change to a block's field's + * value. + * + * @class + */ +// Former goog.module ID: Blockly.Events.BlockFieldIntermediateChange + +import type {Block} from '../block.js'; +import * as registry from '../registry.js'; +import {Workspace} from '../workspace.js'; +import {BlockBase, BlockBaseJson} from './events_block_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners when the value of a block's field has changed but the + * change is not yet complete, and is expected to be followed by a block change + * event. + */ +export class BlockFieldIntermediateChange extends BlockBase { + override type = EventType.BLOCK_FIELD_INTERMEDIATE_CHANGE; + + // Intermediate events do not undo or redo. They may be fired frequently while + // the field editor widget is open. A separate BLOCK_CHANGE event is fired + // when the editor is closed, which combines all of the field value changes + // into a single change that is recorded in the undo history instead. The + // intermediate changes are important for reacting to immediate changes, but + // some event handlers would prefer to handle the less frequent final events, + // like when triggering workspace serialization. Technically, this method of + // grouping changes can result in undo perfoming actions out of order if some + // other event occurs between opening and closing the field editor, but such + // events are unlikely to cause a broken state. + override recordUndo = false; + + /** The name of the field that changed. */ + name?: string; + + /** The original value of the element. */ + oldValue: unknown; + + /** The new value of the element. */ + newValue: unknown; + + /** + * @param opt_block The changed block. Undefined for a blank event. + * @param opt_name Name of the field affected. + * @param opt_oldValue Previous value of element. + * @param opt_newValue New value of element. + */ + constructor( + opt_block?: Block, + opt_name?: string, + opt_oldValue?: unknown, + opt_newValue?: unknown, + ) { + super(opt_block); + if (!opt_block) { + return; // Blank event to be populated by fromJson. + } + + this.name = opt_name; + this.oldValue = opt_oldValue; + this.newValue = opt_newValue; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): BlockFieldIntermediateChangeJson { + const json = super.toJson() as BlockFieldIntermediateChangeJson; + if (!this.name) { + throw new Error( + 'The changed field name is undefined. Either pass a ' + + 'name to the constructor, or call fromJson.', + ); + } + json['name'] = this.name; + json['oldValue'] = this.oldValue; + json['newValue'] = this.newValue; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of BlockFieldIntermediateChange, but we can't specify that due to the + * fact that parameters to static methods in subclasses must be supertypes + * of parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: BlockFieldIntermediateChangeJson, + workspace: Workspace, + event?: any, + ): BlockFieldIntermediateChange { + const newEvent = super.fromJson( + json, + workspace, + event ?? new BlockFieldIntermediateChange(), + ) as BlockFieldIntermediateChange; + newEvent.name = json['name']; + newEvent.oldValue = json['oldValue']; + newEvent.newValue = json['newValue']; + return newEvent; + } + + /** + * Does this event record any change of state? + * + * @returns False if something changed. + */ + override isNull(): boolean { + return this.oldValue === this.newValue; + } + + /** + * Run a change event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.blockId) { + throw new Error( + 'The block ID is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + const block = workspace.getBlockById(this.blockId); + if (!block) { + throw new Error( + 'The associated block is undefined. Either pass a ' + + 'block to the constructor, or call fromJson', + ); + } + + const value = forward ? this.newValue : this.oldValue; + const field = block.getField(this.name!); + if (field) { + field.setValue(value); + } else { + console.warn("Can't set non-existent field: " + this.name); + } + } +} + +export interface BlockFieldIntermediateChangeJson extends BlockBaseJson { + name: string; + newValue: unknown; + oldValue: unknown; +} + +registry.register( + registry.Type.EVENT, + EventType.BLOCK_FIELD_INTERMEDIATE_CHANGE, + BlockFieldIntermediateChange, +); diff --git a/core/events/events_block_move.ts b/core/events/events_block_move.ts new file mode 100644 index 00000000000..99e1622896e --- /dev/null +++ b/core/events/events_block_move.ts @@ -0,0 +1,306 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for a block move event. + * + * @class + */ +// Former goog.module ID: Blockly.Events.BlockMove + +import type {Block} from '../block.js'; +import {ConnectionType} from '../connection_type.js'; +import * as registry from '../registry.js'; +import {Coordinate} from '../utils/coordinate.js'; +import type {Workspace} from '../workspace.js'; +import {BlockBase, BlockBaseJson} from './events_block_base.js'; +import {EventType} from './type.js'; + +interface BlockLocation { + parentId?: string; + inputName?: string; + coordinate?: Coordinate; +} + +/** + * Notifies listeners when a block is moved. This could be from one + * connection to another, or from one location on the workspace to another. + */ +export class BlockMove extends BlockBase { + override type = EventType.BLOCK_MOVE; + + /** The ID of the old parent block. Undefined if it was a top-level block. */ + oldParentId?: string; + + /** + * The name of the old input. Undefined if it was a top-level block or the + * parent's next block. + */ + oldInputName?: string; + + /** + * The old X and Y workspace coordinates of the block if it was a top level + * block. Undefined if it was not a top level block. + */ + oldCoordinate?: Coordinate; + + /** The ID of the new parent block. Undefined if it is a top-level block. */ + newParentId?: string; + + /** + * The name of the new input. Undefined if it is a top-level block or the + * parent's next block. + */ + newInputName?: string; + + /** + * The new X and Y workspace coordinates of the block if it is a top-level + * block. Undefined if it is not a top level block. + */ + newCoordinate?: Coordinate; + + /** + * An explanation of what this move is for. Known values include: + * 'drag' -- A drag operation completed. + * 'bump' -- Block got bumped away from an invalid connection. + * 'snap' -- Block got shifted to line up with the grid. + * 'inbounds' -- Block got pushed back into a non-scrolling workspace. + * 'connect' -- Block got connected to another block. + * 'disconnect' -- Block got disconnected from another block. + * 'create' -- Block created via XML. + * 'cleanup' -- Workspace aligned top-level blocks. + * Event merging may create multiple reasons: ['drag', 'bump', 'snap']. + */ + reason?: string[]; + + /** @param opt_block The moved block. Undefined for a blank event. */ + constructor(opt_block?: Block) { + super(opt_block); + + if (!opt_block) { + return; + } + // Blank event to be populated by fromJson. + if (opt_block.isShadow()) { + // Moving shadow blocks is handled via disconnection. + this.recordUndo = false; + } + + const location = this.currentLocation(); + this.oldParentId = location.parentId; + this.oldInputName = location.inputName; + this.oldCoordinate = location.coordinate; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): BlockMoveJson { + const json = super.toJson() as BlockMoveJson; + json['oldParentId'] = this.oldParentId; + json['oldInputName'] = this.oldInputName; + if (this.oldCoordinate) { + json['oldCoordinate'] = + `${Math.round(this.oldCoordinate.x)}, ` + + `${Math.round(this.oldCoordinate.y)}`; + } + json['newParentId'] = this.newParentId; + json['newInputName'] = this.newInputName; + if (this.newCoordinate) { + json['newCoordinate'] = + `${Math.round(this.newCoordinate.x)}, ` + + `${Math.round(this.newCoordinate.y)}`; + } + if (this.reason) { + json['reason'] = this.reason; + } + if (!this.recordUndo) { + json['recordUndo'] = this.recordUndo; + } + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of BlockMove, but we can't specify that due to the fact that parameters + * to static methods in subclasses must be supertypes of parameters to + * static methods in superclasses. + * @internal + */ + static fromJson( + json: BlockMoveJson, + workspace: Workspace, + event?: any, + ): BlockMove { + const newEvent = super.fromJson( + json, + workspace, + event ?? new BlockMove(), + ) as BlockMove; + newEvent.oldParentId = json['oldParentId']; + newEvent.oldInputName = json['oldInputName']; + if (json['oldCoordinate']) { + const xy = json['oldCoordinate'].split(','); + newEvent.oldCoordinate = new Coordinate(Number(xy[0]), Number(xy[1])); + } + newEvent.newParentId = json['newParentId']; + newEvent.newInputName = json['newInputName']; + if (json['newCoordinate']) { + const xy = json['newCoordinate'].split(','); + newEvent.newCoordinate = new Coordinate(Number(xy[0]), Number(xy[1])); + } + if (json['reason'] !== undefined) { + newEvent.reason = json['reason']; + } + if (json['recordUndo'] !== undefined) { + newEvent.recordUndo = json['recordUndo']; + } + return newEvent; + } + + /** Record the block's new location. Called after the move. */ + recordNew() { + const location = this.currentLocation(); + this.newParentId = location.parentId; + this.newInputName = location.inputName; + this.newCoordinate = location.coordinate; + } + + /** + * Set the reason for a move event. + * + * @param reason Why is this move happening? 'drag', 'bump', 'snap', ... + */ + setReason(reason: string[]) { + this.reason = reason; + } + + /** + * Returns the parentId and input if the block is connected, + * or the XY location if disconnected. + * + * @returns Collection of location info. + */ + private currentLocation(): BlockLocation { + const workspace = this.getEventWorkspace_(); + if (!this.blockId) { + throw new Error( + 'The block ID is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + const block = workspace.getBlockById(this.blockId); + if (!block) { + throw new Error( + 'The block associated with the block move event ' + + 'could not be found', + ); + } + const location = {} as BlockLocation; + const parent = block.getParent(); + if (parent) { + location.parentId = parent.id; + const input = parent.getInputWithBlock(block); + if (input) { + location.inputName = input.name; + } + } else { + location.coordinate = block.getRelativeToSurfaceXY(); + } + return location; + } + + /** + * Does this event record any change of state? + * + * @returns False if something changed. + */ + override isNull(): boolean { + return ( + this.oldParentId === this.newParentId && + this.oldInputName === this.newInputName && + Coordinate.equals(this.oldCoordinate, this.newCoordinate) + ); + } + + /** + * Run a move event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.blockId) { + throw new Error( + 'The block ID is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + const block = workspace.getBlockById(this.blockId); + if (!block) { + console.warn("Can't move non-existent block: " + this.blockId); + return; + } + const parentId = forward ? this.newParentId : this.oldParentId; + const inputName = forward ? this.newInputName : this.oldInputName; + const coordinate = forward ? this.newCoordinate : this.oldCoordinate; + let parentBlock: Block | null; + if (parentId) { + parentBlock = workspace.getBlockById(parentId); + if (!parentBlock) { + console.warn("Can't connect to non-existent block: " + parentId); + return; + } + } + if (block.getParent()) { + block.unplug(); + } + if (coordinate) { + const xy = block.getRelativeToSurfaceXY(); + block.moveBy(coordinate.x - xy.x, coordinate.y - xy.y, this.reason); + } else { + let blockConnection = block.outputConnection; + if ( + !blockConnection || + (block.previousConnection && block.previousConnection.isConnected()) + ) { + blockConnection = block.previousConnection; + } + let parentConnection; + const connectionType = blockConnection?.type; + if (inputName) { + const input = parentBlock!.getInput(inputName); + if (input) { + parentConnection = input.connection; + } + } else if (connectionType === ConnectionType.PREVIOUS_STATEMENT) { + parentConnection = parentBlock!.nextConnection; + } + if (parentConnection && blockConnection) { + blockConnection.connect(parentConnection); + } else { + console.warn("Can't connect to non-existent input: " + inputName); + } + } + } +} + +export interface BlockMoveJson extends BlockBaseJson { + oldParentId?: string; + oldInputName?: string; + oldCoordinate?: string; + newParentId?: string; + newInputName?: string; + newCoordinate?: string; + reason?: string[]; + recordUndo?: boolean; +} + +registry.register(registry.Type.EVENT, EventType.BLOCK_MOVE, BlockMove); diff --git a/core/events/events_bubble_open.ts b/core/events/events_bubble_open.ts new file mode 100644 index 00000000000..a36bbcd6a93 --- /dev/null +++ b/core/events/events_bubble_open.ts @@ -0,0 +1,121 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Events fired as a result of bubble open. + * + * @class + */ + +// Former goog.module ID: Blockly.Events.BubbleOpen + +import type {BlockSvg} from '../block_svg.js'; +import * as registry from '../registry.js'; +import type {Workspace} from '../workspace.js'; +import type {AbstractEventJson} from './events_abstract.js'; +import {UiBase} from './events_ui_base.js'; +import {EventType} from './type.js'; + +/** + * Class for a bubble open event. + */ +export class BubbleOpen extends UiBase { + /** The ID of the block the bubble is attached to. */ + blockId?: string; + + /** True if the bubble is opening, false if closing. */ + isOpen?: boolean; + + /** The type of bubble; one of 'mutator', 'comment', or 'warning'. */ + bubbleType?: BubbleType; + + override type = EventType.BUBBLE_OPEN; + + /** + * @param opt_block The associated block. Undefined for a blank event. + * @param opt_isOpen Whether the bubble is opening (false if closing). + * Undefined for a blank event. + * @param opt_bubbleType The type of bubble. One of 'mutator', 'comment' or + * 'warning'. Undefined for a blank event. + */ + constructor( + opt_block?: BlockSvg, + opt_isOpen?: boolean, + opt_bubbleType?: BubbleType, + ) { + const workspaceId = opt_block ? opt_block.workspace.id : undefined; + super(workspaceId); + if (!opt_block) return; + + this.blockId = opt_block.id; + this.isOpen = opt_isOpen; + this.bubbleType = opt_bubbleType; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): BubbleOpenJson { + const json = super.toJson() as BubbleOpenJson; + if (this.isOpen === undefined) { + throw new Error( + 'Whether this event is for opening the bubble is undefined. ' + + 'Either pass the value to the constructor, or call fromJson', + ); + } + if (!this.bubbleType) { + throw new Error( + 'The type of bubble is undefined. Either pass the ' + + 'value to the constructor, or call fromJson', + ); + } + json['isOpen'] = this.isOpen; + json['bubbleType'] = this.bubbleType; + json['blockId'] = this.blockId || ''; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of BubbleOpen, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: BubbleOpenJson, + workspace: Workspace, + event?: any, + ): BubbleOpen { + const newEvent = super.fromJson( + json, + workspace, + event ?? new BubbleOpen(), + ) as BubbleOpen; + newEvent.isOpen = json['isOpen']; + newEvent.bubbleType = json['bubbleType']; + newEvent.blockId = json['blockId']; + return newEvent; + } +} + +export enum BubbleType { + MUTATOR = 'mutator', + COMMENT = 'comment', + WARNING = 'warning', +} + +export interface BubbleOpenJson extends AbstractEventJson { + isOpen: boolean; + bubbleType: BubbleType; + blockId: string; +} + +registry.register(registry.Type.EVENT, EventType.BUBBLE_OPEN, BubbleOpen); diff --git a/core/events/events_click.ts b/core/events/events_click.ts new file mode 100644 index 00000000000..c023f20f152 --- /dev/null +++ b/core/events/events_click.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Events fired as a result of UI click in Blockly's editor. + * + * @class + */ + +// Former goog.module ID: Blockly.Events.Click + +import type {Block} from '../block.js'; +import * as registry from '../registry.js'; +import {Workspace} from '../workspace.js'; +import {AbstractEventJson} from './events_abstract.js'; +import {UiBase} from './events_ui_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that some blockly element was clicked. + */ +export class Click extends UiBase { + /** The ID of the block that was clicked, if a block was clicked. */ + blockId?: string; + + /** + * The type of element that was clicked; one of 'block', 'workspace', + * or 'zoom_controls'. + */ + targetType?: ClickTarget; + override type = EventType.CLICK; + + /** + * @param opt_block The affected block. Null for click events that do not have + * an associated block (i.e. workspace click). Undefined for a blank + * event. + * @param opt_workspaceId The workspace identifier for this event. + * Not used if block is passed. Undefined for a blank event. + * @param opt_targetType The type of element targeted by this click event. + * Undefined for a blank event. + */ + constructor( + opt_block?: Block | null, + opt_workspaceId?: string | null, + opt_targetType?: ClickTarget, + ) { + let workspaceId = opt_block ? opt_block.workspace.id : opt_workspaceId; + if (workspaceId === null) { + workspaceId = undefined; + } + super(workspaceId); + + this.blockId = opt_block ? opt_block.id : undefined; + this.targetType = opt_targetType; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): ClickJson { + const json = super.toJson() as ClickJson; + if (!this.targetType) { + throw new Error( + 'The click target type is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + json['targetType'] = this.targetType; + json['blockId'] = this.blockId; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of Click, but we can't specify that due to the fact that parameters to + * static methods in subclasses must be supertypes of parameters to + * static methods in superclasses. + * @internal + */ + static fromJson(json: ClickJson, workspace: Workspace, event?: any): Click { + const newEvent = super.fromJson( + json, + workspace, + event ?? new Click(), + ) as Click; + newEvent.targetType = json['targetType']; + newEvent.blockId = json['blockId']; + return newEvent; + } +} + +export enum ClickTarget { + BLOCK = 'block', + WORKSPACE = 'workspace', + ZOOM_CONTROLS = 'zoom_controls', +} + +export interface ClickJson extends AbstractEventJson { + targetType: ClickTarget; + blockId?: string; +} + +registry.register(registry.Type.EVENT, EventType.CLICK, Click); diff --git a/core/events/events_comment_base.ts b/core/events/events_comment_base.ts new file mode 100644 index 00000000000..e4b76c8e547 --- /dev/null +++ b/core/events/events_comment_base.ts @@ -0,0 +1,126 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Base class for comment events. + * + * @class + */ +// Former goog.module ID: Blockly.Events.CommentBase + +import type {WorkspaceComment} from '../comments/workspace_comment.js'; +import * as comments from '../serialization/workspace_comments.js'; +import type {Workspace} from '../workspace.js'; +import { + Abstract as AbstractEvent, + AbstractEventJson, +} from './events_abstract.js'; +import type {CommentCreate} from './events_comment_create.js'; +import type {CommentDelete} from './events_comment_delete.js'; +import {getGroup, getRecordUndo} from './utils.js'; + +/** + * Abstract class for a comment event. + */ +export class CommentBase extends AbstractEvent { + override isBlank = true; + + /** The ID of the comment that this event references. */ + commentId?: string; + + /** + * @param opt_comment The comment this event corresponds to. Undefined for a + * blank event. + */ + constructor(opt_comment?: WorkspaceComment) { + super(); + /** Whether or not an event is blank. */ + this.isBlank = !opt_comment; + + if (!opt_comment) return; + + this.commentId = opt_comment.id; + this.workspaceId = opt_comment.workspace.id; + this.group = getGroup(); + this.recordUndo = getRecordUndo(); + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): CommentBaseJson { + const json = super.toJson() as CommentBaseJson; + if (!this.commentId) { + throw new Error( + 'The comment ID is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + json['commentId'] = this.commentId; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of CommentBase, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: CommentBaseJson, + workspace: Workspace, + event?: any, + ): CommentBase { + const newEvent = super.fromJson( + json, + workspace, + event ?? new CommentBase(), + ) as CommentBase; + newEvent.commentId = json['commentId']; + return newEvent; + } + + /** + * Helper function for Comment[Create|Delete] + * + * @param event The event to run. + * @param create if True then Create, if False then Delete + */ + static CommentCreateDeleteHelper( + event: CommentCreate | CommentDelete, + create: boolean, + ) { + const workspace = event.getEventWorkspace_(); + if (create) { + if (!event.json) { + throw new Error('Encountered a comment event without proper json'); + } + comments.append(event.json, workspace); + } else { + if (!event.commentId) { + throw new Error( + 'The comment ID is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + const comment = workspace.getCommentById(event.commentId); + if (comment) { + comment.dispose(); + } else { + console.warn("Can't delete non-existent comment: " + event.commentId); + } + } + } +} + +export interface CommentBaseJson extends AbstractEventJson { + commentId: string; +} diff --git a/core/events/events_comment_change.ts b/core/events/events_comment_change.ts new file mode 100644 index 00000000000..4d944ea39af --- /dev/null +++ b/core/events/events_comment_change.ts @@ -0,0 +1,156 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for comment change event. + * + * @class + */ +// Former goog.module ID: Blockly.Events.CommentChange + +import type {WorkspaceComment} from '../comments/workspace_comment.js'; +import * as registry from '../registry.js'; +import type {Workspace} from '../workspace.js'; +import {CommentBase, CommentBaseJson} from './events_comment_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that the contents of a workspace comment has changed. + */ +export class CommentChange extends CommentBase { + override type = EventType.COMMENT_CHANGE; + + // TODO(#6774): We should remove underscores. + /** The previous contents of the comment. */ + oldContents_?: string; + + /** The new contents of the comment. */ + newContents_?: string; + + /** + * @param opt_comment The comment that is being changed. Undefined for a + * blank event. + * @param opt_oldContents Previous contents of the comment. + * @param opt_newContents New contents of the comment. + */ + constructor( + opt_comment?: WorkspaceComment, + opt_oldContents?: string, + opt_newContents?: string, + ) { + super(opt_comment); + + if (!opt_comment) { + return; // Blank event to be populated by fromJson. + } + + this.oldContents_ = + typeof opt_oldContents === 'undefined' ? '' : opt_oldContents; + this.newContents_ = + typeof opt_newContents === 'undefined' ? '' : opt_newContents; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): CommentChangeJson { + const json = super.toJson() as CommentChangeJson; + if (!this.oldContents_) { + throw new Error( + 'The old contents is undefined. Either pass a value to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.newContents_) { + throw new Error( + 'The new contents is undefined. Either pass a value to ' + + 'the constructor, or call fromJson', + ); + } + json['oldContents'] = this.oldContents_; + json['newContents'] = this.newContents_; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of CommentChange, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: CommentChangeJson, + workspace: Workspace, + event?: any, + ): CommentChange { + const newEvent = super.fromJson( + json, + workspace, + event ?? new CommentChange(), + ) as CommentChange; + newEvent.oldContents_ = json['oldContents']; + newEvent.newContents_ = json['newContents']; + return newEvent; + } + + /** + * Does this event record any change of state? + * + * @returns False if something changed. + */ + override isNull(): boolean { + return this.oldContents_ === this.newContents_; + } + + /** + * Run a change event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.commentId) { + throw new Error( + 'The comment ID is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + // TODO: Remove the cast when we fix the type of getCommentById. + const comment = workspace.getCommentById( + this.commentId, + ) as unknown as WorkspaceComment; + if (!comment) { + console.warn("Can't change non-existent comment: " + this.commentId); + return; + } + const contents = forward ? this.newContents_ : this.oldContents_; + if (contents === undefined) { + if (forward) { + throw new Error( + 'The new contents is undefined. Either pass a value to ' + + 'the constructor, or call fromJson', + ); + } + throw new Error( + 'The old contents is undefined. Either pass a value to ' + + 'the constructor, or call fromJson', + ); + } + comment.setText(contents); + } +} + +export interface CommentChangeJson extends CommentBaseJson { + oldContents: string; + newContents: string; +} + +registry.register(registry.Type.EVENT, EventType.COMMENT_CHANGE, CommentChange); diff --git a/core/events/events_comment_collapse.ts b/core/events/events_comment_collapse.ts new file mode 100644 index 00000000000..0f718a040bf --- /dev/null +++ b/core/events/events_comment_collapse.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {WorkspaceComment} from '../comments/workspace_comment.js'; +import * as registry from '../registry.js'; +import type {Workspace} from '../workspace.js'; +import {CommentBase, CommentBaseJson} from './events_comment_base.js'; +import {EventType} from './type.js'; + +export class CommentCollapse extends CommentBase { + override type = EventType.COMMENT_COLLAPSE; + + constructor( + comment?: WorkspaceComment, + public newCollapsed?: boolean, + ) { + super(comment); + + if (!comment) { + return; // Blank event to be populated by fromJson. + } + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): CommentCollapseJson { + const json = super.toJson() as CommentCollapseJson; + if (this.newCollapsed === undefined) { + throw new Error( + 'The new collapse value undefined. Either call recordNew, or ' + + 'call fromJson', + ); + } + json['newCollapsed'] = this.newCollapsed; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of CommentCollapse, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: CommentCollapseJson, + workspace: Workspace, + event?: any, + ): CommentCollapse { + const newEvent = super.fromJson( + json, + workspace, + event ?? new CommentCollapse(), + ) as CommentCollapse; + newEvent.newCollapsed = json.newCollapsed; + return newEvent; + } + + /** + * Run a collapse event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.commentId) { + throw new Error( + 'The comment ID is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + // TODO: Remove cast when we update getCommentById. + const comment = workspace.getCommentById( + this.commentId, + ) as unknown as WorkspaceComment; + if (!comment) { + console.warn( + "Can't collapse or uncollapse non-existent comment: " + this.commentId, + ); + return; + } + + comment.setCollapsed(forward ? !!this.newCollapsed : !this.newCollapsed); + } +} + +export interface CommentCollapseJson extends CommentBaseJson { + newCollapsed: boolean; +} + +registry.register( + registry.Type.EVENT, + EventType.COMMENT_COLLAPSE, + CommentCollapse, +); diff --git a/core/events/events_comment_create.ts b/core/events/events_comment_create.ts new file mode 100644 index 00000000000..637107e3f55 --- /dev/null +++ b/core/events/events_comment_create.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for comment creation event. + * + * @class + */ +// Former goog.module ID: Blockly.Events.CommentCreate + +import type {WorkspaceComment} from '../comments/workspace_comment.js'; +import * as registry from '../registry.js'; +import * as comments from '../serialization/workspace_comments.js'; +import * as utilsXml from '../utils/xml.js'; +import type {Workspace} from '../workspace.js'; +import * as Xml from '../xml.js'; +import {CommentBase, CommentBaseJson} from './events_comment_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that a workspace comment was created. + */ +export class CommentCreate extends CommentBase { + override type = EventType.COMMENT_CREATE; + + /** The XML representation of the created workspace comment. */ + xml?: Element | DocumentFragment; + + /** The JSON representation of the created workspace comment. */ + json?: comments.State; + + /** + * @param opt_comment The created comment. + * Undefined for a blank event. + */ + constructor(opt_comment?: WorkspaceComment) { + super(opt_comment); + + if (!opt_comment) { + return; // Blank event to be populated by fromJson. + } + + this.xml = Xml.saveWorkspaceComment(opt_comment); + this.json = comments.save(opt_comment, {addCoordinates: true}); + } + + // TODO (#1266): "Full" and "minimal" serialization. + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): CommentCreateJson { + const json = super.toJson() as CommentCreateJson; + if (!this.xml) { + throw new Error( + 'The comment XML is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.json) { + throw new Error( + 'The comment JSON is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + json['xml'] = Xml.domToText(this.xml); + json['json'] = this.json; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of CommentCreate, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: CommentCreateJson, + workspace: Workspace, + event?: any, + ): CommentCreate { + const newEvent = super.fromJson( + json, + workspace, + event ?? new CommentCreate(), + ) as CommentCreate; + newEvent.xml = utilsXml.textToDom(json['xml']); + newEvent.json = json['json']; + return newEvent; + } + + /** + * Run a creation event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + CommentBase.CommentCreateDeleteHelper(this, forward); + } +} + +export interface CommentCreateJson extends CommentBaseJson { + xml: string; + json: object; +} + +registry.register(registry.Type.EVENT, EventType.COMMENT_CREATE, CommentCreate); diff --git a/core/events/events_comment_delete.ts b/core/events/events_comment_delete.ts new file mode 100644 index 00000000000..579131e5033 --- /dev/null +++ b/core/events/events_comment_delete.ts @@ -0,0 +1,113 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for comment deletion event. + * + * @class + */ +// Former goog.module ID: Blockly.Events.CommentDelete + +import type {WorkspaceComment} from '../comments/workspace_comment.js'; +import * as registry from '../registry.js'; +import * as comments from '../serialization/workspace_comments.js'; +import * as utilsXml from '../utils/xml.js'; +import type {Workspace} from '../workspace.js'; +import * as Xml from '../xml.js'; +import {CommentBase, CommentBaseJson} from './events_comment_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that a workspace comment has been deleted. + */ +export class CommentDelete extends CommentBase { + override type = EventType.COMMENT_DELETE; + + /** The XML representation of the deleted workspace comment. */ + xml?: Element; + + /** The JSON representation of the created workspace comment. */ + json?: comments.State; + + /** + * @param opt_comment The deleted comment. + * Undefined for a blank event. + */ + constructor(opt_comment?: WorkspaceComment) { + super(opt_comment); + + if (!opt_comment) { + return; // Blank event to be populated by fromJson. + } + + this.xml = Xml.saveWorkspaceComment(opt_comment); + this.json = comments.save(opt_comment, {addCoordinates: true}); + } + + /** + * Run a creation event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + CommentBase.CommentCreateDeleteHelper(this, !forward); + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): CommentDeleteJson { + const json = super.toJson() as CommentDeleteJson; + if (!this.xml) { + throw new Error( + 'The comment XML is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.json) { + throw new Error( + 'The comment JSON is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + json['xml'] = Xml.domToText(this.xml); + json['json'] = this.json; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of CommentDelete, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: CommentDeleteJson, + workspace: Workspace, + event?: any, + ): CommentDelete { + const newEvent = super.fromJson( + json, + workspace, + event ?? new CommentDelete(), + ) as CommentDelete; + newEvent.xml = utilsXml.textToDom(json['xml']); + newEvent.json = json['json']; + return newEvent; + } +} + +export interface CommentDeleteJson extends CommentBaseJson { + xml: string; + json: object; +} + +registry.register(registry.Type.EVENT, EventType.COMMENT_DELETE, CommentDelete); diff --git a/core/events/events_comment_drag.ts b/core/events/events_comment_drag.ts new file mode 100644 index 00000000000..b25ca5b7382 --- /dev/null +++ b/core/events/events_comment_drag.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Events fired when a workspace comment is dragged. + */ + +import type {WorkspaceComment} from '../comments/workspace_comment.js'; +import * as registry from '../registry.js'; +import {Workspace} from '../workspace.js'; +import {AbstractEventJson} from './events_abstract.js'; +import {UiBase} from './events_ui_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners when a comment is being manually dragged/dropped. + */ +export class CommentDrag extends UiBase { + /** The ID of the top-level comment being dragged. */ + commentId?: string; + + /** True if this is the start of a drag, false if this is the end of one. */ + isStart?: boolean; + + override type = EventType.COMMENT_DRAG; + + /** + * @param opt_comment The comment that is being dragged. + * Undefined for a blank event. + * @param opt_isStart Whether this is the start of a comment drag. + * Undefined for a blank event. + */ + constructor(opt_comment?: WorkspaceComment, opt_isStart?: boolean) { + const workspaceId = opt_comment ? opt_comment.workspace.id : undefined; + super(workspaceId); + if (!opt_comment) return; + + this.commentId = opt_comment.id; + this.isStart = opt_isStart; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): CommentDragJson { + const json = super.toJson() as CommentDragJson; + if (this.isStart === undefined) { + throw new Error( + 'Whether this event is the start of a drag is undefined. ' + + 'Either pass the value to the constructor, or call fromJson', + ); + } + if (this.commentId === undefined) { + throw new Error( + 'The comment ID is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + json['isStart'] = this.isStart; + json['commentId'] = this.commentId; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of CommentDrag, but we can't specify that due to the fact that parameters + * to static methods in subclasses must be supertypes of parameters to + * static methods in superclasses. + * @internal + */ + static fromJson( + json: CommentDragJson, + workspace: Workspace, + event?: any, + ): CommentDrag { + const newEvent = super.fromJson( + json, + workspace, + event ?? new CommentDrag(), + ) as CommentDrag; + newEvent.isStart = json['isStart']; + newEvent.commentId = json['commentId']; + return newEvent; + } +} + +export interface CommentDragJson extends AbstractEventJson { + isStart: boolean; + commentId: string; +} + +registry.register(registry.Type.EVENT, EventType.COMMENT_DRAG, CommentDrag); diff --git a/core/events/events_comment_move.ts b/core/events/events_comment_move.ts new file mode 100644 index 00000000000..af5e336165d --- /dev/null +++ b/core/events/events_comment_move.ts @@ -0,0 +1,206 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for comment move event. + * + * @class + */ +// Former goog.module ID: Blockly.Events.CommentMove + +import type {WorkspaceComment} from '../comments/workspace_comment.js'; +import * as registry from '../registry.js'; +import {Coordinate} from '../utils/coordinate.js'; +import type {Workspace} from '../workspace.js'; +import {CommentBase, CommentBaseJson} from './events_comment_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that a workspace comment has moved. + */ +export class CommentMove extends CommentBase { + override type = EventType.COMMENT_MOVE; + + /** The comment that is being moved. */ + comment_?: WorkspaceComment; + + // TODO(#6774): We should remove underscores. + /** The location of the comment before the move, in workspace coordinates. */ + oldCoordinate_?: Coordinate; + + /** The location of the comment after the move, in workspace coordinates. */ + newCoordinate_?: Coordinate; + + /** + * An explanation of what this move is for. Known values include: + * 'drag' -- A drag operation completed. + * 'snap' -- Comment got shifted to line up with the grid. + * 'inbounds' -- Block got pushed back into a non-scrolling workspace. + * 'create' -- Block created via deserialization. + * 'cleanup' -- Workspace aligned top-level blocks. + * Event merging may create multiple reasons: ['drag', 'inbounds', 'snap']. + */ + reason?: string[]; + + /** + * @param opt_comment The comment that is being moved. Undefined for a blank + * event. + */ + constructor(opt_comment?: WorkspaceComment) { + super(opt_comment); + + if (!opt_comment) { + return; // Blank event to be populated by fromJson. + } + + this.comment_ = opt_comment; + this.oldCoordinate_ = opt_comment.getRelativeToSurfaceXY(); + } + + /** + * Record the comment's new location. Called after the move. Can only be + * called once. + */ + recordNew() { + if (this.newCoordinate_) { + throw Error( + 'Tried to record the new position of a comment on the ' + + 'same event twice.', + ); + } + if (!this.comment_) { + throw new Error( + 'The comment is undefined. Pass a comment to ' + + 'the constructor if you want to use the record functionality', + ); + } + this.newCoordinate_ = this.comment_.getRelativeToSurfaceXY(); + } + + /** + * Sets the reason for a move event. + * + * @param reason Why is this move happening? 'drag', 'bump', 'snap', ... + */ + setReason(reason: string[]) { + this.reason = reason; + } + + /** + * Override the location before the move. Use this if you don't create the + * event until the end of the move, but you know the original location. + * + * @param xy The location before the move, in workspace coordinates. + */ + setOldCoordinate(xy: Coordinate) { + this.oldCoordinate_ = xy; + } + + // TODO (#1266): "Full" and "minimal" serialization. + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): CommentMoveJson { + const json = super.toJson() as CommentMoveJson; + if (!this.oldCoordinate_) { + throw new Error( + 'The old comment position is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.newCoordinate_) { + throw new Error( + 'The new comment position is undefined. Either call recordNew, or ' + + 'call fromJson', + ); + } + json['oldCoordinate'] = + `${Math.round(this.oldCoordinate_.x)}, ` + + `${Math.round(this.oldCoordinate_.y)}`; + json['newCoordinate'] = + Math.round(this.newCoordinate_.x) + + ',' + + Math.round(this.newCoordinate_.y); + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of CommentMove, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: CommentMoveJson, + workspace: Workspace, + event?: any, + ): CommentMove { + const newEvent = super.fromJson( + json, + workspace, + event ?? new CommentMove(), + ) as CommentMove; + let xy = json['oldCoordinate'].split(','); + newEvent.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1])); + xy = json['newCoordinate'].split(','); + newEvent.newCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1])); + return newEvent; + } + + /** + * Does this event record any change of state? + * + * @returns False if something changed. + */ + override isNull(): boolean { + return Coordinate.equals(this.oldCoordinate_, this.newCoordinate_); + } + + /** + * Run a move event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.commentId) { + throw new Error( + 'The comment ID is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + // TODO: Remove cast when we update getCommentById. + const comment = workspace.getCommentById( + this.commentId, + ) as unknown as WorkspaceComment; + if (!comment) { + console.warn("Can't move non-existent comment: " + this.commentId); + return; + } + + const target = forward ? this.newCoordinate_ : this.oldCoordinate_; + if (!target) { + throw new Error( + 'Either oldCoordinate_ or newCoordinate_ is undefined. ' + + 'Either pass a comment to the constructor and call recordNew, ' + + 'or call fromJson', + ); + } + comment.moveTo(target); + } +} + +export interface CommentMoveJson extends CommentBaseJson { + oldCoordinate: string; + newCoordinate: string; +} + +registry.register(registry.Type.EVENT, EventType.COMMENT_MOVE, CommentMove); diff --git a/core/events/events_comment_resize.ts b/core/events/events_comment_resize.ts new file mode 100644 index 00000000000..0c59177d9c4 --- /dev/null +++ b/core/events/events_comment_resize.ts @@ -0,0 +1,169 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for comment resize event. + */ + +import type {WorkspaceComment} from '../comments/workspace_comment.js'; +import * as registry from '../registry.js'; +import {Size} from '../utils/size.js'; +import type {Workspace} from '../workspace.js'; +import {CommentBase, CommentBaseJson} from './events_comment_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that a workspace comment has resized. + */ +export class CommentResize extends CommentBase { + override type = EventType.COMMENT_RESIZE; + + /** The size of the comment before the resize. */ + oldSize?: Size; + + /** The size of the comment after the resize. */ + newSize?: Size; + + /** + * @param opt_comment The comment that is being resized. Undefined for a blank + * event. + */ + constructor(opt_comment?: WorkspaceComment) { + super(opt_comment); + + if (!opt_comment) { + return; // Blank event to be populated by fromJson. + } + + this.oldSize = opt_comment.getSize(); + } + + /** + * Record the comment's new size. Called after the resize. Can only be + * called once. + */ + recordCurrentSizeAsNewSize() { + if (this.newSize) { + throw Error( + 'Tried to record the new size of a comment on the ' + + 'same event twice.', + ); + } + const workspace = this.getEventWorkspace_(); + if (!this.commentId) { + throw new Error( + 'The comment ID is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + const comment = workspace.getCommentById(this.commentId); + if (!comment) { + throw new Error( + 'The comment associated with the comment resize event ' + + 'could not be found', + ); + } + this.newSize = comment.getSize(); + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): CommentResizeJson { + const json = super.toJson() as CommentResizeJson; + if (!this.oldSize) { + throw new Error( + 'The old comment size is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.newSize) { + throw new Error( + 'The new comment size is undefined. Either call ' + + 'recordCurrentSizeAsNewSize, or call fromJson', + ); + } + json['oldWidth'] = Math.round(this.oldSize.width); + json['oldHeight'] = Math.round(this.oldSize.height); + json['newWidth'] = Math.round(this.newSize.width); + json['newHeight'] = Math.round(this.newSize.height); + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of CommentResize, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: CommentResizeJson, + workspace: Workspace, + event?: any, + ): CommentResize { + const newEvent = super.fromJson( + json, + workspace, + event ?? new CommentResize(), + ) as CommentResize; + newEvent.oldSize = new Size(json['oldWidth'], json['oldHeight']); + newEvent.newSize = new Size(json['newWidth'], json['newHeight']); + return newEvent; + } + + /** + * Does this event record any change of state? + * + * @returns False if something changed. + */ + override isNull(): boolean { + return Size.equals(this.oldSize, this.newSize); + } + + /** + * Run a resize event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.commentId) { + throw new Error( + 'The comment ID is undefined. Either pass a comment to ' + + 'the constructor, or call fromJson', + ); + } + const comment = workspace.getCommentById(this.commentId); + if (!comment) { + console.warn("Can't resize non-existent comment: " + this.commentId); + return; + } + + const size = forward ? this.newSize : this.oldSize; + if (!size) { + throw new Error( + 'Either oldSize or newSize is undefined. ' + + 'Either pass a comment to the constructor and call ' + + 'recordCurrentSizeAsNewSize, or call fromJson', + ); + } + comment.setSize(size); + } +} + +export interface CommentResizeJson extends CommentBaseJson { + oldWidth: number; + oldHeight: number; + newWidth: number; + newHeight: number; +} + +registry.register(registry.Type.EVENT, EventType.COMMENT_RESIZE, CommentResize); diff --git a/core/events/events_marker_move.ts b/core/events/events_marker_move.ts new file mode 100644 index 00000000000..58309df5896 --- /dev/null +++ b/core/events/events_marker_move.ts @@ -0,0 +1,133 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Events fired as a result of a marker move. + * + * @class + */ +// Former goog.module ID: Blockly.Events.MarkerMove + +import type {Block} from '../block.js'; +import {ASTNode} from '../keyboard_nav/ast_node.js'; +import * as registry from '../registry.js'; +import type {Workspace} from '../workspace.js'; +import {AbstractEventJson} from './events_abstract.js'; +import {UiBase} from './events_ui_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that a marker (used for keyboard navigation) has + * moved. + */ +export class MarkerMove extends UiBase { + /** The ID of the block the marker is now on, if any. */ + blockId?: string; + + /** The old node the marker used to be on, if any. */ + oldNode?: ASTNode; + + /** The new node the marker is now on. */ + newNode?: ASTNode; + + /** + * True if this is a cursor event, false otherwise. + * For information about cursors vs markers see {@link + * https://blocklycodelabs.dev/codelabs/keyboard-navigation/index.html?index=..%2F..index#1}. + */ + isCursor?: boolean; + + override type = EventType.MARKER_MOVE; + + /** + * @param opt_block The affected block. Null if current node is of type + * workspace. Undefined for a blank event. + * @param isCursor Whether this is a cursor event. Undefined for a blank + * event. + * @param opt_oldNode The old node the marker used to be on. + * Undefined for a blank event. + * @param opt_newNode The new node the marker is now on. + * Undefined for a blank event. + */ + constructor( + opt_block?: Block | null, + isCursor?: boolean, + opt_oldNode?: ASTNode | null, + opt_newNode?: ASTNode, + ) { + let workspaceId = opt_block ? opt_block.workspace.id : undefined; + if (opt_newNode && opt_newNode.getType() === ASTNode.types.WORKSPACE) { + workspaceId = (opt_newNode.getLocation() as Workspace).id; + } + super(workspaceId); + + this.blockId = opt_block?.id; + this.oldNode = opt_oldNode || undefined; + this.newNode = opt_newNode; + this.isCursor = isCursor; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): MarkerMoveJson { + const json = super.toJson() as MarkerMoveJson; + if (this.isCursor === undefined) { + throw new Error( + 'Whether this is a cursor event or not is undefined. Either pass ' + + 'a value to the constructor, or call fromJson', + ); + } + if (!this.newNode) { + throw new Error( + 'The new node is undefined. Either pass a node to ' + + 'the constructor, or call fromJson', + ); + } + json['isCursor'] = this.isCursor; + json['blockId'] = this.blockId; + json['oldNode'] = this.oldNode; + json['newNode'] = this.newNode; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of MarkerMove, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: MarkerMoveJson, + workspace: Workspace, + event?: any, + ): MarkerMove { + const newEvent = super.fromJson( + json, + workspace, + event ?? new MarkerMove(), + ) as MarkerMove; + newEvent.isCursor = json['isCursor']; + newEvent.blockId = json['blockId']; + newEvent.oldNode = json['oldNode']; + newEvent.newNode = json['newNode']; + return newEvent; + } +} + +export interface MarkerMoveJson extends AbstractEventJson { + isCursor: boolean; + blockId?: string; + oldNode?: ASTNode; + newNode: ASTNode; +} + +registry.register(registry.Type.EVENT, EventType.MARKER_MOVE, MarkerMove); diff --git a/core/events/events_selected.ts b/core/events/events_selected.ts new file mode 100644 index 00000000000..e4a7774966b --- /dev/null +++ b/core/events/events_selected.ts @@ -0,0 +1,97 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Events fired as a result of element select action. + * + * @class + */ +// Former goog.module ID: Blockly.Events.Selected + +import * as registry from '../registry.js'; +import type {Workspace} from '../workspace.js'; +import {AbstractEventJson} from './events_abstract.js'; +import {UiBase} from './events_ui_base.js'; +import {EventType} from './type.js'; + +/** + * Class for a selected event. + * Notifies listeners that a new element has been selected. + */ +export class Selected extends UiBase { + /** The id of the last selected selectable element. */ + oldElementId?: string; + + /** + * The id of the newly selected selectable element, + * or undefined if unselected. + */ + newElementId?: string; + + override type = EventType.SELECTED; + + /** + * @param opt_oldElementId The ID of the previously selected element. Null if + * no element last selected. Undefined for a blank event. + * @param opt_newElementId The ID of the selected element. Null if no element + * currently selected (deselect). Undefined for a blank event. + * @param opt_workspaceId The workspace identifier for this event. + * Null if no element previously selected. Undefined for a blank event. + */ + constructor( + opt_oldElementId?: string | null, + opt_newElementId?: string | null, + opt_workspaceId?: string, + ) { + super(opt_workspaceId); + + this.oldElementId = opt_oldElementId ?? undefined; + this.newElementId = opt_newElementId ?? undefined; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): SelectedJson { + const json = super.toJson() as SelectedJson; + json['oldElementId'] = this.oldElementId; + json['newElementId'] = this.newElementId; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of Selected, but we can't specify that due to the fact that parameters + * to static methods in subclasses must be supertypes of parameters to + * static methods in superclasses. + * @internal + */ + static fromJson( + json: SelectedJson, + workspace: Workspace, + event?: any, + ): Selected { + const newEvent = super.fromJson( + json, + workspace, + event ?? new Selected(), + ) as Selected; + newEvent.oldElementId = json['oldElementId']; + newEvent.newElementId = json['newElementId']; + return newEvent; + } +} + +export interface SelectedJson extends AbstractEventJson { + oldElementId?: string; + newElementId?: string; +} + +registry.register(registry.Type.EVENT, EventType.SELECTED, Selected); diff --git a/core/events/events_theme_change.ts b/core/events/events_theme_change.ts new file mode 100644 index 00000000000..b142b9f148b --- /dev/null +++ b/core/events/events_theme_change.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Events fired as a result of a theme update. + * + * @class + */ +// Former goog.module ID: Blockly.Events.ThemeChange + +import * as registry from '../registry.js'; +import type {Workspace} from '../workspace.js'; +import {AbstractEventJson} from './events_abstract.js'; +import {UiBase} from './events_ui_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that the workspace theme has changed. + */ +export class ThemeChange extends UiBase { + /** The name of the new theme that has been set. */ + themeName?: string; + + override type = EventType.THEME_CHANGE; + + /** + * @param opt_themeName The theme name. Undefined for a blank event. + * @param opt_workspaceId The workspace identifier for this event. + * event. Undefined for a blank event. + */ + constructor(opt_themeName?: string, opt_workspaceId?: string) { + super(opt_workspaceId); + this.themeName = opt_themeName; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): ThemeChangeJson { + const json = super.toJson() as ThemeChangeJson; + if (!this.themeName) { + throw new Error( + 'The theme name is undefined. Either pass a theme name to ' + + 'the constructor, or call fromJson', + ); + } + json['themeName'] = this.themeName; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of ThemeChange, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: ThemeChangeJson, + workspace: Workspace, + event?: any, + ): ThemeChange { + const newEvent = super.fromJson( + json, + workspace, + event ?? new ThemeChange(), + ) as ThemeChange; + newEvent.themeName = json['themeName']; + return newEvent; + } +} + +export interface ThemeChangeJson extends AbstractEventJson { + themeName: string; +} + +registry.register(registry.Type.EVENT, EventType.THEME_CHANGE, ThemeChange); diff --git a/core/events/events_toolbox_item_select.ts b/core/events/events_toolbox_item_select.ts new file mode 100644 index 00000000000..6a93dbfde2f --- /dev/null +++ b/core/events/events_toolbox_item_select.ts @@ -0,0 +1,96 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Events fired as a result of selecting an item on the toolbox. + * + * @class + */ +// Former goog.module ID: Blockly.Events.ToolboxItemSelect + +import * as registry from '../registry.js'; +import type {Workspace} from '../workspace.js'; +import {AbstractEventJson} from './events_abstract.js'; +import {UiBase} from './events_ui_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that a toolbox item has been selected. + */ +export class ToolboxItemSelect extends UiBase { + /** The previously selected toolbox item. */ + oldItem?: string; + + /** The newly selected toolbox item. */ + newItem?: string; + + override type = EventType.TOOLBOX_ITEM_SELECT; + + /** + * @param opt_oldItem The previously selected toolbox item. + * Undefined for a blank event. + * @param opt_newItem The newly selected toolbox item. Undefined for a blank + * event. + * @param opt_workspaceId The workspace identifier for this event. + * Undefined for a blank event. + */ + constructor( + opt_oldItem?: string | null, + opt_newItem?: string | null, + opt_workspaceId?: string, + ) { + super(opt_workspaceId); + this.oldItem = opt_oldItem ?? undefined; + this.newItem = opt_newItem ?? undefined; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): ToolboxItemSelectJson { + const json = super.toJson() as ToolboxItemSelectJson; + json['oldItem'] = this.oldItem; + json['newItem'] = this.newItem; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of ToolboxItemSelect, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: ToolboxItemSelectJson, + workspace: Workspace, + event?: any, + ): ToolboxItemSelect { + const newEvent = super.fromJson( + json, + workspace, + event ?? new ToolboxItemSelect(), + ) as ToolboxItemSelect; + newEvent.oldItem = json['oldItem']; + newEvent.newItem = json['newItem']; + return newEvent; + } +} + +export interface ToolboxItemSelectJson extends AbstractEventJson { + oldItem?: string; + newItem?: string; +} + +registry.register( + registry.Type.EVENT, + EventType.TOOLBOX_ITEM_SELECT, + ToolboxItemSelect, +); diff --git a/core/events/events_trashcan_open.ts b/core/events/events_trashcan_open.ts new file mode 100644 index 00000000000..af06d9f8f4f --- /dev/null +++ b/core/events/events_trashcan_open.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Events fired as a result of trashcan flyout open and close. + * + * @class + */ +// Former goog.module ID: Blockly.Events.TrashcanOpen + +import * as registry from '../registry.js'; +import type {Workspace} from '../workspace.js'; +import {AbstractEventJson} from './events_abstract.js'; +import {UiBase} from './events_ui_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners when the trashcan is opening or closing. + */ +export class TrashcanOpen extends UiBase { + /** + * True if the trashcan is currently opening (previously closed). + * False if it is currently closing (previously open). + */ + isOpen?: boolean; + override type = EventType.TRASHCAN_OPEN; + + /** + * @param opt_isOpen Whether the trashcan flyout is opening (false if + * opening). Undefined for a blank event. + * @param opt_workspaceId The workspace identifier for this event. + * Undefined for a blank event. + */ + constructor(opt_isOpen?: boolean, opt_workspaceId?: string) { + super(opt_workspaceId); + this.isOpen = opt_isOpen; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): TrashcanOpenJson { + const json = super.toJson() as TrashcanOpenJson; + if (this.isOpen === undefined) { + throw new Error( + 'Whether this is already open or not is undefined. Either pass ' + + 'a value to the constructor, or call fromJson', + ); + } + json['isOpen'] = this.isOpen; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of TrashcanOpen, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: TrashcanOpenJson, + workspace: Workspace, + event?: any, + ): TrashcanOpen { + const newEvent = super.fromJson( + json, + workspace, + event ?? new TrashcanOpen(), + ) as TrashcanOpen; + newEvent.isOpen = json['isOpen']; + return newEvent; + } +} + +export interface TrashcanOpenJson extends AbstractEventJson { + isOpen: boolean; +} + +registry.register(registry.Type.EVENT, EventType.TRASHCAN_OPEN, TrashcanOpen); diff --git a/core/events/events_ui_base.ts b/core/events/events_ui_base.ts new file mode 100644 index 00000000000..23fe3b4e273 --- /dev/null +++ b/core/events/events_ui_base.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Base class for events fired as a result of UI actions in + * Blockly's editor. + * + * @class + */ +// Former goog.module ID: Blockly.Events.UiBase + +import {Abstract as AbstractEvent} from './events_abstract.js'; + +/** + * Base class for a UI event. + * UI events are events that don't need to be sent over the wire for multi-user + * editing to work (e.g. scrolling the workspace, zooming, opening toolbox + * categories). + * UI events do not undo or redo. + */ +export class UiBase extends AbstractEvent { + override isBlank = true; + override workspaceId: string; + + // UI events do not undo or redo. + override recordUndo = false; + + /** Whether or not the event is a UI event. */ + override isUiEvent = true; + + /** + * @param opt_workspaceId The workspace identifier for this event. + * Undefined for a blank event. + */ + constructor(opt_workspaceId?: string) { + super(); + + /** Whether or not the event is blank (to be populated by fromJson). */ + this.isBlank = typeof opt_workspaceId === 'undefined'; + + /** The workspace identifier for this event. */ + this.workspaceId = opt_workspaceId ? opt_workspaceId : ''; + } +} diff --git a/core/events/events_var_base.ts b/core/events/events_var_base.ts new file mode 100644 index 00000000000..8e359de517f --- /dev/null +++ b/core/events/events_var_base.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Abstract class for a variable event. + * + * @class + */ +// Former goog.module ID: Blockly.Events.VarBase + +import type {VariableModel} from '../variable_model.js'; +import type {Workspace} from '../workspace.js'; +import { + Abstract as AbstractEvent, + AbstractEventJson, +} from './events_abstract.js'; + +/** + * Abstract class for a variable event. + */ +export class VarBase extends AbstractEvent { + override isBlank = true; + /** The ID of the variable this event references. */ + varId?: string; + + /** + * @param opt_variable The variable this event corresponds to. Undefined for + * a blank event. + */ + constructor(opt_variable?: VariableModel) { + super(); + this.isBlank = typeof opt_variable === 'undefined'; + if (!opt_variable) return; + + this.varId = opt_variable.getId(); + this.workspaceId = opt_variable.workspace.id; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): VarBaseJson { + const json = super.toJson() as VarBaseJson; + if (!this.varId) { + throw new Error( + 'The var ID is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + json['varId'] = this.varId; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of VarBase, but we can't specify that due to the fact that parameters + * to static methods in subclasses must be supertypes of parameters to + * static methods in superclasses. + * @internal + */ + static fromJson( + json: VarBaseJson, + workspace: Workspace, + event?: any, + ): VarBase { + const newEvent = super.fromJson( + json, + workspace, + event ?? new VarBase(), + ) as VarBase; + newEvent.varId = json['varId']; + return newEvent; + } +} + +export interface VarBaseJson extends AbstractEventJson { + varId: string; +} diff --git a/core/events/events_var_create.ts b/core/events/events_var_create.ts new file mode 100644 index 00000000000..b3ae548aa0d --- /dev/null +++ b/core/events/events_var_create.ts @@ -0,0 +1,125 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for a variable creation event. + * + * @class + */ +// Former goog.module ID: Blockly.Events.VarCreate + +import * as registry from '../registry.js'; +import type {VariableModel} from '../variable_model.js'; +import type {Workspace} from '../workspace.js'; +import {VarBase, VarBaseJson} from './events_var_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that a variable model has been created. + */ +export class VarCreate extends VarBase { + override type = EventType.VAR_CREATE; + + /** The type of the variable that was created. */ + varType?: string; + + /** The name of the variable that was created. */ + varName?: string; + + /** + * @param opt_variable The created variable. Undefined for a blank event. + */ + constructor(opt_variable?: VariableModel) { + super(opt_variable); + + if (!opt_variable) { + return; // Blank event to be populated by fromJson. + } + this.varType = opt_variable.type; + this.varName = opt_variable.name; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): VarCreateJson { + const json = super.toJson() as VarCreateJson; + if (this.varType === undefined) { + throw new Error( + 'The var type is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.varName) { + throw new Error( + 'The var name is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + json['varType'] = this.varType; + json['varName'] = this.varName; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of VarCreate, but we can't specify that due to the fact that parameters + * to static methods in subclasses must be supertypes of parameters to + * static methods in superclasses. + * @internal + */ + static fromJson( + json: VarCreateJson, + workspace: Workspace, + event?: any, + ): VarCreate { + const newEvent = super.fromJson( + json, + workspace, + event ?? new VarCreate(), + ) as VarCreate; + newEvent.varType = json['varType']; + newEvent.varName = json['varName']; + return newEvent; + } + + /** + * Run a variable creation event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.varId) { + throw new Error( + 'The var ID is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.varName) { + throw new Error( + 'The var name is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + if (forward) { + workspace.createVariable(this.varName, this.varType, this.varId); + } else { + workspace.deleteVariableById(this.varId); + } + } +} + +export interface VarCreateJson extends VarBaseJson { + varType: string; + varName: string; +} + +registry.register(registry.Type.EVENT, EventType.VAR_CREATE, VarCreate); diff --git a/core/events/events_var_delete.ts b/core/events/events_var_delete.ts new file mode 100644 index 00000000000..caaa1f4874a --- /dev/null +++ b/core/events/events_var_delete.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.Events.VarDelete + +import * as registry from '../registry.js'; +import type {VariableModel} from '../variable_model.js'; +import type {Workspace} from '../workspace.js'; +import {VarBase, VarBaseJson} from './events_var_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that a variable model has been deleted. + * + * @class + */ +export class VarDelete extends VarBase { + override type = EventType.VAR_DELETE; + /** The type of the variable that was deleted. */ + varType?: string; + /** The name of the variable that was deleted. */ + varName?: string; + + /** + * @param opt_variable The deleted variable. Undefined for a blank event. + */ + constructor(opt_variable?: VariableModel) { + super(opt_variable); + + if (!opt_variable) { + return; // Blank event to be populated by fromJson. + } + this.varType = opt_variable.type; + this.varName = opt_variable.name; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): VarDeleteJson { + const json = super.toJson() as VarDeleteJson; + if (this.varType === undefined) { + throw new Error( + 'The var type is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.varName) { + throw new Error( + 'The var name is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + json['varType'] = this.varType; + json['varName'] = this.varName; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of VarDelete, but we can't specify that due to the fact that parameters + * to static methods in subclasses must be supertypes of parameters to + * static methods in superclasses. + * @internal + */ + static fromJson( + json: VarDeleteJson, + workspace: Workspace, + event?: any, + ): VarDelete { + const newEvent = super.fromJson( + json, + workspace, + event ?? new VarDelete(), + ) as VarDelete; + newEvent.varType = json['varType']; + newEvent.varName = json['varName']; + return newEvent; + } + + /** + * Run a variable deletion event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.varId) { + throw new Error( + 'The var ID is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.varName) { + throw new Error( + 'The var name is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + if (forward) { + workspace.deleteVariableById(this.varId); + } else { + workspace.createVariable(this.varName, this.varType, this.varId); + } + } +} + +export interface VarDeleteJson extends VarBaseJson { + varType: string; + varName: string; +} + +registry.register(registry.Type.EVENT, EventType.VAR_DELETE, VarDelete); diff --git a/core/events/events_var_rename.ts b/core/events/events_var_rename.ts new file mode 100644 index 00000000000..b461184cab1 --- /dev/null +++ b/core/events/events_var_rename.ts @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.Events.VarRename + +import * as registry from '../registry.js'; +import type {VariableModel} from '../variable_model.js'; +import type {Workspace} from '../workspace.js'; +import {VarBase, VarBaseJson} from './events_var_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that a variable model was renamed. + * + * @class + */ +export class VarRename extends VarBase { + override type = EventType.VAR_RENAME; + + /** The previous name of the variable. */ + oldName?: string; + + /** The new name of the variable. */ + newName?: string; + + /** + * @param opt_variable The renamed variable. Undefined for a blank event. + * @param newName The new name the variable will be changed to. + */ + constructor(opt_variable?: VariableModel, newName?: string) { + super(opt_variable); + + if (!opt_variable) { + return; // Blank event to be populated by fromJson. + } + this.oldName = opt_variable.name; + this.newName = typeof newName === 'undefined' ? '' : newName; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): VarRenameJson { + const json = super.toJson() as VarRenameJson; + if (!this.oldName) { + throw new Error( + 'The old var name is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.newName) { + throw new Error( + 'The new var name is undefined. Either pass a value to ' + + 'the constructor, or call fromJson', + ); + } + json['oldName'] = this.oldName; + json['newName'] = this.newName; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of VarRename, but we can't specify that due to the fact that parameters + * to static methods in subclasses must be supertypes of parameters to + * static methods in superclasses. + * @internal + */ + static fromJson( + json: VarRenameJson, + workspace: Workspace, + event?: any, + ): VarRename { + const newEvent = super.fromJson( + json, + workspace, + event ?? new VarRename(), + ) as VarRename; + newEvent.oldName = json['oldName']; + newEvent.newName = json['newName']; + return newEvent; + } + + /** + * Run a variable rename event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.varId) { + throw new Error( + 'The var ID is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.oldName) { + throw new Error( + 'The old var name is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.newName) { + throw new Error( + 'The new var name is undefined. Either pass a value to ' + + 'the constructor, or call fromJson', + ); + } + if (forward) { + workspace.renameVariableById(this.varId, this.newName); + } else { + workspace.renameVariableById(this.varId, this.oldName); + } + } +} + +export interface VarRenameJson extends VarBaseJson { + oldName: string; + newName: string; +} + +registry.register(registry.Type.EVENT, EventType.VAR_RENAME, VarRename); diff --git a/core/events/events_viewport.ts b/core/events/events_viewport.ts new file mode 100644 index 00000000000..b7a05b8d61e --- /dev/null +++ b/core/events/events_viewport.ts @@ -0,0 +1,149 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Events fired as a result of a viewport change. + * + * @class + */ +// Former goog.module ID: Blockly.Events.ViewportChange + +import * as registry from '../registry.js'; +import type {Workspace} from '../workspace.js'; +import {AbstractEventJson} from './events_abstract.js'; +import {UiBase} from './events_ui_base.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners that the workspace surface's position or scale has + * changed. + * + * Does not notify when the workspace itself resizes. + */ +export class ViewportChange extends UiBase { + /** + * Top edge of the visible portion of the workspace, relative to the + * workspace origin. + */ + viewTop?: number; + + /** + * The left edge of the visible portion of the workspace, relative to + * the workspace origin. + */ + viewLeft?: number; + + /** The scale of the workpace. */ + scale?: number; + + /** The previous scale of the workspace. */ + oldScale?: number; + + override type = EventType.VIEWPORT_CHANGE; + + /** + * @param opt_top Top-edge of the visible portion of the workspace, relative + * to the workspace origin. Undefined for a blank event. + * @param opt_left Left-edge of the visible portion of the workspace relative + * to the workspace origin. Undefined for a blank event. + * @param opt_scale The scale of the workspace. Undefined for a blank event. + * @param opt_workspaceId The workspace identifier for this event. + * Undefined for a blank event. + * @param opt_oldScale The old scale of the workspace. Undefined for a blank + * event. + */ + constructor( + opt_top?: number, + opt_left?: number, + opt_scale?: number, + opt_workspaceId?: string, + opt_oldScale?: number, + ) { + super(opt_workspaceId); + + this.viewTop = opt_top; + this.viewLeft = opt_left; + this.scale = opt_scale; + this.oldScale = opt_oldScale; + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): ViewportChangeJson { + const json = super.toJson() as ViewportChangeJson; + if (this.viewTop === undefined) { + throw new Error( + 'The view top is undefined. Either pass a value to ' + + 'the constructor, or call fromJson', + ); + } + if (this.viewLeft === undefined) { + throw new Error( + 'The view left is undefined. Either pass a value to ' + + 'the constructor, or call fromJson', + ); + } + if (this.scale === undefined) { + throw new Error( + 'The scale is undefined. Either pass a value to ' + + 'the constructor, or call fromJson', + ); + } + if (this.oldScale === undefined) { + throw new Error( + 'The old scale is undefined. Either pass a value to ' + + 'the constructor, or call fromJson', + ); + } + json['viewTop'] = this.viewTop; + json['viewLeft'] = this.viewLeft; + json['scale'] = this.scale; + json['oldScale'] = this.oldScale; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of Viewport, but we can't specify that due to the fact that parameters + * to static methods in subclasses must be supertypes of parameters to + * static methods in superclasses. + * @internal + */ + static fromJson( + json: ViewportChangeJson, + workspace: Workspace, + event?: any, + ): ViewportChange { + const newEvent = super.fromJson( + json, + workspace, + event ?? new ViewportChange(), + ) as ViewportChange; + newEvent.viewTop = json['viewTop']; + newEvent.viewLeft = json['viewLeft']; + newEvent.scale = json['scale']; + newEvent.oldScale = json['oldScale']; + return newEvent; + } +} + +export interface ViewportChangeJson extends AbstractEventJson { + viewTop: number; + viewLeft: number; + scale: number; + oldScale: number; +} + +registry.register( + registry.Type.EVENT, + EventType.VIEWPORT_CHANGE, + ViewportChange, +); diff --git a/core/events/predicates.ts b/core/events/predicates.ts new file mode 100644 index 00000000000..79d8ca284e4 --- /dev/null +++ b/core/events/predicates.ts @@ -0,0 +1,172 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file Predicates for testing Abstract event subclasses based on + * their .type properties. These are useful because there are places + * where it is not possible to use instanceof tests + * for type narrowing due to load ordering issues that would be caused + * by the need to import (rather than just import type) the class + * constructors in question. + */ + +import type {Abstract} from './events_abstract.js'; +import type {BlockChange} from './events_block_change.js'; +import type {BlockCreate} from './events_block_create.js'; +import type {BlockDelete} from './events_block_delete.js'; +import type {BlockDrag} from './events_block_drag.js'; +import type {BlockFieldIntermediateChange} from './events_block_field_intermediate_change.js'; +import type {BlockMove} from './events_block_move.js'; +import type {BubbleOpen} from './events_bubble_open.js'; +import type {Click} from './events_click.js'; +import type {CommentChange} from './events_comment_change.js'; +import type {CommentCollapse} from './events_comment_collapse.js'; +import type {CommentCreate} from './events_comment_create.js'; +import type {CommentDelete} from './events_comment_delete.js'; +import type {CommentDrag} from './events_comment_drag.js'; +import type {CommentMove} from './events_comment_move.js'; +import type {CommentResize} from './events_comment_resize.js'; +import type {MarkerMove} from './events_marker_move.js'; +import type {Selected} from './events_selected.js'; +import type {ThemeChange} from './events_theme_change.js'; +import type {ToolboxItemSelect} from './events_toolbox_item_select.js'; +import type {TrashcanOpen} from './events_trashcan_open.js'; +import type {VarCreate} from './events_var_create.js'; +import type {VarDelete} from './events_var_delete.js'; +import type {VarRename} from './events_var_rename.js'; +import type {ViewportChange} from './events_viewport.js'; +import type {FinishedLoading} from './workspace_events.js'; + +import {EventType} from './type.js'; + +/** @returns true iff event.type is EventType.BLOCK_CREATE */ +export function isBlockCreate(event: Abstract): event is BlockCreate { + return event.type === EventType.BLOCK_CREATE; +} + +/** @returns true iff event.type is EventType.BLOCK_DELETE */ +export function isBlockDelete(event: Abstract): event is BlockDelete { + return event.type === EventType.BLOCK_DELETE; +} + +/** @returns true iff event.type is EventType.BLOCK_CHANGE */ +export function isBlockChange(event: Abstract): event is BlockChange { + return event.type === EventType.BLOCK_CHANGE; +} + +/** @returns true iff event.type is EventType.BLOCK_FIELD_INTERMEDIATE_CHANGE */ +export function isBlockFieldIntermediateChange( + event: Abstract, +): event is BlockFieldIntermediateChange { + return event.type === EventType.BLOCK_FIELD_INTERMEDIATE_CHANGE; +} + +/** @returns true iff event.type is EventType.BLOCK_MOVE */ +export function isBlockMove(event: Abstract): event is BlockMove { + return event.type === EventType.BLOCK_MOVE; +} + +/** @returns true iff event.type is EventType.VAR_CREATE */ +export function isVarCreate(event: Abstract): event is VarCreate { + return event.type === EventType.VAR_CREATE; +} + +/** @returns true iff event.type is EventType.VAR_DELETE */ +export function isVarDelete(event: Abstract): event is VarDelete { + return event.type === EventType.VAR_DELETE; +} + +/** @returns true iff event.type is EventType.VAR_RENAME */ +export function isVarRename(event: Abstract): event is VarRename { + return event.type === EventType.VAR_RENAME; +} + +/** @returns true iff event.type is EventType.BLOCK_DRAG */ +export function isBlockDrag(event: Abstract): event is BlockDrag { + return event.type === EventType.BLOCK_DRAG; +} + +/** @returns true iff event.type is EventType.SELECTED */ +export function isSelected(event: Abstract): event is Selected { + return event.type === EventType.SELECTED; +} + +/** @returns true iff event.type is EventType.CLICK */ +export function isClick(event: Abstract): event is Click { + return event.type === EventType.CLICK; +} + +/** @returns true iff event.type is EventType.MARKER_MOVE */ +export function isMarkerMove(event: Abstract): event is MarkerMove { + return event.type === EventType.MARKER_MOVE; +} + +/** @returns true iff event.type is EventType.BUBBLE_OPEN */ +export function isBubbleOpen(event: Abstract): event is BubbleOpen { + return event.type === EventType.BUBBLE_OPEN; +} + +/** @returns true iff event.type is EventType.TRASHCAN_OPEN */ +export function isTrashcanOpen(event: Abstract): event is TrashcanOpen { + return event.type === EventType.TRASHCAN_OPEN; +} + +/** @returns true iff event.type is EventType.TOOLBOX_ITEM_SELECT */ +export function isToolboxItemSelect( + event: Abstract, +): event is ToolboxItemSelect { + return event.type === EventType.TOOLBOX_ITEM_SELECT; +} + +/** @returns true iff event.type is EventType.THEME_CHANGE */ +export function isThemeChange(event: Abstract): event is ThemeChange { + return event.type === EventType.THEME_CHANGE; +} + +/** @returns true iff event.type is EventType.VIEWPORT_CHANGE */ +export function isViewportChange(event: Abstract): event is ViewportChange { + return event.type === EventType.VIEWPORT_CHANGE; +} + +/** @returns true iff event.type is EventType.COMMENT_CREATE */ +export function isCommentCreate(event: Abstract): event is CommentCreate { + return event.type === EventType.COMMENT_CREATE; +} + +/** @returns true iff event.type is EventType.COMMENT_DELETE */ +export function isCommentDelete(event: Abstract): event is CommentDelete { + return event.type === EventType.COMMENT_DELETE; +} + +/** @returns true iff event.type is EventType.COMMENT_CHANGE */ +export function isCommentChange(event: Abstract): event is CommentChange { + return event.type === EventType.COMMENT_CHANGE; +} + +/** @returns true iff event.type is EventType.COMMENT_MOVE */ +export function isCommentMove(event: Abstract): event is CommentMove { + return event.type === EventType.COMMENT_MOVE; +} + +/** @returns true iff event.type is EventType.COMMENT_RESIZE */ +export function isCommentResize(event: Abstract): event is CommentResize { + return event.type === EventType.COMMENT_RESIZE; +} + +/** @returns true iff event.type is EventType.COMMENT_DRAG */ +export function isCommentDrag(event: Abstract): event is CommentDrag { + return event.type === EventType.COMMENT_DRAG; +} + +/** @returns true iff event.type is EventType.COMMENT_COLLAPSE */ +export function isCommentCollapse(event: Abstract): event is CommentCollapse { + return event.type === EventType.COMMENT_COLLAPSE; +} + +/** @returns true iff event.type is EventType.FINISHED_LOADING */ +export function isFinishedLoading(event: Abstract): event is FinishedLoading { + return event.type === EventType.FINISHED_LOADING; +} diff --git a/core/events/type.ts b/core/events/type.ts new file mode 100644 index 00000000000..db9ad6c96a3 --- /dev/null +++ b/core/events/type.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Enum of values for the .type property for event classes (concrete subclasses + * of Abstract). + */ +export enum EventType { + /** Type of event that creates a block. */ + BLOCK_CREATE = 'create', + /** Type of event that deletes a block. */ + BLOCK_DELETE = 'delete', + /** Type of event that changes a block. */ + BLOCK_CHANGE = 'change', + /** + * Type of event representing an in-progress change to a field of a + * block, which is expected to be followed by a block change event. + */ + BLOCK_FIELD_INTERMEDIATE_CHANGE = 'block_field_intermediate_change', + /** Type of event that moves a block. */ + BLOCK_MOVE = 'move', + /** Type of event that creates a variable. */ + VAR_CREATE = 'var_create', + /** Type of event that deletes a variable. */ + VAR_DELETE = 'var_delete', + /** Type of event that renames a variable. */ + VAR_RENAME = 'var_rename', + /** + * Type of generic event that records a UI change. + * + * @deprecated Was only ever intended for internal use. + */ + UI = 'ui', + /** Type of event that drags a block. */ + BLOCK_DRAG = 'drag', + /** Type of event that records a change in selected element. */ + SELECTED = 'selected', + /** Type of event that records a click. */ + CLICK = 'click', + /** Type of event that records a marker move. */ + MARKER_MOVE = 'marker_move', + /** Type of event that records a bubble open. */ + BUBBLE_OPEN = 'bubble_open', + /** Type of event that records a trashcan open. */ + TRASHCAN_OPEN = 'trashcan_open', + /** Type of event that records a toolbox item select. */ + TOOLBOX_ITEM_SELECT = 'toolbox_item_select', + /** Type of event that records a theme change. */ + THEME_CHANGE = 'theme_change', + /** Type of event that records a viewport change. */ + VIEWPORT_CHANGE = 'viewport_change', + /** Type of event that creates a comment. */ + COMMENT_CREATE = 'comment_create', + /** Type of event that deletes a comment. */ + COMMENT_DELETE = 'comment_delete', + /** Type of event that changes a comment. */ + COMMENT_CHANGE = 'comment_change', + /** Type of event that moves a comment. */ + COMMENT_MOVE = 'comment_move', + /** Type of event that resizes a comment. */ + COMMENT_RESIZE = 'comment_resize', + /** Type of event that drags a comment. */ + COMMENT_DRAG = 'comment_drag', + /** Type of event that collapses a comment. */ + COMMENT_COLLAPSE = 'comment_collapse', + /** Type of event that records a workspace load. */ + FINISHED_LOADING = 'finished_loading', +} + +/** + * List of events that cause objects to be bumped back into the visible + * portion of the workspace. + * + * Not to be confused with bumping so that disconnected connections do not + * appear connected. + */ +export const BUMP_EVENTS: string[] = [ + EventType.BLOCK_CREATE, + EventType.BLOCK_MOVE, + EventType.COMMENT_CREATE, + EventType.COMMENT_MOVE, +]; diff --git a/core/events/utils.ts b/core/events/utils.ts new file mode 100644 index 00000000000..4753e7783d9 --- /dev/null +++ b/core/events/utils.ts @@ -0,0 +1,469 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.Events.utils + +import type {Block} from '../block.js'; +import * as common from '../common.js'; +import * as registry from '../registry.js'; +import * as deprecation from '../utils/deprecation.js'; +import * as idGenerator from '../utils/idgenerator.js'; +import type {Workspace} from '../workspace.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import type {Abstract} from './events_abstract.js'; +import type {BlockCreate} from './events_block_create.js'; +import type {BlockMove} from './events_block_move.js'; +import type {CommentCreate} from './events_comment_create.js'; +import type {CommentMove} from './events_comment_move.js'; +import type {CommentResize} from './events_comment_resize.js'; +import { + isBlockChange, + isBlockCreate, + isBlockMove, + isBubbleOpen, + isClick, + isViewportChange, +} from './predicates.js'; + +/** Group ID for new events. Grouped events are indivisible. */ +let group = ''; + +/** Sets whether the next event should be added to the undo stack. */ +let recordUndo = true; + +/** + * Sets whether events should be added to the undo stack. + * + * @param newValue True if events should be added to the undo stack. + */ +export function setRecordUndo(newValue: boolean) { + recordUndo = newValue; +} + +/** + * Returns whether or not events will be added to the undo stack. + * + * @returns True if events will be added to the undo stack. + */ +export function getRecordUndo(): boolean { + return recordUndo; +} + +/** Allow change events to be created and fired. */ +let disabled = 0; + +/** + * The language-neutral ID for when the reason why a block is disabled is + * because the block is not descended from a root block. + */ +const ORPHANED_BLOCK_DISABLED_REASON = 'ORPHANED_BLOCK'; + +/** + * Type of events that cause objects to be bumped back into the visible + * portion of the workspace. + * + * Not to be confused with bumping so that disconnected connections do not + * appear connected. + */ +export type BumpEvent = + | BlockCreate + | BlockMove + | CommentCreate + | CommentMove + | CommentResize; + +/** List of events queued for firing. */ +const FIRE_QUEUE: Abstract[] = []; + +/** + * Enqueue an event to be dispatched to change listeners. + * + * Notes: + * + * - Events are enqueued until a timeout, generally after rendering is + * complete or at the end of the current microtask, if not running + * in a browser. + * - Queued events are subject to destructive modification by being + * combined with later-enqueued events, but only until they are + * fired. + * - Events are dispatched via the fireChangeListener method on the + * affected workspace. + * + * @param event Any Blockly event. + */ +export function fire(event: Abstract) { + TEST_ONLY.fireInternal(event); +} + +/** + * Private version of fireInternal for stubbing in tests. + */ +function fireInternal(event: Abstract) { + if (!isEnabled()) { + return; + } + if (!FIRE_QUEUE.length) { + // First event added; schedule a firing of the event queue. + try { + // If we are in a browser context, we want to make sure that the event + // fires after blocks have been rerendered this frame. + requestAnimationFrame(() => { + setTimeout(fireNow, 0); + }); + } catch { + // Otherwise we just want to delay so events can be coallesced. + // requestAnimationFrame will error triggering this. + setTimeout(fireNow, 0); + } + } + enqueueEvent(event); +} + +/** Dispatch all queued events. */ +function fireNow() { + const queue = filter(FIRE_QUEUE, true); + FIRE_QUEUE.length = 0; + for (const event of queue) { + if (!event.workspaceId) continue; + common.getWorkspaceById(event.workspaceId)?.fireChangeListener(event); + } +} + +/** + * Enqueue an event on FIRE_QUEUE. + * + * Normally this is equivalent to FIRE_QUEUE.push(event), but if the + * enqueued event is a BlockChange event and the most recent event(s) + * on the queue are BlockMove events that (re)connect other blocks to + * the changed block (and belong to the same event group) then the + * enqueued event will be enqueued before those events rather than + * after. + * + * This is a workaround for a problem caused by the fact that + * MutatorIcon.prototype.recomposeSourceBlock can only fire a + * BlockChange event after the mutating block's compose method + * returns, meaning that if the compose method reconnects child blocks + * the corresponding BlockMove events are emitted _before_ the + * BlockChange event, causing issues with undo, mirroring, etc.; see + * https://github.com/google/blockly/issues/8225#issuecomment-2195751783 + * (and following) for details. + */ +function enqueueEvent(event: Abstract) { + if (isBlockChange(event) && event.element === 'mutation') { + let i; + for (i = FIRE_QUEUE.length; i > 0; i--) { + const otherEvent = FIRE_QUEUE[i - 1]; + if ( + otherEvent.group !== event.group || + otherEvent.workspaceId !== event.workspaceId || + !isBlockMove(otherEvent) || + otherEvent.newParentId !== event.blockId + ) { + break; + } + } + FIRE_QUEUE.splice(i, 0, event); + return; + } + + FIRE_QUEUE.push(event); +} + +/** + * Filter the queued events by merging duplicates, removing null + * events and reording BlockChange events. + * + * History of this function: + * + * This function was originally added in commit cf257ea5 with the + * intention of dramatically reduing the total number of dispatched + * events. Initialy it affected only BlockMove events but others were + * added over time. + * + * Code was added to reorder BlockChange events added in commit + * 5578458, for uncertain reasons but most probably as part of an + * only-partially-successful attemp to fix problems with event + * ordering during block mutations. This code should probably have + * been added to the top of the function, before merging and + * null-removal, but was added at the bottom for now-forgotten + * reasons. See these bug investigations for a fuller discussion of + * the underlying issue and some of the failures that arose because of + * this incomplete/incorrect fix: + * + * https://github.com/google/blockly/issues/8225#issuecomment-2195751783 + * https://github.com/google/blockly/issues/2037#issuecomment-2209696351 + * + * Later, in PR #1205 the original O(n^2) implementation was replaced + * by a linear-time implementation, though additonal fixes were made + * subsequently. + * + * In August 2024 a number of significant simplifications were made: + * + * This function was previously called from Workspace.prototype.undo, + * but the mutation of events by this function was the cause of issue + * #7026 (note that events would combine differently in reverse order + * vs. forward order). The originally-chosen fix for this was the + * addition (in PR #7069) of code to fireNow to post-filter the + * .undoStack_ and .redoStack_ of any workspace that had just been + * involved in dispatching events; this apparently resolved the issue + * but added considerable additional complexity and made it difficult + * to reason about how events are processed for undo/redo, so both the + * call from undo and the post-processing code was removed, and + * forward=true was made the default while calling the function with + * forward=false was deprecated. + * + * At the same time, the buggy code to reorder BlockChange events was + * replaced by a less-buggy version of the same functionality in a new + * function, enqueueEvent, called from fireInternal, thus assuring + * that events will be in the correct order at the time filter is + * called. + * + * Additionally, the event merging code was modified so that only + * immediately adjacent events would be merged. This simplified the + * implementation while ensuring that the merging of events cannot + * cause them to be reordered. + * + * @param queue Array of events. + * @param forward True if forward (redo), false if backward (undo). + * This parameter is deprecated: true is now the default and + * calling filter with it set to false will in future not be + * supported. + * @returns Array of filtered events. + */ +export function filter(queue: Abstract[], forward = true): Abstract[] { + if (!forward) { + deprecation.warn('filter(queue, /*forward=*/false)', 'v11.2', 'v12'); + // Undo was merged in reverse order. + queue = queue.slice().reverse(); // Copy before reversing in place. + } + const mergedQueue: Abstract[] = []; + // Merge duplicates. + for (const event of queue) { + const lastEvent = mergedQueue[mergedQueue.length - 1]; + if (event.isNull()) continue; + if ( + !lastEvent || + lastEvent.workspaceId !== event.workspaceId || + lastEvent.group !== event.group + ) { + mergedQueue.push(event); + continue; + } + if ( + isBlockMove(event) && + isBlockMove(lastEvent) && + event.blockId === lastEvent.blockId + ) { + // Merge move events. + lastEvent.newParentId = event.newParentId; + lastEvent.newInputName = event.newInputName; + lastEvent.newCoordinate = event.newCoordinate; + // Concatenate reasons without duplicates. + if (lastEvent.reason || event.reason) { + lastEvent.reason = Array.from( + new Set((lastEvent.reason ?? []).concat(event.reason ?? [])), + ); + } + } else if ( + isBlockChange(event) && + isBlockChange(lastEvent) && + event.blockId === lastEvent.blockId && + event.element === lastEvent.element && + event.name === lastEvent.name + ) { + // Merge change events. + lastEvent.newValue = event.newValue; + } else if (isViewportChange(event) && isViewportChange(lastEvent)) { + // Merge viewport change events. + lastEvent.viewTop = event.viewTop; + lastEvent.viewLeft = event.viewLeft; + lastEvent.scale = event.scale; + lastEvent.oldScale = event.oldScale; + } else if (isClick(event) && isBubbleOpen(lastEvent)) { + // Drop click events caused by opening/closing bubbles. + } else { + mergedQueue.push(event); + } + } + // Filter out any events that have become null due to merging. + queue = mergedQueue.filter((e) => !e.isNull()); + if (!forward) { + // Restore undo order. + queue.reverse(); + } + return queue; +} + +/** + * Modify pending undo events so that when they are fired they don't land + * in the undo stack. Called by Workspace.clearUndo. + */ +export function clearPendingUndo() { + for (let i = 0, event; (event = FIRE_QUEUE[i]); i++) { + event.recordUndo = false; + } +} + +/** + * Stop sending events. Every call to this function MUST also call enable. + */ +export function disable() { + disabled++; +} + +/** + * Start sending events. Unless events were already disabled when the + * corresponding call to disable was made. + */ +export function enable() { + disabled--; +} + +/** + * Returns whether events may be fired or not. + * + * @returns True if enabled. + */ +export function isEnabled(): boolean { + return disabled === 0; +} + +/** + * Current group. + * + * @returns ID string. + */ +export function getGroup(): string { + return group; +} + +/** + * Start or stop a group. + * + * @param state True to start new group, false to end group. + * String to set group explicitly. + */ +export function setGroup(state: boolean | string) { + TEST_ONLY.setGroupInternal(state); +} + +/** + * Private version of setGroup for stubbing in tests. + */ +function setGroupInternal(state: boolean | string) { + if (typeof state === 'boolean') { + group = state ? idGenerator.genUid() : ''; + } else { + group = state; + } +} + +/** + * Compute a list of the IDs of the specified block and all its descendants. + * + * @param block The root block. + * @returns List of block IDs. + * @internal + */ +export function getDescendantIds(block: Block): string[] { + const ids = []; + const descendants = block.getDescendants(false); + for (let i = 0, descendant; (descendant = descendants[i]); i++) { + ids[i] = descendant.id; + } + return ids; +} + +/** + * Decode the JSON into an event. + * + * @param json JSON representation. + * @param workspace Target workspace for event. + * @returns The event represented by the JSON. + * @throws {Error} if an event type is not found in the registry. + */ +export function fromJson( + json: AnyDuringMigration, + workspace: Workspace, +): Abstract { + const eventClass = get(json['type']); + if (!eventClass) throw Error('Unknown event type.'); + + return (eventClass as any).fromJson(json, workspace); +} + +/** + * Gets the class for a specific event type from the registry. + * + * @param eventType The type of the event to get. + * @returns The event class with the given type. + */ +export function get( + eventType: string, +): new (...p1: AnyDuringMigration[]) => Abstract { + const event = registry.getClass(registry.Type.EVENT, eventType); + if (!event) { + throw new Error(`Event type ${eventType} not found in registry.`); + } + return event; +} + +/** + * Set if a block is disabled depending on whether it is properly connected. + * Use this on applications where all blocks should be connected to a top block. + * + * @param event Custom data for event. + */ +export function disableOrphans(event: Abstract) { + if (isBlockMove(event) || isBlockCreate(event)) { + const blockEvent = event as BlockMove | BlockCreate; + if (!blockEvent.workspaceId) { + return; + } + const eventWorkspace = common.getWorkspaceById( + blockEvent.workspaceId, + ) as WorkspaceSvg; + if (!blockEvent.blockId) { + throw new Error('Encountered a blockEvent without a proper blockId'); + } + let block = eventWorkspace.getBlockById(blockEvent.blockId); + if (block) { + // Changing blocks as part of this event shouldn't be undoable. + const initialUndoFlag = recordUndo; + try { + recordUndo = false; + const parent = block.getParent(); + if ( + parent && + !parent.hasDisabledReason(ORPHANED_BLOCK_DISABLED_REASON) + ) { + const children = block.getDescendants(false); + for (let i = 0, child; (child = children[i]); i++) { + child.setDisabledReason(false, ORPHANED_BLOCK_DISABLED_REASON); + } + } else if ( + (block.outputConnection || block.previousConnection) && + !eventWorkspace.isDragging() + ) { + do { + block.setDisabledReason(true, ORPHANED_BLOCK_DISABLED_REASON); + block = block.getNextBlock(); + } while (block); + } + } finally { + recordUndo = initialUndoFlag; + } + } + } +} + +export const TEST_ONLY = { + FIRE_QUEUE, + enqueueEvent, + fireNow, + fireInternal, + setGroupInternal, +}; diff --git a/core/events/workspace_events.ts b/core/events/workspace_events.ts new file mode 100644 index 00000000000..1a2ff54735b --- /dev/null +++ b/core/events/workspace_events.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for a finished loading workspace event. + * + * @class + */ +// Former goog.module ID: Blockly.Events.FinishedLoading + +import * as registry from '../registry.js'; +import type {Workspace} from '../workspace.js'; +import {Abstract as AbstractEvent} from './events_abstract.js'; +import {EventType} from './type.js'; + +/** + * Notifies listeners when the workspace has finished deserializing from + * JSON/XML. + */ +export class FinishedLoading extends AbstractEvent { + override isBlank = true; + override recordUndo = false; + override type = EventType.FINISHED_LOADING; + + /** + * @param opt_workspace The workspace that has finished loading. Undefined + * for a blank event. + */ + constructor(opt_workspace?: Workspace) { + super(); + this.isBlank = !!opt_workspace; + + if (!opt_workspace) return; + + this.workspaceId = opt_workspace.id; + } +} + +registry.register( + registry.Type.EVENT, + EventType.FINISHED_LOADING, + FinishedLoading, +); diff --git a/core/extensions.js b/core/extensions.js deleted file mode 100644 index 66602688855..00000000000 --- a/core/extensions.js +++ /dev/null @@ -1,446 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Extensions are functions that help initialize blocks, usually - * adding dynamic behavior such as onchange handlers and mutators. These - * are applied using Block.applyExtension(), or the JSON "extensions" - * array attribute. - * @author Anm@anm.me (Andrew n marshall) - */ -'use strict'; - -/** - * @name Blockly.Extensions - * @namespace - **/ -goog.provide('Blockly.Extensions'); - - -/** - * The set of all registered extensions, keyed by extension name/id. - * @private - */ -Blockly.Extensions.ALL_ = {}; - -/** - * The set of properties on a block that may only be set by a mutator. - * @type {!Array.} - * @private - * @constant - */ -Blockly.Extensions.MUTATOR_PROPERTIES_ = - ['domToMutation', 'mutationToDom', 'compose', 'decompose']; - -/** - * Registers a new extension function. Extensions are functions that help - * initialize blocks, usually adding dynamic behavior such as onchange - * handlers and mutators. These are applied using Block.applyExtension(), or - * the JSON "extensions" array attribute. - * @param {string} name The name of this extension. - * @param {function} initFn The function to initialize an extended block. - * @throws {Error} if the extension name is empty, the extension is already - * registered, or extensionFn is not a function. - */ -Blockly.Extensions.register = function(name, initFn) { - if (!goog.isString(name) || goog.string.isEmptyOrWhitespace(name)) { - throw new Error('Error: Invalid extension name "' + name + '"'); - } - if (Blockly.Extensions.ALL_[name]) { - throw new Error('Error: Extension "' + name + '" is already registered.'); - } - if (!goog.isFunction(initFn)) { - throw new Error('Error: Extension "' + name + '" must be a function'); - } - Blockly.Extensions.ALL_[name] = initFn; -}; - -/** - * Registers a new extension function that adds all key/value of mixinObj. - * @param {string} name The name of this extension. - * @param {!Object} mixinObj The values to mix in. - * @throws {Error} if the extension name is empty or the extension is already - * registered. - */ -Blockly.Extensions.registerMixin = function(name, mixinObj) { - Blockly.Extensions.register(name, function() { - this.mixin(mixinObj); - }); -}; - -/** - * Registers a new extension function that adds a mutator to the block. - * At register time this performs some basic sanity checks on the mutator. - * The wrapper may also add a mutator dialog to the block, if both compose and - * decompose are defined on the mixin. - * @param {string} name The name of this mutator extension. - * @param {!Object} mixinObj The values to mix in. - * @param {function()=} opt_helperFn An optional function to apply after mixing - * in the object. - * @param {Array.=} opt_blockList A list of blocks to appear in the - * flyout of the mutator dialog. - * @throws {Error} if the mutation is invalid or can't be applied to the block. - */ -Blockly.Extensions.registerMutator = function(name, mixinObj, opt_helperFn, - opt_blockList) { - var errorPrefix = 'Error when registering mutator "' + name + '": '; - - // Sanity check the mixin object before registering it. - Blockly.Extensions.checkHasFunction_(errorPrefix, mixinObj, 'domToMutation'); - Blockly.Extensions.checkHasFunction_(errorPrefix, mixinObj, 'mutationToDom'); - - var hasMutatorDialog = Blockly.Extensions.checkMutatorDialog_(mixinObj, - errorPrefix); - - if (opt_helperFn && !goog.isFunction(opt_helperFn)) { - throw new Error('Extension "' + name + '" is not a function'); - } - - // Sanity checks passed. - Blockly.Extensions.register(name, function() { - if (hasMutatorDialog) { - this.setMutator(new Blockly.Mutator(opt_blockList)); - } - // Mixin the object. - this.mixin(mixinObj); - - if (opt_helperFn) { - opt_helperFn.apply(this); - } - }); -}; - -/** - * Applies an extension method to a block. This should only be called during - * block construction. - * @param {string} name The name of the extension. - * @param {!Blockly.Block} block The block to apply the named extension to. - * @param {boolean} isMutator True if this extension defines a mutator. - * @throws {Error} if the extension is not found. - */ -Blockly.Extensions.apply = function(name, block, isMutator) { - var extensionFn = Blockly.Extensions.ALL_[name]; - if (!goog.isFunction(extensionFn)) { - throw new Error('Error: Extension "' + name + '" not found.'); - } - if (isMutator) { - // Fail early if the block already has mutation properties. - Blockly.Extensions.checkNoMutatorProperties_(name, block); - } else { - // Record the old properties so we can make sure they don't change after - // applying the extension. - var mutatorProperties = Blockly.Extensions.getMutatorProperties_(block); - } - extensionFn.apply(block); - - if (isMutator) { - var errorPrefix = 'Error after applying mutator "' + name + '": '; - Blockly.Extensions.checkBlockHasMutatorProperties_(name, block, errorPrefix); - } else { - if (!Blockly.Extensions.mutatorPropertiesMatch_(mutatorProperties, block)) { - throw new Error('Error when applying extension "' + name + - '": mutation properties changed when applying a non-mutator extension.'); - } - } -}; - -/** - * Check that the given object has a property with the given name, and that the - * property is a function. - * @param {string} errorPrefix The string to prepend to any error message. - * @param {!Object} object The object to check. - * @param {string} propertyName Which property to check. - * @throws {Error} if the property does not exist or is not a function. - * @private - */ -Blockly.Extensions.checkHasFunction_ = function(errorPrefix, object, - propertyName) { - if (!object.hasOwnProperty(propertyName)) { - throw new Error(errorPrefix + - 'missing required property "' + propertyName + '"'); - } else if (typeof object[propertyName] !== "function") { - throw new Error(errorPrefix + - '" required property "' + propertyName + '" must be a function'); - } -}; - -/** - * Check that the given block does not have any of the four mutator properties - * defined on it. This function should be called before applying a mutator - * extension to a block, to make sure we are not overwriting properties. - * @param {string} mutationName The name of the mutation to reference in error - * messages. - * @param {!Blockly.Block} block The block to check. - * @throws {Error} if any of the properties already exist on the block. - * @private - */ -Blockly.Extensions.checkNoMutatorProperties_ = function(mutationName, block) { - for (var i = 0; i < Blockly.Extensions.MUTATOR_PROPERTIES_.length; i++) { - var propertyName = Blockly.Extensions.MUTATOR_PROPERTIES_[i]; - if (block.hasOwnProperty(propertyName)) { - throw new Error('Error: tried to apply mutation "' + mutationName + - '" to a block that already has a "' + propertyName + - '" function. Block id: ' + block.id); - } - } -}; - -/** - * Check that the given object has both or neither of the functions required - * to have a mutator dialog. - * These functions are 'compose' and 'decompose'. If a block has one, it must - * have both. - * @param {!Object} object The object to check. - * @param {string} errorPrefix The string to prepend to any error message. - * @return {boolean} True if the object has both functions. False if it has - * neither function. - * @throws {Error} if the object has only one of the functions. - * @private - */ -Blockly.Extensions.checkMutatorDialog_ = function(object, errorPrefix) { - var hasCompose = object.hasOwnProperty('compose'); - var hasDecompose = object.hasOwnProperty('decompose'); - - if (hasCompose && hasDecompose) { - if (typeof object['compose'] !== "function") { - throw new Error(errorPrefix + 'compose must be a function.'); - } else if (typeof object['decompose'] !== "function") { - throw new Error(errorPrefix + 'decompose must be a function.'); - } - return true; - } else if (!hasCompose && !hasDecompose) { - return false; - } else { - throw new Error(errorPrefix + - 'Must have both or neither of "compose" and "decompose"'); - } -}; - -/** - * Check that a block has required mutator properties. This should be called - * after applying a mutation extension. - * @param {string} errorPrefix The string to prepend to any error message. - * @param {!Blockly.Block} block The block to inspect. - * @private - */ -Blockly.Extensions.checkBlockHasMutatorProperties_ = function(errorPrefix, - block) { - if (!block.hasOwnProperty('domToMutation')) { - throw new Error(errorPrefix + 'Applying a mutator didn\'t add "domToMutation"'); - } - if (!block.hasOwnProperty('mutationToDom')) { - throw new Error(errorPrefix + 'Applying a mutator didn\'t add "mutationToDom"'); - } - - // A block with a mutator isn't required to have a mutation dialog, but - // it should still have both or neither of compose and decompose. - Blockly.Extensions.checkMutatorDialog_(block, errorPrefix); -}; - -/** - * Get a list of values of mutator properties on the given block. - * @param {!Blockly.Block} block The block to inspect. - * @return {!Array.} a list with all of the properties, which should be - * functions or undefined, but are not guaranteed to be. - * @private - */ -Blockly.Extensions.getMutatorProperties_ = function(block) { - var result = []; - for (var i = 0; i < Blockly.Extensions.MUTATOR_PROPERTIES_.length; i++) { - result.push(block[Blockly.Extensions.MUTATOR_PROPERTIES_[i]]); - } - return result; -}; - -/** - * Check that the current mutator properties match a list of old mutator - * properties. This should be called after applying a non-mutator extension, - * to verify that the extension didn't change properties it shouldn't. - * @param {!Array.} oldProperties The old values to compare to. - * @param {!Blockly.Block} block The block to inspect for new values. - * @return {boolean} True if the property lists match. - * @private - */ -Blockly.Extensions.mutatorPropertiesMatch_ = function(oldProperties, block) { - var match = true; - var newProperties = Blockly.Extensions.getMutatorProperties_(block); - if (newProperties.length != oldProperties.length) { - match = false; - } else { - for (var i = 0; i < newProperties.length; i++) { - if (oldProperties[i] != newProperties[i]) { - match = false; - } - } - } - - return match; -}; - -/** - * Builds an extension function that will map a dropdown value to a tooltip - * string. - * - * This method includes multiple checks to ensure tooltips, dropdown options, - * and message references are aligned. This aims to catch errors as early as - * possible, without requiring developers to manually test tooltips under each - * option. After the page is loaded, each tooltip text string will be checked - * for matching message keys in the internationalized string table. Deferring - * this until the page is loaded decouples loading dependencies. Later, upon - * loading the first block of any given type, the extension will validate every - * dropdown option has a matching tooltip in the lookupTable. Errors are - * reported as warnings in the console, and are never fatal. - * @param {string} dropdownName The name of the field whose value is the key - * to the lookup table. - * @param {!Object} lookupTable The table of field values to - * tooltip text. - * @return {Function} The extension function. - */ -Blockly.Extensions.buildTooltipForDropdown = function(dropdownName, lookupTable) { - // List of block types already validated, to minimize duplicate warnings. - var blockTypesChecked = []; - - // Check the tooltip string messages for invalid references. - // Wait for load, in case Blockly.Msg is not yet populated. - // runAfterPageLoad() does not run in a Node.js environment due to lack of - // document object, in which case skip the validation. - if (document) { // Relies on document.readyState - Blockly.utils.runAfterPageLoad(function() { - for (var key in lookupTable) { - // Will print warnings is reference is missing. - Blockly.utils.checkMessageReferences(lookupTable[key]); - } - }); - } - - /** - * The actual extension. - * @this {Blockly.Block} - */ - var extensionFn = function() { - if (this.type && blockTypesChecked.indexOf(this.type) === -1) { - Blockly.Extensions.checkDropdownOptionsInTable_( - this, dropdownName, lookupTable); - blockTypesChecked.push(this.type); - } - - this.setTooltip(function() { - var value = this.getFieldValue(dropdownName); - var tooltip = lookupTable[value]; - if (tooltip == null) { - if (blockTypesChecked.indexOf(this.type) === -1) { - // Warn for missing values on generated tooltips - var warning = 'No tooltip mapping for value ' + value + - ' of field ' + dropdownName; - if (this.type != null) { - warning += (' of block type ' + this.type); - } - console.warn(warning + '.'); - } - } else { - tooltip = Blockly.utils.replaceMessageReferences(tooltip); - } - return tooltip; - }.bind(this)); - }; - return extensionFn; -}; - -/** - * Checks all options keys are present in the provided string lookup table. - * Emits console warnings when they are not. - * @param {!Blockly.Block} block The block containing the dropdown - * @param {string} dropdownName The name of the dropdown - * @param {!Object} lookupTable The string lookup table - * @private - */ -Blockly.Extensions.checkDropdownOptionsInTable_ = - function(block, dropdownName, lookupTable) { - // Validate all dropdown options have values. - var dropdown = block.getField(dropdownName); - if (!dropdown.isOptionListDynamic()) { - var options = dropdown.getOptions(); - for (var i = 0; i < options.length; ++i) { - var optionKey = options[i][1]; // label, then value - if (lookupTable[optionKey] == null) { - console.warn('No tooltip mapping for value ' + optionKey + - ' of field ' + dropdownName + ' of block type ' + block.type); - } - } - } - }; - -/** - * Builds an extension function that will install a dynamic tooltip. The - * tooltip message should include the string '%1' and that string will be - * replaced with the value of the named field. - * @param {string} msgTemplate The template form to of the message text, with - * %1 placeholder. - * @param {string} fieldName The field with the replacement value. - * @returns {Function} The extension function. - */ -Blockly.Extensions.buildTooltipWithFieldValue = - function(msgTemplate, fieldName) { - // Check the tooltip string messages for invalid references. - // Wait for load, in case Blockly.Msg is not yet populated. - // runAfterPageLoad() does not run in a Node.js environment due to lack of - // document object, in which case skip the validation. - if (document) { // Relies on document.readyState - Blockly.utils.runAfterPageLoad(function() { - // Will print warnings is reference is missing. - Blockly.utils.checkMessageReferences(msgTemplate); - }); - } - - /** - * The actual extension. - * @this {Blockly.Block} - */ - var extensionFn = function() { - this.setTooltip(function() { - return Blockly.utils.replaceMessageReferences(msgTemplate) - .replace('%1', this.getFieldValue(fieldName)); - }.bind(this)); - }; - return extensionFn; - }; - -/** - * Configures the tooltip to mimic the parent block when connected. Otherwise, - * uses the tooltip text at the time this extension is initialized. This takes - * advantage of the fact that all other values from JSON are initialized before - * extensions. - * @this {Blockly.Block} - * @private - */ -Blockly.Extensions.extensionParentTooltip_ = function() { - this.tooltipWhenNotConnected_ = this.tooltip; - this.setTooltip(function() { - var parent = this.getParent(); - return (parent && - parent.getInputsInline() && - parent.tooltip) || - this.tooltipWhenNotConnected_; - }.bind(this)); -}; -Blockly.Extensions.register('parent_tooltip_when_inline', - Blockly.Extensions.extensionParentTooltip_); - - diff --git a/core/extensions.ts b/core/extensions.ts new file mode 100644 index 00000000000..0957b7f86ca --- /dev/null +++ b/core/extensions.ts @@ -0,0 +1,494 @@ +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.Extensions + +import type {Block} from './block.js'; +import type {BlockSvg} from './block_svg.js'; +import {FieldDropdown} from './field_dropdown.js'; +import {MutatorIcon} from './icons/mutator_icon.js'; +import * as parsing from './utils/parsing.js'; + +/** The set of all registered extensions, keyed by extension name/id. */ +const allExtensions = Object.create(null); +export const TEST_ONLY = {allExtensions}; + +/** + * Registers a new extension function. Extensions are functions that help + * initialize blocks, usually adding dynamic behavior such as onchange + * handlers and mutators. These are applied using Block.applyExtension(), or + * the JSON "extensions" array attribute. + * + * @param name The name of this extension. + * @param initFn The function to initialize an extended block. + * @throws {Error} if the extension name is empty, the extension is already + * registered, or extensionFn is not a function. + */ +export function register( + name: string, + initFn: (this: T) => void, +) { + if (typeof name !== 'string' || name.trim() === '') { + throw Error('Error: Invalid extension name "' + name + '"'); + } + if (allExtensions[name]) { + throw Error('Error: Extension "' + name + '" is already registered.'); + } + if (typeof initFn !== 'function') { + throw Error('Error: Extension "' + name + '" must be a function'); + } + allExtensions[name] = initFn; +} + +/** + * Registers a new extension function that adds all key/value of mixinObj. + * + * @param name The name of this extension. + * @param mixinObj The values to mix in. + * @throws {Error} if the extension name is empty or the extension is already + * registered. + */ +export function registerMixin(name: string, mixinObj: AnyDuringMigration) { + if (!mixinObj || typeof mixinObj !== 'object') { + throw Error('Error: Mixin "' + name + '" must be a object'); + } + register(name, function (this: Block) { + this.mixin(mixinObj); + }); +} + +/** + * Registers a new extension function that adds a mutator to the block. + * At register time this performs some basic sanity checks on the mutator. + * The wrapper may also add a mutator dialog to the block, if both compose and + * decompose are defined on the mixin. + * + * @param name The name of this mutator extension. + * @param mixinObj The values to mix in. + * @param opt_helperFn An optional function to apply after mixing in the object. + * @param opt_blockList A list of blocks to appear in the flyout of the mutator + * dialog. + * @throws {Error} if the mutation is invalid or can't be applied to the block. + */ +export function registerMutator( + name: string, + mixinObj: AnyDuringMigration, + opt_helperFn?: () => AnyDuringMigration, + opt_blockList?: string[], +) { + const errorPrefix = 'Error when registering mutator "' + name + '": '; + + checkHasMutatorProperties(errorPrefix, mixinObj); + const hasMutatorDialog = checkMutatorDialog(mixinObj, errorPrefix); + + if (opt_helperFn && typeof opt_helperFn !== 'function') { + throw Error(errorPrefix + 'Extension "' + name + '" is not a function'); + } + + // Sanity checks passed. + register(name, function (this: Block) { + if (hasMutatorDialog) { + this.setMutator(new MutatorIcon(opt_blockList || [], this as BlockSvg)); + } + // Mixin the object. + this.mixin(mixinObj); + + if (opt_helperFn) { + opt_helperFn.apply(this); + } + }); +} + +/** + * Unregisters the extension registered with the given name. + * + * @param name The name of the extension to unregister. + */ +export function unregister(name: string) { + if (isRegistered(name)) { + delete allExtensions[name]; + } else { + console.warn( + 'No extension mapping for name "' + name + '" found to unregister', + ); + } +} + +/** + * Returns whether an extension is registered with the given name. + * + * @param name The name of the extension to check for. + * @returns True if the extension is registered. False if it is not registered. + */ +export function isRegistered(name: string): boolean { + return !!allExtensions[name]; +} + +/** + * Applies an extension method to a block. This should only be called during + * block construction. + * + * @param name The name of the extension. + * @param block The block to apply the named extension to. + * @param isMutator True if this extension defines a mutator. + * @throws {Error} if the extension is not found. + */ +export function apply(name: string, block: Block, isMutator: boolean) { + const extensionFn = allExtensions[name]; + if (typeof extensionFn !== 'function') { + throw Error('Error: Extension "' + name + '" not found.'); + } + let mutatorProperties; + if (isMutator) { + // Fail early if the block already has mutation properties. + checkNoMutatorProperties(name, block); + } else { + // Record the old properties so we can make sure they don't change after + // applying the extension. + mutatorProperties = getMutatorProperties(block); + } + extensionFn.apply(block); + + if (isMutator) { + const errorPrefix = 'Error after applying mutator "' + name + '": '; + checkHasMutatorProperties(errorPrefix, block); + } else { + if ( + !mutatorPropertiesMatch(mutatorProperties as AnyDuringMigration[], block) + ) { + throw Error( + 'Error when applying extension "' + + name + + '": ' + + 'mutation properties changed when applying a non-mutator extension.', + ); + } + } +} + +/** + * Check that the given block does not have any of the four mutator properties + * defined on it. This function should be called before applying a mutator + * extension to a block, to make sure we are not overwriting properties. + * + * @param mutationName The name of the mutation to reference in error messages. + * @param block The block to check. + * @throws {Error} if any of the properties already exist on the block. + */ +function checkNoMutatorProperties(mutationName: string, block: Block) { + const properties = getMutatorProperties(block); + if (properties.length) { + throw Error( + 'Error: tried to apply mutation "' + + mutationName + + '" to a block that already has mutator functions.' + + ' Block id: ' + + block.id, + ); + } +} + +/** + * Checks if the given object has both the 'mutationToDom' and 'domToMutation' + * functions. + * + * @param object The object to check. + * @param errorPrefix The string to prepend to any error message. + * @returns True if the object has both functions. False if it has neither + * function. + * @throws {Error} if the object has only one of the functions, or either is not + * actually a function. + */ +function checkXmlHooks( + object: AnyDuringMigration, + errorPrefix: string, +): boolean { + return checkHasFunctionPair( + object.mutationToDom, + object.domToMutation, + errorPrefix + ' mutationToDom/domToMutation', + ); +} +/** + * Checks if the given object has both the 'saveExtraState' and 'loadExtraState' + * functions. + * + * @param object The object to check. + * @param errorPrefix The string to prepend to any error message. + * @returns True if the object has both functions. False if it has neither + * function. + * @throws {Error} if the object has only one of the functions, or either is not + * actually a function. + */ +function checkJsonHooks( + object: AnyDuringMigration, + errorPrefix: string, +): boolean { + return checkHasFunctionPair( + object.saveExtraState, + object.loadExtraState, + errorPrefix + ' saveExtraState/loadExtraState', + ); +} + +/** + * Checks if the given object has both the 'compose' and 'decompose' functions. + * + * @param object The object to check. + * @param errorPrefix The string to prepend to any error message. + * @returns True if the object has both functions. False if it has neither + * function. + * @throws {Error} if the object has only one of the functions, or either is not + * actually a function. + */ +function checkMutatorDialog( + object: AnyDuringMigration, + errorPrefix: string, +): boolean { + return checkHasFunctionPair( + object.compose, + object.decompose, + errorPrefix + ' compose/decompose', + ); +} + +/** + * Checks that both or neither of the given functions exist and that they are + * indeed functions. + * + * @param func1 The first function in the pair. + * @param func2 The second function in the pair. + * @param errorPrefix The string to prepend to any error message. + * @returns True if the object has both functions. False if it has neither + * function. + * @throws {Error} If the object has only one of the functions, or either is not + * actually a function. + */ +function checkHasFunctionPair( + func1: AnyDuringMigration, + func2: AnyDuringMigration, + errorPrefix: string, +): boolean { + if (func1 && func2) { + if (typeof func1 !== 'function' || typeof func2 !== 'function') { + throw Error(errorPrefix + ' must be a function'); + } + return true; + } else if (!func1 && !func2) { + return false; + } + throw Error(errorPrefix + 'Must have both or neither functions'); +} + +/** + * Checks that the given object required mutator properties. + * + * @param errorPrefix The string to prepend to any error message. + * @param object The object to inspect. + */ +function checkHasMutatorProperties( + errorPrefix: string, + object: AnyDuringMigration, +) { + const hasXmlHooks = checkXmlHooks(object, errorPrefix); + const hasJsonHooks = checkJsonHooks(object, errorPrefix); + if (!hasXmlHooks && !hasJsonHooks) { + throw Error( + errorPrefix + + 'Mutations must contain either XML hooks, or JSON hooks, or both', + ); + } + // A block with a mutator isn't required to have a mutation dialog, but + // it should still have both or neither of compose and decompose. + checkMutatorDialog(object, errorPrefix); +} + +/** + * Get a list of values of mutator properties on the given block. + * + * @param block The block to inspect. + * @returns A list with all of the defined properties, which should be + * functions, but may be anything other than undefined. + */ +function getMutatorProperties(block: Block): AnyDuringMigration[] { + const result = []; + // List each function explicitly by reference to allow for renaming + // during compilation. + if (block.domToMutation !== undefined) { + result.push(block.domToMutation); + } + if (block.mutationToDom !== undefined) { + result.push(block.mutationToDom); + } + if (block.saveExtraState !== undefined) { + result.push(block.saveExtraState); + } + if (block.loadExtraState !== undefined) { + result.push(block.loadExtraState); + } + if (block.compose !== undefined) { + result.push(block.compose); + } + if (block.decompose !== undefined) { + result.push(block.decompose); + } + return result; +} + +/** + * Check that the current mutator properties match a list of old mutator + * properties. This should be called after applying a non-mutator extension, + * to verify that the extension didn't change properties it shouldn't. + * + * @param oldProperties The old values to compare to. + * @param block The block to inspect for new values. + * @returns True if the property lists match. + */ +function mutatorPropertiesMatch( + oldProperties: AnyDuringMigration[], + block: Block, +): boolean { + const newProperties = getMutatorProperties(block); + if (newProperties.length !== oldProperties.length) { + return false; + } + for (let i = 0; i < newProperties.length; i++) { + if (oldProperties[i] !== newProperties[i]) { + return false; + } + } + return true; +} + +/** + * Calls a function after the page has loaded, possibly immediately. + * + * @param fn Function to run. + * @throws Error Will throw if no global document can be found (e.g., Node.js). + * @internal + */ +export function runAfterPageLoad(fn: () => void) { + if (typeof document !== 'object') { + throw Error('runAfterPageLoad() requires browser document.'); + } + if (document.readyState === 'complete') { + fn(); // Page has already loaded. Call immediately. + } else { + // Poll readyState. + const readyStateCheckInterval = setInterval(function () { + if (document.readyState === 'complete') { + clearInterval(readyStateCheckInterval); + fn(); + } + }, 10); + } +} + +/** + * Builds an extension function that will map a dropdown value to a tooltip + * string. + * + * @param dropdownName The name of the field whose value is the key to the + * lookup table. + * @param lookupTable The table of field values to tooltip text. + * @returns The extension function. + */ +export function buildTooltipForDropdown( + dropdownName: string, + lookupTable: {[key: string]: string}, +): (this: Block) => void { + // List of block types already validated, to minimize duplicate warnings. + const blockTypesChecked: string[] = []; + + return function (this: Block) { + if (!blockTypesChecked.includes(this.type)) { + checkDropdownOptionsInTable(this, dropdownName, lookupTable); + blockTypesChecked.push(this.type); + } + + this.setTooltip( + function (this: Block) { + const value = String(this.getFieldValue(dropdownName)); + return parsing.replaceMessageReferences(lookupTable[value]); + }.bind(this), + ); + }; +} + +/** + * Checks all options keys are present in the provided string lookup table. + * Emits console warnings when they are not. + * + * @param block The block containing the dropdown + * @param dropdownName The name of the dropdown + * @param lookupTable The string lookup table + */ +function checkDropdownOptionsInTable( + block: Block, + dropdownName: string, + lookupTable: {[key: string]: string}, +) { + const dropdown = block.getField(dropdownName); + if (!(dropdown instanceof FieldDropdown) || dropdown.isOptionListDynamic()) { + return; + } + + const options = dropdown.getOptions(); + for (const [, key] of options) { + if (lookupTable[key] === undefined) { + console.warn( + `No tooltip mapping for value ${key} of field ` + + `${dropdownName} of block type ${block.type}.`, + ); + } + } +} + +/** + * Builds an extension function that will install a dynamic tooltip. The + * tooltip message should include the string '%1' and that string will be + * replaced with the text of the named field. + * + * @param msgTemplate The template form to of the message text, with %1 + * placeholder. + * @param fieldName The field with the replacement text. + * @returns The extension function. + */ +export function buildTooltipWithFieldText( + msgTemplate: string, + fieldName: string, +): (this: Block) => void { + return function (this: Block) { + this.setTooltip( + function (this: Block) { + const field = this.getField(fieldName); + return parsing + .replaceMessageReferences(msgTemplate) + .replace('%1', field ? field.getText() : ''); + }.bind(this), + ); + }; +} + +/** + * Configures the tooltip to mimic the parent block when connected. Otherwise, + * uses the tooltip text at the time this extension is initialized. This takes + * advantage of the fact that all other values from JSON are initialized before + * extensions. + */ +function extensionParentTooltip(this: Block) { + const tooltipWhenNotConnected = this.tooltip; + this.setTooltip( + function (this: Block) { + const parent = this.getParent(); + return ( + (parent && parent.getInputsInline() && parent.tooltip) || + tooltipWhenNotConnected + ); + }.bind(this), + ); +} +register('parent_tooltip_when_inline', extensionParentTooltip); diff --git a/core/field.js b/core/field.js deleted file mode 100644 index 4060ac62b3a..00000000000 --- a/core/field.js +++ /dev/null @@ -1,537 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Field. Used for editable titles, variables, etc. - * This is an abstract class that defines the UI on the block. Actual - * instances would be Blockly.FieldTextInput, Blockly.FieldDropdown, etc. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Field'); - -goog.require('Blockly.Gesture'); - -goog.require('goog.asserts'); -goog.require('goog.dom'); -goog.require('goog.math.Size'); -goog.require('goog.style'); -goog.require('goog.userAgent'); - - -/** - * Abstract class for an editable field. - * @param {string} text The initial content of the field. - * @param {Function=} opt_validator An optional function that is called - * to validate any constraints on what the user entered. Takes the new - * text as an argument and returns either the accepted text, a replacement - * text, or null to abort the change. - * @constructor - */ -Blockly.Field = function(text, opt_validator) { - this.size_ = new goog.math.Size(0, Blockly.BlockSvg.MIN_BLOCK_Y); - this.setValue(text); - this.setValidator(opt_validator); -}; - -/** - * Temporary cache of text widths. - * @type {Object} - * @private - */ -Blockly.Field.cacheWidths_ = null; - -/** - * Number of current references to cache. - * @type {number} - * @private - */ -Blockly.Field.cacheReference_ = 0; - - -/** - * Name of field. Unique within each block. - * Static labels are usually unnamed. - * @type {string|undefined} - */ -Blockly.Field.prototype.name = undefined; - -/** - * Maximum characters of text to display before adding an ellipsis. - * @type {number} - */ -Blockly.Field.prototype.maxDisplayLength = 50; - -/** - * Visible text to display. - * @type {string} - * @private - */ -Blockly.Field.prototype.text_ = ''; - -/** - * Block this field is attached to. Starts as null, then in set in init. - * @type {Blockly.Block} - * @private - */ -Blockly.Field.prototype.sourceBlock_ = null; - -/** - * Is the field visible, or hidden due to the block being collapsed? - * @type {boolean} - * @private - */ -Blockly.Field.prototype.visible_ = true; - -/** - * Validation function called when user edits an editable field. - * @type {Function} - * @private - */ -Blockly.Field.prototype.validator_ = null; - -/** - * Non-breaking space. - * @const - */ -Blockly.Field.NBSP = '\u00A0'; - -/** - * Editable fields are saved by the XML renderer, non-editable fields are not. - */ -Blockly.Field.prototype.EDITABLE = true; - -/** - * Attach this field to a block. - * @param {!Blockly.Block} block The block containing this field. - */ -Blockly.Field.prototype.setSourceBlock = function(block) { - goog.asserts.assert(!this.sourceBlock_, 'Field already bound to a block.'); - this.sourceBlock_ = block; -}; - -/** - * Install this field on a block. - */ -Blockly.Field.prototype.init = function() { - if (this.fieldGroup_) { - // Field has already been initialized once. - return; - } - // Build the DOM. - this.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null); - if (!this.visible_) { - this.fieldGroup_.style.display = 'none'; - } - this.borderRect_ = Blockly.utils.createSvgElement('rect', - {'rx': 4, - 'ry': 4, - 'x': -Blockly.BlockSvg.SEP_SPACE_X / 2, - 'y': 0, - 'height': 16}, this.fieldGroup_, this.sourceBlock_.workspace); - /** @type {!Element} */ - this.textElement_ = Blockly.utils.createSvgElement('text', - {'class': 'blocklyText', 'y': this.size_.height - 12.5}, - this.fieldGroup_); - - this.updateEditable(); - this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_); - this.mouseDownWrapper_ = - Blockly.bindEventWithChecks_(this.fieldGroup_, 'mousedown', this, - this.onMouseDown_); - // Force a render. - this.render_(); -}; - -/** - * Initializes the model of the field after it has been installed on a block. - * No-op by default. - */ -Blockly.Field.prototype.initModel = function() { -}; - -/** - * Dispose of all DOM objects belonging to this editable field. - */ -Blockly.Field.prototype.dispose = function() { - if (this.mouseDownWrapper_) { - Blockly.unbindEvent_(this.mouseDownWrapper_); - this.mouseDownWrapper_ = null; - } - this.sourceBlock_ = null; - goog.dom.removeNode(this.fieldGroup_); - this.fieldGroup_ = null; - this.textElement_ = null; - this.borderRect_ = null; - this.validator_ = null; -}; - -/** - * Add or remove the UI indicating if this field is editable or not. - */ -Blockly.Field.prototype.updateEditable = function() { - var group = this.fieldGroup_; - if (!this.EDITABLE || !group) { - return; - } - if (this.sourceBlock_.isEditable()) { - Blockly.utils.addClass(group, 'blocklyEditableText'); - Blockly.utils.removeClass(group, 'blocklyNonEditableText'); - this.fieldGroup_.style.cursor = this.CURSOR; - } else { - Blockly.utils.addClass(group, 'blocklyNonEditableText'); - Blockly.utils.removeClass(group, 'blocklyEditableText'); - this.fieldGroup_.style.cursor = ''; - } -}; - -/** - * Check whether this field is currently editable. Some fields are never - * editable (e.g. text labels). Those fields are not serialized to XML. Other - * fields may be editable, and therefore serialized, but may exist on - * non-editable blocks. - * @return {boolean} whether this field is editable and on an editable block - */ -Blockly.Field.prototype.isCurrentlyEditable = function() { - return this.EDITABLE && !!this.sourceBlock_ && this.sourceBlock_.isEditable(); -}; - -/** - * Gets whether this editable field is visible or not. - * @return {boolean} True if visible. - */ -Blockly.Field.prototype.isVisible = function() { - return this.visible_; -}; - -/** - * Sets whether this editable field is visible or not. - * @param {boolean} visible True if visible. - */ -Blockly.Field.prototype.setVisible = function(visible) { - if (this.visible_ == visible) { - return; - } - this.visible_ = visible; - var root = this.getSvgRoot(); - if (root) { - root.style.display = visible ? 'block' : 'none'; - this.render_(); - } -}; - -/** - * Sets a new validation function for editable fields. - * @param {Function} handler New validation function, or null. - */ -Blockly.Field.prototype.setValidator = function(handler) { - this.validator_ = handler; -}; - -/** - * Gets the validation function for editable fields. - * @return {Function} Validation function, or null. - */ -Blockly.Field.prototype.getValidator = function() { - return this.validator_; -}; - -/** - * Validates a change. Does nothing. Subclasses may override this. - * @param {string} text The user's text. - * @return {string} No change needed. - */ -Blockly.Field.prototype.classValidator = function(text) { - return text; -}; - -/** - * Calls the validation function for this field, as well as all the validation - * function for the field's class and its parents. - * @param {string} text Proposed text. - * @return {?string} Revised text, or null if invalid. - */ -Blockly.Field.prototype.callValidator = function(text) { - var classResult = this.classValidator(text); - if (classResult === null) { - // Class validator rejects value. Game over. - return null; - } else if (classResult !== undefined) { - text = classResult; - } - var userValidator = this.getValidator(); - if (userValidator) { - var userResult = userValidator.call(this, text); - if (userResult === null) { - // User validator rejects value. Game over. - return null; - } else if (userResult !== undefined) { - text = userResult; - } - } - return text; -}; - -/** - * Gets the group element for this editable field. - * Used for measuring the size and for positioning. - * @return {!Element} The group element. - */ -Blockly.Field.prototype.getSvgRoot = function() { - return /** @type {!Element} */ (this.fieldGroup_); -}; - -/** - * Draws the border with the correct width. - * Saves the computed width in a property. - * @private - */ -Blockly.Field.prototype.render_ = function() { - if (!this.visible_) { - this.size_.width = 0; - return; - } - - // Replace the text. - goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_)); - var textNode = document.createTextNode(this.getDisplayText_()); - this.textElement_.appendChild(textNode); - - this.updateWidth(); -}; - -/** - * Updates thw width of the field. This calls getCachedWidth which won't cache - * the approximated width on IE/Edge when `getComputedTextLength` fails. Once - * it eventually does succeed, the result will be cached. - **/ -Blockly.Field.prototype.updateWidth = function() { - var width = Blockly.Field.getCachedWidth(this.textElement_); - if (this.borderRect_) { - this.borderRect_.setAttribute('width', - width + Blockly.BlockSvg.SEP_SPACE_X); - } - this.size_.width = width; -}; - -/** - * Gets the width of a text element, caching it in the process. - * @param {!Element} textElement An SVG 'text' element. - * @return {number} Width of element. - */ -Blockly.Field.getCachedWidth = function(textElement) { - var key = textElement.textContent + '\n' + textElement.className.baseVal; - var width; - - // Return the cached width if it exists. - if (Blockly.Field.cacheWidths_) { - width = Blockly.Field.cacheWidths_[key]; - if (width) { - return width; - } - } - - // Attempt to compute fetch the width of the SVG text element. - try { - width = textElement.getComputedTextLength(); - } catch (e) { - // MSIE 11 and Edge are known to throw "Unexpected call to method or - // property access." if the block is hidden. Instead, use an - // approximation and do not cache the result. At some later point in time - // when the block is inserted into the visible DOM, this method will be - // called again and, at that point in time, will not throw an exception. - return textElement.textContent.length * 8; - } - - // Cache the computed width and return. - if (Blockly.Field.cacheWidths_) { - Blockly.Field.cacheWidths_[key] = width; - } - return width; -}; - -/** - * Start caching field widths. Every call to this function MUST also call - * stopCache. Caches must not survive between execution threads. - */ -Blockly.Field.startCache = function() { - Blockly.Field.cacheReference_++; - if (!Blockly.Field.cacheWidths_) { - Blockly.Field.cacheWidths_ = {}; - } -}; - -/** - * Stop caching field widths. Unless caching was already on when the - * corresponding call to startCache was made. - */ -Blockly.Field.stopCache = function() { - Blockly.Field.cacheReference_--; - if (!Blockly.Field.cacheReference_) { - Blockly.Field.cacheWidths_ = null; - } -}; - -/** - * Returns the height and width of the field. - * @return {!goog.math.Size} Height and width. - */ -Blockly.Field.prototype.getSize = function() { - if (!this.size_.width) { - this.render_(); - } - return this.size_; -}; - -/** - * Returns the height and width of the field, - * accounting for the workspace scaling. - * @return {!goog.math.Size} Height and width. - * @private - */ -Blockly.Field.prototype.getScaledBBox_ = function() { - var bBox = this.borderRect_.getBBox(); - // Create new object, as getBBox can return an uneditable SVGRect in IE. - return new goog.math.Size(bBox.width * this.sourceBlock_.workspace.scale, - bBox.height * this.sourceBlock_.workspace.scale); -}; - -/** - * Get the text from this field as displayed on screen. May differ from getText - * due to ellipsis, and other formatting. - * @return {string} Currently displayed text. - * @private - */ -Blockly.Field.prototype.getDisplayText_ = function() { - var text = this.text_; - if (!text) { - // Prevent the field from disappearing if empty. - return Blockly.Field.NBSP; - } - if (text.length > this.maxDisplayLength) { - // Truncate displayed string and add an ellipsis ('...'). - text = text.substring(0, this.maxDisplayLength - 2) + '\u2026'; - } - // Replace whitespace with non-breaking spaces so the text doesn't collapse. - text = text.replace(/\s/g, Blockly.Field.NBSP); - if (this.sourceBlock_.RTL) { - // The SVG is LTR, force text to be RTL. - text += '\u200F'; - } - return text; -}; - -/** - * Get the text from this field. - * @return {string} Current text. - */ -Blockly.Field.prototype.getText = function() { - return this.text_; -}; - -/** - * Set the text in this field. Trigger a rerender of the source block. - * @param {*} newText New text. - */ -Blockly.Field.prototype.setText = function(newText) { - if (newText === null) { - // No change if null. - return; - } - newText = String(newText); - if (newText === this.text_) { - // No change. - return; - } - this.text_ = newText; - // Set width to 0 to force a rerender of this field. - this.size_.width = 0; - - if (this.sourceBlock_ && this.sourceBlock_.rendered) { - this.sourceBlock_.render(); - this.sourceBlock_.bumpNeighbours_(); - } -}; - -/** - * By default there is no difference between the human-readable text and - * the language-neutral values. Subclasses (such as dropdown) may define this. - * @return {string} Current value. - */ -Blockly.Field.prototype.getValue = function() { - return this.getText(); -}; - -/** - * By default there is no difference between the human-readable text and - * the language-neutral values. Subclasses (such as dropdown) may define this. - * @param {string} newValue New value. - */ -Blockly.Field.prototype.setValue = function(newValue) { - if (newValue === null) { - // No change if null. - return; - } - var oldValue = this.getValue(); - if (oldValue == newValue) { - return; - } - if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, oldValue, newValue)); - } - this.setText(newValue); -}; - -/** - * Handle a mouse down event on a field. - * @param {!Event} e Mouse down event. - * @private - */ -Blockly.Field.prototype.onMouseDown_ = function(e) { - if (!this.sourceBlock_ || !this.sourceBlock_.workspace) { - return; - } - var gesture = this.sourceBlock_.workspace.getGesture(e); - if (gesture) { - gesture.setStartField(this); - } -}; - - -/** - * Change the tooltip text for this field. - * @param {string|!Element} newTip Text for tooltip or a parent element to - * link to for its tooltip. - */ -Blockly.Field.prototype.setTooltip = function(newTip) { - // Non-abstract sub-classes may wish to implement this. See FieldLabel. -}; - -/** - * Return the absolute coordinates of the top-left corner of this field. - * The origin (0,0) is the top-left corner of the page body. - * @return {!goog.math.Coordinate} Object with .x and .y properties. - * @private - */ -Blockly.Field.prototype.getAbsoluteXY_ = function() { - return goog.style.getPageOffset(this.borderRect_); -}; diff --git a/core/field.ts b/core/field.ts new file mode 100644 index 00000000000..4c4b90cf55a --- /dev/null +++ b/core/field.ts @@ -0,0 +1,1496 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Field. Used for editable titles, variables, etc. + * This is an abstract class that defines the UI on the block. Actual + * instances would be FieldTextInput, FieldDropdown, etc. + * + * @class + */ +// Former goog.module ID: Blockly.Field + +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_block_change.js'; + +import type {Block} from './block.js'; +import type {BlockSvg} from './block_svg.js'; +import * as browserEvents from './browser_events.js'; +import * as dropDownDiv from './dropdowndiv.js'; +import {EventType} from './events/type.js'; +import * as eventUtils from './events/utils.js'; +import type {Input} from './inputs/input.js'; +import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; +import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js'; +import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js'; +import type {IRegistrable} from './interfaces/i_registrable.js'; +import {ISerializable} from './interfaces/i_serializable.js'; +import {MarkerManager} from './marker_manager.js'; +import type {ConstantProvider} from './renderers/common/constants.js'; +import type {KeyboardShortcut} from './shortcut_registry.js'; +import * as Tooltip from './tooltip.js'; +import type {Coordinate} from './utils/coordinate.js'; +import * as dom from './utils/dom.js'; +import * as parsing from './utils/parsing.js'; +import {Rect} from './utils/rect.js'; +import {Size} from './utils/size.js'; +import * as style from './utils/style.js'; +import {Svg} from './utils/svg.js'; +import * as userAgent from './utils/useragent.js'; +import * as utilsXml from './utils/xml.js'; +import * as WidgetDiv from './widgetdiv.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * A function that is called to validate changes to the field's value before + * they are set. + * + * @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values} + * @param newValue The value to be validated. + * @returns One of three instructions for setting the new value: `T`, `null`, + * or `undefined`. + * + * - `T` to set this function's returned value instead of `newValue`. + * + * - `null` to invoke `doValueInvalid_` and not set a value. + * + * - `undefined` to set `newValue` as is. + */ +export type FieldValidator = (newValue: T) => T | null | undefined; + +/** + * Abstract class for an editable field. + * + * @typeParam T - The value stored on the field. + */ +export abstract class Field + implements + IASTNodeLocationSvg, + IASTNodeLocationWithBlock, + IKeyboardAccessible, + IRegistrable, + ISerializable +{ + /** + * To overwrite the default value which is set in **Field**, directly update + * the prototype. + * + * Example: + * `FieldImage.prototype.DEFAULT_VALUE = null;` + */ + DEFAULT_VALUE: T | null = null; + + /** Non-breaking space. */ + static readonly NBSP = '\u00A0'; + + /** + * A value used to signal when a field's constructor should *not* set the + * field's value or run configure_, and should allow a subclass to do that + * instead. + */ + static readonly SKIP_SETUP = Symbol('SKIP_SETUP'); + + /** + * Name of field. Unique within each block. + * Static labels are usually unnamed. + */ + name?: string = undefined; + protected value_: T | null; + + /** Validation function called when user edits an editable field. */ + protected validator_: FieldValidator | null = null; + + /** + * Used to cache the field's tooltip value if setTooltip is called when the + * field is not yet initialized. Is *not* guaranteed to be accurate. + */ + private tooltip: Tooltip.TipInfo | null = null; + protected size_: Size; + + /** + * Holds the cursors svg element when the cursor is attached to the field. + * This is null if there is no cursor on the field. + */ + private cursorSvg: SVGElement | null = null; + + /** + * Holds the markers svg element when the marker is attached to the field. + * This is null if there is no marker on the field. + */ + private markerSvg: SVGElement | null = null; + + /** The rendered field's SVG group element. */ + protected fieldGroup_: SVGGElement | null = null; + + /** The rendered field's SVG border element. */ + protected borderRect_: SVGRectElement | null = null; + + /** The rendered field's SVG text element. */ + protected textElement_: SVGTextElement | null = null; + + /** The rendered field's text content element. */ + protected textContent_: Text | null = null; + + /** Mouse down event listener data. */ + private mouseDownWrapper: browserEvents.Data | null = null; + + /** Constants associated with the source block's renderer. */ + protected constants_: ConstantProvider | null = null; + + /** + * Has this field been disposed of? + * + * @internal + */ + disposed = false; + + /** Maximum characters of text to display before adding an ellipsis. */ + maxDisplayLength = 50; + + /** Block this field is attached to. Starts as null, then set in init. */ + protected sourceBlock_: Block | null = null; + + /** Does this block need to be re-rendered? */ + protected isDirty_ = true; + + /** Is the field visible, or hidden due to the block being collapsed? */ + protected visible_ = true; + + /** + * Can the field value be changed using the editor on an editable block? + */ + protected enabled_ = true; + + /** The element the click handler is bound to. */ + protected clickTarget_: Element | null = null; + + /** + * The prefix field. + * + * @internal + */ + prefixField: string | null = null; + + /** + * The suffix field. + * + * @internal + */ + suffixField: string | null = null; + + /** + * Editable fields usually show some sort of UI indicating they are + * editable. They will also be saved by the serializer. + */ + EDITABLE = true; + + /** + * Serializable fields are saved by the serializer, non-serializable fields + * are not. Editable fields should also be serializable. This is not the + * case by default so that SERIALIZABLE is backwards compatible. + */ + SERIALIZABLE = false; + + /** Mouse cursor style when over the hotspot that initiates the editor. */ + CURSOR = ''; + + /** + * @param value The initial value of the field. + * Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by + * subclasses that want to handle configuration and setting the field value + * after their own constructors have run). + * @param validator A function that is called to validate changes to the + * field's value. Takes in a value & returns a validated value, or null to + * abort the change. + * @param config A map of options used to configure the field. + * Refer to the individual field's documentation for a list of properties + * this parameter supports. + */ + constructor( + value: T | typeof Field.SKIP_SETUP, + validator?: FieldValidator | null, + config?: FieldConfig, + ) { + /** + * A generic value possessed by the field. + * Should generally be non-null, only null when the field is created. + */ + this.value_ = + 'DEFAULT_VALUE' in new.target.prototype + ? new.target.prototype.DEFAULT_VALUE + : this.DEFAULT_VALUE; + + /** The size of the area rendered by the field. */ + this.size_ = new Size(0, 0); + + if (value === Field.SKIP_SETUP) return; + if (config) { + this.configure_(config); + } + this.setValue(value); + if (validator) { + this.setValidator(validator); + } + } + + /** + * Process the configuration map passed to the field. + * + * @param config A map of options used to configure the field. See the + * individual field's documentation for a list of properties this + * parameter supports. + */ + protected configure_(config: FieldConfig) { + // TODO (#2884): Possibly add CSS class config option. + // TODO (#2885): Possibly add cursor config option. + if (config.tooltip) { + this.setTooltip(parsing.replaceMessageReferences(config.tooltip)); + } + } + + /** + * Attach this field to a block. + * + * @param block The block containing this field. + */ + setSourceBlock(block: Block) { + if (this.sourceBlock_) { + throw Error('Field already bound to a block'); + } + this.sourceBlock_ = block; + } + + /** + * Get the renderer constant provider. + * + * @returns The renderer constant provider. + */ + getConstants(): ConstantProvider | null { + if ( + !this.constants_ && + this.sourceBlock_ && + !this.sourceBlock_.isDeadOrDying() && + this.sourceBlock_.workspace.rendered + ) { + this.constants_ = (this.sourceBlock_.workspace as WorkspaceSvg) + .getRenderer() + .getConstants(); + } + return this.constants_; + } + + /** + * Get the block this field is attached to. + * + * @returns The block containing this field. + * @throws An error if the source block is not defined. + */ + getSourceBlock(): Block | null { + return this.sourceBlock_; + } + + /** + * Initialize everything to render this field. Override + * methods initModel and initView rather than this method. + * + * @sealed + * @internal + */ + init() { + if (this.fieldGroup_) { + // Field has already been initialized once. + return; + } + this.fieldGroup_ = dom.createSvgElement(Svg.G, {}); + if (!this.isVisible()) { + this.fieldGroup_.style.display = 'none'; + } + const sourceBlockSvg = this.sourceBlock_ as BlockSvg; + sourceBlockSvg.getSvgRoot().appendChild(this.fieldGroup_); + this.initView(); + this.updateEditable(); + this.setTooltip(this.tooltip); + this.bindEvents_(); + this.initModel(); + this.applyColour(); + } + + /** + * Create the block UI for this field. + */ + protected initView() { + this.createBorderRect_(); + this.createTextElement_(); + } + + /** + * Initializes the model of the field after it has been installed on a block. + * No-op by default. + */ + initModel() {} + + /** + * Defines whether this field should take up the full block or not. + * + * Be cautious when overriding this function. It may not work as you expect / + * intend because the behavior was kind of hacked in. If you are thinking + * about overriding this function, post on the forum with your intended + * behavior to see if there's another approach. + */ + protected isFullBlockField(): boolean { + return !this.borderRect_; + } + + /** + * Create a field border rect element. Not to be overridden by subclasses. + * Instead modify the result of the function inside initView, or create a + * separate function to call. + */ + protected createBorderRect_() { + this.borderRect_ = dom.createSvgElement( + Svg.RECT, + { + 'rx': this.getConstants()!.FIELD_BORDER_RECT_RADIUS, + 'ry': this.getConstants()!.FIELD_BORDER_RECT_RADIUS, + 'x': 0, + 'y': 0, + 'height': this.size_.height, + 'width': this.size_.width, + 'class': 'blocklyFieldRect', + }, + this.fieldGroup_, + ); + } + + /** + * Create a field text element. Not to be overridden by subclasses. Instead + * modify the result of the function inside initView, or create a separate + * function to call. + */ + protected createTextElement_() { + this.textElement_ = dom.createSvgElement( + Svg.TEXT, + { + 'class': 'blocklyText', + }, + this.fieldGroup_, + ); + if (this.getConstants()!.FIELD_TEXT_BASELINE_CENTER) { + this.textElement_.setAttribute('dominant-baseline', 'central'); + } + this.textContent_ = document.createTextNode(''); + this.textElement_.appendChild(this.textContent_); + } + + /** + * Bind events to the field. Can be overridden by subclasses if they need to + * do custom input handling. + */ + protected bindEvents_() { + const clickTarget = this.getClickTarget_(); + if (!clickTarget) throw new Error('A click target has not been set.'); + Tooltip.bindMouseEvents(clickTarget); + this.mouseDownWrapper = browserEvents.conditionalBind( + clickTarget, + 'pointerdown', + this, + this.onMouseDown_, + ); + } + + /** + * Sets the field's value based on the given XML element. Should only be + * called by Blockly.Xml. + * + * @param fieldElement The element containing info about the field's state. + * @internal + */ + fromXml(fieldElement: Element) { + // Any because gremlins live here. No touchie! + this.setValue(fieldElement.textContent as any); + } + + /** + * Serializes this field's value to XML. Should only be called by Blockly.Xml. + * + * @param fieldElement The element to populate with info about the field's + * state. + * @returns The element containing info about the field's state. + * @internal + */ + toXml(fieldElement: Element): Element { + // Any because gremlins live here. No touchie! + fieldElement.textContent = this.getValue() as any; + return fieldElement; + } + + /** + * Saves this fields value as something which can be serialized to JSON. + * Should only be called by the serialization system. + * + * @param _doFullSerialization If true, this signals to the field that if it + * normally just saves a reference to some state (eg variable fields) it + * should instead serialize the full state of the thing being referenced. + * See the + * {@link https://developers.devsite.google.com/blockly/guides/create-custom-blocks/fields/customizing-fields/creating#full_serialization_and_backing_data | field serialization docs} + * for more information. + * @returns JSON serializable state. + * @internal + */ + saveState(_doFullSerialization?: boolean): AnyDuringMigration { + const legacyState = this.saveLegacyState(Field); + if (legacyState !== null) { + return legacyState; + } + return this.getValue(); + } + + /** + * Sets the field's state based on the given state value. Should only be + * called by the serialization system. + * + * @param state The state we want to apply to the field. + * @internal + */ + loadState(state: AnyDuringMigration) { + if (this.loadLegacyState(Field, state)) { + return; + } + this.setValue(state); + } + + /** + * Returns a stringified version of the XML state, if it should be used. + * Otherwise this returns null, to signal the field should use its own + * serialization. + * + * @param callingClass The class calling this method. + * Used to see if `this` has overridden any relevant hooks. + * @returns The stringified version of the XML state, or null. + */ + protected saveLegacyState(callingClass: FieldProto): string | null { + if ( + callingClass.prototype.saveState === this.saveState && + callingClass.prototype.toXml !== this.toXml + ) { + const elem = utilsXml.createElement('field'); + elem.setAttribute('name', this.name || ''); + const text = utilsXml.domToText(this.toXml(elem)); + return text.replace( + ' xmlns="https://developers.google.com/blockly/xml"', + '', + ); + } + // Either they called this on purpose from their saveState, or they have + // no implementations of either hook. Just do our thing. + return null; + } + + /** + * Loads the given state using either the old XML hooks, if they should be + * used. Returns true to indicate loading has been handled, false otherwise. + * + * @param callingClass The class calling this method. + * Used to see if `this` has overridden any relevant hooks. + * @param state The state to apply to the field. + * @returns Whether the state was applied or not. + */ + loadLegacyState( + callingClass: FieldProto, + state: AnyDuringMigration, + ): boolean { + if ( + callingClass.prototype.loadState === this.loadState && + callingClass.prototype.fromXml !== this.fromXml + ) { + this.fromXml(utilsXml.textToDom(state as string)); + return true; + } + // Either they called this on purpose from their loadState, or they have + // no implementations of either hook. Just do our thing. + return false; + } + + /** + * Dispose of all DOM objects and events belonging to this editable field. + * + * @internal + */ + dispose() { + dropDownDiv.hideIfOwner(this); + WidgetDiv.hideIfOwner(this); + + if (!this.getSourceBlock()?.isDeadOrDying()) { + dom.removeNode(this.fieldGroup_); + } + + this.disposed = true; + } + + /** Add or remove the UI indicating if this field is editable or not. */ + updateEditable() { + const group = this.fieldGroup_; + const block = this.getSourceBlock(); + if (!this.EDITABLE || !group || !block) { + return; + } + if (this.enabled_ && block.isEditable()) { + dom.addClass(group, 'blocklyEditableText'); + dom.removeClass(group, 'blocklyNonEditableText'); + group.style.cursor = this.CURSOR; + } else { + dom.addClass(group, 'blocklyNonEditableText'); + dom.removeClass(group, 'blocklyEditableText'); + group.style.cursor = ''; + } + } + + /** + * Set whether this field's value can be changed using the editor when the + * source block is editable. + * + * @param enabled True if enabled. + */ + setEnabled(enabled: boolean) { + this.enabled_ = enabled; + this.updateEditable(); + } + + /** + * Check whether this field's value can be changed using the editor when the + * source block is editable. + * + * @returns Whether this field is enabled. + */ + isEnabled(): boolean { + return this.enabled_; + } + + /** + * Check whether this field defines the showEditor_ function. + * + * @returns Whether this field is clickable. + */ + isClickable(): boolean { + return ( + this.enabled_ && + !!this.sourceBlock_ && + this.sourceBlock_.isEditable() && + this.showEditor_ !== Field.prototype.showEditor_ + ); + } + + /** + * Check whether the field should be clickable while the block is in a flyout. + * The default is that fields are clickable in always-open flyouts such as the + * simple toolbox, but not in autoclosing flyouts such as the category toolbox. + * Subclasses may override this function to change this behavior. Note that + * `isClickable` must also return true for this to have any effect. + * + * @param autoClosingFlyout true if the containing flyout is an auto-closing one. + * @returns Whether the field should be clickable while the block is in a flyout. + */ + isClickableInFlyout(autoClosingFlyout: boolean): boolean { + return !autoClosingFlyout; + } + + /** + * Check whether this field is currently editable. Some fields are never + * EDITABLE (e.g. text labels). Other fields may be EDITABLE but may exist on + * non-editable blocks or be currently disabled. + * + * @returns Whether this field is currently enabled, editable and on an + * editable block. + */ + isCurrentlyEditable(): boolean { + return ( + this.enabled_ && + this.EDITABLE && + !!this.sourceBlock_ && + this.sourceBlock_.isEditable() + ); + } + + /** + * Check whether this field should be serialized by the XML renderer. + * Handles the logic for backwards compatibility and incongruous states. + * + * @returns Whether this field should be serialized or not. + */ + isSerializable(): boolean { + let isSerializable = false; + if (this.name) { + if (this.SERIALIZABLE) { + isSerializable = true; + } else if (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.', + ); + isSerializable = true; + } + } + return isSerializable; + } + + /** + * Gets whether this editable field is visible or not. + * + * @returns True if visible. + */ + isVisible(): boolean { + return this.visible_; + } + + /** + * Sets whether this editable field is visible or not. Should only be called + * by input.setVisible. + * + * @param visible True if visible. + * @internal + */ + setVisible(visible: boolean) { + if (this.visible_ === visible) { + return; + } + this.visible_ = visible; + const root = this.fieldGroup_; + if (root) { + root.style.display = visible ? 'block' : 'none'; + } + } + + /** + * Sets a new validation function for editable fields, or clears a previously + * set validator. + * + * The validator function takes in the new field value, and returns + * validated value. The validated value could be the input value, a modified + * version of the input value, or null to abort the change. + * + * If the function does not return anything (or returns undefined) the new + * value is accepted as valid. This is to allow for fields using the + * validated function as a field-level change event notification. + * + * @param handler The validator function or null to clear a previous + * validator. + */ + setValidator(handler: FieldValidator) { + this.validator_ = handler; + } + + /** + * Gets the validation function for editable fields, or null if not set. + * + * @returns Validation function, or null. + */ + getValidator(): FieldValidator | null { + return this.validator_; + } + + /** + * Gets the group element for this editable field. + * Used for measuring the size and for positioning. + * + * @returns The group element. + */ + getSvgRoot(): SVGGElement | null { + return this.fieldGroup_; + } + + /** + * Gets the border rectangle element. + * + * @returns The border rectangle element. + * @throws An error if the border rectangle element is not defined. + */ + protected getBorderRect(): SVGRectElement { + if (!this.borderRect_) { + throw new Error(`The border rectangle is ${this.borderRect_}.`); + } + return this.borderRect_; + } + + /** + * Gets the text element. + * + * @returns The text element. + * @throws An error if the text element is not defined. + */ + protected getTextElement(): SVGTextElement { + if (!this.textElement_) { + throw new Error(`The text element is ${this.textElement_}.`); + } + return this.textElement_; + } + + /** + * Gets the text content. + * + * @returns The text content. + * @throws An error if the text content is not defined. + */ + protected getTextContent(): Text { + if (!this.textContent_) { + throw new Error(`The text content is ${this.textContent_}.`); + } + return this.textContent_; + } + + /** + * Updates the field to match the colour/style of the block. + * + * Non-abstract sub-classes may wish to implement this if the colour of the + * field depends on the colour of the block. It will automatically be called + * at relevant times, such as when the parent block or renderer changes. + * + * See {@link + * https://developers.google.com/blockly/guides/create-custom-blocks/fields/customizing-fields/creating#matching_block_colours + * | the field documentation} for more information, or FieldDropdown for an + * example. + */ + applyColour() {} + + /** + * Used by getSize() to move/resize any DOM elements, and get the new size. + * + * All rendering that has an effect on the size/shape of the block should be + * done here, and should be triggered by getSize(). + */ + protected render_() { + if (this.textContent_) { + this.textContent_.nodeValue = this.getDisplayText_(); + } + this.updateSize_(); + } + + /** + * Calls showEditor_ when the field is clicked if the field is clickable. + * Do not override. + * + * @param e Optional mouse event that triggered the field to open, or + * undefined if triggered programmatically. + * @sealed + * @internal + */ + showEditor(e?: Event) { + if (this.isClickable()) { + this.showEditor_(e); + } + } + + /** + * A developer hook to create an editor for the field. This is no-op by + * default, and must be overriden to create an editor. + * + * @param _e Optional mouse event that triggered the field to open, or + * undefined if triggered programmatically. + */ + protected showEditor_(_e?: Event): void {} + // NOP + + /** + * A developer hook to reposition the WidgetDiv during a window resize. You + * need to define this hook if your field has a WidgetDiv that needs to + * reposition itself when the window is resized. For example, text input + * fields define this hook so that the input WidgetDiv can reposition itself + * on a window resize event. This is especially important when modal inputs + * have been disabled, as Android devices will fire a window resize event when + * the soft keyboard opens. + * + * If you want the WidgetDiv to hide itself instead of repositioning, return + * false. This is the default behavior. + * + * DropdownDivs already handle their own positioning logic, so you do not need + * to override this function if your field only has a DropdownDiv. + * + * @returns True if the field should be repositioned, + * false if the WidgetDiv should hide itself instead. + */ + repositionForWindowResize(): boolean { + return false; + } + + /** + * Updates the size of the field based on the text. + * + * @param margin margin to use when positioning the text element. + */ + protected updateSize_(margin?: number) { + const constants = this.getConstants(); + const xOffset = + margin !== undefined + ? margin + : !this.isFullBlockField() + ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING + : 0; + let totalWidth = xOffset * 2; + let totalHeight = constants!.FIELD_TEXT_HEIGHT; + + let contentWidth = 0; + if (this.textElement_) { + contentWidth = dom.getFastTextWidth( + this.textElement_, + constants!.FIELD_TEXT_FONTSIZE, + constants!.FIELD_TEXT_FONTWEIGHT, + constants!.FIELD_TEXT_FONTFAMILY, + ); + totalWidth += contentWidth; + } + if (!this.isFullBlockField()) { + totalHeight = Math.max(totalHeight, constants!.FIELD_BORDER_RECT_HEIGHT); + } + + this.size_.height = totalHeight; + this.size_.width = totalWidth; + + this.positionTextElement_(xOffset, contentWidth); + this.positionBorderRect_(); + } + + /** + * Position a field's text element after a size change. This handles both LTR + * and RTL positioning. + * + * @param xOffset x offset to use when positioning the text element. + * @param contentWidth The content width. + */ + protected positionTextElement_(xOffset: number, contentWidth: number) { + if (!this.textElement_) { + return; + } + const constants = this.getConstants(); + const halfHeight = this.size_.height / 2; + + this.textElement_.setAttribute( + 'x', + String( + this.getSourceBlock()?.RTL + ? this.size_.width - contentWidth - xOffset + : xOffset, + ), + ); + this.textElement_.setAttribute( + 'y', + String( + constants!.FIELD_TEXT_BASELINE_CENTER + ? halfHeight + : halfHeight - + constants!.FIELD_TEXT_HEIGHT / 2 + + constants!.FIELD_TEXT_BASELINE, + ), + ); + } + + /** Position a field's border rect after a size change. */ + protected positionBorderRect_() { + if (!this.borderRect_) { + return; + } + this.borderRect_.setAttribute('width', String(this.size_.width)); + this.borderRect_.setAttribute('height', String(this.size_.height)); + this.borderRect_.setAttribute( + 'rx', + String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS), + ); + this.borderRect_.setAttribute( + 'ry', + String(this.getConstants()!.FIELD_BORDER_RECT_RADIUS), + ); + } + + /** + * Returns the height and width of the field. + * + * This should *in general* be the only place render_ gets called from. + * + * @returns Height and width. + */ + getSize(): Size { + if (!this.isVisible()) { + return new Size(0, 0); + } + + if (this.isDirty_) { + this.render_(); + this.isDirty_ = false; + } else if (this.visible_ && this.size_.width === 0) { + // If the field is not visible the width will be 0 as well, one of the + // problems with the old system. + this.render_(); + // Don't issue a warning if the field is actually zero width. + if (this.size_.width !== 0) { + console.warn( + 'Deprecated use of setting size_.width to 0 to rerender a' + + ' field. Set field.isDirty_ to true instead.', + ); + } + } + return this.size_; + } + + /** + * Returns the bounding box of the rendered field, accounting for workspace + * scaling. + * + * @returns An object with top, bottom, left, and right in pixels relative to + * the top left corner of the page (window coordinates). + * @internal + */ + getScaledBBox(): Rect { + let scaledWidth; + let scaledHeight; + let xy; + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + + if (this.isFullBlockField()) { + // Browsers are inconsistent in what they return for a bounding box. + // - Webkit / Blink: fill-box / object bounding box + // - Gecko: stroke-box + const bBox = (this.sourceBlock_ as BlockSvg).getHeightWidth(); + const scale = (block.workspace as WorkspaceSvg).scale; + xy = this.getAbsoluteXY_(); + scaledWidth = (bBox.width + 1) * scale; + scaledHeight = (bBox.height + 1) * scale; + + if (userAgent.GECKO) { + xy.x += 1.5 * scale; + xy.y += 1.5 * scale; + } else { + xy.x -= 0.5 * scale; + xy.y -= 0.5 * scale; + } + } else { + const bBox = this.borderRect_!.getBoundingClientRect(); + xy = style.getPageOffset(this.borderRect_!); + scaledWidth = bBox.width; + scaledHeight = bBox.height; + } + return new Rect(xy.y, xy.y + scaledHeight, xy.x, xy.x + scaledWidth); + } + + /** + * Notifies the field that it has changed locations. + * + * @param _ The location of this field's block's top-start corner + * in workspace coordinates. + */ + onLocationChange(_: Coordinate) {} + + /** + * Get the text from this field to display on the block. May differ from + * `getText` due to ellipsis, and other formatting. + * + * @returns Text to display. + */ + protected getDisplayText_(): string { + let text = this.getText(); + if (!text) { + // Prevent the field from disappearing if empty. + return Field.NBSP; + } + if (text.length > this.maxDisplayLength) { + // Truncate displayed string and add an ellipsis ('...'). + text = text.substring(0, this.maxDisplayLength - 2) + '…'; + } + // Replace whitespace with non-breaking spaces so the text doesn't collapse. + text = text.replace(/\s/g, Field.NBSP); + if (this.sourceBlock_ && this.sourceBlock_.RTL) { + // The SVG is LTR, force text to be RTL by adding an RLM. + text += '\u200F'; + } + return text; + } + + /** + * Get the text from this field. + * Override getText_ to provide a different behavior than simply casting the + * value to a string. + * + * @returns Current text. + * @sealed + */ + getText(): string { + // this.getText_ was intended so that devs don't have to remember to call + // super when overriding how the text of the field is generated. (#2910) + const text = this.getText_(); + if (text !== null) { + return String(text); + } + return String(this.getValue()); + } + + /** + * A developer hook to override the returned text of this field. + * Override if the text representation of the value of this field + * is not just a string cast of its value. + * Return null to resort to a string cast. + * + * @returns Current text or null. + */ + protected getText_(): string | null { + return null; + } + + /** + * Force a rerender of the block that this field is installed on, which will + * rerender this field and adjust for any sizing changes. + * Other fields on the same block will not rerender, because their sizes have + * already been recorded. + * + * @internal + */ + markDirty() { + this.isDirty_ = true; + this.constants_ = null; + } + + /** + * Force a rerender of the block that this field is installed on, which will + * rerender this field and adjust for any sizing changes. + * Other fields on the same block will not rerender, because their sizes have + * already been recorded. + * + * @internal + */ + forceRerender() { + this.isDirty_ = true; + if (this.sourceBlock_ && this.sourceBlock_.rendered) { + (this.sourceBlock_ as BlockSvg).queueRender(); + } + } + + /** + * Used to change the value of the field. Handles validation and events. + * Subclasses should override doClassValidation_ and doValueUpdate_ rather + * than this method. + * + * @param newValue New value. + * @param fireChangeEvent Whether to fire a change event. Defaults to true. + * Should usually be true unless the change will be reported some other + * way, e.g. an intermediate field change event. + * @sealed + */ + setValue(newValue: AnyDuringMigration, fireChangeEvent = true) { + const doLogging = false; + if (newValue === null) { + if (doLogging) console.log('null, return'); + // Not a valid value to check. + return; + } + + // Field validators are allowed to make changes to the workspace, which + // should get grouped with the field value change event. + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } + + try { + const classValidation = this.doClassValidation_(newValue); + const classValue = this.processValidation( + newValue, + classValidation, + fireChangeEvent, + ); + if (classValue instanceof Error) { + if (doLogging) console.log('invalid class validation, return'); + return; + } + + const localValidation = this.getValidator()?.call(this, classValue); + const localValue = this.processValidation( + classValue, + localValidation, + fireChangeEvent, + ); + if (localValue instanceof Error) { + if (doLogging) console.log('invalid local validation, return'); + return; + } + + const source = this.sourceBlock_; + if (source && source.disposed) { + if (doLogging) console.log('source disposed, return'); + return; + } + + const oldValue = this.getValue(); + if (oldValue === localValue) { + if (doLogging) console.log('same, doValueUpdate_, return'); + this.doValueUpdate_(localValue); + return; + } + + this.doValueUpdate_(localValue); + if (fireChangeEvent && source && eventUtils.isEnabled()) { + eventUtils.fire( + new (eventUtils.get(EventType.BLOCK_CHANGE))( + source, + 'field', + this.name || null, + oldValue, + localValue, + ), + ); + } + if (this.isDirty_) { + this.forceRerender(); + } + if (doLogging) console.log(this.value_); + } finally { + eventUtils.setGroup(existingGroup); + } + } + + /** + * Process the result of validation. + * + * @param newValue New value. + * @param validatedValue Validated value. + * @param fireChangeEvent Whether to fire a change event if the value changes. + * @returns New value, or an Error object. + */ + private processValidation( + newValue: AnyDuringMigration, + validatedValue: T | null | undefined, + fireChangeEvent: boolean, + ): T | Error { + if (validatedValue === null) { + this.doValueInvalid_(newValue, fireChangeEvent); + if (this.isDirty_) { + this.forceRerender(); + } + return Error(); + } + return validatedValue === undefined ? (newValue as T) : validatedValue; + } + + /** + * Get the current value of the field. + * + * @returns Current value. + */ + getValue(): T | null { + return this.value_; + } + + /** + * Validate the changes to a field's value before they are set. See + * **FieldDropdown** for an example of subclass implementation. + * + * **NOTE:** Validation returns one option between `T`, `null`, and + * `undefined`. **Field**'s implementation will never return `undefined`, but + * it is valid for a subclass to return `undefined` if the new value is + * compatible with `T`. + * + * @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values} + * @param newValue - The value to be validated. + * @returns One of three instructions for setting the new value: `T`, `null`, + * or `undefined`. + * + * - `T` to set this function's returned value instead of `newValue`. + * + * - `null` to invoke `doValueInvalid_` and not set a value. + * + * - `undefined` to set `newValue` as is. + */ + protected doClassValidation_(newValue: T): T | null | undefined; + protected doClassValidation_(newValue?: AnyDuringMigration): T | null; + protected doClassValidation_( + newValue?: T | AnyDuringMigration, + ): T | null | undefined { + if (newValue === null || newValue === undefined) { + return null; + } + + return newValue as T; + } + + /** + * Used to update the value of a field. Can be overridden by subclasses to do + * custom storage of values/updating of external things. + * + * @param newValue The value to be saved. + */ + protected doValueUpdate_(newValue: T) { + this.value_ = newValue; + this.isDirty_ = true; + } + + /** + * Used to notify the field an invalid value was input. Can be overridden by + * subclasses, see FieldTextInput. + * No-op by default. + * + * @param _invalidValue The input value that was determined to be invalid. + * @param _fireChangeEvent Whether to fire a change event if the value changes. + */ + protected doValueInvalid_( + _invalidValue: AnyDuringMigration, + _fireChangeEvent: boolean = true, + ) {} + // NOP + + /** + * Handle a pointerdown event on a field. + * + * @param e Pointer down event. + */ + protected onMouseDown_(e: PointerEvent) { + if (!this.sourceBlock_ || this.sourceBlock_.isDeadOrDying()) { + return; + } + const gesture = (this.sourceBlock_.workspace as WorkspaceSvg).getGesture(e); + if (gesture) { + gesture.setStartField(this); + } + } + + /** + * Sets the tooltip for this field. + * + * @param newTip The text for the tooltip, a function that returns the text + * for the tooltip, a parent object whose tooltip will be used, or null to + * display the tooltip of the parent block. To not display a tooltip pass + * the empty string. + */ + setTooltip(newTip: Tooltip.TipInfo | null) { + if (!newTip && newTip !== '') { + // If null or undefined. + newTip = this.sourceBlock_; + } + const clickTarget = this.getClickTarget_(); + if (clickTarget) { + (clickTarget as AnyDuringMigration).tooltip = newTip; + } else { + // Field has not been initialized yet. + this.tooltip = newTip; + } + } + + /** + * Returns the tooltip text for this field. + * + * @returns The tooltip text for this field. + */ + getTooltip(): string { + const clickTarget = this.getClickTarget_(); + if (clickTarget) { + return Tooltip.getTooltipOfObject(clickTarget); + } + // Field has not been initialized yet. Return stashed this.tooltip value. + return Tooltip.getTooltipOfObject({tooltip: this.tooltip}); + } + + /** + * The element to bind the click handler to. If not set explicitly, defaults + * to the SVG root of the field. When this element is + * clicked on an editable field, the editor will open. + * + * @returns Element to bind click handler to. + */ + protected getClickTarget_(): Element | null { + return this.clickTarget_ || this.getSvgRoot(); + } + + /** + * Return the absolute coordinates of the top-left corner of this field. + * The origin (0,0) is the top-left corner of the page body. + * + * @returns Object with .x and .y properties. + */ + protected getAbsoluteXY_(): Coordinate { + return style.getPageOffset(this.getClickTarget_() as SVGRectElement); + } + + /** + * Whether this field references any Blockly variables. If true it may need + * to be handled differently during serialization and deserialization. + * Subclasses may override this. + * + * @returns True if this field has any variable references. + * @internal + */ + referencesVariables(): boolean { + return false; + } + + /** + * Refresh the variable name referenced by this field if this field references + * variables. + * + * @internal + */ + refreshVariableName() {} + // NOP + + /** + * Search through the list of inputs and their fields in order to find the + * parent input of a field. + * + * @returns The input that the field belongs to. + * @internal + */ + getParentInput(): Input { + let parentInput = null; + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + const inputs = block.inputList; + + for (let idx = 0; idx < block.inputList.length; idx++) { + const input = inputs[idx]; + const fieldRows = input.fieldRow; + for (let j = 0; j < fieldRows.length; j++) { + if (fieldRows[j] === this) { + parentInput = input; + break; + } + } + } + return parentInput!; + } + + /** + * Returns whether or not we should flip the field in RTL. + * + * @returns True if we should flip in RTL. + */ + getFlipRtl(): boolean { + return false; + } + + /** + * Returns whether or not the field is tab navigable. + * + * @returns True if the field is tab navigable. + */ + isTabNavigable(): boolean { + return false; + } + + /** + * Handles the given keyboard shortcut. + * + * @param _shortcut The shortcut to be handled. + * @returns True if the shortcut has been handled, false otherwise. + */ + onShortcut(_shortcut: KeyboardShortcut): boolean { + return false; + } + + /** + * Add the cursor SVG to this fields SVG group. + * + * @param cursorSvg The SVG root of the cursor to be added to the field group. + * @internal + */ + setCursorSvg(cursorSvg: SVGElement) { + if (!cursorSvg) { + this.cursorSvg = null; + return; + } + + if (!this.fieldGroup_) { + throw new Error(`The field group is ${this.fieldGroup_}.`); + } + this.fieldGroup_.appendChild(cursorSvg); + this.cursorSvg = cursorSvg; + } + + /** + * Add the marker SVG to this fields SVG group. + * + * @param markerSvg The SVG root of the marker to be added to the field group. + * @internal + */ + setMarkerSvg(markerSvg: SVGElement) { + if (!markerSvg) { + this.markerSvg = null; + return; + } + + if (!this.fieldGroup_) { + throw new Error(`The field group is ${this.fieldGroup_}.`); + } + this.fieldGroup_.appendChild(markerSvg); + this.markerSvg = markerSvg; + } + + /** + * Redraw any attached marker or cursor svgs if needed. + * + * @internal + */ + updateMarkers_() { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + const workspace = block.workspace as WorkspaceSvg; + if (workspace.keyboardAccessibilityMode && this.cursorSvg) { + workspace.getCursor()!.draw(); + } + if (workspace.keyboardAccessibilityMode && this.markerSvg) { + // TODO(#4592): Update all markers on the field. + workspace.getMarker(MarkerManager.LOCAL_MARKER)!.draw(); + } + } + + /** + * Subclasses should reimplement this method to construct their Field + * subclass from a JSON arg object. + * + * It is an error to attempt to register a field subclass in the + * FieldRegistry if that subclass has not overridden this method. + * + * @param _options JSON configuration object with properties needed + * to configure a specific field. + */ + static fromJson(_options: FieldConfig): Field { + throw new Error( + `Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`, + ); + } +} + +/** + * Extra configuration options for the base field. + */ +export interface FieldConfig { + tooltip?: string; +} + +/** + * Represents an object that has all the prototype properties of the `Field` + * class. This is necessary because constructors can change + * in descendants, though they should contain all of Field's prototype methods. + * + * This type should only be used in places where we directly access the prototype + * of a Field class or subclass. + */ +type FieldProto = Pick; + +/** + * Represents an error where the field is trying to access its block or + * information about its block before it has actually been attached to said + * block. + */ +export class UnattachedFieldError extends Error { + /** @internal */ + constructor() { + super( + 'The field has not yet been attached to its input. ' + + 'Call appendField to attach it.', + ); + } +} diff --git a/core/field_angle.js b/core/field_angle.js deleted file mode 100644 index 55b15d2634a..00000000000 --- a/core/field_angle.js +++ /dev/null @@ -1,320 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2013 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Angle input field. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.FieldAngle'); - -goog.require('Blockly.FieldTextInput'); -goog.require('goog.math'); -goog.require('goog.userAgent'); - - -/** - * Class for an editable angle field. - * @param {(string|number)=} opt_value The initial content of the field. The - * value should cast to a number, and if it does not, '0' will be used. - * @param {Function=} opt_validator An optional function that is called - * to validate any constraints on what the user entered. Takes the new - * text as an argument and returns the accepted text or null to abort - * the change. - * @extends {Blockly.FieldTextInput} - * @constructor - */ -Blockly.FieldAngle = function(opt_value, opt_validator) { - // Add degree symbol: '360°' (LTR) or '°360' (RTL) - this.symbol_ = Blockly.utils.createSvgElement('tspan', {}, null); - this.symbol_.appendChild(document.createTextNode('\u00B0')); - - opt_value = (opt_value && !isNaN(opt_value)) ? String(opt_value) : '0'; - Blockly.FieldAngle.superClass_.constructor.call( - this, opt_value, opt_validator); -}; -goog.inherits(Blockly.FieldAngle, Blockly.FieldTextInput); - -/** - * Round angles to the nearest 15 degrees when using mouse. - * Set to 0 to disable rounding. - */ -Blockly.FieldAngle.ROUND = 15; - -/** - * Half the width of protractor image. - */ -Blockly.FieldAngle.HALF = 100 / 2; - -/* The following two settings work together to set the behaviour of the angle - * picker. While many combinations are possible, two modes are typical: - * Math mode. - * 0 deg is right, 90 is up. This is the style used by protractors. - * Blockly.FieldAngle.CLOCKWISE = false; - * Blockly.FieldAngle.OFFSET = 0; - * Compass mode. - * 0 deg is up, 90 is right. This is the style used by maps. - * Blockly.FieldAngle.CLOCKWISE = true; - * Blockly.FieldAngle.OFFSET = 90; - */ - -/** - * Angle increases clockwise (true) or counterclockwise (false). - */ -Blockly.FieldAngle.CLOCKWISE = false; - -/** - * Offset the location of 0 degrees (and all angles) by a constant. - * Usually either 0 (0 = right) or 90 (0 = up). - */ -Blockly.FieldAngle.OFFSET = 0; - -/** - * Maximum allowed angle before wrapping. - * Usually either 360 (for 0 to 359.9) or 180 (for -179.9 to 180). - */ -Blockly.FieldAngle.WRAP = 360; - -/** - * Radius of protractor circle. Slightly smaller than protractor size since - * otherwise SVG crops off half the border at the edges. - */ -Blockly.FieldAngle.RADIUS = Blockly.FieldAngle.HALF - 1; - -/** - * Adds degree symbol and recalculates width. - * Saves the computed width in a property. - * @private - */ -Blockly.FieldAngle.prototype.render_ = function() { - if (!this.visible_) { - this.size_.width = 0; - return; - } - - // Update textElement. - this.textElement_.textContent = this.getDisplayText_(); - - // Insert degree symbol. - if (this.sourceBlock_.RTL) { - this.textElement_.insertBefore(this.symbol_, this.textElement_.firstChild); - } else { - this.textElement_.appendChild(this.symbol_); - } - this.updateWidth(); -}; - -/** - * Clean up this FieldAngle, as well as the inherited FieldTextInput. - * @return {!Function} Closure to call on destruction of the WidgetDiv. - * @private - */ -Blockly.FieldAngle.prototype.dispose_ = function() { - var thisField = this; - return function() { - Blockly.FieldAngle.superClass_.dispose_.call(thisField)(); - thisField.gauge_ = null; - if (thisField.clickWrapper_) { - Blockly.unbindEvent_(thisField.clickWrapper_); - } - if (thisField.moveWrapper1_) { - Blockly.unbindEvent_(thisField.moveWrapper1_); - } - if (thisField.moveWrapper2_) { - Blockly.unbindEvent_(thisField.moveWrapper2_); - } - }; -}; - -/** - * Show the inline free-text editor on top of the text. - * @private - */ -Blockly.FieldAngle.prototype.showEditor_ = function() { - var noFocus = - goog.userAgent.MOBILE || goog.userAgent.ANDROID || goog.userAgent.IPAD; - // Mobile browsers have issues with in-line textareas (focus & keyboards). - Blockly.FieldAngle.superClass_.showEditor_.call(this, noFocus); - var div = Blockly.WidgetDiv.DIV; - if (!div.firstChild) { - // Mobile interface uses Blockly.prompt. - return; - } - // Build the SVG DOM. - var svg = Blockly.utils.createSvgElement('svg', { - 'xmlns': 'http://www.w3.org/2000/svg', - 'xmlns:html': 'http://www.w3.org/1999/xhtml', - 'xmlns:xlink': 'http://www.w3.org/1999/xlink', - 'version': '1.1', - 'height': (Blockly.FieldAngle.HALF * 2) + 'px', - 'width': (Blockly.FieldAngle.HALF * 2) + 'px' - }, div); - var circle = Blockly.utils.createSvgElement('circle', { - 'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF, - 'r': Blockly.FieldAngle.RADIUS, - 'class': 'blocklyAngleCircle' - }, svg); - this.gauge_ = Blockly.utils.createSvgElement('path', - {'class': 'blocklyAngleGauge'}, svg); - this.line_ = Blockly.utils.createSvgElement('line',{ - 'x1': Blockly.FieldAngle.HALF, - 'y1': Blockly.FieldAngle.HALF, - 'class': 'blocklyAngleLine', - }, svg); - // Draw markers around the edge. - for (var angle = 0; angle < 360; angle += 15) { - Blockly.utils.createSvgElement('line', { - 'x1': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS, - 'y1': Blockly.FieldAngle.HALF, - 'x2': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS - - (angle % 45 == 0 ? 10 : 5), - 'y2': Blockly.FieldAngle.HALF, - 'class': 'blocklyAngleMarks', - 'transform': 'rotate(' + angle + ',' + - Blockly.FieldAngle.HALF + ',' + Blockly.FieldAngle.HALF + ')' - }, svg); - } - svg.style.marginLeft = (15 - Blockly.FieldAngle.RADIUS) + 'px'; - - // The angle picker is different from other fields in that it updates on - // mousemove even if it's not in the middle of a drag. In future we may - // change this behavior. For now, using bindEvent_ instead of - // bindEventWithChecks_ allows it to work without a mousedown/touchstart. - this.clickWrapper_ = - Blockly.bindEvent_(svg, 'click', this, Blockly.WidgetDiv.hide); - this.moveWrapper1_ = - Blockly.bindEvent_(circle, 'mousemove', this, this.onMouseMove); - this.moveWrapper2_ = - Blockly.bindEvent_(this.gauge_, 'mousemove', this, - this.onMouseMove); - this.updateGraph_(); -}; - -/** - * Set the angle to match the mouse's position. - * @param {!Event} e Mouse move event. - */ -Blockly.FieldAngle.prototype.onMouseMove = function(e) { - var bBox = this.gauge_.ownerSVGElement.getBoundingClientRect(); - var dx = e.clientX - bBox.left - Blockly.FieldAngle.HALF; - var dy = e.clientY - bBox.top - Blockly.FieldAngle.HALF; - var angle = Math.atan(-dy / dx); - if (isNaN(angle)) { - // This shouldn't happen, but let's not let this error propagate further. - return; - } - angle = goog.math.toDegrees(angle); - // 0: East, 90: North, 180: West, 270: South. - if (dx < 0) { - angle += 180; - } else if (dy > 0) { - angle += 360; - } - if (Blockly.FieldAngle.CLOCKWISE) { - angle = Blockly.FieldAngle.OFFSET + 360 - angle; - } else { - angle -= Blockly.FieldAngle.OFFSET; - } - if (Blockly.FieldAngle.ROUND) { - angle = Math.round(angle / Blockly.FieldAngle.ROUND) * - Blockly.FieldAngle.ROUND; - } - angle = this.callValidator(angle); - Blockly.FieldTextInput.htmlInput_.value = angle; - this.setValue(angle); - this.validate_(); - this.resizeEditor_(); -}; - -/** - * Insert a degree symbol. - * @param {?string} text New text. - */ -Blockly.FieldAngle.prototype.setText = function(text) { - Blockly.FieldAngle.superClass_.setText.call(this, text); - if (!this.textElement_) { - // Not rendered yet. - return; - } - this.updateGraph_(); - // Cached width is obsolete. Clear it. - this.size_.width = 0; -}; - -/** - * Redraw the graph with the current angle. - * @private - */ -Blockly.FieldAngle.prototype.updateGraph_ = function() { - if (!this.gauge_) { - return; - } - var angleDegrees = Number(this.getText()) + Blockly.FieldAngle.OFFSET; - var angleRadians = goog.math.toRadians(angleDegrees); - var path = ['M ', Blockly.FieldAngle.HALF, ',', Blockly.FieldAngle.HALF]; - var x2 = Blockly.FieldAngle.HALF; - var y2 = Blockly.FieldAngle.HALF; - if (!isNaN(angleRadians)) { - var angle1 = goog.math.toRadians(Blockly.FieldAngle.OFFSET); - var x1 = Math.cos(angle1) * Blockly.FieldAngle.RADIUS; - var y1 = Math.sin(angle1) * -Blockly.FieldAngle.RADIUS; - if (Blockly.FieldAngle.CLOCKWISE) { - angleRadians = 2 * angle1 - angleRadians; - } - x2 += Math.cos(angleRadians) * Blockly.FieldAngle.RADIUS; - y2 -= Math.sin(angleRadians) * Blockly.FieldAngle.RADIUS; - // Don't ask how the flag calculations work. They just do. - var largeFlag = Math.abs(Math.floor((angleRadians - angle1) / Math.PI) % 2); - if (Blockly.FieldAngle.CLOCKWISE) { - largeFlag = 1 - largeFlag; - } - var sweepFlag = Number(Blockly.FieldAngle.CLOCKWISE); - path.push(' l ', x1, ',', y1, - ' A ', Blockly.FieldAngle.RADIUS, ',', Blockly.FieldAngle.RADIUS, - ' 0 ', largeFlag, ' ', sweepFlag, ' ', x2, ',', y2, ' z'); - } - this.gauge_.setAttribute('d', path.join('')); - this.line_.setAttribute('x2', x2); - this.line_.setAttribute('y2', y2); -}; - -/** - * Ensure that only an angle may be entered. - * @param {string} text The user's text. - * @return {?string} A string representing a valid angle, or null if invalid. - */ -Blockly.FieldAngle.prototype.classValidator = function(text) { - if (text === null) { - return null; - } - var n = parseFloat(text || 0); - if (isNaN(n)) { - return null; - } - n = n % 360; - if (n < 0) { - n += 360; - } - if (n > Blockly.FieldAngle.WRAP) { - n -= 360; - } - return String(n); -}; \ No newline at end of file diff --git a/core/field_checkbox.js b/core/field_checkbox.js deleted file mode 100644 index 0f2c78c6af6..00000000000 --- a/core/field_checkbox.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Checkbox field. Checked or not checked. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.FieldCheckbox'); - -goog.require('Blockly.Field'); - - -/** - * Class for a checkbox field. - * @param {string} state The initial state of the field ('TRUE' or 'FALSE'). - * @param {Function=} opt_validator A function that is executed when a new - * option is selected. Its sole argument is the new checkbox state. If - * it returns a value, this becomes the new checkbox state, unless the - * value is null, in which case the change is aborted. - * @extends {Blockly.Field} - * @constructor - */ -Blockly.FieldCheckbox = function(state, opt_validator) { - Blockly.FieldCheckbox.superClass_.constructor.call(this, '', opt_validator); - // Set the initial state. - this.setValue(state); -}; -goog.inherits(Blockly.FieldCheckbox, Blockly.Field); - -/** - * Character for the checkmark. - */ -Blockly.FieldCheckbox.CHECK_CHAR = '\u2713'; - -/** - * Mouse cursor style when over the hotspot that initiates editability. - */ -Blockly.FieldCheckbox.prototype.CURSOR = 'default'; - -/** - * Install this checkbox on a block. - */ -Blockly.FieldCheckbox.prototype.init = function() { - if (this.fieldGroup_) { - // Checkbox has already been initialized once. - return; - } - Blockly.FieldCheckbox.superClass_.init.call(this); - // The checkbox doesn't use the inherited text element. - // Instead it uses a custom checkmark element that is either visible or not. - this.checkElement_ = Blockly.utils.createSvgElement('text', - {'class': 'blocklyText blocklyCheckbox', 'x': -3, 'y': 14}, - this.fieldGroup_); - var textNode = document.createTextNode(Blockly.FieldCheckbox.CHECK_CHAR); - this.checkElement_.appendChild(textNode); - this.checkElement_.style.display = this.state_ ? 'block' : 'none'; -}; - -/** - * Return 'TRUE' if the checkbox is checked, 'FALSE' otherwise. - * @return {string} Current state. - */ -Blockly.FieldCheckbox.prototype.getValue = function() { - return String(this.state_).toUpperCase(); -}; - -/** - * Set the checkbox to be checked if newBool is 'TRUE' or true, - * unchecks otherwise. - * @param {string|boolean} newBool New state. - */ -Blockly.FieldCheckbox.prototype.setValue = function(newBool) { - var newState = (typeof newBool == 'string') ? - (newBool.toUpperCase() == 'TRUE') : !!newBool; - if (this.state_ !== newState) { - if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, this.state_, newState)); - } - this.state_ = newState; - if (this.checkElement_) { - this.checkElement_.style.display = newState ? 'block' : 'none'; - } - } -}; - -/** - * Toggle the state of the checkbox. - * @private - */ -Blockly.FieldCheckbox.prototype.showEditor_ = function() { - var newState = !this.state_; - if (this.sourceBlock_) { - // Call any validation function, and allow it to override. - newState = this.callValidator(newState); - } - if (newState !== null) { - this.setValue(String(newState).toUpperCase()); - } -}; diff --git a/core/field_checkbox.ts b/core/field_checkbox.ts new file mode 100644 index 00000000000..5ae3dfda1ae --- /dev/null +++ b/core/field_checkbox.ts @@ -0,0 +1,271 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Checkbox field. Checked or not checked. + * + * @class + */ +// Former goog.module ID: Blockly.FieldCheckbox + +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_block_change.js'; + +import {Field, FieldConfig, FieldValidator} from './field.js'; +import * as fieldRegistry from './field_registry.js'; +import * as dom from './utils/dom.js'; + +type BoolString = 'TRUE' | 'FALSE'; +type CheckboxBool = BoolString | boolean; + +/** + * Class for a checkbox field. + */ +export class FieldCheckbox extends Field { + /** Default character for the checkmark. */ + static readonly CHECK_CHAR = '✓'; + private checkChar: string; + + /** + * Serializable fields are saved by the serializer, non-serializable fields + * are not. Editable fields should also be serializable. + */ + override SERIALIZABLE = true; + + /** + * Mouse cursor style when over the hotspot that initiates editability. + */ + override CURSOR = 'default'; + + /** + * NOTE: The default value is set in `Field`, so maintain that value instead + * of overwriting it here or in the constructor. + */ + override value_: boolean | null = this.value_; + + /** + * @param value The initial value of the field. Should either be 'TRUE', + * 'FALSE' or a boolean. Defaults to 'FALSE'. Also accepts + * Field.SKIP_SETUP if you wish to skip setup (only used by subclasses + * that want to handle configuration and setting the field value after + * their own constructors have run). + * @param validator A function that is called to validate changes to the + * field's value. Takes in a value ('TRUE' or 'FALSE') & returns a + * validated value ('TRUE' or 'FALSE'), or null to abort the change. + * @param config A map of options used to configure the field. + * See the [field creation documentation]{@link + * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/checkbox#creation} + * for a list of properties this parameter supports. + */ + constructor( + value?: CheckboxBool | typeof Field.SKIP_SETUP, + validator?: FieldCheckboxValidator, + config?: FieldCheckboxConfig, + ) { + super(Field.SKIP_SETUP); + + /** + * Character for the check mark. Used to apply a different check mark + * character to individual fields. + */ + this.checkChar = FieldCheckbox.CHECK_CHAR; + + if (value === Field.SKIP_SETUP) return; + if (config) { + this.configure_(config); + } + this.setValue(value); + if (validator) { + this.setValidator(validator); + } + } + + /** + * Configure the field based on the given map of options. + * + * @param config A map of options to configure the field based on. + */ + protected override configure_(config: FieldCheckboxConfig) { + super.configure_(config); + if (config.checkCharacter) this.checkChar = config.checkCharacter; + } + + /** + * Saves this field's value. + * + * @returns The boolean value held by this field. + * @internal + */ + override saveState(): AnyDuringMigration { + const legacyState = this.saveLegacyState(FieldCheckbox); + if (legacyState !== null) { + return legacyState; + } + return this.getValueBoolean(); + } + + /** + * Create the block UI for this checkbox. + */ + override initView() { + super.initView(); + + const textElement = this.getTextElement(); + dom.addClass(textElement, 'blocklyCheckbox'); + textElement.style.display = this.value_ ? 'block' : 'none'; + } + + override render_() { + if (this.textContent_) { + this.textContent_.nodeValue = this.getDisplayText_(); + } + this.updateSize_(this.getConstants()!.FIELD_CHECKBOX_X_OFFSET); + } + + override getDisplayText_() { + return this.checkChar; + } + + /** + * Set the character used for the check mark. + * + * @param character The character to use for the check mark, or null to use + * the default. + */ + setCheckCharacter(character: string | null) { + this.checkChar = character || FieldCheckbox.CHECK_CHAR; + this.forceRerender(); + } + + /** Toggle the state of the checkbox on click. */ + protected override showEditor_() { + this.setValue(!this.value_); + } + + /** + * Ensure that the input value is valid ('TRUE' or 'FALSE'). + * + * @param newValue The input value. + * @returns A valid value ('TRUE' or 'FALSE), or null if invalid. + */ + protected override doClassValidation_( + newValue?: AnyDuringMigration, + ): BoolString | null { + if (newValue === true || newValue === 'TRUE') { + return 'TRUE'; + } + if (newValue === false || newValue === 'FALSE') { + return 'FALSE'; + } + return null; + } + + /** + * Update the value of the field, and update the checkElement. + * + * @param newValue The value to be saved. The default validator guarantees + * that this is a either 'TRUE' or 'FALSE'. + */ + protected override doValueUpdate_(newValue: BoolString) { + this.value_ = this.convertValueToBool(newValue); + // Update visual. + if (this.textElement_) { + this.textElement_.style.display = this.value_ ? 'block' : 'none'; + } + } + + /** + * Get the value of this field, either 'TRUE' or 'FALSE'. + * + * @returns The value of this field. + */ + override getValue(): BoolString { + return this.value_ ? 'TRUE' : 'FALSE'; + } + + /** + * Get the boolean value of this field. + * + * @returns The boolean value of this field. + */ + getValueBoolean(): boolean | null { + return this.value_; + } + + /** + * Get the text of this field. Used when the block is collapsed. + * + * @returns Text representing the value of this field ('true' or 'false'). + */ + override getText(): string { + return String(this.convertValueToBool(this.value_)); + } + + /** + * Convert a value into a pure boolean. + * + * Converts 'TRUE' to true and 'FALSE' to false correctly, everything else + * is cast to a boolean. + * + * @param value The value to convert. + * @returns The converted value. + */ + private convertValueToBool(value: CheckboxBool | null): boolean { + if (typeof value === 'string') return value === 'TRUE'; + return !!value; + } + + /** + * Construct a FieldCheckbox from a JSON arg object. + * + * @param options A JSON object with options (checked). + * @returns The new field instance. + * @nocollapse + * @internal + */ + static override fromJson( + options: FieldCheckboxFromJsonConfig, + ): FieldCheckbox { + // `this` might be a subclass of FieldCheckbox if that class doesn't + // 'override' the static fromJson method. + return new this(options.checked, undefined, options); + } +} + +fieldRegistry.register('field_checkbox', FieldCheckbox); + +FieldCheckbox.prototype.DEFAULT_VALUE = false; + +/** + * Config options for the checkbox field. + */ +export interface FieldCheckboxConfig extends FieldConfig { + checkCharacter?: string; +} + +/** + * fromJson config options for the checkbox field. + */ +export interface FieldCheckboxFromJsonConfig extends FieldCheckboxConfig { + checked?: boolean; +} + +/** + * A function that is called to validate changes to the field's value before + * they are set. + * + * @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values} + * @param newValue The value to be validated. + * @returns One of three instructions for setting the new value: `T`, `null`, + * or `undefined`. + * + * - `T` to set this function's returned value instead of `newValue`. + * + * - `null` to invoke `doValueInvalid_` and not set a value. + * + * - `undefined` to set `newValue` as is. + */ +export type FieldCheckboxValidator = FieldValidator; diff --git a/core/field_colour.js b/core/field_colour.js deleted file mode 100644 index 425e4a35c55..00000000000 --- a/core/field_colour.js +++ /dev/null @@ -1,235 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Colour input field. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.FieldColour'); - -goog.require('Blockly.Field'); -goog.require('goog.dom'); -goog.require('goog.events'); -goog.require('goog.style'); -goog.require('goog.ui.ColorPicker'); - - -/** - * Class for a colour input field. - * @param {string} colour The initial colour in '#rrggbb' format. - * @param {Function=} opt_validator A function that is executed when a new - * colour is selected. Its sole argument is the new colour value. Its - * return value becomes the selected colour, unless it is undefined, in - * which case the new colour stands, or it is null, in which case the change - * is aborted. - * @extends {Blockly.Field} - * @constructor - */ -Blockly.FieldColour = function(colour, opt_validator) { - Blockly.FieldColour.superClass_.constructor.call(this, colour, opt_validator); - this.setText(Blockly.Field.NBSP + Blockly.Field.NBSP + Blockly.Field.NBSP); -}; -goog.inherits(Blockly.FieldColour, Blockly.Field); - -/** - * By default use the global constants for colours. - * @type {Array.} - * @private - */ -Blockly.FieldColour.prototype.colours_ = null; - -/** - * By default use the global constants for columns. - * @type {number} - * @private - */ -Blockly.FieldColour.prototype.columns_ = 0; - -/** - * Install this field on a block. - */ -Blockly.FieldColour.prototype.init = function() { - Blockly.FieldColour.superClass_.init.call(this); - this.borderRect_.style['fillOpacity'] = 1; - this.setValue(this.getValue()); -}; - -/** - * Mouse cursor style when over the hotspot that initiates the editor. - */ -Blockly.FieldColour.prototype.CURSOR = 'default'; - -/** - * Close the colour picker if this input is being deleted. - */ -Blockly.FieldColour.prototype.dispose = function() { - Blockly.WidgetDiv.hideIfOwner(this); - Blockly.FieldColour.superClass_.dispose.call(this); -}; - -/** - * Return the current colour. - * @return {string} Current colour in '#rrggbb' format. - */ -Blockly.FieldColour.prototype.getValue = function() { - return this.colour_; -}; - -/** - * Set the colour. - * @param {string} colour The new colour in '#rrggbb' format. - */ -Blockly.FieldColour.prototype.setValue = function(colour) { - if (this.sourceBlock_ && Blockly.Events.isEnabled() && - this.colour_ != colour) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, this.colour_, colour)); - } - this.colour_ = colour; - if (this.borderRect_) { - this.borderRect_.style.fill = colour; - } -}; - -/** - * Get the text from this field. Used when the block is collapsed. - * @return {string} Current text. - */ -Blockly.FieldColour.prototype.getText = function() { - var colour = this.colour_; - // Try to use #rgb format if possible, rather than #rrggbb. - var m = colour.match(/^#(.)\1(.)\2(.)\3$/); - if (m) { - colour = '#' + m[1] + m[2] + m[3]; - } - return colour; -}; - -/** - * An array of colour strings for the palette. - * See bottom of this page for the default: - * http://docs.closure-library.googlecode.com/git/closure_goog_ui_colorpicker.js.source.html - * @type {!Array.} - */ -Blockly.FieldColour.COLOURS = goog.ui.ColorPicker.SIMPLE_GRID_COLORS; - -/** - * Number of columns in the palette. - */ -Blockly.FieldColour.COLUMNS = 7; - -/** - * Set a custom colour grid for this field. - * @param {Array.} colours Array of colours for this block, - * or null to use default (Blockly.FieldColour.COLOURS). - * @return {!Blockly.FieldColour} Returns itself (for method chaining). - */ -Blockly.FieldColour.prototype.setColours = function(colours) { - this.colours_ = colours; - return this; -}; - -/** - * Set a custom grid size for this field. - * @param {number} columns Number of columns for this block, - * or 0 to use default (Blockly.FieldColour.COLUMNS). - * @return {!Blockly.FieldColour} Returns itself (for method chaining). - */ -Blockly.FieldColour.prototype.setColumns = function(columns) { - this.columns_ = columns; - return this; -}; - -/** - * Create a palette under the colour field. - * @private - */ -Blockly.FieldColour.prototype.showEditor_ = function() { - Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, - Blockly.FieldColour.widgetDispose_); - // Create the palette using Closure. - var picker = new goog.ui.ColorPicker(); - picker.setSize(this.columns_ || Blockly.FieldColour.COLUMNS); - picker.setColors(this.colours_ || Blockly.FieldColour.COLOURS); - - // Position the palette to line up with the field. - // Record windowSize and scrollOffset before adding the palette. - var windowSize = goog.dom.getViewportSize(); - var scrollOffset = goog.style.getViewportPageOffset(document); - var xy = this.getAbsoluteXY_(); - var borderBBox = this.getScaledBBox_(); - var div = Blockly.WidgetDiv.DIV; - picker.render(div); - picker.setSelectedColor(this.getValue()); - // Record paletteSize after adding the palette. - var paletteSize = goog.style.getSize(picker.getElement()); - - // Flip the palette vertically if off the bottom. - if (xy.y + paletteSize.height + borderBBox.height >= - windowSize.height + scrollOffset.y) { - xy.y -= paletteSize.height - 1; - } else { - xy.y += borderBBox.height - 1; - } - if (this.sourceBlock_.RTL) { - xy.x += borderBBox.width; - xy.x -= paletteSize.width; - // Don't go offscreen left. - if (xy.x < scrollOffset.x) { - xy.x = scrollOffset.x; - } - } else { - // Don't go offscreen right. - if (xy.x > windowSize.width + scrollOffset.x - paletteSize.width) { - xy.x = windowSize.width + scrollOffset.x - paletteSize.width; - } - } - Blockly.WidgetDiv.position(xy.x, xy.y, windowSize, scrollOffset, - this.sourceBlock_.RTL); - - // Configure event handler. - var thisField = this; - Blockly.FieldColour.changeEventKey_ = goog.events.listen(picker, - goog.ui.ColorPicker.EventType.CHANGE, - function(event) { - var colour = event.target.getSelectedColor() || '#000000'; - Blockly.WidgetDiv.hide(); - if (thisField.sourceBlock_) { - // Call any validation function, and allow it to override. - colour = thisField.callValidator(colour); - } - if (colour !== null) { - thisField.setValue(colour); - } - }); -}; - -/** - * Hide the colour palette. - * @private - */ -Blockly.FieldColour.widgetDispose_ = function() { - if (Blockly.FieldColour.changeEventKey_) { - goog.events.unlistenByKey(Blockly.FieldColour.changeEventKey_); - } - Blockly.Events.setGroup(false); -}; diff --git a/core/field_date.js b/core/field_date.js deleted file mode 100644 index 9e19c63acc8..00000000000 --- a/core/field_date.js +++ /dev/null @@ -1,347 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2015 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Date input field. - * @author pkendall64@gmail.com (Paul Kendall) - */ -'use strict'; - -goog.provide('Blockly.FieldDate'); - -goog.require('Blockly.Field'); -goog.require('goog.date'); -goog.require('goog.dom'); -goog.require('goog.events'); -goog.require('goog.i18n.DateTimeSymbols'); -goog.require('goog.i18n.DateTimeSymbols_he'); -goog.require('goog.style'); -goog.require('goog.ui.DatePicker'); - - -/** - * Class for a date input field. - * @param {string} date The initial date. - * @param {Function=} opt_validator A function that is executed when a new - * date is selected. Its sole argument is the new date value. Its - * return value becomes the selected date, unless it is undefined, in - * which case the new date stands, or it is null, in which case the change - * is aborted. - * @extends {Blockly.Field} - * @constructor - */ -Blockly.FieldDate = function(date, opt_validator) { - if (!date) { - date = new goog.date.Date().toIsoString(true); - } - Blockly.FieldDate.superClass_.constructor.call(this, date, opt_validator); - this.setValue(date); -}; -goog.inherits(Blockly.FieldDate, Blockly.Field); - -/** - * Mouse cursor style when over the hotspot that initiates the editor. - */ -Blockly.FieldDate.prototype.CURSOR = 'text'; - -/** - * Close the colour picker if this input is being deleted. - */ -Blockly.FieldDate.prototype.dispose = function() { - Blockly.WidgetDiv.hideIfOwner(this); - Blockly.FieldDate.superClass_.dispose.call(this); -}; - -/** - * Return the current date. - * @return {string} Current date. - */ -Blockly.FieldDate.prototype.getValue = function() { - return this.date_; -}; - -/** - * Set the date. - * @param {string} date The new date. - */ -Blockly.FieldDate.prototype.setValue = function(date) { - if (this.sourceBlock_) { - var validated = this.callValidator(date); - // If the new date is invalid, validation returns null. - // In this case we still want to display the illegal result. - if (validated !== null) { - date = validated; - } - } - this.date_ = date; - Blockly.Field.prototype.setText.call(this, date); -}; - -/** - * Create a date picker under the date field. - * @private - */ -Blockly.FieldDate.prototype.showEditor_ = function() { - Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, - Blockly.FieldDate.widgetDispose_); - // Create the date picker using Closure. - Blockly.FieldDate.loadLanguage_(); - var picker = new goog.ui.DatePicker(); - picker.setAllowNone(false); - picker.setShowWeekNum(false); - - // Position the picker to line up with the field. - // Record windowSize and scrollOffset before adding the picker. - var windowSize = goog.dom.getViewportSize(); - var scrollOffset = goog.style.getViewportPageOffset(document); - var xy = this.getAbsoluteXY_(); - var borderBBox = this.getScaledBBox_(); - var div = Blockly.WidgetDiv.DIV; - picker.render(div); - picker.setDate(goog.date.fromIsoString(this.getValue())); - // Record pickerSize after adding the date picker. - var pickerSize = goog.style.getSize(picker.getElement()); - - // Flip the picker vertically if off the bottom. - if (xy.y + pickerSize.height + borderBBox.height >= - windowSize.height + scrollOffset.y) { - xy.y -= pickerSize.height - 1; - } else { - xy.y += borderBBox.height - 1; - } - if (this.sourceBlock_.RTL) { - xy.x += borderBBox.width; - xy.x -= pickerSize.width; - // Don't go offscreen left. - if (xy.x < scrollOffset.x) { - xy.x = scrollOffset.x; - } - } else { - // Don't go offscreen right. - if (xy.x > windowSize.width + scrollOffset.x - pickerSize.width) { - xy.x = windowSize.width + scrollOffset.x - pickerSize.width; - } - } - Blockly.WidgetDiv.position(xy.x, xy.y, windowSize, scrollOffset, - this.sourceBlock_.RTL); - - // Configure event handler. - var thisField = this; - Blockly.FieldDate.changeEventKey_ = goog.events.listen(picker, - goog.ui.DatePicker.Events.CHANGE, - function(event) { - var date = event.date ? event.date.toIsoString(true) : ''; - Blockly.WidgetDiv.hide(); - if (thisField.sourceBlock_) { - // Call any validation function, and allow it to override. - date = thisField.callValidator(date); - } - thisField.setValue(date); - }); -}; - -/** - * Hide the date picker. - * @private - */ -Blockly.FieldDate.widgetDispose_ = function() { - if (Blockly.FieldDate.changeEventKey_) { - goog.events.unlistenByKey(Blockly.FieldDate.changeEventKey_); - } - Blockly.Events.setGroup(false); -}; - -/** - * Load the best language pack by scanning the Blockly.Msg object for a - * language that matches the available languages in Closure. - * @private - */ -Blockly.FieldDate.loadLanguage_ = function() { - var reg = /^DateTimeSymbols_(.+)$/; - for (var prop in goog.i18n) { - var m = prop.match(reg); - if (m) { - var lang = m[1].toLowerCase().replace('_', '.'); // E.g. 'pt.br' - if (goog.getObjectByName(lang, Blockly.Msg)) { - goog.i18n.DateTimeSymbols = goog.i18n[prop]; - } - } - } -}; - -/** - * CSS for date picker. See css.js for use. - */ -Blockly.FieldDate.CSS = [ - /* Copied from: goog/css/datepicker.css */ - /** - * Copyright 2009 The Closure Library Authors. All Rights Reserved. - * - * Use of this source code is governed by the Apache License, Version 2.0. - * See the COPYING file for details. - */ - - /** - * Standard styling for a goog.ui.DatePicker. - * - * @author arv@google.com (Erik Arvidsson) - */ - - '.blocklyWidgetDiv .goog-date-picker,', - '.blocklyWidgetDiv .goog-date-picker th,', - '.blocklyWidgetDiv .goog-date-picker td {', - ' font: 13px Arial, sans-serif;', - '}', - - '.blocklyWidgetDiv .goog-date-picker {', - ' -moz-user-focus: normal;', - ' -moz-user-select: none;', - ' position: relative;', - ' border: 1px solid #000;', - ' float: left;', - ' padding: 2px;', - ' color: #000;', - ' background: #c3d9ff;', - ' cursor: default;', - '}', - - '.blocklyWidgetDiv .goog-date-picker th {', - ' text-align: center;', - '}', - - '.blocklyWidgetDiv .goog-date-picker td {', - ' text-align: center;', - ' vertical-align: middle;', - ' padding: 1px 3px;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-menu {', - ' position: absolute;', - ' background: threedface;', - ' border: 1px solid gray;', - ' -moz-user-focus: normal;', - ' z-index: 1;', - ' outline: none;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-menu ul {', - ' list-style: none;', - ' margin: 0px;', - ' padding: 0px;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-menu ul li {', - ' cursor: default;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-menu-selected {', - ' background: #ccf;', - '}', - - '.blocklyWidgetDiv .goog-date-picker th {', - ' font-size: .9em;', - '}', - - '.blocklyWidgetDiv .goog-date-picker td div {', - ' float: left;', - '}', - - '.blocklyWidgetDiv .goog-date-picker button {', - ' padding: 0px;', - ' margin: 1px 0;', - ' border: 0;', - ' color: #20c;', - ' font-weight: bold;', - ' background: transparent;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-date {', - ' background: #fff;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-week,', - '.blocklyWidgetDiv .goog-date-picker-wday {', - ' padding: 1px 3px;', - ' border: 0;', - ' border-color: #a2bbdd;', - ' border-style: solid;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-week {', - ' border-right-width: 1px;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-wday {', - ' border-bottom-width: 1px;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-head td {', - ' text-align: center;', - '}', - - /** Use td.className instead of !important */ - '.blocklyWidgetDiv td.goog-date-picker-today-cont {', - ' text-align: center;', - '}', - - /** Use td.className instead of !important */ - '.blocklyWidgetDiv td.goog-date-picker-none-cont {', - ' text-align: center;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-month {', - ' min-width: 11ex;', - ' white-space: nowrap;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-year {', - ' min-width: 6ex;', - ' white-space: nowrap;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-monthyear {', - ' white-space: nowrap;', - '}', - - '.blocklyWidgetDiv .goog-date-picker table {', - ' border-collapse: collapse;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-other-month {', - ' color: #888;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-wkend-start,', - '.blocklyWidgetDiv .goog-date-picker-wkend-end {', - ' background: #eee;', - '}', - - /** Use td.className instead of !important */ - '.blocklyWidgetDiv td.goog-date-picker-selected {', - ' background: #c3d9ff;', - '}', - - '.blocklyWidgetDiv .goog-date-picker-today {', - ' background: #9ab;', - ' font-weight: bold !important;', - ' border-color: #246 #9bd #9bd #246;', - ' color: #fff;', - '}' -]; diff --git a/core/field_dropdown.js b/core/field_dropdown.js deleted file mode 100644 index 7ece4e8dc02..00000000000 --- a/core/field_dropdown.js +++ /dev/null @@ -1,422 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Dropdown input field. Used for editable titles and variables. - * In the interests of a consistent UI, the toolbox shares some functions and - * properties with the context menu. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.FieldDropdown'); - -goog.require('Blockly.Field'); -goog.require('goog.dom'); -goog.require('goog.events'); -goog.require('goog.style'); -goog.require('goog.ui.Menu'); -goog.require('goog.ui.MenuItem'); -goog.require('goog.userAgent'); - - -/** - * Class for an editable dropdown field. - * @param {(!Array.|!Function)} menuGenerator An array of options - * for a dropdown list, or a function which generates these options. - * @param {Function=} opt_validator A function that is executed when a new - * option is selected, with the newly selected value as its sole argument. - * If it returns a value, that value (which must be one of the options) will - * become selected in place of the newly selected option, unless the return - * value is null, in which case the change is aborted. - * @extends {Blockly.Field} - * @constructor - */ -Blockly.FieldDropdown = function(menuGenerator, opt_validator) { - this.menuGenerator_ = menuGenerator; - this.trimOptions_(); - var firstTuple = this.getOptions()[0]; - - // Call parent's constructor. - Blockly.FieldDropdown.superClass_.constructor.call(this, firstTuple[1], - opt_validator); -}; -goog.inherits(Blockly.FieldDropdown, Blockly.Field); - -/** - * Horizontal distance that a checkmark overhangs the dropdown. - */ -Blockly.FieldDropdown.CHECKMARK_OVERHANG = 25; - -/** - * Android can't (in 2014) display "▾", so use "▼" instead. - */ -Blockly.FieldDropdown.ARROW_CHAR = goog.userAgent.ANDROID ? '\u25BC' : '\u25BE'; - -/** - * Mouse cursor style when over the hotspot that initiates the editor. - */ -Blockly.FieldDropdown.prototype.CURSOR = 'default'; - -/** - * Language-neutral currently selected string or image object. - * @type {string|!Object} - * @private - */ -Blockly.FieldDropdown.prototype.value_ = ''; - -/** - * SVG image element if currently selected option is an image, or null. - * @type {SVGElement} - * @private - */ -Blockly.FieldDropdown.prototype.imageElement_ = null; - -/** - * Object with src, height, width, and alt attributes if currently selected - * option is an image, or null. - * @type {Object} - * @private - */ -Blockly.FieldDropdown.prototype.imageJson_ = null; - -/** - * Install this dropdown on a block. - */ -Blockly.FieldDropdown.prototype.init = function() { - if (this.fieldGroup_) { - // Dropdown has already been initialized once. - return; - } - // Add dropdown arrow: "option ▾" (LTR) or "▾ אופציה" (RTL) - this.arrow_ = Blockly.utils.createSvgElement('tspan', {}, null); - this.arrow_.appendChild(document.createTextNode(this.sourceBlock_.RTL ? - Blockly.FieldDropdown.ARROW_CHAR + ' ' : - ' ' + Blockly.FieldDropdown.ARROW_CHAR)); - - Blockly.FieldDropdown.superClass_.init.call(this); - // Force a reset of the text to add the arrow. - var text = this.text_; - this.text_ = null; - this.setText(text); -}; - -/** - * Create a dropdown menu under the text. - * @private - */ -Blockly.FieldDropdown.prototype.showEditor_ = function() { - Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, null); - var thisField = this; - - function callback(e) { - var menu = this; - var menuItem = e.target; - if (menuItem) { - thisField.onItemSelected(menu, menuItem); - } - Blockly.WidgetDiv.hideIfOwner(thisField); - Blockly.Events.setGroup(false); - } - - var menu = new goog.ui.Menu(); - menu.setRightToLeft(this.sourceBlock_.RTL); - var options = this.getOptions(); - for (var i = 0; i < options.length; i++) { - var content = options[i][0]; // Human-readable text or image. - var value = options[i][1]; // Language-neutral value. - if (typeof content == 'object') { - // An image, not text. - var image = new Image(content['width'], content['height']); - image.src = content['src']; - image.alt = content['alt'] || ''; - content = image; - } - var menuItem = new goog.ui.MenuItem(content); - menuItem.setRightToLeft(this.sourceBlock_.RTL); - menuItem.setValue(value); - menuItem.setCheckable(true); - menu.addChild(menuItem, true); - menuItem.setChecked(value == this.value_); - } - // Listen for mouse/keyboard events. - goog.events.listen(menu, goog.ui.Component.EventType.ACTION, callback); - // Listen for touch events (why doesn't Closure handle this already?). - function callbackTouchStart(e) { - var control = this.getOwnerControl(/** @type {Node} */ (e.target)); - // Highlight the menu item. - control.handleMouseDown(e); - } - function callbackTouchEnd(e) { - var control = this.getOwnerControl(/** @type {Node} */ (e.target)); - // Activate the menu item. - control.performActionInternal(e); - } - menu.getHandler().listen(menu.getElement(), goog.events.EventType.TOUCHSTART, - callbackTouchStart); - menu.getHandler().listen(menu.getElement(), goog.events.EventType.TOUCHEND, - callbackTouchEnd); - - // Record windowSize and scrollOffset before adding menu. - var windowSize = goog.dom.getViewportSize(); - var scrollOffset = goog.style.getViewportPageOffset(document); - var xy = this.getAbsoluteXY_(); - var borderBBox = this.getScaledBBox_(); - var div = Blockly.WidgetDiv.DIV; - menu.render(div); - var menuDom = menu.getElement(); - Blockly.utils.addClass(menuDom, 'blocklyDropdownMenu'); - // Record menuSize after adding menu. - var menuSize = goog.style.getSize(menuDom); - // Recalculate height for the total content, not only box height. - menuSize.height = menuDom.scrollHeight; - - // Position the menu. - // Flip menu vertically if off the bottom. - if (xy.y + menuSize.height + borderBBox.height >= - windowSize.height + scrollOffset.y) { - xy.y -= menuSize.height + 2; - } else { - xy.y += borderBBox.height; - } - if (this.sourceBlock_.RTL) { - xy.x += borderBBox.width; - xy.x += Blockly.FieldDropdown.CHECKMARK_OVERHANG; - // Don't go offscreen left. - if (xy.x < scrollOffset.x + menuSize.width) { - xy.x = scrollOffset.x + menuSize.width; - } - } else { - xy.x -= Blockly.FieldDropdown.CHECKMARK_OVERHANG; - // Don't go offscreen right. - if (xy.x > windowSize.width + scrollOffset.x - menuSize.width) { - xy.x = windowSize.width + scrollOffset.x - menuSize.width; - } - } - Blockly.WidgetDiv.position(xy.x, xy.y, windowSize, scrollOffset, - this.sourceBlock_.RTL); - menu.setAllowAutoFocus(true); - menuDom.focus(); -}; - -/** - * Handle the selection of an item in the dropdown menu. - * @param {!goog.ui.Menu} menu The Menu component clicked. - * @param {!goog.ui.MenuItem} menuItem The MenuItem selected within menu. - */ -Blockly.FieldDropdown.prototype.onItemSelected = function(menu, menuItem) { - var value = menuItem.getValue(); - if (this.sourceBlock_) { - // Call any validation function, and allow it to override. - value = this.callValidator(value); - } - if (value !== null) { - this.setValue(value); - } -}; - -/** - * Factor out common words in statically defined options. - * Create prefix and/or suffix labels. - * @private - */ -Blockly.FieldDropdown.prototype.trimOptions_ = function() { - this.prefixField = null; - this.suffixField = null; - var options = this.menuGenerator_; - if (!goog.isArray(options)) { - return; - } - var hasImages = false; - - // Localize label text and image alt text. - for (var i = 0; i < options.length; i++) { - var label = options[i][0]; - if (typeof label == 'string') { - options[i][0] = Blockly.utils.replaceMessageReferences(label); - } else { - if (label.alt != null) { - options[i][0].alt = Blockly.utils.replaceMessageReferences(label.alt); - } - hasImages = true; - } - } - if (hasImages || options.length < 2) { - return; // Do nothing if too few items or at least one label is an image. - } - var strings = []; - for (var i = 0; i < options.length; i++) { - strings.push(options[i][0]); - } - var shortest = Blockly.utils.shortestStringLength(strings); - var prefixLength = Blockly.utils.commonWordPrefix(strings, shortest); - var suffixLength = Blockly.utils.commonWordSuffix(strings, shortest); - if (!prefixLength && !suffixLength) { - return; - } - if (shortest <= prefixLength + suffixLength) { - // One or more strings will entirely vanish if we proceed. Abort. - return; - } - if (prefixLength) { - this.prefixField = strings[0].substring(0, prefixLength - 1); - } - if (suffixLength) { - this.suffixField = strings[0].substr(1 - suffixLength); - } - // Remove the prefix and suffix from the options. - var newOptions = []; - for (var i = 0; i < options.length; i++) { - var text = options[i][0]; - var value = options[i][1]; - text = text.substring(prefixLength, text.length - suffixLength); - newOptions[i] = [text, value]; - } - this.menuGenerator_ = newOptions; -}; - -/** - * @return {boolean} True if the option list is generated by a function. Otherwise false. - */ -Blockly.FieldDropdown.prototype.isOptionListDynamic = function() { - return goog.isFunction(this.menuGenerator_); -}; - -/** - * Return a list of the options for this dropdown. - * @return {!Array.} Array of option tuples: - * (human-readable text or image, language-neutral name). - */ -Blockly.FieldDropdown.prototype.getOptions = function() { - if (goog.isFunction(this.menuGenerator_)) { - return this.menuGenerator_.call(this); - } - return /** @type {!Array.>} */ (this.menuGenerator_); -}; - -/** - * Get the language-neutral value from this dropdown menu. - * @return {string} Current text. - */ -Blockly.FieldDropdown.prototype.getValue = function() { - return this.value_; -}; - -/** - * Set the language-neutral value for this dropdown menu. - * @param {string} newValue New value to set. - */ -Blockly.FieldDropdown.prototype.setValue = function(newValue) { - if (newValue === null || newValue === this.value_) { - return; // No change if null. - } - if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, this.value_, newValue)); - } - this.value_ = newValue; - // Look up and display the human-readable text. - var options = this.getOptions(); - for (var i = 0; i < options.length; i++) { - // Options are tuples of human-readable text and language-neutral values. - if (options[i][1] == newValue) { - var content = options[i][0]; - if (typeof content == 'object') { - this.imageJson_ = content; - this.setText(content.alt); - } else { - this.imageJson_ = null; - this.setText(content); - } - return; - } - } - // Value not found. Add it, maybe it will become valid once set - // (like variable names). - this.setText(newValue); -}; - -/** - * Draws the border with the correct width. - * @private - */ -Blockly.FieldDropdown.prototype.render_ = function() { - if (!this.visible_) { - this.size_.width = 0; - return; - } - if (this.sourceBlock_ && this.arrow_) { - // Update arrow's colour. - this.arrow_.style.fill = this.sourceBlock_.getColour(); - } - goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_)); - goog.dom.removeNode(this.imageElement_); - this.imageElement_ = null; - - if (this.imageJson_) { - // Image option is selected. - this.imageElement_ = Blockly.utils.createSvgElement('image', - {'y': 5, - 'height': this.imageJson_.height + 'px', - 'width': this.imageJson_.width + 'px'}, this.fieldGroup_); - this.imageElement_.setAttributeNS('http://www.w3.org/1999/xlink', - 'xlink:href', this.imageJson_.src); - // Insert dropdown arrow. - this.textElement_.appendChild(this.arrow_); - var arrowWidth = Blockly.Field.getCachedWidth(this.arrow_); - this.size_.height = Number(this.imageJson_.height) + 19; - this.size_.width = Number(this.imageJson_.width) + arrowWidth; - if (this.sourceBlock_.RTL) { - this.imageElement_.setAttribute('x', arrowWidth); - this.textElement_.setAttribute('x', -1); - } else { - this.textElement_.setAttribute('text-anchor', 'end'); - this.textElement_.setAttribute('x', this.size_.width + 1); - } - - } else { - // Text option is selected. - // Replace the text. - var textNode = document.createTextNode(this.getDisplayText_()); - this.textElement_.appendChild(textNode); - // Insert dropdown arrow. - if (this.sourceBlock_.RTL) { - this.textElement_.insertBefore(this.arrow_, this.textElement_.firstChild); - } else { - this.textElement_.appendChild(this.arrow_); - } - this.textElement_.setAttribute('text-anchor', 'start'); - this.textElement_.setAttribute('x', 0); - - this.size_.height = Blockly.BlockSvg.MIN_BLOCK_Y; - this.size_.width = Blockly.Field.getCachedWidth(this.textElement_); - } - this.borderRect_.setAttribute('height', this.size_.height - 9); - this.borderRect_.setAttribute('width', - this.size_.width + Blockly.BlockSvg.SEP_SPACE_X); -}; - -/** - * Close the dropdown menu if this input is being deleted. - */ -Blockly.FieldDropdown.prototype.dispose = function() { - Blockly.WidgetDiv.hideIfOwner(this); - Blockly.FieldDropdown.superClass_.dispose.call(this); -}; diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts new file mode 100644 index 00000000000..b1e3b5af26c --- /dev/null +++ b/core/field_dropdown.ts @@ -0,0 +1,854 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Dropdown input field. Used for editable titles and variables. + * In the interests of a consistent UI, the toolbox shares some functions and + * properties with the context menu. + * + * @class + */ +// Former goog.module ID: Blockly.FieldDropdown + +import type {BlockSvg} from './block_svg.js'; +import * as dropDownDiv from './dropdowndiv.js'; +import { + Field, + FieldConfig, + FieldValidator, + UnattachedFieldError, +} from './field.js'; +import * as fieldRegistry from './field_registry.js'; +import {Menu} from './menu.js'; +import {MenuItem} from './menuitem.js'; +import * as aria from './utils/aria.js'; +import {Coordinate} from './utils/coordinate.js'; +import * as dom from './utils/dom.js'; +import * as parsing from './utils/parsing.js'; +import * as utilsString from './utils/string.js'; +import * as style from './utils/style.js'; +import {Svg} from './utils/svg.js'; + +/** + * Class for an editable dropdown field. + */ +export class FieldDropdown extends Field { + /** Horizontal distance that a checkmark overhangs the dropdown. */ + static CHECKMARK_OVERHANG = 25; + + /** + * Maximum height of the dropdown menu, as a percentage of the viewport + * height. + */ + static MAX_MENU_HEIGHT_VH = 0.45; + + static ARROW_CHAR = '▾'; + + /** A reference to the currently selected menu item. */ + private selectedMenuItem: MenuItem | null = null; + + /** The dropdown menu. */ + protected menu_: Menu | null = null; + + /** + * SVG image element if currently selected option is an image, or null. + */ + private imageElement: SVGImageElement | null = null; + + /** Tspan based arrow element. */ + private arrow: SVGTSpanElement | null = null; + + /** SVG based arrow element. */ + private svgArrow: SVGElement | null = null; + + /** + * Serializable fields are saved by the serializer, non-serializable fields + * are not. Editable fields should also be serializable. + */ + override SERIALIZABLE = true; + + /** Mouse cursor style when over the hotspot that initiates the editor. */ + override CURSOR = 'default'; + + protected menuGenerator_?: MenuGenerator; + + /** A cache of the most recently generated options. */ + private generatedOptions: MenuOption[] | null = null; + + /** + * The prefix field label, of common words set after options are trimmed. + * + * @internal + */ + override prefixField: string | null = null; + + /** + * The suffix field label, of common words set after options are trimmed. + * + * @internal + */ + override suffixField: string | null = null; + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + private selectedOption!: MenuOption; + override clickTarget_: SVGElement | null = null; + + /** + * The y offset from the top of the field to the top of the image, if an image + * is selected. + */ + protected static IMAGE_Y_OFFSET = 5; + + /** The total vertical padding above and below an image. */ + protected static IMAGE_Y_PADDING = FieldDropdown.IMAGE_Y_OFFSET * 2; + + /** + * @param menuGenerator A non-empty array of options for a dropdown list, or a + * function which generates these options. Also accepts Field.SKIP_SETUP + * if you wish to skip setup (only used by subclasses that want to handle + * configuration and setting the field value after their own constructors + * have run). + * @param validator A function that is called to validate changes to the + * field's value. Takes in a language-neutral dropdown option & returns a + * validated language-neutral dropdown option, or null to abort the + * change. + * @param config A map of options used to configure the field. + * See the [field creation documentation]{@link + * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/dropdown#creation} + * for a list of properties this parameter supports. + * @throws {TypeError} If `menuGenerator` options are incorrectly structured. + */ + constructor( + menuGenerator: MenuGenerator, + validator?: FieldDropdownValidator, + config?: FieldDropdownConfig, + ); + constructor(menuGenerator: typeof Field.SKIP_SETUP); + constructor( + menuGenerator: MenuGenerator | typeof Field.SKIP_SETUP, + validator?: FieldDropdownValidator, + config?: FieldDropdownConfig, + ) { + super(Field.SKIP_SETUP); + + // If we pass SKIP_SETUP, don't do *anything* with the menu generator. + if (menuGenerator === Field.SKIP_SETUP) return; + + if (Array.isArray(menuGenerator)) { + this.validateOptions(menuGenerator); + const trimmed = this.trimOptions(menuGenerator); + this.menuGenerator_ = trimmed.options; + this.prefixField = trimmed.prefix || null; + this.suffixField = trimmed.suffix || null; + } else { + this.menuGenerator_ = menuGenerator; + } + + /** + * The currently selected option. The field is initialized with the + * first option selected. + */ + this.selectedOption = this.getOptions(false)[0]; + + if (config) { + this.configure_(config); + } + this.setValue(this.selectedOption[1]); + if (validator) { + this.setValidator(validator); + } + } + + /** + * Sets the field's value based on the given XML element. Should only be + * called by Blockly.Xml. + * + * @param fieldElement The element containing info about the field's state. + * @internal + */ + override fromXml(fieldElement: Element) { + if (this.isOptionListDynamic()) { + this.getOptions(false); + } + this.setValue(fieldElement.textContent); + } + + /** + * Sets the field's value based on the given state. + * + * @param state The state to apply to the dropdown field. + * @internal + */ + override loadState(state: AnyDuringMigration) { + if (this.loadLegacyState(FieldDropdown, state)) { + return; + } + if (this.isOptionListDynamic()) { + this.getOptions(false); + } + this.setValue(state); + } + + /** + * Create the block UI for this dropdown. + */ + override initView() { + if (this.shouldAddBorderRect_()) { + this.createBorderRect_(); + } else { + this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot(); + } + this.createTextElement_(); + + this.imageElement = dom.createSvgElement(Svg.IMAGE, {}, this.fieldGroup_); + + if (this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW) { + this.createSVGArrow_(); + } else { + this.createTextArrow_(); + } + + if (this.borderRect_) { + dom.addClass(this.borderRect_, 'blocklyDropdownRect'); + } + } + + /** + * Whether or not the dropdown should add a border rect. + * + * @returns True if the dropdown field should add a border rect. + */ + protected shouldAddBorderRect_(): boolean { + return ( + !this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW || + (this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW && + !this.getSourceBlock()?.isShadow()) + ); + } + + /** Create a tspan based arrow. */ + protected createTextArrow_() { + this.arrow = dom.createSvgElement(Svg.TSPAN, {}, this.textElement_); + this.arrow!.appendChild( + document.createTextNode( + this.getSourceBlock()?.RTL + ? FieldDropdown.ARROW_CHAR + ' ' + : ' ' + FieldDropdown.ARROW_CHAR, + ), + ); + if (this.getConstants()!.FIELD_TEXT_BASELINE_CENTER) { + this.arrow.setAttribute('dominant-baseline', 'central'); + } + if (this.getSourceBlock()?.RTL) { + this.getTextElement().insertBefore(this.arrow, this.textContent_); + } else { + this.getTextElement().appendChild(this.arrow); + } + } + + /** Create an SVG based arrow. */ + protected createSVGArrow_() { + this.svgArrow = dom.createSvgElement( + 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( + dom.XLINK_NS, + 'xlink:href', + this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_DATAURI, + ); + } + + /** + * Create a dropdown menu under the text. + * + * @param e Optional mouse event that triggered the field to open, or + * undefined if triggered programmatically. + */ + protected override showEditor_(e?: MouseEvent) { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + this.dropdownCreate(); + if (e && typeof e.clientX === 'number') { + this.menu_!.openingCoords = new Coordinate(e.clientX, e.clientY); + } else { + this.menu_!.openingCoords = null; + } + + // Remove any pre-existing elements in the dropdown. + dropDownDiv.clearContent(); + // Element gets created in render. + const menuElement = this.menu_!.render(dropDownDiv.getContentDiv()); + dom.addClass(menuElement, 'blocklyDropdownMenu'); + + if (this.getConstants()!.FIELD_DROPDOWN_COLOURED_DIV) { + const primaryColour = block.getColour(); + const borderColour = (this.sourceBlock_ as BlockSvg).style.colourTertiary; + dropDownDiv.setColour(primaryColour, borderColour); + } + + dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this)); + + // Focusing needs to be handled after the menu is rendered and positioned. + // Otherwise it will cause a page scroll to get the misplaced menu in + // view. See issue #1329. + this.menu_!.focus(); + + if (this.selectedMenuItem) { + this.menu_!.setHighlighted(this.selectedMenuItem); + style.scrollIntoContainerView( + this.selectedMenuItem.getElement()!, + dropDownDiv.getContentDiv(), + true, + ); + } + + this.applyColour(); + } + + /** Create the dropdown editor. */ + private dropdownCreate() { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + const menu = new Menu(); + menu.setRole(aria.Role.LISTBOX); + this.menu_ = menu; + + const options = this.getOptions(false); + this.selectedMenuItem = null; + for (let i = 0; i < options.length; i++) { + const [label, value] = options[i]; + const content = (() => { + if (typeof label === 'object') { + // Convert ImageProperties to an HTMLImageElement. + const image = new Image(label['width'], label['height']); + image.src = label['src']; + image.alt = label['alt'] || ''; + return image; + } + return label; + })(); + const menuItem = new MenuItem(content, value); + menuItem.setRole(aria.Role.OPTION); + menuItem.setRightToLeft(block.RTL); + menuItem.setCheckable(true); + menu.addChild(menuItem); + menuItem.setChecked(value === this.value_); + if (value === this.value_) { + this.selectedMenuItem = menuItem; + } + menuItem.onAction(this.handleMenuActionEvent, this); + } + } + + /** + * Disposes of events and DOM-references belonging to the dropdown editor. + */ + protected dropdownDispose_() { + if (this.menu_) { + this.menu_.dispose(); + } + this.menu_ = null; + this.selectedMenuItem = null; + this.applyColour(); + } + + /** + * Handle an action in the dropdown menu. + * + * @param menuItem The MenuItem selected within menu. + */ + private handleMenuActionEvent(menuItem: MenuItem) { + dropDownDiv.hideIfOwner(this, true); + this.onItemSelected_(this.menu_ as Menu, menuItem); + } + + /** + * Handle the selection of an item in the dropdown menu. + * + * @param menu The Menu component clicked. + * @param menuItem The MenuItem selected within menu. + */ + protected onItemSelected_(menu: Menu, menuItem: MenuItem) { + this.setValue(menuItem.getValue()); + } + + /** + * @returns True if the option list is generated by a function. + * Otherwise false. + */ + isOptionListDynamic(): boolean { + return typeof this.menuGenerator_ === 'function'; + } + + /** + * Return a list of the options for this dropdown. + * + * @param useCache For dynamic options, whether or not to use the cached + * options or to re-generate them. + * @returns A non-empty array of option tuples: + * (human-readable text or image, language-neutral name). + * @throws {TypeError} If generated options are incorrectly structured. + */ + getOptions(useCache?: boolean): MenuOption[] { + if (!this.menuGenerator_) { + // A subclass improperly skipped setup without defining the menu + // generator. + throw TypeError('A menu generator was never defined.'); + } + if (Array.isArray(this.menuGenerator_)) return this.menuGenerator_; + if (useCache && this.generatedOptions) return this.generatedOptions; + + this.generatedOptions = this.menuGenerator_(); + this.validateOptions(this.generatedOptions); + return this.generatedOptions; + } + + /** + * Ensure that the input value is a valid language-neutral option. + * + * @param newValue The input value. + * @returns A valid language-neutral option, or null if invalid. + */ + protected override doClassValidation_( + newValue: string, + ): string | null | undefined; + protected override doClassValidation_(newValue?: string): string | null; + protected override doClassValidation_( + newValue?: string, + ): string | null | undefined { + const options = this.getOptions(true); + const isValueValid = options.some((option) => option[1] === newValue); + + if (!isValueValid) { + if (this.sourceBlock_) { + console.warn( + "Cannot set the dropdown's value to an unavailable option." + + ' Block type: ' + + this.sourceBlock_.type + + ', Field name: ' + + this.name + + ', Value: ' + + newValue, + ); + } + return null; + } + return newValue; + } + + /** + * Update the value of this dropdown field. + * + * @param newValue The value to be saved. The default validator guarantees + * that this is one of the valid dropdown options. + */ + protected override doValueUpdate_(newValue: string) { + super.doValueUpdate_(newValue); + const options = this.getOptions(true); + for (let i = 0, option; (option = options[i]); i++) { + if (option[1] === this.value_) { + this.selectedOption = option; + } + } + } + + /** + * Updates the dropdown arrow to match the colour/style of the block. + */ + override applyColour() { + const style = (this.sourceBlock_ as BlockSvg).style; + if (this.borderRect_) { + this.borderRect_.setAttribute('stroke', style.colourTertiary); + if (this.menu_) { + this.borderRect_.setAttribute('fill', style.colourTertiary); + } else { + this.borderRect_.setAttribute('fill', 'transparent'); + } + } + // Update arrow's colour. + if (this.sourceBlock_ && this.arrow) { + if (this.sourceBlock_.isShadow()) { + this.arrow.style.fill = style.colourSecondary; + } else { + this.arrow.style.fill = style.colourPrimary; + } + } + } + + /** Draws the border with the correct width. */ + protected override render_() { + // Hide both elements. + this.getTextContent().nodeValue = ''; + this.imageElement!.style.display = 'none'; + + // Show correct element. + const option = this.selectedOption && this.selectedOption[0]; + if (option && typeof option === 'object') { + this.renderSelectedImage(option); + } else { + this.renderSelectedText(); + } + + this.positionBorderRect_(); + } + + /** + * Renders the selected option, which must be an image. + * + * @param imageJson Selected option that must be an image. + */ + private renderSelectedImage(imageJson: ImageProperties) { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + this.imageElement!.style.display = ''; + this.imageElement!.setAttributeNS( + dom.XLINK_NS, + 'xlink:href', + imageJson.src, + ); + this.imageElement!.setAttribute('height', String(imageJson.height)); + this.imageElement!.setAttribute('width', String(imageJson.width)); + + const imageHeight = Number(imageJson.height); + const imageWidth = Number(imageJson.width); + + // Height and width include the border rect. + const hasBorder = !!this.borderRect_; + const height = Math.max( + hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0, + imageHeight + FieldDropdown.IMAGE_Y_PADDING, + ); + const xPadding = hasBorder + ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING + : 0; + let arrowWidth = 0; + if (this.svgArrow) { + arrowWidth = this.positionSVGArrow( + imageWidth + xPadding, + height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2, + ); + } else { + arrowWidth = dom.getFastTextWidth( + this.arrow as SVGTSpanElement, + this.getConstants()!.FIELD_TEXT_FONTSIZE, + this.getConstants()!.FIELD_TEXT_FONTWEIGHT, + this.getConstants()!.FIELD_TEXT_FONTFAMILY, + ); + } + this.size_.width = imageWidth + arrowWidth + xPadding * 2; + this.size_.height = height; + + let arrowX = 0; + if (block.RTL) { + const imageX = xPadding + arrowWidth; + this.imageElement!.setAttribute('x', `${imageX}`); + } else { + arrowX = imageWidth + arrowWidth; + this.getTextElement().setAttribute('text-anchor', 'end'); + this.imageElement!.setAttribute('x', `${xPadding}`); + } + this.imageElement!.setAttribute('y', String(height / 2 - imageHeight / 2)); + + this.positionTextElement_(arrowX + xPadding, imageWidth + arrowWidth); + } + + /** Renders the selected option, which must be text. */ + private renderSelectedText() { + // Retrieves the selected option to display through getText_. + this.getTextContent().nodeValue = this.getDisplayText_(); + const textElement = this.getTextElement(); + dom.addClass(textElement, 'blocklyDropdownText'); + textElement.setAttribute('text-anchor', 'start'); + + // Height and width include the border rect. + const hasBorder = !!this.borderRect_; + const height = Math.max( + hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0, + this.getConstants()!.FIELD_TEXT_HEIGHT, + ); + const textWidth = dom.getFastTextWidth( + this.getTextElement(), + this.getConstants()!.FIELD_TEXT_FONTSIZE, + this.getConstants()!.FIELD_TEXT_FONTWEIGHT, + this.getConstants()!.FIELD_TEXT_FONTFAMILY, + ); + const xPadding = hasBorder + ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING + : 0; + let arrowWidth = 0; + if (this.svgArrow) { + arrowWidth = this.positionSVGArrow( + textWidth + xPadding, + height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2, + ); + } + this.size_.width = textWidth + arrowWidth + xPadding * 2; + this.size_.height = height; + + this.positionTextElement_(xPadding, textWidth); + } + + /** + * Position a drop-down arrow at the appropriate location at render-time. + * + * @param x X position the arrow is being rendered at, in px. + * @param y Y position the arrow is being rendered at, in px. + * @returns Amount of space the arrow is taking up, in px. + */ + private positionSVGArrow(x: number, y: number): number { + if (!this.svgArrow) { + return 0; + } + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + const hasBorder = !!this.borderRect_; + const xPadding = hasBorder + ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING + : 0; + const textPadding = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_PADDING; + const svgArrowSize = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE; + const arrowX = block.RTL ? xPadding : x + textPadding; + this.svgArrow.setAttribute( + 'transform', + 'translate(' + arrowX + ',' + y + ')', + ); + return svgArrowSize + textPadding; + } + + /** + * Use the `getText_` developer hook to override the field's text + * representation. Get the selected option text. If the selected option is + * an image we return the image alt text. + * + * @returns Selected option text. + */ + protected override getText_(): string | null { + if (!this.selectedOption) { + return null; + } + const option = this.selectedOption[0]; + if (typeof option === 'object') { + return option['alt']; + } + return option; + } + + /** + * Construct a FieldDropdown from a JSON arg object. + * + * @param options A JSON object with options (options). + * @returns The new field instance. + * @nocollapse + * @internal + */ + static override fromJson( + options: FieldDropdownFromJsonConfig, + ): FieldDropdown { + if (!options.options) { + throw new Error( + 'options are required for the dropdown field. The ' + + 'options property must be assigned an array of ' + + '[humanReadableValue, languageNeutralValue] tuples.', + ); + } + // `this` might be a subclass of FieldDropdown if that class doesn't + // override the static fromJson method. + return new this(options.options, undefined, options); + } + + /** + * Factor out common words in statically defined options. + * Create prefix and/or suffix labels. + */ + protected trimOptions(options: MenuOption[]): { + options: MenuOption[]; + prefix?: string; + suffix?: string; + } { + let hasImages = false; + const trimmedOptions = options.map(([label, value]): MenuOption => { + if (typeof label === 'string') { + return [parsing.replaceMessageReferences(label), value]; + } + + hasImages = true; + // Copy the image properties so they're not influenced by the original. + // NOTE: No need to deep copy since image properties are only 1 level deep. + const imageLabel = + label.alt !== null + ? {...label, alt: parsing.replaceMessageReferences(label.alt)} + : {...label}; + return [imageLabel, value]; + }); + + if (hasImages || options.length < 2) return {options: trimmedOptions}; + + const stringOptions = trimmedOptions as [string, string][]; + const stringLabels = stringOptions.map(([label]) => label); + + const shortest = utilsString.shortestStringLength(stringLabels); + const prefixLength = utilsString.commonWordPrefix(stringLabels, shortest); + const suffixLength = utilsString.commonWordSuffix(stringLabels, shortest); + + if ( + (!prefixLength && !suffixLength) || + shortest <= prefixLength + suffixLength + ) { + // One or more strings will entirely vanish if we proceed. Abort. + return {options: stringOptions}; + } + + const prefix = prefixLength + ? stringLabels[0].substring(0, prefixLength - 1) + : undefined; + const suffix = suffixLength + ? stringLabels[0].substr(1 - suffixLength) + : undefined; + return { + options: this.applyTrim(stringOptions, prefixLength, suffixLength), + prefix, + suffix, + }; + } + + /** + * Use the calculated prefix and suffix lengths to trim all of the options in + * the given array. + * + * @param options Array of option tuples: + * (human-readable text or image, language-neutral name). + * @param prefixLength The length of the common prefix. + * @param suffixLength The length of the common suffix + * @returns A new array with all of the option text trimmed. + */ + private applyTrim( + options: [string, string][], + prefixLength: number, + suffixLength: number, + ): MenuOption[] { + return options.map(([text, value]) => [ + text.substring(prefixLength, text.length - suffixLength), + value, + ]); + } + + /** + * Validates the data structure to be processed as an options list. + * + * @param options The proposed dropdown options. + * @throws {TypeError} If proposed options are incorrectly structured. + */ + protected validateOptions(options: MenuOption[]) { + if (!Array.isArray(options)) { + throw TypeError('FieldDropdown options must be an array.'); + } + if (!options.length) { + throw TypeError('FieldDropdown options must not be an empty array.'); + } + let foundError = false; + for (let i = 0; i < options.length; i++) { + const tuple = options[i]; + if (!Array.isArray(tuple)) { + foundError = true; + console.error( + `Invalid option[${i}]: Each FieldDropdown option must be an array. + Found: ${tuple}`, + ); + } else if (typeof tuple[1] !== 'string') { + foundError = true; + console.error( + `Invalid option[${i}]: Each FieldDropdown option id must be a string. + Found ${tuple[1]} in: ${tuple}`, + ); + } else if ( + tuple[0] && + typeof tuple[0] !== 'string' && + typeof tuple[0].src !== 'string' + ) { + foundError = true; + console.error( + `Invalid option[${i}]: Each FieldDropdown option must have a string + label or image description. Found ${tuple[0]} in: ${tuple}`, + ); + } + } + if (foundError) { + throw TypeError('Found invalid FieldDropdown options.'); + } + } +} + +/** + * Definition of a human-readable image dropdown option. + */ +export interface ImageProperties { + src: string; + alt: string; + width: number; + height: number; +} + +/** + * An individual option in the dropdown menu. The first element is the human- + * readable value (text or image), and the second element is the language- + * neutral value. + */ +export type MenuOption = [string | ImageProperties, string]; + +/** + * A function that generates an array of menu options for FieldDropdown + * or its descendants. + */ +export type MenuGeneratorFunction = (this: FieldDropdown) => MenuOption[]; + +/** + * Either an array of menu options or a function that generates an array of + * menu options for FieldDropdown or its descendants. + */ +export type MenuGenerator = MenuOption[] | MenuGeneratorFunction; + +/** + * Config options for the dropdown field. + */ +export type FieldDropdownConfig = FieldConfig; + +/** + * fromJson config for the dropdown field. + */ +export interface FieldDropdownFromJsonConfig extends FieldDropdownConfig { + options?: MenuOption[]; +} + +/** + * A function that is called to validate changes to the field's value before + * they are set. + * + * @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values} + * @param newValue The value to be validated. + * @returns One of three instructions for setting the new value: `T`, `null`, + * or `undefined`. + * + * - `T` to set this function's returned value instead of `newValue`. + * + * - `null` to invoke `doValueInvalid_` and not set a value. + * + * - `undefined` to set `newValue` as is. + */ +export type FieldDropdownValidator = FieldValidator; + +fieldRegistry.register('field_dropdown', FieldDropdown); diff --git a/core/field_image.js b/core/field_image.js deleted file mode 100644 index ede3e2b92f9..00000000000 --- a/core/field_image.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Image field. Used for pictures, icons, etc. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.FieldImage'); - -goog.require('Blockly.Field'); -goog.require('goog.dom'); -goog.require('goog.math.Size'); -goog.require('goog.userAgent'); - - -/** - * Class for an image on a block. - * @param {string} src The URL of the image. - * @param {number} width Width of the image. - * @param {number} height Height of the image. - * @param {string=} opt_alt Optional alt text for when block is collapsed. - * @param {function=} opt_onClick Optional function to be called when image is clicked - * @extends {Blockly.Field} - * @constructor - */ -Blockly.FieldImage = function(src, width, height, opt_alt, opt_onClick) { - this.sourceBlock_ = null; - - // Ensure height and width are numbers. Strings are bad at math. - this.height_ = Number(height); - this.width_ = Number(width); - this.size_ = new goog.math.Size(this.width_, - this.height_ + 2 * Blockly.BlockSvg.INLINE_PADDING_Y); - this.text_ = opt_alt || ''; - this.setValue(src); - - if (typeof opt_onClick === "function") { - this.clickHandler_ = opt_onClick; - } -}; -goog.inherits(Blockly.FieldImage, Blockly.Field); - -/** - * Editable fields are saved by the XML renderer, non-editable fields are not. - */ -Blockly.FieldImage.prototype.EDITABLE = false; - -/** - * Install this image on a block. - */ -Blockly.FieldImage.prototype.init = function() { - if (this.fieldGroup_) { - // Image has already been initialized once. - return; - } - // Build the DOM. - /** @type {SVGElement} */ - this.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null); - if (!this.visible_) { - this.fieldGroup_.style.display = 'none'; - } - /** @type {SVGElement} */ - this.imageElement_ = Blockly.utils.createSvgElement( - 'image', - { - 'height': this.height_ + 'px', - 'width': this.width_ + 'px' - }, - this.fieldGroup_); - this.setValue(this.src_); - this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_); - - // Configure the field to be transparent with respect to tooltips. - this.setTooltip(this.sourceBlock_); - Blockly.Tooltip.bindMouseEvents(this.imageElement_); -}; - -/** - * Dispose of all DOM objects belonging to this text. - */ -Blockly.FieldImage.prototype.dispose = function() { - goog.dom.removeNode(this.fieldGroup_); - this.fieldGroup_ = null; - this.imageElement_ = null; -}; - -/** - * Change the tooltip text for this field. - * @param {string|!Element} newTip Text for tooltip or a parent element to - * link to for its tooltip. - */ -Blockly.FieldImage.prototype.setTooltip = function(newTip) { - this.imageElement_.tooltip = newTip; -}; - -/** - * Get the source URL of this image. - * @return {string} Current text. - * @override - */ -Blockly.FieldImage.prototype.getValue = function() { - return this.src_; -}; - -/** - * Set the source URL of this image. - * @param {?string} src New source. - * @override - */ -Blockly.FieldImage.prototype.setValue = function(src) { - if (src === null) { - // No change if null. - return; - } - this.src_ = src; - if (this.imageElement_) { - this.imageElement_.setAttributeNS('http://www.w3.org/1999/xlink', - 'xlink:href', src || ''); - } -}; - -/** - * Set the alt text of this image. - * @param {?string} alt New alt text. - * @override - */ -Blockly.FieldImage.prototype.setText = function(alt) { - if (alt === null) { - // No change if null. - return; - } - this.text_ = alt; -}; - -/** - * Images are fixed width, no need to render. - * @private - */ -Blockly.FieldImage.prototype.render_ = function() { - // NOP -}; - -/** - * Images are fixed width, no need to update. - * @private - */ -Blockly.FieldImage.prototype.updateWidth = function() { - // NOP -}; - -/** - * If field click is called, and click handler defined, - * call the handler. - */ - Blockly.FieldImage.prototype.showEditor = function() { - if (this.clickHandler_){ - this.clickHandler_(this); - } - }; diff --git a/core/field_image.ts b/core/field_image.ts new file mode 100644 index 00000000000..6e83e3405c6 --- /dev/null +++ b/core/field_image.ts @@ -0,0 +1,293 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Image field. Used for pictures, icons, etc. + * + * @class + */ +// Former goog.module ID: Blockly.FieldImage + +import {Field, FieldConfig} from './field.js'; +import * as fieldRegistry from './field_registry.js'; +import * as dom from './utils/dom.js'; +import * as parsing from './utils/parsing.js'; +import {Size} from './utils/size.js'; +import {Svg} from './utils/svg.js'; + +/** + * Class for an image on a block. + */ +export class FieldImage extends Field { + /** + * Vertical padding below the image, which is included in the reported height + * of the field. + */ + private static readonly Y_PADDING = 1; + protected override size_: Size; + protected readonly imageHeight: number; + + /** The function to be called when this field is clicked. */ + private clickHandler: ((p1: FieldImage) => void) | null = null; + + /** The rendered field's image element. */ + protected imageElement: SVGImageElement | null = null; + + /** + * Editable fields usually show some sort of UI indicating they are + * editable. This field should not. + */ + override readonly EDITABLE = false; + + /** + * Used to tell if the field needs to be rendered the next time the block is + * rendered. Image fields are statically sized, and only need to be + * rendered at initialization. + */ + protected override isDirty_ = false; + + /** Whether to flip this image in RTL. */ + private flipRtl = false; + + /** Alt text of this image. */ + private altText = ''; + + /** + * @param src The URL of the image. + * Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by + * subclasses that want to handle configuration and setting the field value + * after their own constructors have run). + * @param width Width of the image. + * @param height Height of the image. + * @param alt Optional alt text for when block is collapsed. + * @param onClick Optional function to be called when the image is + * clicked. If onClick is defined, alt must also be defined. + * @param flipRtl Whether to flip the icon in RTL. + * @param config A map of options used to configure the field. + * See the [field creation documentation]{@link + * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/image#creation} + * for a list of properties this parameter supports. + */ + constructor( + src: string | typeof Field.SKIP_SETUP, + width: string | number, + height: string | number, + alt?: string, + onClick?: (p1: FieldImage) => void, + flipRtl?: boolean, + config?: FieldImageConfig, + ) { + super(Field.SKIP_SETUP); + + const imageHeight = Number(parsing.replaceMessageReferences(height)); + const imageWidth = Number(parsing.replaceMessageReferences(width)); + if (isNaN(imageHeight) || isNaN(imageWidth)) { + throw Error( + 'Height and width values of an image field must cast to' + ' numbers.', + ); + } + if (imageHeight <= 0 || imageWidth <= 0) { + throw Error( + 'Height and width values of an image field must be greater' + + ' than 0.', + ); + } + + /** The size of the area rendered by the field. */ + this.size_ = new Size(imageWidth, imageHeight + FieldImage.Y_PADDING); + + /** + * Store the image height, since it is different from the field height. + */ + this.imageHeight = imageHeight; + + if (typeof onClick === 'function') { + this.clickHandler = onClick; + } + + if (src === Field.SKIP_SETUP) return; + + if (config) { + this.configure_(config); + } else { + this.flipRtl = !!flipRtl; + this.altText = parsing.replaceMessageReferences(alt) || ''; + } + this.setValue(parsing.replaceMessageReferences(src)); + } + + /** + * Configure the field based on the given map of options. + * + * @param config A map of options to configure the field based on. + */ + protected override configure_(config: FieldImageConfig) { + super.configure_(config); + if (config.flipRtl) this.flipRtl = config.flipRtl; + if (config.alt) { + this.altText = parsing.replaceMessageReferences(config.alt); + } + } + + /** + * Create the block UI for this image. + */ + override initView() { + this.imageElement = dom.createSvgElement( + Svg.IMAGE, + { + 'height': this.imageHeight + 'px', + 'width': this.size_.width + 'px', + 'alt': this.altText, + }, + this.fieldGroup_, + ); + this.imageElement.setAttributeNS( + dom.XLINK_NS, + 'xlink:href', + this.value_ as string, + ); + + if (this.clickHandler) { + this.imageElement.style.cursor = 'pointer'; + } + } + + override updateSize_() {} + // NOP + + /** + * Ensure that the input value (the source URL) is a string. + * + * @param newValue The input value. + * @returns A string, or null if invalid. + */ + protected override doClassValidation_(newValue?: any): string | null { + if (typeof newValue !== 'string') { + return null; + } + return newValue; + } + + /** + * Update the value of this image field, and update the displayed image. + * + * @param newValue The value to be saved. The default validator guarantees + * that this is a string. + */ + protected override doValueUpdate_(newValue: string) { + this.value_ = newValue; + if (this.imageElement) { + this.imageElement.setAttributeNS(dom.XLINK_NS, 'xlink:href', this.value_); + } + } + + /** + * Get whether to flip this image in RTL + * + * @returns True if we should flip in RTL. + */ + override getFlipRtl(): boolean { + return this.flipRtl; + } + + /** + * Set the alt text of this image. + * + * @param alt New alt text. + */ + setAlt(alt: string | null) { + if (alt === this.altText) { + return; + } + this.altText = alt || ''; + if (this.imageElement) { + this.imageElement.setAttribute('alt', this.altText); + } + } + + /** + * If field click is called, and click handler defined, + * call the handler. + */ + protected override showEditor_() { + if (this.clickHandler) { + this.clickHandler(this); + } + } + + /** + * Set the function that is called when this image is clicked. + * + * @param func The function that is called when the image is clicked, or null + * to remove. + */ + setOnClickHandler(func: ((p1: FieldImage) => void) | null) { + this.clickHandler = func; + } + + /** + * Use the `getText_` developer hook to override the field's text + * representation. + * Return the image alt text instead. + * + * @returns The image alt text. + */ + protected override getText_(): string | null { + return this.altText; + } + + /** + * Construct a FieldImage from a JSON arg object, + * dereferencing any string table references. + * + * @param options A JSON object with options (src, width, height, alt, and + * flipRtl). + * @returns The new field instance. + * @nocollapse + * @internal + */ + static override fromJson(options: FieldImageFromJsonConfig): FieldImage { + if (!options.src || !options.width || !options.height) { + throw new Error( + 'src, width, and height values for an image field are' + + 'required. The width and height must be non-zero.', + ); + } + // `this` might be a subclass of FieldImage if that class doesn't override + // the static fromJson method. + return new this( + options.src, + options.width, + options.height, + undefined, + undefined, + undefined, + options, + ); + } +} + +fieldRegistry.register('field_image', FieldImage); + +FieldImage.prototype.DEFAULT_VALUE = ''; + +/** + * Config options for the image field. + */ +export interface FieldImageConfig extends FieldConfig { + flipRtl?: boolean; + alt?: string; +} + +/** + * fromJson config options for the image field. + */ +export interface FieldImageFromJsonConfig extends FieldImageConfig { + src?: string; + width?: number; + height?: number; +} diff --git a/core/field_input.ts b/core/field_input.ts new file mode 100644 index 00000000000..eecb4ec94e8 --- /dev/null +++ b/core/field_input.ts @@ -0,0 +1,756 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Text input field. + * + * @class + */ +// Former goog.module ID: Blockly.FieldInput + +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_block_change.js'; + +import {BlockSvg} from './block_svg.js'; +import * as browserEvents from './browser_events.js'; +import * as bumpObjects from './bump_objects.js'; +import * as dialog from './dialog.js'; +import * as dropDownDiv from './dropdowndiv.js'; +import {EventType} from './events/type.js'; +import * as eventUtils from './events/utils.js'; +import { + Field, + FieldConfig, + FieldValidator, + UnattachedFieldError, +} from './field.js'; +import {Msg} from './msg.js'; +import * as renderManagement from './render_management.js'; +import * as aria from './utils/aria.js'; +import * as dom from './utils/dom.js'; +import {Size} from './utils/size.js'; +import * as userAgent from './utils/useragent.js'; +import * as WidgetDiv from './widgetdiv.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Supported types for FieldInput subclasses. + * + * @internal + */ +type InputTypes = string | number; + +/** + * Abstract class for an editable input field. + * + * @typeParam T - The value stored on the field. + * @internal + */ +export abstract class FieldInput extends Field< + string | T +> { + /** + * Pixel size of input border radius. + * Should match blocklyText's border-radius in CSS. + */ + static BORDERRADIUS = 4; + + /** Allow browser to spellcheck this field. */ + protected spellcheck_ = true; + + /** The HTML input element. */ + protected htmlInput_: HTMLInputElement | null = null; + + /** True if the field's value is currently being edited via the UI. */ + protected isBeingEdited_ = false; + + /** + * True if the value currently displayed in the field's editory UI is valid. + */ + protected isTextValid_ = false; + + /** + * The intial value of the field when the user opened an editor to change its + * value. When the editor is disposed, an event will be fired that uses this + * as the event's oldValue. + */ + protected valueWhenEditorWasOpened_: string | T | null = null; + + /** Key down event data. */ + private onKeyDownWrapper: browserEvents.Data | null = null; + + /** Key input event data. */ + private onKeyInputWrapper: browserEvents.Data | null = null; + + /** + * Whether the field should consider the whole parent block to be its click + * target. + */ + fullBlockClickTarget_: boolean = false; + + /** The workspace that this field belongs to. */ + protected workspace_: WorkspaceSvg | null = null; + + /** + * Serializable fields are saved by the serializer, non-serializable fields + * are not. Editable fields should also be serializable. + */ + override SERIALIZABLE = true; + + /** Mouse cursor style when over the hotspot that initiates the editor. */ + override CURSOR = 'text'; + + /** + * @param value The initial value of the field. Should cast to a string. + * Defaults to an empty string if null or undefined. Also accepts + * Field.SKIP_SETUP if you wish to skip setup (only used by subclasses + * that want to handle configuration and setting the field value after + * their own constructors have run). + * @param validator A function that is called to validate changes to the + * field's value. Takes in a string & returns a validated string, or null + * to abort the change. + * @param config A map of options used to configure the field. + * See the [field creation documentation]{@link + * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/text-input#creation} + * for a list of properties this parameter supports. + */ + constructor( + value?: string | typeof Field.SKIP_SETUP, + validator?: FieldInputValidator | null, + config?: FieldInputConfig, + ) { + super(Field.SKIP_SETUP); + + if (value === Field.SKIP_SETUP) return; + if (config) { + this.configure_(config); + } + this.setValue(value); + if (validator) { + this.setValidator(validator); + } + } + + protected override configure_(config: FieldInputConfig) { + super.configure_(config); + if (config.spellcheck !== undefined) { + this.spellcheck_ = config.spellcheck; + } + } + + override initView() { + const block = this.getSourceBlock(); + if (!block) throw new UnattachedFieldError(); + super.initView(); + + if (this.isFullBlockField()) { + this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot(); + } + } + + protected override isFullBlockField(): boolean { + const block = this.getSourceBlock(); + if (!block) throw new UnattachedFieldError(); + + // Side effect for backwards compatibility. + this.fullBlockClickTarget_ = + !!this.getConstants()?.FULL_BLOCK_FIELDS && block.isSimpleReporter(); + return this.fullBlockClickTarget_; + } + + /** + * Called by setValue if the text input is not valid. If the field is + * currently being edited it reverts value of the field to the previous + * value while allowing the display text to be handled by the htmlInput_. + * + * @param _invalidValue The input value that was determined to be invalid. + * This is not used by the text input because its display value is stored + * on the htmlInput_. + * @param fireChangeEvent Whether to fire a change event if the value changes. + */ + protected override doValueInvalid_( + _invalidValue: AnyDuringMigration, + fireChangeEvent: boolean = true, + ) { + if (this.isBeingEdited_) { + this.isDirty_ = true; + this.isTextValid_ = false; + const oldValue = this.value_; + // Revert value when the text becomes invalid. + this.value_ = this.valueWhenEditorWasOpened_; + if ( + this.sourceBlock_ && + eventUtils.isEnabled() && + this.value_ !== oldValue && + fireChangeEvent + ) { + eventUtils.fire( + new (eventUtils.get(EventType.BLOCK_CHANGE))( + this.sourceBlock_, + 'field', + this.name || null, + oldValue, + this.value_, + ), + ); + } + } + } + + /** + * Called by setValue if the text input is valid. Updates the value of the + * field, and updates the text of the field if it is not currently being + * edited (i.e. handled by the htmlInput_). + * + * @param newValue The value to be saved. The default validator guarantees + * that this is a string. + */ + protected override doValueUpdate_(newValue: string | T) { + this.isDirty_ = true; + this.isTextValid_ = true; + this.value_ = newValue; + } + + /** + * Updates text field to match the colour/style of the block. + */ + override applyColour() { + const block = this.getSourceBlock() as BlockSvg | null; + if (!block) throw new UnattachedFieldError(); + + if (!this.getConstants()!.FULL_BLOCK_FIELDS) return; + if (!this.fieldGroup_) return; + + if (!this.isFullBlockField() && this.borderRect_) { + this.borderRect_!.style.display = 'block'; + this.borderRect_.setAttribute('stroke', block.style.colourTertiary); + } else { + this.borderRect_!.style.display = 'none'; + // In general, do *not* let fields control the color of blocks. Having the + // field control the color is unexpected, and could have performance + // impacts. + block.pathObject.svgPath.setAttribute( + 'fill', + this.getConstants()!.FIELD_BORDER_RECT_COLOUR, + ); + } + } + + /** + * Returns the height and width of the field. + * + * This should *in general* be the only place render_ gets called from. + * + * @returns Height and width. + */ + override getSize(): Size { + if (this.getConstants()?.FULL_BLOCK_FIELDS) { + // In general, do *not* let fields control the color of blocks. Having the + // field control the color is unexpected, and could have performance + // impacts. + // Full block fields have more control of the block than they should + // (i.e. updating fill colour). Whenever we get the size, the field may + // no longer be a full-block field, so we need to rerender. + this.render_(); + this.isDirty_ = false; + } + return super.getSize(); + } + + /** + * Notifies the field that it has changed locations. Moves the widget div to + * be in the correct place if it is open. + */ + onLocationChange(): void { + if (this.isBeingEdited_) this.resizeEditor_(); + } + + /** + * Updates the colour of the htmlInput given the current validity of the + * field's value. + * + * Also updates the colour of the block to reflect whether this is a full + * block field or not. + */ + protected override render_() { + super.render_(); + // This logic is done in render_ rather than doValueInvalid_ or + // doValueUpdate_ so that the code is more centralized. + if (this.isBeingEdited_) { + const htmlInput = this.htmlInput_ as HTMLElement; + if (!this.isTextValid_) { + dom.addClass(htmlInput, 'blocklyInvalidInput'); + aria.setState(htmlInput, aria.State.INVALID, true); + } else { + dom.removeClass(htmlInput, 'blocklyInvalidInput'); + aria.setState(htmlInput, aria.State.INVALID, false); + } + } + + const block = this.getSourceBlock() as BlockSvg | null; + if (!block) throw new UnattachedFieldError(); + // In general, do *not* let fields control the color of blocks. Having the + // field control the color is unexpected, and could have performance + // impacts. + // Whenever we render, the field may no longer be a full-block-field so + // we need to update the colour. + if (this.getConstants()!.FULL_BLOCK_FIELDS) block.applyColour(); + } + + /** + * Set whether this field is spellchecked by the browser. + * + * @param check True if checked. + */ + setSpellcheck(check: boolean) { + if (check === this.spellcheck_) { + return; + } + this.spellcheck_ = check; + if (this.htmlInput_) { + // AnyDuringMigration because: Argument of type 'boolean' is not + // assignable to parameter of type 'string'. + this.htmlInput_.setAttribute( + 'spellcheck', + this.spellcheck_ as AnyDuringMigration, + ); + } + } + + /** + * Show an editor for the field. + * Shows the inline free-text editor on top of the text by default. + * Shows a prompt editor for mobile browsers if the modalInputs option is + * enabled. + * + * @param _e Optional mouse event that triggered the field to open, or + * undefined if triggered programmatically. + * @param quietInput True if editor should be created without focus. + * Defaults to false. + */ + protected override showEditor_(_e?: Event, quietInput = false) { + this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace; + if ( + !quietInput && + this.workspace_.options.modalInputs && + (userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD) + ) { + this.showPromptEditor(); + } else { + this.showInlineEditor(quietInput); + } + } + + /** + * Create and show a text input editor that is a prompt (usually a popup). + * Mobile browsers may have issues with in-line textareas (focus and + * keyboards). + */ + private showPromptEditor() { + dialog.prompt( + Msg['CHANGE_VALUE_TITLE'], + this.getText(), + (text: string | null) => { + // Text is null if user pressed cancel button. + if (text !== null) { + this.setValue(this.getValueFromEditorText_(text)); + } + this.onFinishEditing_(this.value_); + }, + ); + } + + /** + * Create and show a text input editor that sits directly over the text input. + * + * @param quietInput True if editor should be created without focus. + */ + private showInlineEditor(quietInput: boolean) { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + WidgetDiv.show( + this, + block.RTL, + this.widgetDispose_.bind(this), + this.workspace_, + ); + this.htmlInput_ = this.widgetCreate_() as HTMLInputElement; + this.isBeingEdited_ = true; + this.valueWhenEditorWasOpened_ = this.value_; + + if (!quietInput) { + (this.htmlInput_ as HTMLElement).focus({ + preventScroll: true, + }); + this.htmlInput_.select(); + } + } + + /** + * Create the text input editor widget. + * + * @returns The newly created text input editor. + */ + protected widgetCreate_(): HTMLInputElement | HTMLTextAreaElement { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + eventUtils.setGroup(true); + const div = WidgetDiv.getDiv(); + + const clickTarget = this.getClickTarget_(); + if (!clickTarget) throw new Error('A click target has not been set.'); + dom.addClass(clickTarget, 'editing'); + + const htmlInput = document.createElement('input'); + htmlInput.className = 'blocklyHtmlInput'; + // AnyDuringMigration because: Argument of type 'boolean' is not assignable + // to parameter of type 'string'. + htmlInput.setAttribute( + 'spellcheck', + this.spellcheck_ as AnyDuringMigration, + ); + const scale = this.workspace_!.getScale(); + const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt'; + div!.style.fontSize = fontSize; + htmlInput.style.fontSize = fontSize; + let borderRadius = FieldInput.BORDERRADIUS * scale + 'px'; + + if (this.isFullBlockField()) { + const bBox = this.getScaledBBox(); + + // Override border radius. + borderRadius = (bBox.bottom - bBox.top) / 2 + 'px'; + // Pull stroke colour from the existing shadow block + const strokeColour = block.getParent() + ? (block.getParent() as BlockSvg).style.colourTertiary + : (this.sourceBlock_ as BlockSvg).style.colourTertiary; + htmlInput.style.border = 1 * scale + 'px solid ' + strokeColour; + div!.style.borderRadius = borderRadius; + div!.style.transition = 'box-shadow 0.25s ease 0s'; + if (this.getConstants()!.FIELD_TEXTINPUT_BOX_SHADOW) { + div!.style.boxShadow = + 'rgba(255, 255, 255, 0.3) 0 0 0 ' + 4 * scale + 'px'; + } + } + htmlInput.style.borderRadius = borderRadius; + + div!.appendChild(htmlInput); + + htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_); + htmlInput.setAttribute('data-untyped-default-value', String(this.value_)); + + this.resizeEditor_(); + + this.bindInputEvents_(htmlInput); + + return htmlInput; + } + + /** + * Closes the editor, saves the results, and disposes of any events or + * DOM-references belonging to the editor. + */ + protected widgetDispose_() { + // Non-disposal related things that we do when the editor closes. + this.isBeingEdited_ = false; + this.isTextValid_ = true; + // Make sure the field's node matches the field's internal value. + this.forceRerender(); + this.onFinishEditing_(this.value_); + + if ( + this.sourceBlock_ && + eventUtils.isEnabled() && + this.valueWhenEditorWasOpened_ !== null && + this.valueWhenEditorWasOpened_ !== this.value_ + ) { + // When closing a field input widget, fire an event indicating that the + // user has completed a sequence of changes. The value may have changed + // multiple times while the editor was open, but this will fire an event + // containing the value when the editor was opened as well as the new one. + eventUtils.fire( + new (eventUtils.get(EventType.BLOCK_CHANGE))( + this.sourceBlock_, + 'field', + this.name || null, + this.valueWhenEditorWasOpened_, + this.value_, + ), + ); + this.valueWhenEditorWasOpened_ = null; + } + + eventUtils.setGroup(false); + + // Actual disposal. + this.unbindInputEvents_(); + const style = WidgetDiv.getDiv()!.style; + style.width = 'auto'; + style.height = 'auto'; + style.fontSize = ''; + style.transition = ''; + style.boxShadow = ''; + this.htmlInput_ = null; + + const clickTarget = this.getClickTarget_(); + if (!clickTarget) throw new Error('A click target has not been set.'); + dom.removeClass(clickTarget, 'editing'); + } + + /** + * A callback triggered when the user is done editing the field via the UI. + * + * @param _value The new value of the field. + */ + onFinishEditing_(_value: AnyDuringMigration) {} + + /** + * Bind handlers for user input on the text input field's editor. + * + * @param htmlInput The htmlInput to which event handlers will be bound. + */ + protected bindInputEvents_(htmlInput: HTMLElement) { + // Trap Enter without IME and Esc to hide. + this.onKeyDownWrapper = browserEvents.conditionalBind( + htmlInput, + 'keydown', + this, + this.onHtmlInputKeyDown_, + ); + // Resize after every input change. + this.onKeyInputWrapper = browserEvents.conditionalBind( + htmlInput, + 'input', + this, + this.onHtmlInputChange, + ); + } + + /** Unbind handlers for user input and workspace size changes. */ + protected unbindInputEvents_() { + if (this.onKeyDownWrapper) { + browserEvents.unbind(this.onKeyDownWrapper); + this.onKeyDownWrapper = null; + } + if (this.onKeyInputWrapper) { + browserEvents.unbind(this.onKeyInputWrapper); + this.onKeyInputWrapper = null; + } + } + + /** + * Handle key down to the editor. + * + * @param e Keyboard event. + */ + protected onHtmlInputKeyDown_(e: KeyboardEvent) { + if (e.key === 'Enter') { + WidgetDiv.hideIfOwner(this); + dropDownDiv.hideWithoutAnimation(); + } else if (e.key === 'Escape') { + this.setValue( + this.htmlInput_!.getAttribute('data-untyped-default-value'), + false, + ); + WidgetDiv.hideIfOwner(this); + dropDownDiv.hideWithoutAnimation(); + } else if (e.key === 'Tab') { + WidgetDiv.hideIfOwner(this); + dropDownDiv.hideWithoutAnimation(); + (this.sourceBlock_ as BlockSvg).tab(this, !e.shiftKey); + e.preventDefault(); + } + } + + /** + * Handle a change to the editor. + * + * @param _e Keyboard event. + */ + private onHtmlInputChange(_e: Event) { + // Intermediate value changes from user input are not confirmed until the + // user closes the editor, and may be numerous. Inhibit reporting these as + // normal block change events, and instead report them as special + // intermediate changes that do not get recorded in undo history. + const oldValue = this.value_; + // Change the field's value without firing the normal change event. + this.setValue( + this.getValueFromEditorText_(this.htmlInput_!.value), + /* fireChangeEvent= */ false, + ); + if ( + this.sourceBlock_ && + eventUtils.isEnabled() && + this.value_ !== oldValue + ) { + // Fire a special event indicating that the value changed but the change + // isn't complete yet and normal field change listeners can wait. + eventUtils.fire( + new (eventUtils.get(EventType.BLOCK_FIELD_INTERMEDIATE_CHANGE))( + this.sourceBlock_, + this.name || null, + oldValue, + this.value_, + ), + ); + } + } + + /** + * Set the HTML input value and the field's internal value. The difference + * between this and `setValue` is that this also updates the HTML input + * value whilst editing. + * + * @param newValue New value. + * @param fireChangeEvent Whether to fire a change event. Defaults to true. + * Should usually be true unless the change will be reported some other + * way, e.g. an intermediate field change event. + */ + protected setEditorValue_( + newValue: AnyDuringMigration, + fireChangeEvent = true, + ) { + this.isDirty_ = true; + if (this.isBeingEdited_) { + // In the case this method is passed an invalid value, we still + // pass it through the transformation method `getEditorText` to deal + // with. Otherwise, the internal field's state will be inconsistent + // with what's shown to the user. + this.htmlInput_!.value = this.getEditorText_(newValue); + } + this.setValue(newValue, fireChangeEvent); + } + + /** Resize the editor to fit the text. */ + protected resizeEditor_() { + renderManagement.finishQueuedRenders().then(() => { + const block = this.getSourceBlock(); + if (!block) throw new UnattachedFieldError(); + const div = WidgetDiv.getDiv(); + const bBox = this.getScaledBBox(); + div!.style.width = bBox.right - bBox.left + 'px'; + div!.style.height = bBox.bottom - bBox.top + 'px'; + + // In RTL mode block fields and LTR input fields the left edge moves, + // whereas the right edge is fixed. Reposition the editor. + const x = block.RTL ? bBox.right - div!.offsetWidth : bBox.left; + const y = bBox.top; + + div!.style.left = `${x}px`; + div!.style.top = `${y}px`; + }); + } + + /** + * Handles repositioning the WidgetDiv used for input fields when the + * workspace is resized. Will bump the block into the viewport and update the + * position of the text input if necessary. + * + * @returns True for rendered workspaces, as we never want to hide the widget + * div. + */ + override repositionForWindowResize(): boolean { + const block = this.getSourceBlock()?.getRootBlock(); + // This shouldn't be possible. We should never have a WidgetDiv if not using + // rendered blocks. + if (!(block instanceof BlockSvg)) return false; + + const bumped = bumpObjects.bumpIntoBounds( + this.workspace_!, + this.workspace_!.getMetricsManager().getViewMetrics(true), + block, + ); + + if (!bumped) this.resizeEditor_(); + + return true; + } + + /** + * Returns whether or not the field is tab navigable. + * + * @returns True if the field is tab navigable. + */ + override isTabNavigable(): boolean { + return true; + } + + /** + * Use the `getText_` developer hook to override the field's text + * representation. When we're currently editing, return the current HTML value + * instead. Otherwise, return null which tells the field to use the default + * behaviour (which is a string cast of the field's value). + * + * @returns The HTML value if we're editing, otherwise null. + */ + protected override getText_(): string | null { + if (this.isBeingEdited_ && this.htmlInput_) { + // We are currently editing, return the HTML input value instead. + return this.htmlInput_.value; + } + return null; + } + + /** + * Transform the provided value into a text to show in the HTML input. + * Override this method if the field's HTML input representation is different + * than the field's value. This should be coupled with an override of + * `getValueFromEditorText_`. + * + * @param value The value stored in this field. + * @returns The text to show on the HTML input. + */ + protected getEditorText_(value: AnyDuringMigration): string { + return `${value}`; + } + + /** + * Transform the text received from the HTML input into a value to store + * in this field. + * Override this method if the field's HTML input representation is different + * than the field's value. This should be coupled with an override of + * `getEditorText_`. + * + * @param text Text received from the HTML input. + * @returns The value to store. + */ + protected getValueFromEditorText_(text: string): AnyDuringMigration { + return text; + } +} + +/** + * Config options for the input field. + * + * @internal + */ +export interface FieldInputConfig extends FieldConfig { + spellcheck?: boolean; +} + +/** + * A function that is called to validate changes to the field's value before + * they are set. + * + * @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values} + * @param newValue The value to be validated. + * @returns One of three instructions for setting the new value: `T`, `null`, + * or `undefined`. + * + * - `T` to set this function's returned value instead of `newValue`. + * + * - `null` to invoke `doValueInvalid_` and not set a value. + * + * - `undefined` to set `newValue` as is. + * @internal + */ +export type FieldInputValidator = FieldValidator< + string | T +>; diff --git a/core/field_label.js b/core/field_label.js deleted file mode 100644 index f4bca6a24a5..00000000000 --- a/core/field_label.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Non-editable text field. Used for titles, labels, etc. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.FieldLabel'); - -goog.require('Blockly.Field'); -goog.require('Blockly.Tooltip'); -goog.require('goog.dom'); -goog.require('goog.math.Size'); - - -/** - * Class for a non-editable field. - * @param {string} text The initial content of the field. - * @param {string=} opt_class Optional CSS class for the field's text. - * @extends {Blockly.Field} - * @constructor - */ -Blockly.FieldLabel = function(text, opt_class) { - this.size_ = new goog.math.Size(0, 17.5); - this.class_ = opt_class; - this.setValue(text); -}; -goog.inherits(Blockly.FieldLabel, Blockly.Field); - -/** - * Editable fields are saved by the XML renderer, non-editable fields are not. - */ -Blockly.FieldLabel.prototype.EDITABLE = false; - -/** - * Install this text on a block. - */ -Blockly.FieldLabel.prototype.init = function() { - if (this.textElement_) { - // Text has already been initialized once. - return; - } - // Build the DOM. - this.textElement_ = Blockly.utils.createSvgElement('text', - {'class': 'blocklyText', 'y': this.size_.height - 5}, null); - if (this.class_) { - Blockly.utils.addClass(this.textElement_, this.class_); - } - if (!this.visible_) { - this.textElement_.style.display = 'none'; - } - this.sourceBlock_.getSvgRoot().appendChild(this.textElement_); - - // Configure the field to be transparent with respect to tooltips. - this.textElement_.tooltip = this.sourceBlock_; - Blockly.Tooltip.bindMouseEvents(this.textElement_); - // Force a render. - this.render_(); -}; - -/** - * Dispose of all DOM objects belonging to this text. - */ -Blockly.FieldLabel.prototype.dispose = function() { - goog.dom.removeNode(this.textElement_); - this.textElement_ = null; -}; - -/** - * Gets the group element for this field. - * Used for measuring the size and for positioning. - * @return {!Element} The group element. - */ -Blockly.FieldLabel.prototype.getSvgRoot = function() { - return /** @type {!Element} */ (this.textElement_); -}; - -/** - * Change the tooltip text for this field. - * @param {string|!Element} newTip Text for tooltip or a parent element to - * link to for its tooltip. - */ -Blockly.FieldLabel.prototype.setTooltip = function(newTip) { - this.textElement_.tooltip = newTip; -}; diff --git a/core/field_label.ts b/core/field_label.ts new file mode 100644 index 00000000000..2b0ae1eba49 --- /dev/null +++ b/core/field_label.ts @@ -0,0 +1,147 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Non-editable, non-serializable text field. Used for titles, + * labels, etc. + * + * @class + */ +// Former goog.module ID: Blockly.FieldLabel + +import {Field, FieldConfig} from './field.js'; +import * as fieldRegistry from './field_registry.js'; +import * as dom from './utils/dom.js'; +import * as parsing from './utils/parsing.js'; + +/** + * Class for a non-editable, non-serializable text field. + */ +export class FieldLabel extends Field { + /** The HTML class name to use for this field. */ + private class: string | null = null; + + /** + * Editable fields usually show some sort of UI indicating they are + * editable. This field should not. + */ + override EDITABLE = false; + + /** Text labels should not truncate. */ + override maxDisplayLength = Infinity; + + /** + * @param value The initial value of the field. Should cast to a string. + * Defaults to an empty string if null or undefined. Also accepts + * Field.SKIP_SETUP if you wish to skip setup (only used by subclasses + * that want to handle configuration and setting the field value after + * their own constructors have run). + * @param textClass Optional CSS class for the field's text. + * @param config A map of options used to configure the field. + * See the [field creation documentation]{@link + * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label#creation} + * for a list of properties this parameter supports. + */ + constructor( + value?: string | typeof Field.SKIP_SETUP, + textClass?: string, + config?: FieldLabelConfig, + ) { + super(Field.SKIP_SETUP); + + if (value === Field.SKIP_SETUP) return; + if (config) { + this.configure_(config); + } else { + this.class = textClass || null; + } + this.setValue(value); + } + + protected override configure_(config: FieldLabelConfig) { + super.configure_(config); + if (config.class) this.class = config.class; + } + + /** + * Create block UI for this label. + */ + override initView() { + this.createTextElement_(); + if (this.class) { + dom.addClass(this.getTextElement(), this.class); + } + } + + /** + * Ensure that the input value casts to a valid string. + * + * @param newValue The input value. + * @returns A valid string, or null if invalid. + */ + protected override doClassValidation_( + newValue?: AnyDuringMigration, + ): string | null { + if (newValue === null || newValue === undefined) { + return null; + } + return `${newValue}`; + } + + /** + * Set the CSS class applied to the field's textElement_. + * + * @param cssClass The new CSS class name, or null to remove. + */ + setClass(cssClass: string | null) { + if (this.textElement_) { + if (this.class) { + dom.removeClass(this.textElement_, this.class); + } + if (cssClass) { + dom.addClass(this.textElement_, cssClass); + } + } + this.class = cssClass; + } + + /** + * Construct a FieldLabel from a JSON arg object, + * dereferencing any string table references. + * + * @param options A JSON object with options (text, and class). + * @returns The new field instance. + * @nocollapse + * @internal + */ + static override fromJson(options: FieldLabelFromJsonConfig): FieldLabel { + const text = parsing.replaceMessageReferences(options.text); + // `this` might be a subclass of FieldLabel if that class doesn't override + // the static fromJson method. + return new this(text, undefined, options); + } +} + +fieldRegistry.register('field_label', FieldLabel); + +FieldLabel.prototype.DEFAULT_VALUE = ''; + +// clang-format off +// Clang does not like the 'class' keyword being used as a property. +/** + * Config options for the label field. + */ +export interface FieldLabelConfig extends FieldConfig { + class?: string; +} +// clang-format on + +/** + * fromJson config options for the label field. + */ +export interface FieldLabelFromJsonConfig extends FieldLabelConfig { + text?: string; +} diff --git a/core/field_label_serializable.ts b/core/field_label_serializable.ts new file mode 100644 index 00000000000..b2783583a72 --- /dev/null +++ b/core/field_label_serializable.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Non-editable, serializable text field. Behaves like a + * normal label but is serialized to XML. It may only be + * edited programmatically. + * + * @class + */ +// Former goog.module ID: Blockly.FieldLabelSerializable + +import { + FieldLabel, + FieldLabelConfig, + FieldLabelFromJsonConfig, +} from './field_label.js'; +import * as fieldRegistry from './field_registry.js'; +import * as parsing from './utils/parsing.js'; + +/** + * Class for a non-editable, serializable text field. + */ +export class FieldLabelSerializable extends FieldLabel { + /** + * Editable fields usually show some sort of UI indicating they are + * editable. This field should not. + */ + override EDITABLE = false; + + /** + * Serializable fields are saved by the XML renderer, non-serializable + * fields are not. This field should be serialized, but only edited + * programmatically. + */ + override SERIALIZABLE = true; + + /** + * @param value The initial value of the field. Should cast to a string. + * Defaults to an empty string if null or undefined. + * @param textClass Optional CSS class for the field's text. + * @param config A map of options used to configure the field. + * See the [field creation documentation]{@link + * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label-serializable#creation} + * for a list of properties this parameter supports. + */ + constructor(value?: string, textClass?: string, config?: FieldLabelConfig) { + super(String(value ?? ''), textClass, config); + } + + /** + * Construct a FieldLabelSerializable from a JSON arg object, + * dereferencing any string table references. + * + * @param options A JSON object with options (text, and class). + * @returns The new field instance. + * @nocollapse + * @internal + */ + static override fromJson( + options: FieldLabelFromJsonConfig, + ): FieldLabelSerializable { + const text = parsing.replaceMessageReferences(options.text); + // `this` might be a subclass of FieldLabelSerializable if that class + // doesn't override the static fromJson method. + return new this(text, undefined, options); + } +} + +fieldRegistry.register('field_label_serializable', FieldLabelSerializable); diff --git a/core/field_number.js b/core/field_number.js deleted file mode 100644 index 722b0eefa52..00000000000 --- a/core/field_number.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Number input field - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -goog.provide('Blockly.FieldNumber'); - -goog.require('Blockly.FieldTextInput'); -goog.require('goog.math'); - -/** - * Class for an editable number field. - * @param {(string|number)=} opt_value The initial content of the field. The value - * should cast to a number, and if it does not, '0' will be used. - * @param {(string|number)=} opt_min Minimum value. - * @param {(string|number)=} opt_max Maximum value. - * @param {(string|number)=} opt_precision Precision for value. - * @param {Function=} opt_validator An optional function that is called - * to validate any constraints on what the user entered. Takes the new - * text as an argument and returns either the accepted text, a replacement - * text, or null to abort the change. - * @extends {Blockly.FieldTextInput} - * @constructor - */ -Blockly.FieldNumber = function(opt_value, opt_min, opt_max, opt_precision, - opt_validator) { - opt_value = (opt_value && !isNaN(opt_value)) ? String(opt_value) : '0'; - Blockly.FieldNumber.superClass_.constructor.call( - this, opt_value, opt_validator); - this.setConstraints(opt_min, opt_max, opt_precision); -}; -goog.inherits(Blockly.FieldNumber, Blockly.FieldTextInput); - -/** - * Set the maximum, minimum and precision constraints on this field. - * Any of these properties may be undefiend or NaN to be disabled. - * Setting precision (usually a power of 10) enforces a minimum step between - * values. That is, the user's value will rounded to the closest multiple of - * precision. The least significant digit place is inferred from the precision. - * Integers values can be enforces by choosing an integer precision. - * @param {number|string|undefined} min Minimum value. - * @param {number|string|undefined} max Maximum value. - * @param {number|string|undefined} precision Precision for value. - */ -Blockly.FieldNumber.prototype.setConstraints = function(min, max, precision) { - precision = parseFloat(precision); - this.precision_ = isNaN(precision) ? 0 : precision; - min = parseFloat(min); - this.min_ = isNaN(min) ? -Infinity : min; - max = parseFloat(max); - this.max_ = isNaN(max) ? Infinity : max; - this.setValue(this.callValidator(this.getValue())); -}; - -/** - * Ensure that only a number in the correct range may be entered. - * @param {string} text The user's text. - * @return {?string} A string representing a valid number, or null if invalid. - */ -Blockly.FieldNumber.prototype.classValidator = function(text) { - if (text === null) { - return null; - } - text = String(text); - // TODO: Handle cases like 'ten', '1.203,14', etc. - // 'O' is sometimes mistaken for '0' by inexperienced users. - text = text.replace(/O/ig, '0'); - // Strip out thousands separators. - text = text.replace(/,/g, ''); - var n = parseFloat(text || 0); - if (isNaN(n)) { - // Invalid number. - return null; - } - // Round to nearest multiple of precision. - if (this.precision_ && isFinite(n)) { - n = Math.round(n / this.precision_) * this.precision_; - } - // Get the value in range. - n = goog.math.clamp(n, this.min_, this.max_); - return String(n); -}; diff --git a/core/field_number.ts b/core/field_number.ts new file mode 100644 index 00000000000..0641b9ae32b --- /dev/null +++ b/core/field_number.ts @@ -0,0 +1,367 @@ +/** + * @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Number input field + * + * @class + */ +// Former goog.module ID: Blockly.FieldNumber + +import {Field} from './field.js'; +import { + FieldInput, + FieldInputConfig, + FieldInputValidator, +} from './field_input.js'; +import * as fieldRegistry from './field_registry.js'; +import * as aria from './utils/aria.js'; + +/** + * Class for an editable number field. + */ +export class FieldNumber extends FieldInput { + /** The minimum value this number field can contain. */ + protected min_ = -Infinity; + + /** The maximum value this number field can contain. */ + protected max_ = Infinity; + + /** The multiple to which this fields value is rounded. */ + protected precision_ = 0; + + /** + * The number of decimal places to allow, or null to allow any number of + * decimal digits. + */ + private decimalPlaces: number | null = null; + + /** Don't spellcheck numbers. Our validator does a better job. */ + protected override spellcheck_ = false; + + /** + * @param value The initial value of the field. Should cast to a number. + * Defaults to 0. Also accepts Field.SKIP_SETUP if you wish to skip setup + * (only used by subclasses that want to handle configuration and setting + * the field value after their own constructors have run). + * @param min Minimum value. Will only be used if config is not + * provided. + * @param max Maximum value. Will only be used if config is not + * provided. + * @param precision Precision for value. Will only be used if config + * is not provided. + * @param validator A function that is called to validate changes to the + * field's value. Takes in a number & returns a validated number, or null + * to abort the change. + * @param config A map of options used to configure the field. + * See the [field creation documentation]{@link + * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/number#creation} + * for a list of properties this parameter supports. + */ + constructor( + value?: string | number | typeof Field.SKIP_SETUP, + min?: string | number | null, + max?: string | number | null, + precision?: string | number | null, + validator?: FieldNumberValidator | null, + config?: FieldNumberConfig, + ) { + // Pass SENTINEL so that we can define properties before value validation. + super(Field.SKIP_SETUP); + + if (value === Field.SKIP_SETUP) return; + if (config) { + this.configure_(config); + } else { + this.setConstraints(min, max, precision); + } + this.setValue(value); + if (validator) { + this.setValidator(validator); + } + } + + /** + * Configure the field based on the given map of options. + * + * @param config A map of options to configure the field based on. + */ + protected override configure_(config: FieldNumberConfig) { + super.configure_(config); + this.setMinInternal(config.min); + this.setMaxInternal(config.max); + this.setPrecisionInternal(config.precision); + } + + /** + * Set the maximum, minimum and precision constraints on this field. + * Any of these properties may be undefined or NaN to be disabled. + * Setting precision (usually a power of 10) enforces a minimum step between + * values. That is, the user's value will rounded to the closest multiple of + * precision. The least significant digit place is inferred from the + * precision. Integers values can be enforces by choosing an integer + * precision. + * + * @param min Minimum value. + * @param max Maximum value. + * @param precision Precision for value. + */ + setConstraints( + min: number | string | undefined | null, + max: number | string | undefined | null, + precision: number | string | undefined | null, + ) { + this.setMinInternal(min); + this.setMaxInternal(max); + this.setPrecisionInternal(precision); + this.setValue(this.getValue()); + } + + /** + * Sets the minimum value this field can contain. Updates the value to + * reflect. + * + * @param min Minimum value. + */ + setMin(min: number | string | undefined | null) { + this.setMinInternal(min); + this.setValue(this.getValue()); + } + + /** + * Sets the minimum value this field can contain. Called internally to avoid + * value updates. + * + * @param min Minimum value. + */ + private setMinInternal(min: number | string | undefined | null) { + if (min == null) { + this.min_ = -Infinity; + } else { + min = Number(min); + if (!isNaN(min)) { + this.min_ = min; + } + } + } + + /** + * Returns the current minimum value this field can contain. Default is + * -Infinity. + * + * @returns The current minimum value this field can contain. + */ + getMin(): number { + return this.min_; + } + + /** + * Sets the maximum value this field can contain. Updates the value to + * reflect. + * + * @param max Maximum value. + */ + setMax(max: number | string | undefined | null) { + this.setMaxInternal(max); + this.setValue(this.getValue()); + } + + /** + * Sets the maximum value this field can contain. Called internally to avoid + * value updates. + * + * @param max Maximum value. + */ + private setMaxInternal(max: number | string | undefined | null) { + if (max == null) { + this.max_ = Infinity; + } else { + max = Number(max); + if (!isNaN(max)) { + this.max_ = max; + } + } + } + + /** + * Returns the current maximum value this field can contain. Default is + * Infinity. + * + * @returns The current maximum value this field can contain. + */ + getMax(): number { + return this.max_; + } + + /** + * Sets the precision of this field's value, i.e. the number to which the + * value is rounded. Updates the field to reflect. + * + * @param precision The number to which the field's value is rounded. + */ + setPrecision(precision: number | string | undefined | null) { + this.setPrecisionInternal(precision); + this.setValue(this.getValue()); + } + + /** + * Sets the precision of this field's value. Called internally to avoid + * value updates. + * + * @param precision The number to which the field's value is rounded. + */ + private setPrecisionInternal(precision: number | string | undefined | null) { + this.precision_ = Number(precision) || 0; + let precisionString = String(this.precision_); + if (precisionString.includes('e')) { + // String() is fast. But it turns .0000001 into '1e-7'. + // Use the much slower toLocaleString to access all the digits. + precisionString = this.precision_.toLocaleString('en-US', { + maximumFractionDigits: 20, + }); + } + const decimalIndex = precisionString.indexOf('.'); + if (decimalIndex === -1) { + // If the precision is 0 (float) allow any number of decimals, + // otherwise allow none. + this.decimalPlaces = precision ? 0 : null; + } else { + this.decimalPlaces = precisionString.length - decimalIndex - 1; + } + } + + /** + * Returns the current precision of this field. The precision being the + * number to which the field's value is rounded. A precision of 0 means that + * the value is not rounded. + * + * @returns The number to which this field's value is rounded. + */ + getPrecision(): number { + return this.precision_; + } + + /** + * Ensure that the input value is a valid number (must fulfill the + * constraints placed on the field). + * + * @param newValue The input value. + * @returns A valid number, or null if invalid. + */ + protected override doClassValidation_( + newValue?: AnyDuringMigration, + ): number | null { + if (newValue === null) { + return null; + } + + // Clean up text. + newValue = `${newValue}`; + // TODO: Handle cases like 'ten', '1.203,14', etc. + // 'O' is sometimes mistaken for '0' by inexperienced users. + newValue = newValue.replace(/O/gi, '0'); + // Strip out thousands separators. + newValue = newValue.replace(/,/g, ''); + // Ignore case of 'Infinity'. + newValue = newValue.replace(/infinity/i, 'Infinity'); + + // Clean up number. + let n = Number(newValue || 0); + if (isNaN(n)) { + // Invalid number. + return null; + } + // Get the value in range. + n = Math.min(Math.max(n, this.min_), this.max_); + // Round to nearest multiple of precision. + if (this.precision_ && isFinite(n)) { + n = Math.round(n / this.precision_) * this.precision_; + } + // Clean up floating point errors. + if (this.decimalPlaces !== null) { + n = Number(n.toFixed(this.decimalPlaces)); + } + return n; + } + + /** + * Create the number input editor widget. + * + * @returns The newly created number input editor. + */ + protected override widgetCreate_(): HTMLInputElement { + const htmlInput = super.widgetCreate_() as HTMLInputElement; + + // Set the accessibility state + if (this.min_ > -Infinity) { + htmlInput.min = `${this.min_}`; + aria.setState(htmlInput, aria.State.VALUEMIN, this.min_); + } + if (this.max_ < Infinity) { + htmlInput.max = `${this.max_}`; + aria.setState(htmlInput, aria.State.VALUEMAX, this.max_); + } + return htmlInput; + } + + /** + * Construct a FieldNumber from a JSON arg object. + * + * @param options A JSON object with options (value, min, max, and precision). + * @returns The new field instance. + * @nocollapse + * @internal + */ + static override fromJson(options: FieldNumberFromJsonConfig): FieldNumber { + // `this` might be a subclass of FieldNumber if that class doesn't override + // the static fromJson method. + return new this( + options.value, + undefined, + undefined, + undefined, + undefined, + options, + ); + } +} + +fieldRegistry.register('field_number', FieldNumber); + +FieldNumber.prototype.DEFAULT_VALUE = 0; + +/** + * Config options for the number field. + */ +export interface FieldNumberConfig extends FieldInputConfig { + min?: number; + max?: number; + precision?: number; +} + +/** + * fromJson config options for the number field. + */ +export interface FieldNumberFromJsonConfig extends FieldNumberConfig { + value?: number; +} + +/** + * A function that is called to validate changes to the field's value before + * they are set. + * + * @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values} + * @param newValue The value to be validated. + * @returns One of three instructions for setting the new value: `T`, `null`, + * or `undefined`. + * + * - `T` to set this function's returned value instead of `newValue`. + * + * - `null` to invoke `doValueInvalid_` and not set a value. + * + * - `undefined` to set `newValue` as is. + */ +export type FieldNumberValidator = FieldInputValidator; diff --git a/core/field_registry.ts b/core/field_registry.ts new file mode 100644 index 00000000000..06bb9acd045 --- /dev/null +++ b/core/field_registry.ts @@ -0,0 +1,115 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.fieldRegistry + +import type {Field, FieldConfig} from './field.js'; +import * as registry from './registry.js'; + +/** + * When constructing a field from JSON using the registry, the + * `fromJson` method in this file is called with an options parameter + * object consisting of the "type" which is the name of the field, and + * other options that are part of the field's config object. + * + * These options are then passed to the field's static `fromJson` + * method. That method accepts an options parameter with a type that usually + * extends from FieldConfig, and may or may not have a "type" attribute (in + * fact, it shouldn't, because we'd overwrite it as described above!) + * + * Unfortunately the registry has no way of knowing the actual Field subclass + * that will be returned from passing in the name of the field. Therefore it + * also has no way of knowing that the options object not only implements + * `FieldConfig`, but it also should satisfy the Config that belongs to that + * specific class's `fromJson` method. + * + * Because of this uncertainty, we just give up on type checking the properties + * passed to the `fromJson` method, and allow arbitrary string keys with + * unknown types. + */ +type RegistryOptions = FieldConfig & { + // The name of the field, e.g. field_dropdown + type: string; + [key: string]: unknown; +}; + +/** + * Represents the static methods that must be defined on any + * field that is registered, i.e. the constructor and fromJson methods. + * + * Because we don't know which Field subclass will be registered, we + * are unable to typecheck the parameters of the constructor. + */ +export interface RegistrableField { + new (...args: any[]): Field; + fromJson(options: FieldConfig): Field; +} + +/** + * Registers a field type. + * fieldRegistry.fromJson uses this registry to + * find the appropriate field type. + * + * @param type The field type name as used in the JSON definition. + * @param fieldClass The field class containing a fromJson function that can + * construct an instance of the field. + * @throws {Error} if the type name is empty, the field is already registered, + * or the fieldClass is not an object containing a fromJson function. + */ +export function register(type: string, fieldClass: RegistrableField) { + registry.register(registry.Type.FIELD, type, fieldClass); +} + +/** + * Unregisters the field registered with the given type. + * + * @param type The field type name as used in the JSON definition. + */ +export function unregister(type: string) { + registry.unregister(registry.Type.FIELD, type); +} + +/** + * Construct a Field from a JSON arg object. + * Finds the appropriate registered field by the type name as registered using + * fieldRegistry.register. + * + * @param options A JSON object with a type and options specific to the field + * type. + * @returns The new field instance or null if a field wasn't found with the + * given type name + * @internal + */ +export function fromJson(options: RegistryOptions): Field | null { + return TEST_ONLY.fromJsonInternal(options); +} + +/** + * Private version of fromJson for stubbing in tests. + * + * @param options + */ +function fromJsonInternal(options: RegistryOptions): Field | null { + const fieldObject = registry.getObject( + registry.Type.FIELD, + options.type, + ) as unknown as RegistrableField; + if (!fieldObject) { + console.warn( + 'Blockly could not create a field of type ' + + options['type'] + + '. The field is probably not being registered. This could be because' + + ' the file is not loaded, the field does not register itself (Issue' + + ' #1584), or the registration is not being reached.', + ); + return null; + } + return fieldObject.fromJson(options); +} + +export const TEST_ONLY = { + fromJsonInternal, +}; diff --git a/core/field_textinput.js b/core/field_textinput.js deleted file mode 100644 index a8014a187d1..00000000000 --- a/core/field_textinput.js +++ /dev/null @@ -1,355 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Text input field. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.FieldTextInput'); - -goog.require('Blockly.Field'); -goog.require('Blockly.Msg'); -goog.require('goog.asserts'); -goog.require('goog.dom'); -goog.require('goog.dom.TagName'); -goog.require('goog.userAgent'); - - -/** - * Class for an editable text field. - * @param {string} text The initial content of the field. - * @param {Function=} opt_validator An optional function that is called - * to validate any constraints on what the user entered. Takes the new - * text as an argument and returns either the accepted text, a replacement - * text, or null to abort the change. - * @extends {Blockly.Field} - * @constructor - */ -Blockly.FieldTextInput = function(text, opt_validator) { - Blockly.FieldTextInput.superClass_.constructor.call(this, text, - opt_validator); -}; -goog.inherits(Blockly.FieldTextInput, Blockly.Field); - -/** - * Point size of text. Should match blocklyText's font-size in CSS. - */ -Blockly.FieldTextInput.FONTSIZE = 11; - -/** - * Mouse cursor style when over the hotspot that initiates the editor. - */ -Blockly.FieldTextInput.prototype.CURSOR = 'text'; - -/** - * Allow browser to spellcheck this field. - * @private - */ -Blockly.FieldTextInput.prototype.spellcheck_ = true; - -/** - * Close the input widget if this input is being deleted. - */ -Blockly.FieldTextInput.prototype.dispose = function() { - Blockly.WidgetDiv.hideIfOwner(this); - Blockly.FieldTextInput.superClass_.dispose.call(this); -}; - -/** - * Set the value of this field. - * @param {?string} newValue New value. - * @override - */ -Blockly.FieldTextInput.prototype.setValue = function(newValue) { - if (newValue === null) { - return; // No change if null. - } - if (this.sourceBlock_) { - var validated = this.callValidator(newValue); - // If the new value is invalid, validation returns null. - // In this case we still want to display the illegal result. - if (validated !== null) { - newValue = validated; - } - } - Blockly.Field.prototype.setValue.call(this, newValue); -}; - -/** - * Set the text in this field and fire a change event. - * @param {*} newText New text. - */ -Blockly.FieldTextInput.prototype.setText = function(newText) { - if (newText === null) { - // No change if null. - return; - } - newText = String(newText); - if (newText === this.text_) { - // No change. - return; - } - if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, this.text_, newText)); - } - Blockly.Field.prototype.setText.call(this, newText); -}; - -/** - * Set whether this field is spellchecked by the browser. - * @param {boolean} check True if checked. - */ -Blockly.FieldTextInput.prototype.setSpellcheck = function(check) { - this.spellcheck_ = check; -}; - -/** - * Show the inline free-text editor on top of the text. - * @param {boolean=} opt_quietInput True if editor should be created without - * focus. Defaults to false. - * @private - */ -Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) { - this.workspace_ = this.sourceBlock_.workspace; - var quietInput = opt_quietInput || false; - if (!quietInput && (goog.userAgent.MOBILE || goog.userAgent.ANDROID || - goog.userAgent.IPAD)) { - // Mobile browsers have issues with in-line textareas (focus & keyboards). - var fieldText = this; - Blockly.prompt(Blockly.Msg.CHANGE_VALUE_TITLE, this.text_, - function(newValue) { - if (fieldText.sourceBlock_) { - newValue = fieldText.callValidator(newValue); - } - fieldText.setValue(newValue); - }); - return; - } - - Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_()); - var div = Blockly.WidgetDiv.DIV; - // Create the input. - var htmlInput = - goog.dom.createDom(goog.dom.TagName.INPUT, 'blocklyHtmlInput'); - htmlInput.setAttribute('spellcheck', this.spellcheck_); - var fontSize = - (Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt'; - div.style.fontSize = fontSize; - htmlInput.style.fontSize = fontSize; - /** @type {!HTMLInputElement} */ - Blockly.FieldTextInput.htmlInput_ = htmlInput; - div.appendChild(htmlInput); - - htmlInput.value = htmlInput.defaultValue = this.text_; - htmlInput.oldValue_ = null; - this.validate_(); - this.resizeEditor_(); - if (!quietInput) { - htmlInput.focus(); - htmlInput.select(); - } - - // Bind to keydown -- trap Enter without IME and Esc to hide. - htmlInput.onKeyDownWrapper_ = - Blockly.bindEventWithChecks_(htmlInput, 'keydown', this, - this.onHtmlInputKeyDown_); - // Bind to keyup -- trap Enter; resize after every keystroke. - htmlInput.onKeyUpWrapper_ = - Blockly.bindEventWithChecks_(htmlInput, 'keyup', this, - this.onHtmlInputChange_); - // Bind to keyPress -- repeatedly resize when holding down a key. - htmlInput.onKeyPressWrapper_ = - Blockly.bindEventWithChecks_(htmlInput, 'keypress', this, - this.onHtmlInputChange_); - htmlInput.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this); - this.workspace_.addChangeListener(htmlInput.onWorkspaceChangeWrapper_); -}; - -/** - * Handle key down to the editor. - * @param {!Event} e Keyboard event. - * @private - */ -Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) { - var htmlInput = Blockly.FieldTextInput.htmlInput_; - var tabKey = 9, enterKey = 13, escKey = 27; - if (e.keyCode == enterKey) { - Blockly.WidgetDiv.hide(); - } else if (e.keyCode == escKey) { - htmlInput.value = htmlInput.defaultValue; - Blockly.WidgetDiv.hide(); - } else if (e.keyCode == tabKey) { - Blockly.WidgetDiv.hide(); - this.sourceBlock_.tab(this, !e.shiftKey); - e.preventDefault(); - } -}; - -/** - * Handle a change to the editor. - * @param {!Event} e Keyboard event. - * @private - */ -Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(e) { - var htmlInput = Blockly.FieldTextInput.htmlInput_; - // Update source block. - var text = htmlInput.value; - if (text !== htmlInput.oldValue_) { - htmlInput.oldValue_ = text; - this.setValue(text); - this.validate_(); - } else if (goog.userAgent.WEBKIT) { - // Cursor key. Render the source block to show the caret moving. - // Chrome only (version 26, OS X). - this.sourceBlock_.render(); - } - this.resizeEditor_(); - Blockly.svgResize(this.sourceBlock_.workspace); -}; - -/** - * Check to see if the contents of the editor validates. - * Style the editor accordingly. - * @private - */ -Blockly.FieldTextInput.prototype.validate_ = function() { - var valid = true; - goog.asserts.assertObject(Blockly.FieldTextInput.htmlInput_); - var htmlInput = Blockly.FieldTextInput.htmlInput_; - if (this.sourceBlock_) { - valid = this.callValidator(htmlInput.value); - } - if (valid === null) { - Blockly.utils.addClass(htmlInput, 'blocklyInvalidInput'); - } else { - Blockly.utils.removeClass(htmlInput, 'blocklyInvalidInput'); - } -}; - -/** - * Resize the editor and the underlying block to fit the text. - * @private - */ -Blockly.FieldTextInput.prototype.resizeEditor_ = function() { - var div = Blockly.WidgetDiv.DIV; - var bBox = this.fieldGroup_.getBBox(); - div.style.width = bBox.width * this.workspace_.scale + 'px'; - div.style.height = bBox.height * this.workspace_.scale + 'px'; - var xy = this.getAbsoluteXY_(); - // In RTL mode block fields and LTR input fields the left edge moves, - // whereas the right edge is fixed. Reposition the editor. - if (this.sourceBlock_.RTL) { - var borderBBox = this.getScaledBBox_(); - xy.x += borderBBox.width; - xy.x -= div.offsetWidth; - } - // Shift by a few pixels to line up exactly. - xy.y += 1; - if (goog.userAgent.GECKO && Blockly.WidgetDiv.DIV.style.top) { - // Firefox mis-reports the location of the border by a pixel - // once the WidgetDiv is moved into position. - xy.x -= 1; - xy.y -= 1; - } - if (goog.userAgent.WEBKIT) { - xy.y -= 3; - } - div.style.left = xy.x + 'px'; - div.style.top = xy.y + 'px'; -}; - -/** - * Close the editor, save the results, and dispose of the editable - * text field's elements. - * @return {!Function} Closure to call on destruction of the WidgetDiv. - * @private - */ -Blockly.FieldTextInput.prototype.widgetDispose_ = function() { - var thisField = this; - return function() { - var htmlInput = Blockly.FieldTextInput.htmlInput_; - // Save the edit (if it validates). - var text = htmlInput.value; - if (thisField.sourceBlock_) { - var text1 = thisField.callValidator(text); - if (text1 === null) { - // Invalid edit. - text = htmlInput.defaultValue; - } else { - // Validation function has changed the text. - text = text1; - if (thisField.onFinishEditing_) { - thisField.onFinishEditing_(text); - } - } - } - thisField.setText(text); - thisField.sourceBlock_.rendered && thisField.sourceBlock_.render(); - Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_); - Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_); - Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_); - thisField.workspace_.removeChangeListener( - htmlInput.onWorkspaceChangeWrapper_); - Blockly.FieldTextInput.htmlInput_ = null; - Blockly.Events.setGroup(false); - // Delete style properties. - var style = Blockly.WidgetDiv.DIV.style; - style.width = 'auto'; - style.height = 'auto'; - style.fontSize = ''; - }; -}; - -/** - * Ensure that only a number may be entered. - * @param {string} text The user's text. - * @return {?string} A string representing a valid number, or null if invalid. - */ -Blockly.FieldTextInput.numberValidator = function(text) { - console.warn('Blockly.FieldTextInput.numberValidator is deprecated. ' + - 'Use Blockly.FieldNumber instead.'); - if (text === null) { - return null; - } - text = String(text); - // TODO: Handle cases like 'ten', '1.203,14', etc. - // 'O' is sometimes mistaken for '0' by inexperienced users. - text = text.replace(/O/ig, '0'); - // Strip out thousands separators. - text = text.replace(/,/g, ''); - var n = parseFloat(text || 0); - return isNaN(n) ? null : String(n); -}; - -/** - * Ensure that only a nonnegative integer may be entered. - * @param {string} text The user's text. - * @return {?string} A string representing a valid int, or null if invalid. - */ -Blockly.FieldTextInput.nonnegativeIntegerValidator = function(text) { - var n = Blockly.FieldTextInput.numberValidator(text); - if (n) { - n = String(Math.max(0, Math.floor(n))); - } - return n; -}; diff --git a/core/field_textinput.ts b/core/field_textinput.ts new file mode 100644 index 00000000000..39bdca97056 --- /dev/null +++ b/core/field_textinput.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Text input field. + * + * @class + */ +// Former goog.module ID: Blockly.FieldTextInput + +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_block_change.js'; + +import {Field} from './field.js'; +import { + FieldInput, + FieldInputConfig, + FieldInputValidator, +} from './field_input.js'; +import * as fieldRegistry from './field_registry.js'; +import * as parsing from './utils/parsing.js'; + +/** + * Class for an editable text field. + */ +export class FieldTextInput extends FieldInput { + /** + * @param value The initial value of the field. Should cast to a string. + * Defaults to an empty string if null or undefined. Also accepts + * Field.SKIP_SETUP if you wish to skip setup (only used by subclasses + * that want to handle configuration and setting the field value after + * their own constructors have run). + * @param validator A function that is called to validate changes to the + * field's value. Takes in a string & returns a validated string, or null + * to abort the change. + * @param config A map of options used to configure the field. + * See the [field creation documentation]{@link + * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/text-input#creation} + * for a list of properties this parameter supports. + */ + constructor( + value?: string | typeof Field.SKIP_SETUP, + validator?: FieldTextInputValidator | null, + config?: FieldTextInputConfig, + ) { + super(value, validator, config); + } + + /** + * Ensure that the input value casts to a valid string. + * + * @param newValue The input value. + * @returns A valid string, or null if invalid. + */ + protected override doClassValidation_( + newValue?: AnyDuringMigration, + ): string | null { + if (newValue === undefined) { + return null; + } + return `${newValue}`; + } + + /** + * Construct a FieldTextInput from a JSON arg object, + * dereferencing any string table references. + * + * @param options A JSON object with options (text, and spellcheck). + * @returns The new field instance. + * @nocollapse + * @internal + */ + static override fromJson( + options: FieldTextInputFromJsonConfig, + ): FieldTextInput { + const text = parsing.replaceMessageReferences(options.text); + // `this` might be a subclass of FieldTextInput if that class doesn't + // override the static fromJson method. + return new this(text, undefined, options); + } +} + +fieldRegistry.register('field_input', FieldTextInput); + +FieldTextInput.prototype.DEFAULT_VALUE = ''; + +/** + * Config options for the text input field. + */ +export type FieldTextInputConfig = FieldInputConfig; + +/** + * fromJson config options for the text input field. + */ +export interface FieldTextInputFromJsonConfig extends FieldTextInputConfig { + text?: string; +} + +/** + * A function that is called to validate changes to the field's value before + * they are set. + * + * @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values} + * @param newValue The value to be validated. + * @returns One of three instructions for setting the new value: `T`, `null`, + * or `undefined`. + * + * - `T` to set this function's returned value instead of `newValue`. + * + * - `null` to invoke `doValueInvalid_` and not set a value. + * + * - `undefined` to set `newValue` as is. + */ +export type FieldTextInputValidator = FieldInputValidator; diff --git a/core/field_variable.js b/core/field_variable.js deleted file mode 100644 index 7996260d62a..00000000000 --- a/core/field_variable.js +++ /dev/null @@ -1,221 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Variable input field. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.FieldVariable'); - -goog.require('Blockly.FieldDropdown'); -goog.require('Blockly.Msg'); -goog.require('Blockly.VariableModel'); -goog.require('Blockly.Variables'); -goog.require('Blockly.VariableModel'); -goog.require('goog.asserts'); -goog.require('goog.string'); - - -/** - * Class for a variable's dropdown field. - * @param {?string} varname The default name for the variable. If null, - * a unique variable name will be generated. - * @param {Function=} opt_validator A function that is executed when a new - * option is selected. Its sole argument is the new option value. - * @extends {Blockly.FieldDropdown} - * @constructor - */ -Blockly.FieldVariable = function(varname, opt_validator) { - Blockly.FieldVariable.superClass_.constructor.call(this, - Blockly.FieldVariable.dropdownCreate, opt_validator); - this.setValue(varname || ''); -}; -goog.inherits(Blockly.FieldVariable, Blockly.FieldDropdown); - -/** - * Install this dropdown on a block. - */ -Blockly.FieldVariable.prototype.init = function() { - if (this.fieldGroup_) { - // Dropdown has already been initialized once. - return; - } - Blockly.FieldVariable.superClass_.init.call(this); - - // TODO (1010): Change from init/initModel to initView/initModel - this.initModel(); -}; - -Blockly.FieldVariable.prototype.initModel = function() { - if (!this.getValue()) { - // Variables without names get uniquely named for this workspace. - var workspace = - this.sourceBlock_.isInFlyout ? - this.sourceBlock_.workspace.targetWorkspace : - this.sourceBlock_.workspace; - this.setValue(Blockly.Variables.generateUniqueName(workspace)); - } - // If the selected variable doesn't exist yet, create it. - // For instance, some blocks in the toolbox have variable dropdowns filled - // in by default. - if (!this.sourceBlock_.isInFlyout) { - this.sourceBlock_.workspace.createVariable(this.getValue()); - } -}; - -/** - * Attach this field to a block. - * @param {!Blockly.Block} block The block containing this field. - */ -Blockly.FieldVariable.prototype.setSourceBlock = function(block) { - goog.asserts.assert(!block.isShadow(), - 'Variable fields are not allowed to exist on shadow blocks.'); - Blockly.FieldVariable.superClass_.setSourceBlock.call(this, block); -}; - -/** - * Get the variable's name (use a variableDB to convert into a real name). - * Unline a regular dropdown, variables are literal and have no neutral value. - * @return {string} Current text. - */ -Blockly.FieldVariable.prototype.getValue = function() { - return this.getText(); -}; - -/** - * Set the variable name. - * @param {string} value New text. - */ -Blockly.FieldVariable.prototype.setValue = function(value) { - var newValue = value; - var newText = value; - - if (this.sourceBlock_) { - var variable = this.sourceBlock_.workspace.getVariableById(value); - if (variable) { - newText = variable.name; - } - // TODO(marisaleung): Remove name lookup after converting all Field Variable - // instances to use id instead of name. - else if (variable = this.sourceBlock_.workspace.getVariable(value)) { - newValue = variable.getId(); - } - if (Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, this.value_, newValue)); - } - } - this.value_ = newValue; - this.setText(newText); -}; - -/** - * Return a sorted list of variable names for variable dropdown menus. - * Include a special option at the end for creating a new variable name. - * @return {!Array.} Array of variable names. - * @this {Blockly.FieldVariable} - */ -Blockly.FieldVariable.dropdownCreate = function() { - var variableModelList = []; - var name = this.getText(); - // Don't create a new variable if there is nothing selected. - var createSelectedVariable = name ? true : false; - var workspace = null; - if (this.sourceBlock_) { - workspace = this.sourceBlock_.workspace; - } - - if (workspace) { - // Get a copy of the list, so that adding rename and new variable options - // doesn't modify the workspace's list. - variableModelList = workspace.getVariablesOfType(''); - for (var i = 0; i < variableModelList.length; i++){ - if (createSelectedVariable && - goog.string.caseInsensitiveEquals(variableModelList[i].name, name)) { - createSelectedVariable = false; - break; - } - } - } - // Ensure that the currently selected variable is an option. - if (createSelectedVariable && workspace) { - var newVar = workspace.createVariable(name); - variableModelList.push(newVar); - } - variableModelList.sort(Blockly.VariableModel.compareByName); - var options = []; - for (var i = 0; i < variableModelList.length; i++) { - // Set the uuid as the internal representation of the variable. - options[i] = [variableModelList[i].name, variableModelList[i].getId()]; - } - options.push([Blockly.Msg.RENAME_VARIABLE, Blockly.RENAME_VARIABLE_ID]); - if (Blockly.Msg.DELETE_VARIABLE) { - options.push([Blockly.Msg.DELETE_VARIABLE.replace('%1', name), - Blockly.DELETE_VARIABLE_ID]); - } - return options; -}; - -/** - * Handle the selection of an item in the variable dropdown menu. - * Special case the 'Rename variable...' and 'Delete variable...' options. - * In the rename case, prompt the user for a new name. - * @param {!goog.ui.Menu} menu The Menu component clicked. - * @param {!goog.ui.MenuItem} menuItem The MenuItem selected within menu. - */ -Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) { - var id = menuItem.getValue(); - // TODO(marisaleung): change setValue() to take in an id as the parameter. - // Then remove itemText. - var itemText; - if (this.sourceBlock_ && this.sourceBlock_.workspace) { - var workspace = this.sourceBlock_.workspace; - var variable = workspace.getVariableById(id); - // If the item selected is a variable, set itemText to the variable name. - if (variable) { - itemText = variable.name; - } - else if (id == Blockly.RENAME_VARIABLE_ID) { - // Rename variable. - var oldName = this.getText(); - Blockly.hideChaff(); - Blockly.Variables.promptName( - Blockly.Msg.RENAME_VARIABLE_TITLE.replace('%1', oldName), oldName, - function(newName) { - if (newName) { - workspace.renameVariable(oldName, newName); - } - }); - return; - } else if (id == Blockly.DELETE_VARIABLE_ID) { - // Delete variable. - workspace.deleteVariable(this.getText()); - return; - } - - // Call any validation function, and allow it to override. - itemText = this.callValidator(itemText); - } - if (itemText !== null) { - this.setValue(itemText); - } -}; diff --git a/core/field_variable.ts b/core/field_variable.ts new file mode 100644 index 00000000000..539557256b6 --- /dev/null +++ b/core/field_variable.ts @@ -0,0 +1,623 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Variable input field. + * + * @class + */ +// Former goog.module ID: Blockly.FieldVariable + +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_block_change.js'; + +import type {Block} from './block.js'; +import {Field, FieldConfig, UnattachedFieldError} from './field.js'; +import { + FieldDropdown, + FieldDropdownValidator, + MenuGenerator, + MenuOption, +} from './field_dropdown.js'; +import * as fieldRegistry from './field_registry.js'; +import * as internalConstants from './internal_constants.js'; +import type {Menu} from './menu.js'; +import type {MenuItem} from './menuitem.js'; +import {Msg} from './msg.js'; +import * as parsing from './utils/parsing.js'; +import {Size} from './utils/size.js'; +import {VariableModel} from './variable_model.js'; +import * as Variables from './variables.js'; +import * as Xml from './xml.js'; + +/** + * Class for a variable's dropdown field. + */ +export class FieldVariable extends FieldDropdown { + protected override menuGenerator_: MenuGenerator | undefined; + defaultVariableName: string; + + /** The type of the default variable for this field. */ + private defaultType = ''; + + /** + * All of the types of variables that will be available in this field's + * dropdown. + */ + variableTypes: string[] | null = []; + protected override size_: Size; + + /** The variable model associated with this field. */ + private variable: VariableModel | null = null; + + /** + * Serializable fields are saved by the serializer, non-serializable fields + * are not. Editable fields should also be serializable. + */ + override SERIALIZABLE = true; + + /** + * @param varName The default name for the variable. + * If null, a unique variable name will be generated. + * Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by + * subclasses that want to handle configuration and setting the field value + * after their own constructors have run). + * @param validator A function that is called to validate changes to the + * field's value. Takes in a variable ID & returns a validated variable + * ID, or null to abort the change. + * @param variableTypes A list of the types of variables to include in the + * dropdown. Will only be used if config is not provided. + * @param defaultType The type of variable to create if this field's value + * is not explicitly set. Defaults to ''. Will only be used if config + * is not provided. + * @param config A map of options used to configure the field. + * See the [field creation documentation]{@link + * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/variable#creation} + * for a list of properties this parameter supports. + */ + constructor( + varName: string | null | typeof Field.SKIP_SETUP, + validator?: FieldVariableValidator, + variableTypes?: string[], + defaultType?: string, + config?: FieldVariableConfig, + ) { + super(Field.SKIP_SETUP); + + /** + * An array of options for a dropdown list, + * or a function which generates these options. + */ + this.menuGenerator_ = FieldVariable.dropdownCreate as MenuGenerator; + + /** + * The initial variable name passed to this field's constructor, or an + * empty string if a name wasn't provided. Used to create the initial + * variable. + */ + this.defaultVariableName = typeof varName === 'string' ? varName : ''; + + /** The size of the area rendered by the field. */ + this.size_ = new Size(0, 0); + + if (varName === Field.SKIP_SETUP) return; + + if (config) { + this.configure_(config); + } else { + this.setTypes(variableTypes, defaultType); + } + if (validator) { + this.setValidator(validator); + } + } + + /** + * Configure the field based on the given map of options. + * + * @param config A map of options to configure the field based on. + */ + protected override configure_(config: FieldVariableConfig) { + super.configure_(config); + this.setTypes(config.variableTypes, config.defaultType); + } + + /** + * Initialize the model for this field if it has not already been initialized. + * If the value has not been set to a variable by the first render, we make up + * a variable rather than let the value be invalid. + */ + override initModel() { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + if (this.variable) { + return; // Initialization already happened. + } + const variable = Variables.getOrCreateVariablePackage( + block.workspace, + null, + this.defaultVariableName, + this.defaultType, + ); + // Don't call setValue because we don't want to cause a rerender. + this.doValueUpdate_(variable.getId()); + } + + override shouldAddBorderRect_() { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + return ( + super.shouldAddBorderRect_() && + (!this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW || + block.type !== 'variables_get') + ); + } + + /** + * Initialize this field based on the given XML. + * + * @param fieldElement The element containing information about the variable + * field's state. + */ + override fromXml(fieldElement: Element) { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + const id = fieldElement.getAttribute('id'); + const variableName = fieldElement.textContent; + // 'variabletype' should be lowercase, but until July 2019 it was sometimes + // recorded as 'variableType'. Thus we need to check for both. + const variableType = + fieldElement.getAttribute('variabletype') || + fieldElement.getAttribute('variableType') || + ''; + + // AnyDuringMigration because: Argument of type 'string | null' is not + // assignable to parameter of type 'string | undefined'. + const variable = Variables.getOrCreateVariablePackage( + block.workspace, + id, + variableName as AnyDuringMigration, + variableType, + ); + + // This should never happen :) + if (variableType !== null && variableType !== variable.type) { + throw Error( + "Serialized variable type with id '" + + variable.getId() + + "' had type " + + variable.type + + ', and ' + + 'does not match variable field that references it: ' + + Xml.domToText(fieldElement) + + '.', + ); + } + + this.setValue(variable.getId()); + } + + /** + * Serialize this field to XML. + * + * @param fieldElement The element to populate with info about the field's + * state. + * @returns The element containing info about the field's state. + */ + override toXml(fieldElement: Element): Element { + // Make sure the variable is initialized. + this.initModel(); + + fieldElement.id = this.variable!.getId(); + fieldElement.textContent = this.variable!.name; + if (this.variable!.type) { + fieldElement.setAttribute('variabletype', this.variable!.type); + } + return fieldElement; + } + + /** + * Saves this field's value. + * + * @param doFullSerialization If true, the variable field will serialize the + * full state of the field being referenced (ie ID, name, and type) rather + * than just a reference to it (ie ID). + * @returns The state of the variable field. + * @internal + */ + override saveState(doFullSerialization?: boolean): AnyDuringMigration { + const legacyState = this.saveLegacyState(FieldVariable); + if (legacyState !== null) { + return legacyState; + } + // Make sure the variable is initialized. + this.initModel(); + const state = {'id': this.variable!.getId()}; + if (doFullSerialization) { + (state as AnyDuringMigration)['name'] = this.variable!.name; + (state as AnyDuringMigration)['type'] = this.variable!.type; + } + return state; + } + + /** + * Sets the field's value based on the given state. + * + * @param state The state of the variable to assign to this variable field. + * @internal + */ + override loadState(state: AnyDuringMigration) { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + if (this.loadLegacyState(FieldVariable, state)) { + return; + } + // This is necessary so that blocks in the flyout can have custom var names. + const variable = Variables.getOrCreateVariablePackage( + block.workspace, + state['id'] || null, + state['name'], + state['type'] || '', + ); + this.setValue(variable.getId()); + } + + /** + * Attach this field to a block. + * + * @param block The block containing this field. + */ + override setSourceBlock(block: Block) { + if (block.isShadow()) { + throw Error('Variable fields are not allowed to exist on shadow blocks.'); + } + super.setSourceBlock(block); + } + + /** + * Get the variable's ID. + * + * @returns Current variable's ID. + */ + override getValue(): string | null { + return this.variable ? this.variable.getId() : null; + } + + /** + * Get the text from this field, which is the selected variable's name. + * + * @returns The selected variable's name, or the empty string if no variable + * is selected. + */ + override getText(): string { + return this.variable ? this.variable.name : ''; + } + + /** + * Get the variable model for the selected variable. + * Not guaranteed to be in the variable map on the workspace (e.g. if accessed + * after the variable has been deleted). + * + * @returns The selected variable, or null if none was selected. + * @internal + */ + getVariable(): VariableModel | null { + return this.variable; + } + + /** + * Gets the validation function for this field, or null if not set. + * Returns null if the variable is not set, because validators should not + * run on the initial setValue call, because the field won't be attached to + * a block and workspace at that point. + * + * @returns Validation function, or null. + */ + override getValidator(): FieldVariableValidator | null { + // Validators shouldn't operate on the initial setValue call. + // Normally this is achieved by calling setValidator after setValue, but + // this is not a possibility with variable fields. + if (this.variable) { + return this.validator_; + } + return null; + } + + /** + * Ensure that the ID belongs to a valid variable of an allowed type. + * + * @param newValue The ID of the new variable to set. + * @returns The validated ID, or null if invalid. + */ + protected override doClassValidation_( + newValue?: AnyDuringMigration, + ): string | null { + if (newValue === null) { + return null; + } + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + const newId = newValue as string; + const variable = Variables.getVariable(block.workspace, newId); + if (!variable) { + console.warn( + "Variable id doesn't point to a real variable! " + 'ID was ' + newId, + ); + return null; + } + // Type Checks. + const type = variable.type; + if (!this.typeIsAllowed(type)) { + console.warn("Variable type doesn't match this field! Type was " + type); + return null; + } + return newId; + } + + /** + * Update the value of this variable field, as well as its variable and text. + * + * The variable ID should be valid at this point, but if a variable field + * validator returns a bad ID, this could break. + * + * @param newId The value to be saved. + */ + protected override doValueUpdate_(newId: string) { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + this.variable = Variables.getVariable(block.workspace, newId as string); + super.doValueUpdate_(newId); + } + + /** + * Check whether the given variable type is allowed on this field. + * + * @param type The type to check. + * @returns True if the type is in the list of allowed types. + */ + private typeIsAllowed(type: string): boolean { + const typeList = this.getVariableTypes(); + if (!typeList) { + return true; // If it's null, all types are valid. + } + for (let i = 0; i < typeList.length; i++) { + if (type === typeList[i]) { + return true; + } + } + return false; + } + + /** + * Return a list of variable types to include in the dropdown. + * + * @returns Array of variable types. + * @throws {Error} if variableTypes is an empty array. + */ + private getVariableTypes(): string[] { + let variableTypes = this.variableTypes; + if (variableTypes === null) { + // If variableTypes is null, return all variable types. + if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) { + return this.sourceBlock_.workspace.getVariableTypes(); + } + } + variableTypes = variableTypes || ['']; + if (variableTypes.length === 0) { + // Throw an error if variableTypes is an empty list. + const name = this.getText(); + throw Error( + "'variableTypes' of field variable " + name + ' was an empty list', + ); + } + return variableTypes; + } + + /** + * Parse the optional arguments representing the allowed variable types and + * the default variable type. + * + * @param variableTypes A list of the types of variables to include in the + * dropdown. If null or undefined, variables of all types will be + * displayed in the dropdown. + * @param defaultType The type of the variable to create if this field's + * value is not explicitly set. Defaults to ''. + */ + private setTypes(variableTypes: string[] | null = null, defaultType = '') { + // If you expected that the default type would be the same as the only entry + // in the variable types array, tell the Blockly team by commenting on + // #1499. + // Set the allowable variable types. Null means all types on the workspace. + if (Array.isArray(variableTypes)) { + // Make sure the default type is valid. + let isInArray = false; + for (let i = 0; i < variableTypes.length; i++) { + if (variableTypes[i] === defaultType) { + isInArray = true; + } + } + if (!isInArray) { + throw Error( + "Invalid default type '" + + defaultType + + "' in " + + 'the definition of a FieldVariable', + ); + } + } else if (variableTypes !== null) { + throw Error( + "'variableTypes' was not an array in the definition of " + + 'a FieldVariable', + ); + } + // Only update the field once all checks pass. + this.defaultType = defaultType; + this.variableTypes = variableTypes; + } + + /** + * Refreshes the name of the variable by grabbing the name of the model. + * Used when a variable gets renamed, but the ID stays the same. Should only + * be called by the block. + * + * @internal + */ + override refreshVariableName() { + this.forceRerender(); + } + + /** + * Handle the selection of an item in the variable dropdown menu. + * Special case the 'Rename variable...' and 'Delete variable...' options. + * In the rename case, prompt the user for a new name. + * + * @param menu The Menu component clicked. + * @param menuItem The MenuItem selected within menu. + */ + protected override onItemSelected_(menu: Menu, menuItem: MenuItem) { + const id = menuItem.getValue(); + // Handle special cases. + if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) { + if (id === internalConstants.RENAME_VARIABLE_ID) { + // Rename variable. + Variables.renameVariable( + this.sourceBlock_.workspace, + this.variable as VariableModel, + ); + return; + } else if (id === internalConstants.DELETE_VARIABLE_ID) { + // Delete variable. + this.sourceBlock_.workspace.deleteVariableById(this.variable!.getId()); + return; + } + } + // Handle unspecial case. + this.setValue(id); + } + + /** + * Overrides referencesVariables(), indicating this field refers to a + * variable. + * + * @returns True. + * @internal + */ + override referencesVariables(): boolean { + return true; + } + + /** + * Construct a FieldVariable from a JSON arg object, + * dereferencing any string table references. + * + * @param options A JSON object with options (variable, variableTypes, and + * defaultType). + * @returns The new field instance. + * @nocollapse + * @internal + */ + static override fromJson( + options: FieldVariableFromJsonConfig, + ): FieldVariable { + const varName = parsing.replaceMessageReferences(options.variable); + // `this` might be a subclass of FieldVariable if that class doesn't + // override the static fromJson method. + return new this(varName, undefined, undefined, undefined, options); + } + + /** + * Return a sorted list of variable names for variable dropdown menus. + * Include a special option at the end for creating a new variable name. + * + * @returns Array of variable names/id tuples. + */ + static dropdownCreate(this: FieldVariable): MenuOption[] { + if (!this.variable) { + throw Error( + 'Tried to call dropdownCreate on a variable field with no' + + ' variable selected.', + ); + } + const name = this.getText(); + let variableModelList: VariableModel[] = []; + if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) { + const variableTypes = this.getVariableTypes(); + // Get a copy of the list, so that adding rename and new variable options + // doesn't modify the workspace's list. + for (let i = 0; i < variableTypes.length; i++) { + const variableType = variableTypes[i]; + const variables = + this.sourceBlock_.workspace.getVariablesOfType(variableType); + variableModelList = variableModelList.concat(variables); + } + } + variableModelList.sort(VariableModel.compareByName); + + const options: [string, string][] = []; + for (let i = 0; i < variableModelList.length; i++) { + // Set the UUID as the internal representation of the variable. + options[i] = [variableModelList[i].name, variableModelList[i].getId()]; + } + options.push([ + Msg['RENAME_VARIABLE'], + internalConstants.RENAME_VARIABLE_ID, + ]); + if (Msg['DELETE_VARIABLE']) { + options.push([ + Msg['DELETE_VARIABLE'].replace('%1', name), + internalConstants.DELETE_VARIABLE_ID, + ]); + } + + return options; + } +} + +fieldRegistry.register('field_variable', FieldVariable); + +/** + * Config options for the variable field. + */ +export interface FieldVariableConfig extends FieldConfig { + variableTypes?: string[]; + defaultType?: string; +} + +/** + * fromJson config options for the variable field. + */ +export interface FieldVariableFromJsonConfig extends FieldVariableConfig { + variable?: string; +} + +/** + * A function that is called to validate changes to the field's value before + * they are set. + * + * @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values} + * @param newValue The value to be validated. + * @returns One of three instructions for setting the new value: `T`, `null`, + * or `undefined`. + * + * - `T` to set this function's returned value instead of `newValue`. + * + * - `null` to invoke `doValueInvalid_` and not set a value. + * + * - `undefined` to set `newValue` as is. + */ +export type FieldVariableValidator = FieldDropdownValidator; diff --git a/core/flyout_base.js b/core/flyout_base.js deleted file mode 100644 index 18e99d88e5e..00000000000 --- a/core/flyout_base.js +++ /dev/null @@ -1,754 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2011 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Flyout tray containing blocks which may be created. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Flyout'); - -goog.require('Blockly.Block'); -goog.require('Blockly.Events'); -goog.require('Blockly.FlyoutButton'); -goog.require('Blockly.Gesture'); -goog.require('Blockly.Touch'); -goog.require('Blockly.WorkspaceSvg'); -goog.require('goog.dom'); -goog.require('goog.events'); -goog.require('goog.math.Rect'); -goog.require('goog.userAgent'); - - -/** - * Class for a flyout. - * @param {!Object} workspaceOptions Dictionary of options for the workspace. - * @constructor - */ -Blockly.Flyout = function(workspaceOptions) { - workspaceOptions.getMetrics = this.getMetrics_.bind(this); - workspaceOptions.setMetrics = this.setMetrics_.bind(this); - - /** - * @type {!Blockly.Workspace} - * @private - */ - this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions); - this.workspace_.isFlyout = true; - - /** - * Is RTL vs LTR. - * @type {boolean} - */ - this.RTL = !!workspaceOptions.RTL; - - /** - * Position of the toolbox and flyout relative to the workspace. - * @type {number} - * @private - */ - this.toolboxPosition_ = workspaceOptions.toolboxPosition; - - /** - * Opaque data that can be passed to Blockly.unbindEvent_. - * @type {!Array.} - * @private - */ - this.eventWrappers_ = []; - - /** - * List of background buttons that lurk behind each block to catch clicks - * landing in the blocks' lakes and bays. - * @type {!Array.} - * @private - */ - this.backgroundButtons_ = []; - - /** - * List of visible buttons. - * @type {!Array.} - * @private - */ - this.buttons_ = []; - - /** - * List of event listeners. - * @type {!Array.} - * @private - */ - this.listeners_ = []; - - /** - * List of blocks that should always be disabled. - * @type {!Array.} - * @private - */ - this.permanentlyDisabled_ = []; -}; - -/** - * Does the flyout automatically close when a block is created? - * @type {boolean} - */ -Blockly.Flyout.prototype.autoClose = true; - -/** - * Whether the flyout is visible. - * @type {boolean} - * @private - */ -Blockly.Flyout.prototype.isVisible_ = false; - -/** - * Whether the workspace containing this flyout is visible. - * @type {boolean} - * @private - */ -Blockly.Flyout.prototype.containerVisible_ = true; - -/** - * Corner radius of the flyout background. - * @type {number} - * @const - */ -Blockly.Flyout.prototype.CORNER_RADIUS = 8; - -/** - * Margin around the edges of the blocks in the flyout. - * @type {number} - * @const - */ -Blockly.Flyout.prototype.MARGIN = Blockly.Flyout.prototype.CORNER_RADIUS; - -/** - * TODO: Move GAP_X and GAP_Y to their appropriate files. - * Gap between items in horizontal flyouts. Can be overridden with the "sep" - * element. - * @const {number} - */ -Blockly.Flyout.prototype.GAP_X = Blockly.Flyout.prototype.MARGIN * 3; - -/** - * Gap between items in vertical flyouts. Can be overridden with the "sep" - * element. - * @const {number} - */ -Blockly.Flyout.prototype.GAP_Y = Blockly.Flyout.prototype.MARGIN * 3; - -/** - * Top/bottom padding between scrollbar and edge of flyout background. - * @type {number} - * @const - */ -Blockly.Flyout.prototype.SCROLLBAR_PADDING = 2; - -/** - * Width of flyout. - * @type {number} - * @private - */ -Blockly.Flyout.prototype.width_ = 0; - -/** - * Height of flyout. - * @type {number} - * @private - */ -Blockly.Flyout.prototype.height_ = 0; - -/** - * Range of a drag angle from a flyout considered "dragging toward workspace". - * Drags that are within the bounds of this many degrees from the orthogonal - * line to the flyout edge are considered to be "drags toward the workspace". - * Example: - * Flyout Edge Workspace - * [block] / <-within this angle, drags "toward workspace" | - * [block] ---- orthogonal to flyout boundary ---- | - * [block] \ | - * The angle is given in degrees from the orthogonal. - * - * This is used to know when to create a new block and when to scroll the - * flyout. Setting it to 360 means that all drags create a new block. - * @type {number} - * @private -*/ -Blockly.Flyout.prototype.dragAngleRange_ = 70; - -/** - * Creates the flyout's DOM. Only needs to be called once. The flyout can - * either exist as its own svg element or be a g element nested inside a - * separate svg element. - * @param {string} tagName The type of tag to put the flyout in. This - * should be or . - * @return {!Element} The flyout's SVG group. - */ -Blockly.Flyout.prototype.createDom = function(tagName) { - /* - - - - - */ - // Setting style to display:none to start. The toolbox and flyout - // hide/show code will set up proper visibility and size later. - this.svgGroup_ = Blockly.utils.createSvgElement(tagName, - {'class': 'blocklyFlyout', 'style': 'display: none'}, null); - this.svgBackground_ = Blockly.utils.createSvgElement('path', - {'class': 'blocklyFlyoutBackground'}, this.svgGroup_); - this.svgGroup_.appendChild(this.workspace_.createDom()); - return this.svgGroup_; -}; - -/** - * Initializes the flyout. - * @param {!Blockly.Workspace} targetWorkspace The workspace in which to create - * new blocks. - */ -Blockly.Flyout.prototype.init = function(targetWorkspace) { - this.targetWorkspace_ = targetWorkspace; - this.workspace_.targetWorkspace = targetWorkspace; - // Add scrollbar. - this.scrollbar_ = new Blockly.Scrollbar(this.workspace_, - this.horizontalLayout_, false, 'blocklyFlyoutScrollbar'); - - this.hide(); - - Array.prototype.push.apply(this.eventWrappers_, - Blockly.bindEventWithChecks_(this.svgGroup_, 'wheel', this, this.wheel_)); - if (!this.autoClose) { - this.filterWrapper_ = this.filterForCapacity_.bind(this); - this.targetWorkspace_.addChangeListener(this.filterWrapper_); - } - - // Dragging the flyout up and down. - Array.prototype.push.apply(this.eventWrappers_, - Blockly.bindEventWithChecks_(this.svgBackground_, 'mousedown', this, - this.onMouseDown_)); - - // A flyout connected to a workspace doesn't have its own current gesture. - this.workspace_.getGesture = - this.targetWorkspace_.getGesture.bind(this.targetWorkspace_); - - // Get variables from the main workspace rather than the target workspace. - this.workspace_.getVariable = - this.targetWorkspace_.getVariable.bind(this.targetWorkspace_); - - this.workspace_.getVariableById = - this.targetWorkspace_.getVariableById.bind(this.targetWorkspace_); - - this.workspace_.getVariablesOfType = - this.targetWorkspace_.getVariablesOfType.bind(this.targetWorkspace_); - - this.workspace_.deleteVariable = - this.targetWorkspace_.deleteVariable.bind(this.targetWorkspace_); - - this.workspace_.deleteVariableById = - this.targetWorkspace_.deleteVariableById.bind(this.targetWorkspace_); - - this.workspace_.renameVariable = - this.targetWorkspace_.renameVariable.bind(this.targetWorkspace_); - - this.workspace_.renameVariableById = - this.targetWorkspace_.renameVariableById.bind(this.targetWorkspace_); -}; - -/** - * Dispose of this flyout. - * Unlink from all DOM elements to prevent memory leaks. - */ -Blockly.Flyout.prototype.dispose = function() { - this.hide(); - Blockly.unbindEvent_(this.eventWrappers_); - if (this.filterWrapper_) { - this.targetWorkspace_.removeChangeListener(this.filterWrapper_); - this.filterWrapper_ = null; - } - if (this.scrollbar_) { - this.scrollbar_.dispose(); - this.scrollbar_ = null; - } - if (this.workspace_) { - this.workspace_.targetWorkspace = null; - this.workspace_.dispose(); - this.workspace_ = null; - } - if (this.svgGroup_) { - goog.dom.removeNode(this.svgGroup_); - this.svgGroup_ = null; - } - this.svgBackground_ = null; - this.targetWorkspace_ = null; -}; - -/** - * Get the width of the flyout. - * @return {number} The width of the flyout. - */ -Blockly.Flyout.prototype.getWidth = function() { - return this.width_; -}; - -/** - * Get the height of the flyout. - * @return {number} The width of the flyout. - */ -Blockly.Flyout.prototype.getHeight = function() { - return this.height_; -}; - -/** - * Get the workspace inside the flyout. - * @return {!Blockly.WorkspaceSvg} The workspace inside the flyout. - * @package - */ -Blockly.Flyout.prototype.getWorkspace = function() { - return this.workspace_; -}; - -/** - * Is the flyout visible? - * @return {boolean} True if visible. - */ -Blockly.Flyout.prototype.isVisible = function() { - return this.isVisible_; -}; - - /** - * Set whether the flyout is visible. A value of true does not necessarily mean - * that the flyout is shown. It could be hidden because its container is hidden. - * @param {boolean} visible True if visible. - */ -Blockly.Flyout.prototype.setVisible = function(visible) { - var visibilityChanged = (visible != this.isVisible()); - - this.isVisible_ = visible; - if (visibilityChanged) { - this.updateDisplay_(); - } -}; - -/** - * Set whether this flyout's container is visible. - * @param {boolean} visible Whether the container is visible. - */ -Blockly.Flyout.prototype.setContainerVisible = function(visible) { - var visibilityChanged = (visible != this.containerVisible_); - this.containerVisible_ = visible; - if (visibilityChanged) { - this.updateDisplay_(); - } -}; - -/** - * Update the display property of the flyout based whether it thinks it should - * be visible and whether its containing workspace is visible. - * @private - */ -Blockly.Flyout.prototype.updateDisplay_ = function() { - var show = true; - if (!this.containerVisible_) { - show = false; - } else { - show = this.isVisible(); - } - this.svgGroup_.style.display = show ? 'block' : 'none'; - // Update the scrollbar's visiblity too since it should mimic the - // flyout's visibility. - this.scrollbar_.setContainerVisible(show); -}; - -/** - * Update the view based on coordinates calculated in position(). - * @param {number} width The computed width of the flyout's SVG group - * @param {number} height The computed height of the flyout's SVG group. - * @param {number} x The computed x origin of the flyout's SVG group. - * @param {number} y The computed y origin of the flyout's SVG group. - * @private - */ -Blockly.Flyout.prototype.positionAt_ = function(width, height, x, y) { - this.svgGroup_.setAttribute("width", width); - this.svgGroup_.setAttribute("height", height); - var transform = 'translate(' + x + 'px,' + y + 'px)'; - Blockly.utils.setCssTransform(this.svgGroup_, transform); - - // Update the scrollbar (if one exists). - if (this.scrollbar_) { - // Set the scrollbars origin to be the top left of the flyout. - this.scrollbar_.setOrigin(x, y); - this.scrollbar_.resize(); - } -}; - -/** - * Hide and empty the flyout. - */ -Blockly.Flyout.prototype.hide = function() { - if (!this.isVisible()) { - return; - } - this.setVisible(false); - // Delete all the event listeners. - for (var x = 0, listen; listen = this.listeners_[x]; x++) { - Blockly.unbindEvent_(listen); - } - this.listeners_.length = 0; - if (this.reflowWrapper_) { - this.workspace_.removeChangeListener(this.reflowWrapper_); - this.reflowWrapper_ = null; - } - // Do NOT delete the blocks here. Wait until Flyout.show. - // https://neil.fraser.name/news/2014/08/09/ -}; - -/** - * Show and populate the flyout. - * @param {!Array|string} xmlList List of blocks to show. - * Variables and procedures have a custom set of blocks. - */ -Blockly.Flyout.prototype.show = function(xmlList) { - this.workspace_.setResizesEnabled(false); - this.hide(); - this.clearOldBlocks_(); - - // Handle dynamic categories, represented by a name instead of a list of XML. - // Look up the correct category generation function and call that to get a - // valid XML list. - if (typeof xmlList == 'string') { - var fnToApply = this.workspace_.targetWorkspace.getToolboxCategoryCallback( - xmlList); - goog.asserts.assert(goog.isFunction(fnToApply), - 'Couldn\'t find a callback function when opening a toolbox category.'); - xmlList = fnToApply(this.workspace_.targetWorkspace); - goog.asserts.assert(goog.isArray(xmlList), - 'The result of a toolbox category callback must be an array.'); - } - - this.setVisible(true); - // Create the blocks to be shown in this flyout. - var contents = []; - var gaps = []; - this.permanentlyDisabled_.length = 0; - for (var i = 0, xml; xml = xmlList[i]; i++) { - if (xml.tagName) { - var tagName = xml.tagName.toUpperCase(); - var default_gap = this.horizontalLayout_ ? this.GAP_X : this.GAP_Y; - if (tagName == 'BLOCK') { - var curBlock = Blockly.Xml.domToBlock(xml, this.workspace_); - if (curBlock.disabled) { - // Record blocks that were initially disabled. - // Do not enable these blocks as a result of capacity filtering. - this.permanentlyDisabled_.push(curBlock); - } - contents.push({type: 'block', block: curBlock}); - var gap = parseInt(xml.getAttribute('gap'), 10); - gaps.push(isNaN(gap) ? default_gap : gap); - } else if (xml.tagName.toUpperCase() == 'SEP') { - // Change the gap between two blocks. - // - // The default gap is 24, can be set larger or smaller. - // This overwrites the gap attribute on the previous block. - // Note that a deprecated method is to add a gap to a block. - // - var newGap = parseInt(xml.getAttribute('gap'), 10); - // Ignore gaps before the first block. - if (!isNaN(newGap) && gaps.length > 0) { - gaps[gaps.length - 1] = newGap; - } else { - gaps.push(default_gap); - } - } else if (tagName == 'BUTTON' || tagName == 'LABEL') { - // Labels behave the same as buttons, but are styled differently. - var isLabel = tagName == 'LABEL'; - var curButton = new Blockly.FlyoutButton(this.workspace_, - this.targetWorkspace_, xml, isLabel); - contents.push({type: 'button', button: curButton}); - gaps.push(default_gap); - } - } - } - - this.layout_(contents, gaps); - - // IE 11 is an incompetent browser that fails to fire mouseout events. - // When the mouse is over the background, deselect all blocks. - var deselectAll = function() { - var topBlocks = this.workspace_.getTopBlocks(false); - for (var i = 0, block; block = topBlocks[i]; i++) { - block.removeSelect(); - } - }; - - this.listeners_.push(Blockly.bindEventWithChecks_(this.svgBackground_, - 'mouseover', this, deselectAll)); - - if (this.horizontalLayout_) { - this.height_ = 0; - } else { - this.width_ = 0; - } - this.workspace_.setResizesEnabled(true); - this.reflow(); - - this.filterForCapacity_(); - - // Correctly position the flyout's scrollbar when it opens. - this.position(); - - this.reflowWrapper_ = this.reflow.bind(this); - this.workspace_.addChangeListener(this.reflowWrapper_); -}; - -/** - * Delete blocks and background buttons from a previous showing of the flyout. - * @private - */ -Blockly.Flyout.prototype.clearOldBlocks_ = function() { - // Delete any blocks from a previous showing. - var oldBlocks = this.workspace_.getTopBlocks(false); - for (var i = 0, block; block = oldBlocks[i]; i++) { - if (block.workspace == this.workspace_) { - block.dispose(false, false); - } - } - // Delete any background buttons from a previous showing. - for (var j = 0, rect; rect = this.backgroundButtons_[j]; j++) { - goog.dom.removeNode(rect); - } - this.backgroundButtons_.length = 0; - - for (var i = 0, button; button = this.buttons_[i]; i++) { - button.dispose(); - } - this.buttons_.length = 0; -}; - -/** - * Add listeners to a block that has been added to the flyout. - * @param {!Element} root The root node of the SVG group the block is in. - * @param {!Blockly.Block} block The block to add listeners for. - * @param {!Element} rect The invisible rectangle under the block that acts as - * a button for that block. - * @private - */ -Blockly.Flyout.prototype.addBlockListeners_ = function(root, block, rect) { - this.listeners_.push(Blockly.bindEventWithChecks_(root, 'mousedown', null, - this.blockMouseDown_(block))); - this.listeners_.push(Blockly.bindEventWithChecks_(rect, 'mousedown', null, - this.blockMouseDown_(block))); - this.listeners_.push(Blockly.bindEvent_(root, 'mouseover', block, - block.addSelect)); - this.listeners_.push(Blockly.bindEvent_(root, 'mouseout', block, - block.removeSelect)); - this.listeners_.push(Blockly.bindEvent_(rect, 'mouseover', block, - block.addSelect)); - this.listeners_.push(Blockly.bindEvent_(rect, 'mouseout', block, - block.removeSelect)); -}; - -/** - * Handle a mouse-down on an SVG block in a non-closing flyout. - * @param {!Blockly.Block} block The flyout block to copy. - * @return {!Function} Function to call when block is clicked. - * @private - */ -Blockly.Flyout.prototype.blockMouseDown_ = function(block) { - var flyout = this; - return function(e) { - var gesture = flyout.targetWorkspace_.getGesture(e); - if (gesture) { - gesture.setStartBlock(block); - gesture.handleFlyoutStart(e, flyout); - } - }; -}; - -/** - * Mouse down on the flyout background. Start a vertical scroll drag. - * @param {!Event} e Mouse down event. - * @private - */ -Blockly.Flyout.prototype.onMouseDown_ = function(e) { - var gesture = this.targetWorkspace_.getGesture(e); - if (gesture) { - gesture.handleFlyoutStart(e, this); - } -}; - -/** - * Create a copy of this block on the workspace. - * @param {!Blockly.BlockSvg} originalBlock The block to copy from the flyout. - * @return {Blockly.BlockSvg} The newly created block, or null if something - * went wrong with deserialization. - * @package - */ -Blockly.Flyout.prototype.createBlock = function(originalBlock) { - var newBlock = null; - Blockly.Events.disable(); - this.targetWorkspace_.setResizesEnabled(false); - try { - newBlock = this.placeNewBlock_(originalBlock); - //Force a render on IE and Edge to get around the issue described in - //Blockly.Field.getCachedWidth - if (goog.userAgent.IE || goog.userAgent.EDGE) { - var blocks = newBlock.getDescendants(); - for (var i = blocks.length - 1; i >= 0; i--) { - blocks[i].render(false); - } - } - // Close the flyout. - Blockly.hideChaff(); - } finally { - Blockly.Events.enable(); - } - - if (Blockly.Events.isEnabled()) { - Blockly.Events.setGroup(true); - Blockly.Events.fire(new Blockly.Events.Create(newBlock)); - } - if (this.autoClose) { - this.hide(); - } else { - this.filterForCapacity_(); - } - return newBlock; -}; - -/** - * Initialize the given button: move it to the correct location, - * add listeners, etc. - * @param {!Blockly.FlyoutButton} button The button to initialize and place. - * @param {number} x The x position of the cursor during this layout pass. - * @param {number} y The y position of the cursor during this layout pass. - * @private - */ -Blockly.Flyout.prototype.initFlyoutButton_ = function(button, x, y) { - var buttonSvg = button.createDom(); - button.moveTo(x, y); - button.show(); - // Clicking on a flyout button or label is a lot like clicking on the - // flyout background. - this.listeners_.push(Blockly.bindEventWithChecks_(buttonSvg, 'mousedown', - this, this.onMouseDown_)); - - this.buttons_.push(button); -}; - -/** - * Create and place a rectangle corresponding to the given block. - * @param {!Blockly.Block} block The block to associate the rect to. - * @param {number} x The x position of the cursor during this layout pass. - * @param {number} y The y position of the cursor during this layout pass. - * @param {!{height: number, width: number}} blockHW The height and width of the - * block. - * @param {number} index The index into the background buttons list where this - * rect should be placed. - * @return {!SVGElement} Newly created SVG element for the rectangle behind the - * block. - * @private - */ -Blockly.Flyout.prototype.createRect_ = function(block, x, y, blockHW, index) { - // Create an invisible rectangle under the block to act as a button. Just - // using the block as a button is poor, since blocks have holes in them. - var rect = Blockly.utils.createSvgElement('rect', - { - 'fill-opacity': 0, - 'x': x, - 'y': y, - 'height': blockHW.height, - 'width': blockHW.width - }, null); - rect.tooltip = block; - Blockly.Tooltip.bindMouseEvents(rect); - // Add the rectangles under the blocks, so that the blocks' tooltips work. - this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot()); - - block.flyoutRect_ = rect; - this.backgroundButtons_[index] = rect; - return rect; -}; - -/** - * Move a rectangle to sit exactly behind a block, taking into account tabs, - * hats, and any other protrusions we invent. - * @param {!SVGElement} rect The rectangle to move directly behind the block. - * @param {!Blockly.BlockSvg} block The block the rectangle should be behind. - * @private - */ -Blockly.Flyout.prototype.moveRectToBlock_ = function(rect, block) { - var blockHW = block.getHeightWidth(); - rect.setAttribute('width', blockHW.width); - rect.setAttribute('height', blockHW.height); - - // For hat blocks we want to shift them down by the hat height - // since the y coordinate is the corner, not the top of the hat. - var hatOffset = - block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0; - if (hatOffset) { - block.moveBy(0, hatOffset); - } - - // Blocks with output tabs are shifted a bit. - var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; - var blockXY = block.getRelativeToSurfaceXY(); - rect.setAttribute('y', blockXY.y); - rect.setAttribute('x', - this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab); -}; - -/** - * Filter the blocks on the flyout to disable the ones that are above the - * capacity limit. For instance, if the user may only place two more blocks on - * the workspace, an "a + b" block that has two shadow blocks would be disabled. - * @private - */ -Blockly.Flyout.prototype.filterForCapacity_ = function() { - var remainingCapacity = this.targetWorkspace_.remainingCapacity(); - var blocks = this.workspace_.getTopBlocks(false); - for (var i = 0, block; block = blocks[i]; i++) { - if (this.permanentlyDisabled_.indexOf(block) == -1) { - var allBlocks = block.getDescendants(); - block.setDisabled(allBlocks.length > remainingCapacity); - } - } -}; - -/** - * Reflow blocks and their buttons. - */ -Blockly.Flyout.prototype.reflow = function() { - if (this.reflowWrapper_) { - this.workspace_.removeChangeListener(this.reflowWrapper_); - } - var blocks = this.workspace_.getTopBlocks(false); - this.reflowInternal_(blocks); - if (this.reflowWrapper_) { - this.workspace_.addChangeListener(this.reflowWrapper_); - } -}; - -/** - * @return {boolean} True if this flyout may be scrolled with a scrollbar or by - * dragging. - * @package - */ -Blockly.Flyout.prototype.isScrollable = function() { - return this.scrollbar_ ? this.scrollbar_.isVisible() : false; -}; diff --git a/core/flyout_base.ts b/core/flyout_base.ts new file mode 100644 index 00000000000..96d2b27fdcb --- /dev/null +++ b/core/flyout_base.ts @@ -0,0 +1,1376 @@ +/** + * @license + * Copyright 2011 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Flyout tray containing blocks which may be created. + * + * @class + */ +// Former goog.module ID: Blockly.Flyout + +import type {Block} from './block.js'; +import {BlockSvg} from './block_svg.js'; +import * as browserEvents from './browser_events.js'; +import * as common from './common.js'; +import {ComponentManager} from './component_manager.js'; +import {MANUALLY_DISABLED} from './constants.js'; +import {DeleteArea} from './delete_area.js'; +import type {Abstract as AbstractEvent} from './events/events_abstract.js'; +import {EventType} from './events/type.js'; +import * as eventUtils from './events/utils.js'; +import {FlyoutButton} from './flyout_button.js'; +import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; +import {IAutoHideable} from './interfaces/i_autohideable.js'; +import type {IFlyout} from './interfaces/i_flyout.js'; +import type {Options} from './options.js'; +import * as renderManagement from './render_management.js'; +import {ScrollbarPair} from './scrollbar_pair.js'; +import * as blocks from './serialization/blocks.js'; +import * as Tooltip from './tooltip.js'; +import {Coordinate} from './utils/coordinate.js'; +import * as dom from './utils/dom.js'; +import * as idGenerator from './utils/idgenerator.js'; +import {Svg} from './utils/svg.js'; +import * as toolbox from './utils/toolbox.js'; +import * as utilsXml from './utils/xml.js'; +import * as Variables from './variables.js'; +import {WorkspaceSvg} from './workspace_svg.js'; +import * as Xml from './xml.js'; + +enum FlyoutItemType { + BLOCK = 'block', + BUTTON = 'button', +} + +/** + * The language-neutral ID for when the reason why a block is disabled is + * because the workspace is at block capacity. + */ +const WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON = + 'WORKSPACE_AT_BLOCK_CAPACITY'; + +/** + * Class for a flyout. + */ +export abstract class Flyout + extends DeleteArea + implements IAutoHideable, IFlyout +{ + /** + * Position the flyout. + */ + abstract position(): void; + + /** + * Determine if a drag delta is toward the workspace, based on the position + * and orientation of the flyout. This is used in determineDragIntention_ to + * determine if a new block should be created or if the flyout should scroll. + * + * @param currentDragDeltaXY How far the pointer has + * moved from the position at mouse down, in pixel units. + * @returns True if the drag is toward the workspace. + */ + abstract isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean; + + /** + * Sets the translation of the flyout to match the scrollbars. + * + * @param xyRatio Contains a y property which is a float + * between 0 and 1 specifying the degree of scrolling and a + * similar x property. + */ + protected abstract setMetrics_(xyRatio: {x?: number; y?: number}): void; + + /** + * Lay out the blocks in the flyout. + * + * @param contents The blocks and buttons to lay out. + * @param gaps The visible gaps between blocks. + */ + protected abstract layout_(contents: FlyoutItem[], gaps: number[]): void; + + /** + * Scroll the flyout. + * + * @param e Mouse wheel scroll event. + */ + protected abstract wheel_(e: WheelEvent): void; + + /** + * Compute height of flyout. Position mat under each block. + * For RTL: Lay out the blocks right-aligned. + */ + protected abstract reflowInternal_(): void; + + /** + * Calculates the x coordinate for the flyout position. + * + * @returns X coordinate. + */ + abstract getX(): number; + + /** + * Calculates the y coordinate for the flyout position. + * + * @returns Y coordinate. + */ + abstract getY(): number; + + /** + * Scroll the flyout to the beginning of its contents. + */ + abstract scrollToStart(): void; + + /** + * The type of a flyout content item. + */ + static FlyoutItemType = FlyoutItemType; + + protected workspace_: WorkspaceSvg; + RTL: boolean; + /** + * Whether the flyout should be laid out horizontally or not. + * + * @internal + */ + horizontalLayout = false; + protected toolboxPosition_: number; + + /** + * Array holding info needed to unbind events. + * Used for disposing. + * Ex: [[node, name, func], [node, name, func]]. + */ + private boundEvents: browserEvents.Data[] = []; + + /** + * Function that will be registered as a change listener on the workspace + * to reflow when blocks in the flyout workspace change. + */ + private reflowWrapper: ((e: AbstractEvent) => void) | null = null; + + /** + * Function that disables blocks in the flyout based on max block counts + * allowed in the target workspace. Registered as a change listener on the + * target workspace. + */ + private filterWrapper: ((e: AbstractEvent) => void) | null = null; + + /** + * List of background mats that lurk behind each block to catch clicks + * landing in the blocks' lakes and bays. + */ + private mats: SVGElement[] = []; + + /** + * List of visible buttons. + */ + protected buttons_: FlyoutButton[] = []; + + /** + * List of visible buttons and blocks. + */ + protected contents: FlyoutItem[] = []; + + /** + * List of event listeners. + */ + private listeners: browserEvents.Data[] = []; + + /** + * List of blocks that should always be disabled. + */ + private permanentlyDisabled: Block[] = []; + + protected readonly tabWidth_: number; + + /** + * The target workspace. + * + * @internal + */ + targetWorkspace!: WorkspaceSvg; + + /** + * A list of blocks that can be reused. + */ + private recycledBlocks: BlockSvg[] = []; + + /** + * Does the flyout automatically close when a block is created? + */ + autoClose = true; + + /** + * Whether the flyout is visible. + */ + private visible = false; + + /** + * Whether the workspace containing this flyout is visible. + */ + private containerVisible = true; + protected rectMap_: WeakMap; + + /** + * Corner radius of the flyout background. + */ + readonly CORNER_RADIUS: number = 8; + readonly MARGIN: number; + readonly GAP_X: number; + readonly GAP_Y: number; + + /** + * Top/bottom padding between scrollbar and edge of flyout background. + */ + readonly SCROLLBAR_MARGIN: number = 2.5; + + /** + * Width of flyout. + */ + protected width_ = 0; + + /** + * Height of flyout. + */ + protected height_ = 0; + // clang-format off + /** + * Range of a drag angle from a flyout considered "dragging toward + * workspace". Drags that are within the bounds of this many degrees from + * the orthogonal line to the flyout edge are considered to be "drags toward + * the workspace". + * + * @example + * + * ``` + * Flyout Edge Workspace + * [block] / <-within this angle, drags "toward workspace" | + * [block] ---- orthogonal to flyout boundary ---- | + * [block] \ | + * ``` + * + * The angle is given in degrees from the orthogonal. + * + * This is used to know when to create a new block and when to scroll the + * flyout. Setting it to 360 means that all drags create a new block. + */ + // clang-format on + protected dragAngleRange_ = 70; + + /** + * The path around the background of the flyout, which will be filled with a + * background colour. + */ + protected svgBackground_: SVGPathElement | null = null; + + /** + * The root SVG group for the button or label. + */ + protected svgGroup_: SVGGElement | null = null; + /** + * @param workspaceOptions Dictionary of options for the + * workspace. + */ + constructor(workspaceOptions: Options) { + super(); + workspaceOptions.setMetrics = this.setMetrics_.bind(this); + + this.workspace_ = new WorkspaceSvg(workspaceOptions); + this.workspace_.setMetricsManager( + new FlyoutMetricsManager(this.workspace_, this), + ); + + this.workspace_.internalIsFlyout = true; + // Keep the workspace visibility consistent with the flyout's visibility. + this.workspace_.setVisible(this.visible); + + /** + * The unique id for this component that is used to register with the + * ComponentManager. + */ + this.id = idGenerator.genUid(); + + /** + * Is RTL vs LTR. + */ + this.RTL = !!workspaceOptions.RTL; + + /** + * Position of the toolbox and flyout relative to the workspace. + */ + this.toolboxPosition_ = workspaceOptions.toolboxPosition; + + /** + * Width of output tab. + */ + this.tabWidth_ = this.workspace_.getRenderer().getConstants().TAB_WIDTH; + + /** + * A map from blocks to the rects which are beneath them to act as input + * targets. + * + * @internal + */ + this.rectMap_ = new WeakMap(); + + /** + * Margin around the edges of the blocks in the flyout. + */ + this.MARGIN = this.CORNER_RADIUS; + + // TODO: Move GAP_X and GAP_Y to their appropriate files. + /** + * Gap between items in horizontal flyouts. Can be overridden with the "sep" + * element. + */ + this.GAP_X = this.MARGIN * 3; + + /** + * Gap between items in vertical flyouts. Can be overridden with the "sep" + * element. + */ + this.GAP_Y = this.MARGIN * 3; + } + + /** + * Creates the flyout's DOM. Only needs to be called once. The flyout can + * either exist as its own SVG element or be a g element nested inside a + * separate SVG element. + * + * @param tagName The type of tag to + * put the flyout in. This should be or . + * @returns The flyout's SVG group. + */ + createDom( + tagName: string | Svg | Svg, + ): SVGElement { + /* + + + + + */ + // Setting style to display:none to start. The toolbox and flyout + // hide/show code will set up proper visibility and size later. + this.svgGroup_ = dom.createSvgElement(tagName, { + 'class': 'blocklyFlyout', + }); + this.svgGroup_.style.display = 'none'; + this.svgBackground_ = dom.createSvgElement( + Svg.PATH, + {'class': 'blocklyFlyoutBackground'}, + this.svgGroup_, + ); + this.svgGroup_.appendChild(this.workspace_.createDom()); + this.workspace_ + .getThemeManager() + .subscribe(this.svgBackground_, 'flyoutBackgroundColour', 'fill'); + this.workspace_ + .getThemeManager() + .subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity'); + return this.svgGroup_; + } + + /** + * Initializes the flyout. + * + * @param targetWorkspace The workspace in which to + * create new blocks. + */ + init(targetWorkspace: WorkspaceSvg) { + this.targetWorkspace = targetWorkspace; + this.workspace_.targetWorkspace = targetWorkspace; + + this.workspace_.scrollbar = new ScrollbarPair( + this.workspace_, + this.horizontalLayout, + !this.horizontalLayout, + 'blocklyFlyoutScrollbar', + this.SCROLLBAR_MARGIN, + ); + + this.hide(); + + this.boundEvents.push( + browserEvents.conditionalBind( + this.svgGroup_ as SVGGElement, + 'wheel', + this, + this.wheel_, + ), + ); + this.filterWrapper = this.filterForCapacity.bind(this); + this.targetWorkspace.addChangeListener(this.filterWrapper); + + // Dragging the flyout up and down. + this.boundEvents.push( + browserEvents.conditionalBind( + this.svgBackground_ as SVGPathElement, + 'pointerdown', + this, + this.onMouseDown, + ), + ); + + // A flyout connected to a workspace doesn't have its own current gesture. + this.workspace_.getGesture = this.targetWorkspace.getGesture.bind( + this.targetWorkspace, + ); + + // Get variables from the main workspace rather than the target workspace. + this.workspace_.setVariableMap(this.targetWorkspace.getVariableMap()); + + this.workspace_.createPotentialVariableMap(); + + targetWorkspace.getComponentManager().addComponent({ + component: this, + weight: ComponentManager.ComponentWeight.FLYOUT_WEIGHT, + capabilities: [ + ComponentManager.Capability.AUTOHIDEABLE, + ComponentManager.Capability.DELETE_AREA, + ComponentManager.Capability.DRAG_TARGET, + ], + }); + } + + /** + * Dispose of this flyout. + * Unlink from all DOM elements to prevent memory leaks. + */ + dispose() { + this.hide(); + this.targetWorkspace.getComponentManager().removeComponent(this.id); + for (const event of this.boundEvents) { + browserEvents.unbind(event); + } + this.boundEvents.length = 0; + if (this.filterWrapper) { + this.targetWorkspace.removeChangeListener(this.filterWrapper); + } + if (this.workspace_) { + this.workspace_.getThemeManager().unsubscribe(this.svgBackground_!); + this.workspace_.dispose(); + } + if (this.svgGroup_) { + dom.removeNode(this.svgGroup_); + } + } + + /** + * Get the width of the flyout. + * + * @returns The width of the flyout. + */ + getWidth(): number { + return this.width_; + } + + /** + * Get the height of the flyout. + * + * @returns The width of the flyout. + */ + getHeight(): number { + return this.height_; + } + + /** + * Get the scale (zoom level) of the flyout. By default, + * this matches the target workspace scale, but this can be overridden. + * + * @returns Flyout workspace scale. + */ + getFlyoutScale(): number { + return this.targetWorkspace.scale; + } + + /** + * Get the workspace inside the flyout. + * + * @returns The workspace inside the flyout. + */ + getWorkspace(): WorkspaceSvg { + return this.workspace_; + } + + /** + * Sets whether this flyout automatically closes when blocks are dragged out, + * the workspace is clicked, etc, or not. + */ + setAutoClose(autoClose: boolean) { + this.autoClose = autoClose; + this.targetWorkspace.recordDragTargets(); + this.targetWorkspace.resizeContents(); + } + + /** Automatically hides the flyout if it is an autoclosing flyout. */ + autoHide(onlyClosePopups: boolean): void { + if ( + !onlyClosePopups && + this.targetWorkspace.getFlyout(true) === this && + this.autoClose + ) + this.hide(); + } + + /** + * Get the target workspace inside the flyout. + * + * @returns The target workspace inside the flyout. + */ + getTargetWorkspace(): WorkspaceSvg { + return this.targetWorkspace; + } + + /** + * Is the flyout visible? + * + * @returns True if visible. + */ + isVisible(): boolean { + return this.visible; + } + + /** + * Set whether the flyout is visible. A value of true does not necessarily + * mean that the flyout is shown. It could be hidden because its container is + * hidden. + * + * @param visible True if visible. + */ + setVisible(visible: boolean) { + const visibilityChanged = visible !== this.isVisible(); + + this.visible = visible; + if (visibilityChanged) { + if (!this.autoClose) { + // Auto-close flyouts are ignored as drag targets, so only non + // auto-close flyouts need to have their drag target updated. + this.targetWorkspace.recordDragTargets(); + } + this.updateDisplay(); + } + } + + /** + * Set whether this flyout's container is visible. + * + * @param visible Whether the container is visible. + */ + setContainerVisible(visible: boolean) { + const visibilityChanged = visible !== this.containerVisible; + this.containerVisible = visible; + if (visibilityChanged) { + this.updateDisplay(); + } + } + + /** + * Get the list of buttons and blocks of the current flyout. + * + * @returns The array of flyout buttons and blocks. + */ + getContents(): FlyoutItem[] { + return this.contents; + } + + /** + * Store the list of buttons and blocks on the flyout. + * + * @param contents - The array of items for the flyout. + */ + setContents(contents: FlyoutItem[]): void { + this.contents = contents; + } + /** + * Update the display property of the flyout based whether it thinks it should + * be visible and whether its containing workspace is visible. + */ + private updateDisplay() { + let show = true; + if (!this.containerVisible) { + show = false; + } else { + show = this.isVisible(); + } + if (this.svgGroup_) { + this.svgGroup_.style.display = show ? 'block' : 'none'; + } + // Update the scrollbar's visibility too since it should mimic the + // flyout's visibility. + this.workspace_.scrollbar?.setContainerVisible(show); + } + + /** + * Update the view based on coordinates calculated in position(). + * + * @param width The computed width of the flyout's SVG group + * @param height The computed height of the flyout's SVG group. + * @param x The computed x origin of the flyout's SVG group. + * @param y The computed y origin of the flyout's SVG group. + */ + protected positionAt_(width: number, height: number, x: number, y: number) { + this.svgGroup_?.setAttribute('width', `${width}`); + this.svgGroup_?.setAttribute('height', `${height}`); + this.workspace_.setCachedParentSvgSize(width, height); + + if (this.svgGroup_) { + const transform = 'translate(' + x + 'px,' + y + 'px)'; + dom.setCssTransform(this.svgGroup_, transform); + } + + // Update the scrollbar (if one exists). + const scrollbar = this.workspace_.scrollbar; + if (scrollbar) { + // Set the scrollbars origin to be the top left of the flyout. + scrollbar.setOrigin(x, y); + scrollbar.resize(); + // If origin changed and metrics haven't changed enough to trigger + // reposition in resize, we need to call setPosition. See issue #4692. + if (scrollbar.hScroll) { + scrollbar.hScroll.setPosition( + scrollbar.hScroll.position.x, + scrollbar.hScroll.position.y, + ); + } + if (scrollbar.vScroll) { + scrollbar.vScroll.setPosition( + scrollbar.vScroll.position.x, + scrollbar.vScroll.position.y, + ); + } + } + } + + /** + * Hide and empty the flyout. + */ + hide() { + if (!this.isVisible()) { + return; + } + this.setVisible(false); + // Delete all the event listeners. + for (const listen of this.listeners) { + browserEvents.unbind(listen); + } + this.listeners.length = 0; + if (this.reflowWrapper) { + this.workspace_.removeChangeListener(this.reflowWrapper); + this.reflowWrapper = null; + } + // Do NOT delete the blocks here. Wait until Flyout.show. + // https://neil.fraser.name/news/2014/08/09/ + } + + /** + * Show and populate the flyout. + * + * @param flyoutDef Contents to display + * in the flyout. This is either an array of Nodes, a NodeList, a + * toolbox definition, or a string with the name of the dynamic category. + */ + show(flyoutDef: toolbox.FlyoutDefinition | string) { + this.workspace_.setResizesEnabled(false); + this.hide(); + this.clearOldBlocks(); + + // Handle dynamic categories, represented by a name instead of a list. + if (typeof flyoutDef === 'string') { + flyoutDef = this.getDynamicCategoryContents(flyoutDef); + } + this.setVisible(true); + + // Parse the Array, Node or NodeList into a a list of flyout items. + const parsedContent = toolbox.convertFlyoutDefToJsonArray(flyoutDef); + const flyoutInfo = this.createFlyoutInfo(parsedContent); + + renderManagement.triggerQueuedRenders(this.workspace_); + + this.setContents(flyoutInfo.contents); + + this.layout_(flyoutInfo.contents, flyoutInfo.gaps); + + if (this.horizontalLayout) { + this.height_ = 0; + } else { + this.width_ = 0; + } + this.workspace_.setResizesEnabled(true); + this.reflow(); + + this.filterForCapacity(); + + // Correctly position the flyout's scrollbar when it opens. + this.position(); + + this.reflowWrapper = this.reflow.bind(this); + this.workspace_.addChangeListener(this.reflowWrapper); + this.emptyRecycledBlocks(); + } + + /** + * Create the contents array and gaps array necessary to create the layout for + * the flyout. + * + * @param parsedContent The array + * of objects to show in the flyout. + * @returns The list of contents and gaps needed to lay out the flyout. + */ + private createFlyoutInfo(parsedContent: toolbox.FlyoutItemInfoArray): { + contents: FlyoutItem[]; + gaps: number[]; + } { + const contents: FlyoutItem[] = []; + const gaps: number[] = []; + this.permanentlyDisabled.length = 0; + const defaultGap = this.horizontalLayout ? this.GAP_X : this.GAP_Y; + for (const info of parsedContent) { + if ('custom' in info) { + const customInfo = info as toolbox.DynamicCategoryInfo; + const categoryName = customInfo['custom']; + const flyoutDef = this.getDynamicCategoryContents(categoryName); + const parsedDynamicContent = + toolbox.convertFlyoutDefToJsonArray(flyoutDef); + const {contents: dynamicContents, gaps: dynamicGaps} = + this.createFlyoutInfo(parsedDynamicContent); + contents.push(...dynamicContents); + gaps.push(...dynamicGaps); + } + + switch (info['kind'].toUpperCase()) { + case 'BLOCK': { + const blockInfo = info as toolbox.BlockInfo; + const block = this.createFlyoutBlock(blockInfo); + contents.push({type: FlyoutItemType.BLOCK, block: block}); + this.addBlockGap(blockInfo, gaps, defaultGap); + break; + } + case 'SEP': { + const sepInfo = info as toolbox.SeparatorInfo; + this.addSeparatorGap(sepInfo, gaps, defaultGap); + break; + } + case 'LABEL': { + const labelInfo = info as toolbox.LabelInfo; + // A label is a button with different styling. + const label = this.createButton(labelInfo, /** isLabel */ true); + contents.push({type: FlyoutItemType.BUTTON, button: label}); + gaps.push(defaultGap); + break; + } + case 'BUTTON': { + const buttonInfo = info as toolbox.ButtonInfo; + const button = this.createButton(buttonInfo, /** isLabel */ false); + contents.push({type: FlyoutItemType.BUTTON, button: button}); + gaps.push(defaultGap); + break; + } + } + } + + return {contents: contents, gaps: gaps}; + } + + /** + * Gets the flyout definition for the dynamic category. + * + * @param categoryName The name of the dynamic category. + * @returns The definition of the + * flyout in one of its many forms. + */ + private getDynamicCategoryContents( + categoryName: string, + ): toolbox.FlyoutDefinition { + // Look up the correct category generation function and call that to get a + // valid XML list. + const fnToApply = + this.workspace_.targetWorkspace!.getToolboxCategoryCallback(categoryName); + if (typeof fnToApply !== 'function') { + throw TypeError( + "Couldn't find a callback function when opening" + + ' a toolbox category.', + ); + } + return fnToApply(this.workspace_.targetWorkspace!); + } + + /** + * Creates a flyout button or a flyout label. + * + * @param btnInfo The object holding information about a button or a label. + * @param isLabel True if the button is a label, false otherwise. + * @returns The object used to display the button in the + * flyout. + */ + private createButton( + btnInfo: toolbox.ButtonOrLabelInfo, + isLabel: boolean, + ): FlyoutButton { + const curButton = new FlyoutButton( + this.workspace_, + this.targetWorkspace as WorkspaceSvg, + btnInfo, + isLabel, + ); + return curButton; + } + + /** + * Create a block from the xml and permanently disable any blocks that were + * defined as disabled. + * + * @param blockInfo The info of the block. + * @returns The block created from the blockInfo. + */ + private createFlyoutBlock(blockInfo: toolbox.BlockInfo): BlockSvg { + let block; + if (blockInfo['blockxml']) { + const xml = ( + typeof blockInfo['blockxml'] === 'string' + ? utilsXml.textToDom(blockInfo['blockxml']) + : blockInfo['blockxml'] + ) as Element; + block = this.getRecycledBlock(xml.getAttribute('type')!); + if (!block) { + block = Xml.domToBlockInternal(xml, this.workspace_); + } + } else { + block = this.getRecycledBlock(blockInfo['type']!); + if (!block) { + if (blockInfo['enabled'] === undefined) { + blockInfo['enabled'] = + blockInfo['disabled'] !== 'true' && blockInfo['disabled'] !== true; + } + if ( + blockInfo['disabledReasons'] === undefined && + blockInfo['enabled'] === false + ) { + blockInfo['disabledReasons'] = [MANUALLY_DISABLED]; + } + block = blocks.appendInternal( + blockInfo as blocks.State, + this.workspace_, + ); + } + } + + if (!block.isEnabled()) { + // Record blocks that were initially disabled. + // Do not enable these blocks as a result of capacity filtering. + this.permanentlyDisabled.push(block); + } + return block as BlockSvg; + } + + /** + * Returns a block from the array of recycled blocks with the given type, or + * undefined if one cannot be found. + * + * @param blockType The type of the block to try to recycle. + * @returns The recycled block, or undefined if + * one could not be recycled. + */ + private getRecycledBlock(blockType: string): BlockSvg | undefined { + let index = -1; + for (let i = 0; i < this.recycledBlocks.length; i++) { + if (this.recycledBlocks[i].type === blockType) { + index = i; + break; + } + } + return index === -1 ? undefined : this.recycledBlocks.splice(index, 1)[0]; + } + + /** + * Adds a gap in the flyout based on block info. + * + * @param blockInfo Information about a block. + * @param gaps The list of gaps between items in the flyout. + * @param defaultGap The default gap between one element and the + * next. + */ + private addBlockGap( + blockInfo: toolbox.BlockInfo, + gaps: number[], + defaultGap: number, + ) { + let gap; + if (blockInfo['gap']) { + gap = parseInt(String(blockInfo['gap'])); + } else if (blockInfo['blockxml']) { + const xml = ( + typeof blockInfo['blockxml'] === 'string' + ? utilsXml.textToDom(blockInfo['blockxml']) + : blockInfo['blockxml'] + ) as Element; + gap = parseInt(xml.getAttribute('gap')!); + } + gaps.push(!gap || isNaN(gap) ? defaultGap : gap); + } + + /** + * Add the necessary gap in the flyout for a separator. + * + * @param sepInfo The object holding + * information about a separator. + * @param gaps The list gaps between items in the flyout. + * @param defaultGap The default gap between the button and next + * element. + */ + private addSeparatorGap( + sepInfo: toolbox.SeparatorInfo, + gaps: number[], + defaultGap: number, + ) { + // Change the gap between two toolbox elements. + // + // The default gap is 24, can be set larger or smaller. + // This overwrites the gap attribute on the previous element. + const newGap = parseInt(String(sepInfo['gap'])); + // Ignore gaps before the first block. + if (!isNaN(newGap) && gaps.length > 0) { + gaps[gaps.length - 1] = newGap; + } else { + gaps.push(defaultGap); + } + } + + /** + * Delete blocks, mats and buttons from a previous showing of the flyout. + */ + private clearOldBlocks() { + // Delete any blocks from a previous showing. + const oldBlocks = this.workspace_.getTopBlocks(false); + for (let i = 0, block; (block = oldBlocks[i]); i++) { + if (this.blockIsRecyclable_(block)) { + this.recycleBlock(block); + } else { + block.dispose(false, false); + } + } + // Delete any mats from a previous showing. + for (let j = 0; j < this.mats.length; j++) { + const rect = this.mats[j]; + if (rect) { + Tooltip.unbindMouseEvents(rect); + dom.removeNode(rect); + } + } + this.mats.length = 0; + // Delete any buttons from a previous showing. + for (let i = 0, button; (button = this.buttons_[i]); i++) { + button.dispose(); + } + this.buttons_.length = 0; + + // Clear potential variables from the previous showing. + this.workspace_.getPotentialVariableMap()?.clear(); + } + + /** + * Empties all of the recycled blocks, properly disposing of them. + */ + private emptyRecycledBlocks() { + for (let i = 0; i < this.recycledBlocks.length; i++) { + this.recycledBlocks[i].dispose(); + } + this.recycledBlocks = []; + } + + /** + * Returns whether the given block can be recycled or not. + * + * @param _block The block to check for recyclability. + * @returns True if the block can be recycled. False otherwise. + */ + protected blockIsRecyclable_(_block: BlockSvg): boolean { + // By default, recycling is disabled. + return false; + } + + /** + * Puts a previously created block into the recycle bin and moves it to the + * top of the workspace. Used during large workspace swaps to limit the number + * of new DOM elements we need to create. + * + * @param block The block to recycle. + */ + private recycleBlock(block: BlockSvg) { + const xy = block.getRelativeToSurfaceXY(); + block.moveBy(-xy.x, -xy.y); + this.recycledBlocks.push(block); + } + + /** + * Add listeners to a block that has been added to the flyout. + * + * @param root The root node of the SVG group the block is in. + * @param block The block to add listeners for. + * @param rect The invisible rectangle under the block that acts + * as a mat for that block. + */ + protected addBlockListeners_( + root: SVGElement, + block: BlockSvg, + rect: SVGElement, + ) { + this.listeners.push( + browserEvents.conditionalBind( + root, + 'pointerdown', + null, + this.blockMouseDown(block), + ), + ); + this.listeners.push( + browserEvents.conditionalBind( + rect, + 'pointerdown', + null, + this.blockMouseDown(block), + ), + ); + this.listeners.push( + browserEvents.bind(root, 'pointerenter', block, () => { + if (!this.targetWorkspace.isDragging()) { + block.addSelect(); + } + }), + ); + this.listeners.push( + browserEvents.bind(root, 'pointerleave', block, () => { + if (!this.targetWorkspace.isDragging()) { + block.removeSelect(); + } + }), + ); + this.listeners.push( + browserEvents.bind(rect, 'pointerenter', block, () => { + if (!this.targetWorkspace.isDragging()) { + block.addSelect(); + } + }), + ); + this.listeners.push( + browserEvents.bind(rect, 'pointerleave', block, () => { + if (!this.targetWorkspace.isDragging()) { + block.removeSelect(); + } + }), + ); + } + + /** + * Handle a pointerdown on an SVG block in a non-closing flyout. + * + * @param block The flyout block to copy. + * @returns Function to call when block is clicked. + */ + private blockMouseDown(block: BlockSvg) { + return (e: PointerEvent) => { + const gesture = this.targetWorkspace.getGesture(e); + if (gesture) { + gesture.setStartBlock(block); + gesture.handleFlyoutStart(e, this); + } + }; + } + + /** + * Pointer down on the flyout background. Start a vertical scroll drag. + * + * @param e Pointer down event. + */ + private onMouseDown(e: PointerEvent) { + const gesture = this.targetWorkspace.getGesture(e); + if (gesture) { + gesture.handleFlyoutStart(e, this); + } + } + + /** + * Does this flyout allow you to create a new instance of the given block? + * Used for deciding if a block can be "dragged out of" the flyout. + * + * @param block The block to copy from the flyout. + * @returns True if you can create a new instance of the block, false + * otherwise. + * @internal + */ + isBlockCreatable(block: BlockSvg): boolean { + return block.isEnabled(); + } + + /** + * Create a copy of this block on the workspace. + * + * @param originalBlock The block to copy from the flyout. + * @returns The newly created block. + * @throws {Error} if something went wrong with deserialization. + * @internal + */ + createBlock(originalBlock: BlockSvg): BlockSvg { + let newBlock = null; + eventUtils.disable(); + const variablesBeforeCreation = this.targetWorkspace.getAllVariables(); + this.targetWorkspace.setResizesEnabled(false); + try { + newBlock = this.placeNewBlock(originalBlock); + } finally { + eventUtils.enable(); + } + + // Close the flyout. + this.targetWorkspace.hideChaff(); + + const newVariables = Variables.getAddedVariables( + this.targetWorkspace, + variablesBeforeCreation, + ); + + if (eventUtils.isEnabled()) { + eventUtils.setGroup(true); + // Fire a VarCreate event for each (if any) new variable created. + for (let i = 0; i < newVariables.length; i++) { + const thisVariable = newVariables[i]; + eventUtils.fire( + new (eventUtils.get(EventType.VAR_CREATE))(thisVariable), + ); + } + + // Block events come after var events, in case they refer to newly created + // variables. + eventUtils.fire(new (eventUtils.get(EventType.BLOCK_CREATE))(newBlock)); + } + if (this.autoClose) { + this.hide(); + } else { + this.filterForCapacity(); + } + return newBlock; + } + + /** + * Initialize the given button: move it to the correct location, + * add listeners, etc. + * + * @param button The button to initialize and place. + * @param x The x position of the cursor during this layout pass. + * @param y The y position of the cursor during this layout pass. + */ + protected initFlyoutButton_(button: FlyoutButton, x: number, y: number) { + const buttonSvg = button.createDom(); + button.moveTo(x, y); + button.show(); + // Clicking on a flyout button or label is a lot like clicking on the + // flyout background. + this.listeners.push( + browserEvents.conditionalBind( + buttonSvg, + 'pointerdown', + this, + this.onMouseDown, + ), + ); + + this.buttons_.push(button); + } + + /** + * Create and place a rectangle corresponding to the given block. + * + * @param block The block to associate the rect to. + * @param x The x position of the cursor during this layout pass. + * @param y The y position of the cursor during this layout pass. + * @param blockHW The height and width of + * the block. + * @param index The index into the mats list where this rect should + * be placed. + * @returns Newly created SVG element for the rectangle behind + * the block. + */ + protected createRect_( + block: BlockSvg, + x: number, + y: number, + blockHW: {height: number; width: number}, + index: number, + ): SVGElement { + // Create an invisible rectangle under the block to act as a button. Just + // using the block as a button is poor, since blocks have holes in them. + const rect = dom.createSvgElement(Svg.RECT, { + 'fill-opacity': 0, + 'x': x, + 'y': y, + 'height': blockHW.height, + 'width': blockHW.width, + }); + (rect as AnyDuringMigration).tooltip = block; + Tooltip.bindMouseEvents(rect); + // Add the rectangles under the blocks, so that the blocks' tooltips work. + this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot()); + + this.rectMap_.set(block, rect); + this.mats[index] = rect; + return rect; + } + + /** + * Move a rectangle to sit exactly behind a block, taking into account tabs, + * hats, and any other protrusions we invent. + * + * @param rect The rectangle to move directly behind the block. + * @param block The block the rectangle should be behind. + */ + protected moveRectToBlock_(rect: SVGElement, block: BlockSvg) { + const blockHW = block.getHeightWidth(); + rect.setAttribute('width', String(blockHW.width)); + rect.setAttribute('height', String(blockHW.height)); + + const blockXY = block.getRelativeToSurfaceXY(); + rect.setAttribute('y', String(blockXY.y)); + rect.setAttribute( + 'x', + String(this.RTL ? blockXY.x - blockHW.width : blockXY.x), + ); + } + + /** + * Filter the blocks on the flyout to disable the ones that are above the + * capacity limit. For instance, if the user may only place two more blocks + * on the workspace, an "a + b" block that has two shadow blocks would be + * disabled. + */ + private filterForCapacity() { + const blocks = this.workspace_.getTopBlocks(false); + for (let i = 0, block; (block = blocks[i]); i++) { + if (!this.permanentlyDisabled.includes(block)) { + const enable = this.targetWorkspace.isCapacityAvailable( + common.getBlockTypeCounts(block), + ); + while (block) { + block.setDisabledReason( + !enable, + WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON, + ); + block = block.getNextBlock(); + } + } + } + } + + /** + * Reflow blocks and their mats. + */ + reflow() { + if (this.reflowWrapper) { + this.workspace_.removeChangeListener(this.reflowWrapper); + } + this.reflowInternal_(); + if (this.reflowWrapper) { + this.workspace_.addChangeListener(this.reflowWrapper); + } + } + + /** + * @returns True if this flyout may be scrolled with a scrollbar or + * by dragging. + * @internal + */ + isScrollable(): boolean { + return this.workspace_.scrollbar + ? this.workspace_.scrollbar.isVisible() + : false; + } + + /** + * Copy a block from the flyout to the workspace and position it correctly. + * + * @param oldBlock The flyout block to copy. + * @returns The new block in the main workspace. + */ + private placeNewBlock(oldBlock: BlockSvg): BlockSvg { + const targetWorkspace = this.targetWorkspace; + const svgRootOld = oldBlock.getSvgRoot(); + if (!svgRootOld) { + throw Error('oldBlock is not rendered'); + } + + // Clone the block. + const json = this.serializeBlock(oldBlock); + // Normally this resizes leading to weird jumps. Save it for terminateDrag. + targetWorkspace.setResizesEnabled(false); + const block = blocks.append(json, targetWorkspace) as BlockSvg; + + this.positionNewBlock(oldBlock, block); + + return block; + } + + /** + * Serialize a block to JSON. + * + * @param block The block to serialize. + * @returns A serialized representation of the block. + */ + protected serializeBlock(block: BlockSvg): blocks.State { + return blocks.save(block) as blocks.State; + } + + /** + * Positions a block on the target workspace. + * + * @param oldBlock The flyout block being copied. + * @param block The block to posiiton. + */ + private positionNewBlock(oldBlock: BlockSvg, block: BlockSvg) { + const targetWorkspace = this.targetWorkspace; + + // The offset in pixels between the main workspace's origin and the upper + // left corner of the injection div. + const mainOffsetPixels = targetWorkspace.getOriginOffsetInPixels(); + + // The offset in pixels between the flyout workspace's origin and the upper + // left corner of the injection div. + const flyoutOffsetPixels = this.workspace_.getOriginOffsetInPixels(); + + // The position of the old block in flyout workspace coordinates. + const oldBlockPos = oldBlock.getRelativeToSurfaceXY(); + // The position of the old block in pixels relative to the flyout + // workspace's origin. + oldBlockPos.scale(this.workspace_.scale); + + // The position of the old block in pixels relative to the upper left corner + // of the injection div. + const oldBlockOffsetPixels = Coordinate.sum( + flyoutOffsetPixels, + oldBlockPos, + ); + + // The position of the old block in pixels relative to the origin of the + // main workspace. + const finalOffset = Coordinate.difference( + oldBlockOffsetPixels, + mainOffsetPixels, + ); + // The position of the old block in main workspace coordinates. + finalOffset.scale(1 / targetWorkspace.scale); + + // No 'reason' provided since events are disabled. + block.moveTo(new Coordinate(finalOffset.x, finalOffset.y)); + } +} + +/** + * A flyout content item. + */ +export interface FlyoutItem { + type: FlyoutItemType; + button?: FlyoutButton | undefined; + block?: BlockSvg | undefined; +} diff --git a/core/flyout_button.js b/core/flyout_button.js deleted file mode 100644 index 965ce8ef32f..00000000000 --- a/core/flyout_button.js +++ /dev/null @@ -1,246 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Class for a button in the flyout. - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -goog.provide('Blockly.FlyoutButton'); - -goog.require('goog.dom'); -goog.require('goog.math.Coordinate'); - - -/** - * Class for a button in the flyout. - * @param {!Blockly.WorkspaceSvg} workspace The workspace in which to place this - * button. - * @param {!Blockly.WorkspaceSvg} targetWorkspace The flyout's target workspace. - * @param {!Element} xml The XML specifying the label/button. - * @param {boolean} isLabel Whether this button should be styled as a label. - * @constructor - */ -Blockly.FlyoutButton = function(workspace, targetWorkspace, xml, isLabel) { - // Labels behave the same as buttons, but are styled differently. - - /** - * @type {!Blockly.WorkspaceSvg} - * @private - */ - this.workspace_ = workspace; - - /** - * @type {!Blockly.Workspace} - * @private - */ - this.targetWorkspace_ = targetWorkspace; - - /** - * @type {string} - * @private - */ - this.text_ = xml.getAttribute('text'); - - /** - * @type {!goog.math.Coordinate} - * @private - */ - this.position_ = new goog.math.Coordinate(0, 0); - - /** - * Whether this button should be styled as a label. - * @type {boolean} - * @private - */ - this.isLabel_ = isLabel; - - /** - * Function to call when this button is clicked. - * @type {function(!Blockly.FlyoutButton)} - * @private - */ - this.callback_ = null; - - var callbackKey = xml.getAttribute('callbackKey'); - if (this.isLabel_ && callbackKey) { - console.warn('Labels should not have callbacks. Label text: ' + this.text_); - } else if (!this.isLabel_ && - !(callbackKey && targetWorkspace.getButtonCallback(callbackKey))) { - console.warn('Buttons should have callbacks. Button text: ' + this.text_); - } else { - this.callback_ = targetWorkspace.getButtonCallback(callbackKey); - } - - /** - * If specified, a CSS class to add to this button. - * @type {?string} - * @private - */ - this.cssClass_ = xml.getAttribute('web-class') || null; -}; - -/** - * The margin around the text in the button. - */ -Blockly.FlyoutButton.MARGIN = 5; - -/** - * The width of the button's rect. - * @type {number} - */ -Blockly.FlyoutButton.prototype.width = 0; - -/** - * The height of the button's rect. - * @type {number} - */ -Blockly.FlyoutButton.prototype.height = 0; - -/** - * Opaque data that can be passed to Blockly.unbindEvent_. - * @type {Array.} - * @private - */ -Blockly.FlyoutButton.prototype.onMouseUpWrapper_ = null; - -/** - * Create the button elements. - * @return {!Element} The button's SVG group. - */ -Blockly.FlyoutButton.prototype.createDom = function() { - var cssClass = this.isLabel_ ? 'blocklyFlyoutLabel' : 'blocklyFlyoutButton'; - if (this.cssClass_) { - cssClass += ' ' + this.cssClass_; - } - - this.svgGroup_ = Blockly.utils.createSvgElement('g', {'class': cssClass}, - this.workspace_.getCanvas()); - - if (!this.isLabel_) { - // Shadow rectangle (light source does not mirror in RTL). - var shadow = Blockly.utils.createSvgElement('rect', - {'class': 'blocklyFlyoutButtonShadow', - 'rx': 4, 'ry': 4, 'x': 1, 'y': 1}, - this.svgGroup_); - } - // Background rectangle. - var rect = Blockly.utils.createSvgElement('rect', - {'class': this.isLabel_ ? - 'blocklyFlyoutLabelBackground' : 'blocklyFlyoutButtonBackground', - 'rx': 4, 'ry': 4}, - this.svgGroup_); - - var svgText = Blockly.utils.createSvgElement('text', - {'class': this.isLabel_ ? 'blocklyFlyoutLabelText' : 'blocklyText', - 'x': 0, 'y': 0, 'text-anchor': 'middle'}, - this.svgGroup_); - svgText.textContent = this.text_; - - this.width = svgText.getComputedTextLength() + - 2 * Blockly.FlyoutButton.MARGIN; - this.height = 20; // Can't compute it :( - - if (!this.isLabel_) { - shadow.setAttribute('width', this.width); - shadow.setAttribute('height', this.height); - } - rect.setAttribute('width', this.width); - rect.setAttribute('height', this.height); - - svgText.setAttribute('x', this.width / 2); - svgText.setAttribute('y', this.height - Blockly.FlyoutButton.MARGIN); - - this.updateTransform_(); - - this.mouseUpWrapper_ = Blockly.bindEventWithChecks_(this.svgGroup_, 'mouseup', - this, this.onMouseUp_); - return this.svgGroup_; -}; - -/** - * Correctly position the flyout button and make it visible. - */ -Blockly.FlyoutButton.prototype.show = function() { - this.updateTransform_(); - this.svgGroup_.setAttribute('display', 'block'); -}; - -/** - * Update svg attributes to match internal state. - * @private - */ -Blockly.FlyoutButton.prototype.updateTransform_ = function() { - this.svgGroup_.setAttribute('transform', - 'translate(' + this.position_.x + ',' + this.position_.y + ')'); -}; - -/** - * Move the button to the given x, y coordinates. - * @param {number} x The new x coordinate. - * @param {number} y The new y coordinate. - */ -Blockly.FlyoutButton.prototype.moveTo = function(x, y) { - this.position_.x = x; - this.position_.y = y; - this.updateTransform_(); -}; - -/** - * Get the button's target workspace. - * @return {!Blockly.WorkspaceSvg} The target workspace of the flyout where this - * button resides. - */ -Blockly.FlyoutButton.prototype.getTargetWorkspace = function() { - return this.targetWorkspace_; -}; - -/** - * Dispose of this button. - */ -Blockly.FlyoutButton.prototype.dispose = function() { - if (this.onMouseUpWrapper_) { - Blockly.unbindEvent_(this.onMouseUpWrapper_); - } - if (this.svgGroup_) { - goog.dom.removeNode(this.svgGroup_); - this.svgGroup_ = null; - } - this.workspace_ = null; - this.targetWorkspace_ = null; -}; - -/** - * Do something when the button is clicked. - * @param {!Event} e Mouse up event. - * @private - */ -Blockly.FlyoutButton.prototype.onMouseUp_ = function(e) { - var gesture = this.targetWorkspace_.getGesture(e); - if (gesture) { - gesture.cancel(); - } - - // Call the callback registered to this button. - if (this.callback_) { - this.callback_(this); - } -}; diff --git a/core/flyout_button.ts b/core/flyout_button.ts new file mode 100644 index 00000000000..b03a8d9615c --- /dev/null +++ b/core/flyout_button.ts @@ -0,0 +1,369 @@ +/** + * @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for a button in the flyout. + * + * @class + */ +// Former goog.module ID: Blockly.FlyoutButton + +import type {IASTNodeLocationSvg} from './blockly.js'; +import * as browserEvents from './browser_events.js'; +import * as Css from './css.js'; +import {Coordinate} from './utils/coordinate.js'; +import * as dom from './utils/dom.js'; +import * as parsing from './utils/parsing.js'; +import * as style from './utils/style.js'; +import {Svg} from './utils/svg.js'; +import type * as toolbox from './utils/toolbox.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Class for a button or label in the flyout. + */ +export class FlyoutButton implements IASTNodeLocationSvg { + /** The horizontal margin around the text in the button. */ + static TEXT_MARGIN_X = 5; + + /** The vertical margin around the text in the button. */ + static TEXT_MARGIN_Y = 2; + + /** The radius of the flyout button's borders. */ + static BORDER_RADIUS = 4; + + private readonly text: string; + private readonly position: Coordinate; + private readonly callbackKey: string; + private readonly cssClass: string | null; + + /** Mouse up event data. */ + private onMouseUpWrapper: browserEvents.Data | null = null; + info: toolbox.ButtonOrLabelInfo; + + /** The width of the button's rect. */ + width = 0; + + /** The height of the button's rect. */ + height = 0; + + /** The root SVG group for the button or label. */ + private svgGroup: SVGGElement | null = null; + + /** The SVG element with the text of the label or button. */ + private svgText: SVGTextElement | null = null; + + /** + * Holds the cursors svg element when the cursor is attached to the button. + * This is null if there is no cursor on the button. + */ + cursorSvg: SVGElement | null = null; + + /** + * @param workspace The workspace in which to place this button. + * @param targetWorkspace The flyout's target workspace. + * @param json The JSON specifying the label/button. + * @param isFlyoutLabel Whether this button should be styled as a label. + * @internal + */ + constructor( + private readonly workspace: WorkspaceSvg, + private readonly targetWorkspace: WorkspaceSvg, + json: toolbox.ButtonOrLabelInfo, + private readonly isFlyoutLabel: boolean, + ) { + this.text = json['text']; + + this.position = new Coordinate(0, 0); + + /** The key to the function called when this button is clicked. */ + this.callbackKey = + (json as AnyDuringMigration)[ + 'callbackKey' + ] /* Check the lower case version + too to satisfy IE */ || + (json as AnyDuringMigration)['callbackkey']; + + /** If specified, a CSS class to add to this button. */ + this.cssClass = (json as AnyDuringMigration)['web-class'] || null; + + /** The JSON specifying the label / button. */ + this.info = json; + } + + /** + * Create the button elements. + * + * @returns The button's SVG group. + */ + createDom(): SVGElement { + let cssClass = this.isFlyoutLabel + ? 'blocklyFlyoutLabel' + : 'blocklyFlyoutButton'; + if (this.cssClass) { + cssClass += ' ' + this.cssClass; + } + + this.svgGroup = dom.createSvgElement( + Svg.G, + {'class': cssClass}, + this.workspace.getCanvas(), + ); + + let shadow; + if (!this.isFlyoutLabel) { + // Shadow rectangle (light source does not mirror in RTL). + shadow = dom.createSvgElement( + Svg.RECT, + { + 'class': 'blocklyFlyoutButtonShadow', + 'rx': FlyoutButton.BORDER_RADIUS, + 'ry': FlyoutButton.BORDER_RADIUS, + 'x': 1, + 'y': 1, + }, + this.svgGroup!, + ); + } + // Background rectangle. + const rect = dom.createSvgElement( + Svg.RECT, + { + 'class': this.isFlyoutLabel + ? 'blocklyFlyoutLabelBackground' + : 'blocklyFlyoutButtonBackground', + 'rx': FlyoutButton.BORDER_RADIUS, + 'ry': FlyoutButton.BORDER_RADIUS, + }, + this.svgGroup!, + ); + + const svgText = dom.createSvgElement( + Svg.TEXT, + { + 'class': this.isFlyoutLabel ? 'blocklyFlyoutLabelText' : 'blocklyText', + 'x': 0, + 'y': 0, + 'text-anchor': 'middle', + }, + this.svgGroup!, + ); + let text = parsing.replaceMessageReferences(this.text); + if (this.workspace.RTL) { + // Force text to be RTL by adding an RLM. + text += '\u200F'; + } + svgText.textContent = text; + if (this.isFlyoutLabel) { + this.svgText = svgText; + this.workspace + .getThemeManager() + .subscribe(this.svgText, 'flyoutForegroundColour', 'fill'); + } + + const fontSize = style.getComputedStyle(svgText, 'fontSize'); + const fontWeight = style.getComputedStyle(svgText, 'fontWeight'); + const fontFamily = style.getComputedStyle(svgText, 'fontFamily'); + this.width = dom.getFastTextWidthWithSizeString( + svgText, + fontSize, + fontWeight, + fontFamily, + ); + const fontMetrics = dom.measureFontMetrics( + text, + fontSize, + fontWeight, + fontFamily, + ); + this.height = fontMetrics.height; + + if (!this.isFlyoutLabel) { + this.width += 2 * FlyoutButton.TEXT_MARGIN_X; + this.height += 2 * FlyoutButton.TEXT_MARGIN_Y; + shadow?.setAttribute('width', String(this.width)); + shadow?.setAttribute('height', String(this.height)); + } + rect.setAttribute('width', String(this.width)); + rect.setAttribute('height', String(this.height)); + + svgText.setAttribute('x', String(this.width / 2)); + svgText.setAttribute( + 'y', + String(this.height / 2 - fontMetrics.height / 2 + fontMetrics.baseline), + ); + + this.updateTransform(); + + // AnyDuringMigration because: Argument of type 'SVGGElement | null' is not + // assignable to parameter of type 'EventTarget'. + this.onMouseUpWrapper = browserEvents.conditionalBind( + this.svgGroup as AnyDuringMigration, + 'pointerup', + this, + this.onMouseUp, + ); + return this.svgGroup!; + } + + /** Correctly position the flyout button and make it visible. */ + show() { + this.updateTransform(); + this.svgGroup!.setAttribute('display', 'block'); + } + + /** Update SVG attributes to match internal state. */ + private updateTransform() { + this.svgGroup!.setAttribute( + 'transform', + 'translate(' + this.position.x + ',' + this.position.y + ')', + ); + } + + /** + * Move the button to the given x, y coordinates. + * + * @param x The new x coordinate. + * @param y The new y coordinate. + */ + moveTo(x: number, y: number) { + this.position.x = x; + this.position.y = y; + this.updateTransform(); + } + + /** @returns Whether or not the button is a label. */ + isLabel(): boolean { + return this.isFlyoutLabel; + } + + /** + * Location of the button. + * + * @returns x, y coordinates. + * @internal + */ + getPosition(): Coordinate { + return this.position; + } + + /** @returns Text of the button. */ + getButtonText(): string { + return this.text; + } + + /** + * Get the button's target workspace. + * + * @returns The target workspace of the flyout where this button resides. + */ + getTargetWorkspace(): WorkspaceSvg { + return this.targetWorkspace; + } + + /** + * Get the button's workspace. + * + * @returns The workspace in which to place this button. + */ + getWorkspace(): WorkspaceSvg { + return this.workspace; + } + + /** Dispose of this button. */ + dispose() { + if (this.onMouseUpWrapper) { + browserEvents.unbind(this.onMouseUpWrapper); + } + if (this.svgGroup) { + dom.removeNode(this.svgGroup); + } + if (this.svgText) { + this.workspace.getThemeManager().unsubscribe(this.svgText); + } + } + + /** + * Add the cursor SVG to this buttons's SVG group. + * + * @param cursorSvg The SVG root of the cursor to be added to the button SVG + * group. + */ + setCursorSvg(cursorSvg: SVGElement) { + if (!cursorSvg) { + this.cursorSvg = null; + return; + } + if (this.svgGroup) { + this.svgGroup.appendChild(cursorSvg); + this.cursorSvg = cursorSvg; + } + } + + /** + * Required by IASTNodeLocationSvg, but not used. A marker cannot be set on a + * button. If the 'mark' shortcut is used on a button, its associated callback + * function is triggered. + */ + setMarkerSvg() { + throw new Error('Attempted to set a marker on a button.'); + } + + /** + * Do something when the button is clicked. + * + * @param e Pointer up event. + */ + private onMouseUp(e: PointerEvent) { + const gesture = this.targetWorkspace.getGesture(e); + if (gesture) { + gesture.cancel(); + } + + if (this.isFlyoutLabel && this.callbackKey) { + console.warn( + 'Labels should not have callbacks. Label text: ' + this.text, + ); + } else if ( + !this.isFlyoutLabel && + !( + this.callbackKey && + this.targetWorkspace.getButtonCallback(this.callbackKey) + ) + ) { + console.warn('Buttons should have callbacks. Button text: ' + this.text); + } else if (!this.isFlyoutLabel) { + const callback = this.targetWorkspace.getButtonCallback(this.callbackKey); + if (callback) { + callback(this); + } + } + } +} + +/** CSS for buttons and labels. See css.js for use. */ +Css.register(` +.blocklyFlyoutButton { + fill: #888; + cursor: default; +} + +.blocklyFlyoutButtonShadow { + fill: #666; +} + +.blocklyFlyoutButton:hover { + fill: #aaa; +} + +.blocklyFlyoutLabel { + cursor: default; +} + +.blocklyFlyoutLabelBackground { + opacity: 0; +} +`); diff --git a/core/flyout_dragger.js b/core/flyout_dragger.js deleted file mode 100644 index c3909eaf2f3..00000000000 --- a/core/flyout_dragger.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Methods for dragging a flyout visually. - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -goog.provide('Blockly.FlyoutDragger'); - -goog.require('Blockly.WorkspaceDragger'); - -goog.require('goog.asserts'); -goog.require('goog.math.Coordinate'); - - -/** - * Class for a flyout dragger. It moves a flyout workspace around when it is - * being dragged by a mouse or touch. - * Note that the workspace itself manages whether or not it has a drag surface - * and how to do translations based on that. This simply passes the right - * commands based on events. - * @param {!Blockly.Flyout} flyout The flyout to drag. - * @constructor - */ -Blockly.FlyoutDragger = function(flyout) { - Blockly.FlyoutDragger.superClass_.constructor.call(this, - flyout.getWorkspace()); - - /** - * The scrollbar to update to move the flyout. - * Unlike the main workspace, the flyout has only one scrollbar, in either the - * horizontal or the vertical direction. - * @type {!Blockly.Scrollbar} - * @private - */ - this.scrollbar_ = flyout.scrollbar_; - - /** - * Whether the flyout scrolls horizontally. If false, the flyout scrolls - * vertically. - * @type {boolean} - * @private - */ - this.horizontalLayout_ = flyout.horizontalLayout_; -}; -goog.inherits(Blockly.FlyoutDragger, Blockly.WorkspaceDragger); - -/** - * Move the appropriate scrollbar to drag the flyout. - * Since flyouts only scroll in one direction at a time, this will discard one - * of the calculated values. - * x and y are in pixels. - * @param {number} x The new x position to move the scrollbar to. - * @param {number} y The new y position to move the scrollbar to. - * @private - */ -Blockly.FlyoutDragger.prototype.updateScroll_ = function(x, y) { - // Move the scrollbar and the flyout will scroll automatically. - if (this.horizontalLayout_) { - this.scrollbar_.set(x); - } else { - this.scrollbar_.set(y); - } -}; diff --git a/core/flyout_horizontal.js b/core/flyout_horizontal.js deleted file mode 100644 index 0c1c8e52cb2..00000000000 --- a/core/flyout_horizontal.js +++ /dev/null @@ -1,456 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Horizontal flyout tray containing blocks which may be created. - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -goog.provide('Blockly.HorizontalFlyout'); - -goog.require('Blockly.Block'); -goog.require('Blockly.Events'); -goog.require('Blockly.FlyoutButton'); -goog.require('Blockly.Flyout'); -goog.require('Blockly.WorkspaceSvg'); -goog.require('goog.dom'); -goog.require('goog.events'); -goog.require('goog.math.Rect'); -goog.require('goog.userAgent'); - - -/** - * Class for a flyout. - * @param {!Object} workspaceOptions Dictionary of options for the workspace. - * @extends {Blockly.Flyout} - * @constructor - */ -Blockly.HorizontalFlyout = function(workspaceOptions) { - workspaceOptions.getMetrics = this.getMetrics_.bind(this); - workspaceOptions.setMetrics = this.setMetrics_.bind(this); - - Blockly.HorizontalFlyout.superClass_.constructor.call(this, workspaceOptions); - /** - * Flyout should be laid out horizontally. - * @type {boolean} - * @private - */ - this.horizontalLayout_ = true; -}; -goog.inherits(Blockly.HorizontalFlyout, Blockly.Flyout); - -/** - * Return an object with all the metrics required to size scrollbars for the - * flyout. The following properties are computed: - * .viewHeight: Height of the visible rectangle, - * .viewWidth: Width of the visible rectangle, - * .contentHeight: Height of the contents, - * .contentWidth: Width of the contents, - * .viewTop: Offset of top edge of visible rectangle from parent, - * .contentTop: Offset of the top-most content from the y=0 coordinate, - * .absoluteTop: Top-edge of view. - * .viewLeft: Offset of the left edge of visible rectangle from parent, - * .contentLeft: Offset of the left-most content from the x=0 coordinate, - * .absoluteLeft: Left-edge of view. - * @return {Object} Contains size and position metrics of the flyout. - * @private - */ -Blockly.HorizontalFlyout.prototype.getMetrics_ = function() { - if (!this.isVisible()) { - // Flyout is hidden. - return null; - } - - try { - var optionBox = this.workspace_.getCanvas().getBBox(); - } catch (e) { - // Firefox has trouble with hidden elements (Bug 528969). - var optionBox = {height: 0, y: 0, width: 0, x: 0}; - } - - var absoluteTop = this.SCROLLBAR_PADDING; - var absoluteLeft = this.SCROLLBAR_PADDING; - if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { - absoluteTop = 0; - } - var viewHeight = this.height_; - if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) { - viewHeight -= this.SCROLLBAR_PADDING; - } - var viewWidth = this.width_ - 2 * this.SCROLLBAR_PADDING; - - var metrics = { - viewHeight: viewHeight, - viewWidth: viewWidth, - contentHeight: (optionBox.height + 2 * this.MARGIN) * this.workspace_.scale, - contentWidth: (optionBox.width + 2 * this.MARGIN) * this.workspace_.scale, - viewTop: -this.workspace_.scrollY, - viewLeft: -this.workspace_.scrollX, - contentTop: optionBox.y, - contentLeft: optionBox.x, - absoluteTop: absoluteTop, - absoluteLeft: absoluteLeft - }; - return metrics; -}; - -/** - * Sets the translation of the flyout to match the scrollbars. - * @param {!Object} xyRatio Contains a y property which is a float - * between 0 and 1 specifying the degree of scrolling and a - * similar x property. - * @private - */ -Blockly.HorizontalFlyout.prototype.setMetrics_ = function(xyRatio) { - var metrics = this.getMetrics_(); - // This is a fix to an apparent race condition. - if (!metrics) { - return; - } - - if (goog.isNumber(xyRatio.x)) { - this.workspace_.scrollX = -metrics.contentWidth * xyRatio.x; - } - - this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft, - this.workspace_.scrollY + metrics.absoluteTop); -}; - -/** - * Move the flyout to the edge of the workspace. - */ -Blockly.HorizontalFlyout.prototype.position = function() { - if (!this.isVisible()) { - return; - } - var targetWorkspaceMetrics = this.targetWorkspace_.getMetrics(); - if (!targetWorkspaceMetrics) { - // Hidden components will return null. - return; - } - // Record the width for Blockly.Flyout.getMetrics_. - this.width_ = targetWorkspaceMetrics.viewWidth; - - var edgeWidth = targetWorkspaceMetrics.viewWidth - 2 * this.CORNER_RADIUS; - var edgeHeight = this.height_ - this.CORNER_RADIUS; - this.setBackgroundPath_(edgeWidth, edgeHeight); - - var x = targetWorkspaceMetrics.absoluteLeft; - var y = targetWorkspaceMetrics.absoluteTop; - if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { - y += (targetWorkspaceMetrics.viewHeight - this.height_); - } - this.positionAt_(this.width_, this.height_, x, y); -}; - -/** - * Create and set the path for the visible boundaries of the flyout. - * @param {number} width The width of the flyout, not including the - * rounded corners. - * @param {number} height The height of the flyout, not including - * rounded corners. - * @private - */ -Blockly.HorizontalFlyout.prototype.setBackgroundPath_ = function(width, - height) { - var atTop = this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP; - // Start at top left. - var path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)]; - - if (atTop) { - // Top. - path.push('h', width + 2 * this.CORNER_RADIUS); - // Right. - path.push('v', height); - // Bottom. - path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, - -this.CORNER_RADIUS, this.CORNER_RADIUS); - path.push('h', -1 * width); - // Left. - path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, - -this.CORNER_RADIUS, -this.CORNER_RADIUS); - path.push('z'); - } else { - // Top. - path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, - this.CORNER_RADIUS, -this.CORNER_RADIUS); - path.push('h', width); - // Right. - path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, - this.CORNER_RADIUS, this.CORNER_RADIUS); - path.push('v', height); - // Bottom. - path.push('h', -width - 2 * this.CORNER_RADIUS); - // Left. - path.push('z'); - } - this.svgBackground_.setAttribute('d', path.join(' ')); -}; - -/** - * Scroll the flyout to the top. - */ -Blockly.HorizontalFlyout.prototype.scrollToStart = function() { - this.scrollbar_.set(this.RTL ? Infinity : 0); -}; - -/** - * Scroll the flyout. - * @param {!Event} e Mouse wheel scroll event. - * @private - */ -Blockly.HorizontalFlyout.prototype.wheel_ = function(e) { - var delta = e.deltaX; - - if (delta) { - if (goog.userAgent.GECKO) { - // Firefox's deltas are a tenth that of Chrome/Safari. - delta *= 10; - } - // TODO: #1093 - var metrics = this.getMetrics_(); - var pos = metrics.viewLeft + delta; - var limit = metrics.contentWidth - metrics.viewWidth; - pos = Math.min(pos, limit); - pos = Math.max(pos, 0); - this.scrollbar_.set(pos); - // When the flyout moves from a wheel event, hide WidgetDiv. - Blockly.WidgetDiv.hide(); - } - - // Don't scroll the page. - e.preventDefault(); - // Don't propagate mousewheel event (zooming). - e.stopPropagation(); -}; - -/** - * Lay out the blocks in the flyout. - * @param {!Array.} contents The blocks and buttons to lay out. - * @param {!Array.} gaps The visible gaps between blocks. - * @private - */ -Blockly.HorizontalFlyout.prototype.layout_ = function(contents, gaps) { - this.workspace_.scale = this.targetWorkspace_.scale; - var margin = this.MARGIN; - var cursorX = this.RTL ? margin : margin + Blockly.BlockSvg.TAB_WIDTH; - var cursorY = margin; - if (this.RTL) { - contents = contents.reverse(); - } - - for (var i = 0, item; item = contents[i]; i++) { - if (item.type == 'block') { - var block = item.block; - var allBlocks = block.getDescendants(); - for (var j = 0, child; child = allBlocks[j]; j++) { - // Mark blocks as being inside a flyout. This is used to detect and - // prevent the closure of the flyout if the user right-clicks on such a - // block. - child.isInFlyout = true; - } - block.render(); - var root = block.getSvgRoot(); - var blockHW = block.getHeightWidth(); - - // Figure out where to place the block. - var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; - if (this.RTL) { - var moveX = cursorX + blockHW.width; - } else { - var moveX = cursorX + tab; - } - block.moveBy(moveX, cursorY); - - var rect = this.createRect_(block, moveX, cursorY, blockHW, i); - cursorX += (blockHW.width + gaps[i]); - - this.addBlockListeners_(root, block, rect); - } else if (item.type == 'button') { - this.initFlyoutButton_(item.button, cursorX, cursorY); - cursorX += (item.button.width + gaps[i]); - } - } -}; - -/** - * Determine if a drag delta is toward the workspace, based on the position - * and orientation of the flyout. This is used in determineDragIntention_ to - * determine if a new block should be created or if the flyout should scroll. - * @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has - * moved from the position at mouse down, in pixel units. - * @return {boolean} true if the drag is toward the workspace. - * @package - */ -Blockly.HorizontalFlyout.prototype.isDragTowardWorkspace = function( - currentDragDeltaXY) { - var dx = currentDragDeltaXY.x; - var dy = currentDragDeltaXY.y; - // Direction goes from -180 to 180, with 0 toward the right and 90 on top. - var dragDirection = Math.atan2(dy, dx) / Math.PI * 180; - - var range = this.dragAngleRange_; - if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) { - // Horizontal at top. - if (dragDirection < 90 + range && dragDirection > 90 - range) { - return true; - } - } else { - // Horizontal at bottom. - if (dragDirection > -90 - range && dragDirection < -90 + range) { - return true; - } - } - return false; -}; - -/** - * Copy a block from the flyout to the workspace and position it correctly. - * @param {!Blockly.Block} originBlock The flyout block to copy.. - * @return {!Blockly.Block} The new block in the main workspace. - * @private - */ -Blockly.HorizontalFlyout.prototype.placeNewBlock_ = function(originBlock) { - var targetWorkspace = this.targetWorkspace_; - var svgRootOld = originBlock.getSvgRoot(); - if (!svgRootOld) { - throw 'originBlock is not rendered.'; - } - // Figure out where the original block is on the screen, relative to the upper - // left corner of the main workspace. - if (targetWorkspace.isMutator) { - var xyOld = this.workspace_.getSvgXY(/** @type {!Element} */ (svgRootOld)); - } else { - var xyOld = Blockly.utils.getInjectionDivXY_(svgRootOld); - } - - // Take into account that the flyout might have been scrolled horizontally - // (separately from the main workspace). - // Generally a no-op in vertical mode but likely to happen in horizontal - // mode. - var scrollX = this.workspace_.scrollX; - var scale = this.workspace_.scale; - xyOld.x += scrollX / scale - scrollX; - - // Take into account that the flyout might have been scrolled vertically - // (separately from the main workspace). - // Generally a no-op in horizontal mode but likely to happen in vertical - // mode. - var scrollY = this.workspace_.scrollY; - scale = this.workspace_.scale; - xyOld.y += scrollY / scale - scrollY; - // If the flyout is on the bottom, (0, 0) in the flyout is offset to be below - // (0, 0) in the main workspace. Add an offset to take that into account. - if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { - scrollY = targetWorkspace.getMetrics().viewHeight - this.height_; - scale = targetWorkspace.scale; - xyOld.y += scrollY / scale - scrollY; - } - - // Create the new block by cloning the block in the flyout (via XML). - var xml = Blockly.Xml.blockToDom(originBlock); - var block = Blockly.Xml.domToBlock(xml, targetWorkspace); - var svgRootNew = block.getSvgRoot(); - if (!svgRootNew) { - throw 'block is not rendered.'; - } - // Figure out where the new block got placed on the screen, relative to the - // upper left corner of the workspace. This may not be the same as the - // original block because the flyout's origin may not be the same as the - // main workspace's origin. - if (targetWorkspace.isMutator) { - var xyNew = targetWorkspace.getSvgXY(/* @type {!Element} */(svgRootNew)); - } else { - var xyNew = Blockly.utils.getInjectionDivXY_(svgRootNew); - } - - // Scale the scroll (getSvgXY_ did not do this). - xyNew.x += - targetWorkspace.scrollX / targetWorkspace.scale - targetWorkspace.scrollX; - xyNew.y += - targetWorkspace.scrollY / targetWorkspace.scale - targetWorkspace.scrollY; - // If the flyout is collapsible and the workspace can't be scrolled. - if (targetWorkspace.toolbox_ && !targetWorkspace.scrollbar) { - xyNew.x += targetWorkspace.toolbox_.getWidth() / targetWorkspace.scale; - xyNew.y += targetWorkspace.toolbox_.getHeight() / targetWorkspace.scale; - } - - // Move the new block to where the old block is. - block.moveBy(xyOld.x - xyNew.x, xyOld.y - xyNew.y); - return block; -}; - -/** - * Return the deletion rectangle for this flyout in viewport coordinates. - * @return {goog.math.Rect} Rectangle in which to delete. - */ -Blockly.HorizontalFlyout.prototype.getClientRect = function() { - if (!this.svgGroup_) { - return null; - } - - var flyoutRect = this.svgGroup_.getBoundingClientRect(); - // BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout - // area are still deleted. Must be larger than the largest screen size, - // but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE). - var BIG_NUM = 1000000000; - var y = flyoutRect.top; - var height = flyoutRect.height; - - if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) { - return new goog.math.Rect(-BIG_NUM, y - BIG_NUM, BIG_NUM * 2, - BIG_NUM + height); - } else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { - return new goog.math.Rect(-BIG_NUM, y, BIG_NUM * 2, - BIG_NUM + height); - } - // TODO: Else throw error (should never happen). -}; - -/** - * Compute height of flyout. Position button under each block. - * For RTL: Lay out the blocks right-aligned. - * @param {!Array} blocks The blocks to reflow. - * @private - */ -Blockly.HorizontalFlyout.prototype.reflowInternal_ = function(blocks) { - this.workspace_.scale = this.targetWorkspace_.scale; - var flyoutHeight = 0; - for (var i = 0, block; block = blocks[i]; i++) { - flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height); - } - flyoutHeight += this.MARGIN * 1.5; - flyoutHeight *= this.workspace_.scale; - flyoutHeight += Blockly.Scrollbar.scrollbarThickness; - - if (this.height_ != flyoutHeight) { - for (var i = 0, block; block = blocks[i]; i++) { - if (block.flyoutRect_) { - this.moveRectToBlock_(block.flyoutRect_, block); - } - } - // Record the height for .getMetrics_ and .position. - this.height_ = flyoutHeight; - // Call this since it is possible the trash and zoom buttons need - // to move. e.g. on a bottom positioned flyout when zoom is clicked. - this.targetWorkspace_.resize(); - } -}; diff --git a/core/flyout_horizontal.ts b/core/flyout_horizontal.ts new file mode 100644 index 00000000000..6e77636e86b --- /dev/null +++ b/core/flyout_horizontal.ts @@ -0,0 +1,417 @@ +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Horizontal flyout tray containing blocks which may be created. + * + * @class + */ +// Former goog.module ID: Blockly.HorizontalFlyout + +import * as browserEvents from './browser_events.js'; +import * as dropDownDiv from './dropdowndiv.js'; +import {Flyout, FlyoutItem} from './flyout_base.js'; +import type {FlyoutButton} from './flyout_button.js'; +import type {Options} from './options.js'; +import * as registry from './registry.js'; +import {Scrollbar} from './scrollbar.js'; +import type {Coordinate} from './utils/coordinate.js'; +import {Rect} from './utils/rect.js'; +import * as toolbox from './utils/toolbox.js'; +import * as WidgetDiv from './widgetdiv.js'; + +/** + * Class for a flyout. + */ +export class HorizontalFlyout extends Flyout { + override horizontalLayout = true; + + /** @param workspaceOptions Dictionary of options for the workspace. */ + constructor(workspaceOptions: Options) { + super(workspaceOptions); + } + + /** + * Sets the translation of the flyout to match the scrollbars. + * + * @param xyRatio Contains a y property which is a float between 0 and 1 + * specifying the degree of scrolling and a similar x property. + */ + protected override setMetrics_(xyRatio: {x: number; y: number}) { + if (!this.isVisible()) { + return; + } + + const metricsManager = this.workspace_.getMetricsManager(); + const scrollMetrics = metricsManager.getScrollMetrics(); + const viewMetrics = metricsManager.getViewMetrics(); + const absoluteMetrics = metricsManager.getAbsoluteMetrics(); + + if (typeof xyRatio.x === 'number') { + this.workspace_.scrollX = -( + scrollMetrics.left + + (scrollMetrics.width - viewMetrics.width) * xyRatio.x + ); + } + + this.workspace_.translate( + this.workspace_.scrollX + absoluteMetrics.left, + this.workspace_.scrollY + absoluteMetrics.top, + ); + } + + /** + * Calculates the x coordinate for the flyout position. + * + * @returns X coordinate. + */ + override getX(): number { + // X is always 0 since this is a horizontal flyout. + return 0; + } + + /** + * Calculates the y coordinate for the flyout position. + * + * @returns Y coordinate. + */ + override getY(): number { + if (!this.isVisible()) { + return 0; + } + const metricsManager = this.targetWorkspace!.getMetricsManager(); + const absoluteMetrics = metricsManager.getAbsoluteMetrics(); + const viewMetrics = metricsManager.getViewMetrics(); + const toolboxMetrics = metricsManager.getToolboxMetrics(); + + let y = 0; + const atTop = this.toolboxPosition_ === toolbox.Position.TOP; + // If this flyout is not the trashcan flyout (e.g. toolbox or mutator). + // Trashcan flyout is opposite the main flyout. + if (this.targetWorkspace!.toolboxPosition === this.toolboxPosition_) { + // If there is a category toolbox. + // Simple (flyout-only) toolbox. + if (this.targetWorkspace!.getToolbox()) { + if (atTop) { + y = toolboxMetrics.height; + } else { + y = viewMetrics.height - this.height_; + } + } else { + if (atTop) { + y = 0; + } else { + // The simple flyout does not cover the workspace. + y = viewMetrics.height; + } + } + } else { + if (atTop) { + y = 0; + } else { + // Because the anchor point of the flyout is on the top, but we want + // to align the bottom edge of the flyout with the bottom edge of the + // blocklyDiv, we calculate the full height of the div minus the height + // of the flyout. + y = viewMetrics.height + absoluteMetrics.top - this.height_; + } + } + + return y; + } + + /** Move the flyout to the edge of the workspace. */ + override position() { + if (!this.isVisible() || !this.targetWorkspace!.isVisible()) { + return; + } + const metricsManager = this.targetWorkspace!.getMetricsManager(); + const targetWorkspaceViewMetrics = metricsManager.getViewMetrics(); + this.width_ = targetWorkspaceViewMetrics.width; + + const edgeWidth = targetWorkspaceViewMetrics.width - 2 * this.CORNER_RADIUS; + const edgeHeight = this.height_ - this.CORNER_RADIUS; + this.setBackgroundPath(edgeWidth, edgeHeight); + + const x = this.getX(); + const y = this.getY(); + + this.positionAt_(this.width_, this.height_, x, y); + } + + /** + * Create and set the path for the visible boundaries of the flyout. + * + * @param width The width of the flyout, not including the rounded corners. + * @param height The height of the flyout, not including rounded corners. + */ + private setBackgroundPath(width: number, height: number) { + const atTop = this.toolboxPosition_ === toolbox.Position.TOP; + // Start at top left. + const path: (string | number)[] = [ + 'M 0,' + (atTop ? 0 : this.CORNER_RADIUS), + ]; + + if (atTop) { + // Top. + path.push('h', width + 2 * this.CORNER_RADIUS); + // Right. + path.push('v', height); + // Bottom. + path.push( + 'a', + this.CORNER_RADIUS, + this.CORNER_RADIUS, + 0, + 0, + 1, + -this.CORNER_RADIUS, + this.CORNER_RADIUS, + ); + path.push('h', -width); + // Left. + path.push( + 'a', + this.CORNER_RADIUS, + this.CORNER_RADIUS, + 0, + 0, + 1, + -this.CORNER_RADIUS, + -this.CORNER_RADIUS, + ); + path.push('z'); + } else { + // Top. + path.push( + 'a', + this.CORNER_RADIUS, + this.CORNER_RADIUS, + 0, + 0, + 1, + this.CORNER_RADIUS, + -this.CORNER_RADIUS, + ); + path.push('h', width); + // Right. + path.push( + 'a', + this.CORNER_RADIUS, + this.CORNER_RADIUS, + 0, + 0, + 1, + this.CORNER_RADIUS, + this.CORNER_RADIUS, + ); + path.push('v', height); + // Bottom. + path.push('h', -width - 2 * this.CORNER_RADIUS); + // Left. + path.push('z'); + } + this.svgBackground_!.setAttribute('d', path.join(' ')); + } + + /** Scroll the flyout to the top. */ + override scrollToStart() { + this.workspace_.scrollbar?.setX(this.RTL ? Infinity : 0); + } + + /** + * Scroll the flyout. + * + * @param e Mouse wheel scroll event. + */ + protected override wheel_(e: WheelEvent) { + const scrollDelta = browserEvents.getScrollDeltaPixels(e); + const delta = scrollDelta.x || scrollDelta.y; + + if (delta) { + const metricsManager = this.workspace_.getMetricsManager(); + const scrollMetrics = metricsManager.getScrollMetrics(); + const viewMetrics = metricsManager.getViewMetrics(); + + const pos = viewMetrics.left - scrollMetrics.left + delta; + this.workspace_.scrollbar?.setX(pos); + // When the flyout moves from a wheel event, hide WidgetDiv and + // dropDownDiv. + WidgetDiv.hideIfOwnerIsInWorkspace(this.workspace_); + dropDownDiv.hideWithoutAnimation(); + } + // Don't scroll the page. + e.preventDefault(); + // Don't propagate mousewheel event (zooming). + e.stopPropagation(); + } + + /** + * Lay out the blocks in the flyout. + * + * @param contents The blocks and buttons to lay out. + * @param gaps The visible gaps between blocks. + */ + protected override layout_(contents: FlyoutItem[], gaps: number[]) { + this.workspace_.scale = this.targetWorkspace!.scale; + const margin = this.MARGIN; + let cursorX = margin + this.tabWidth_; + const cursorY = margin; + if (this.RTL) { + contents = contents.reverse(); + } + + for (let i = 0, item; (item = contents[i]); i++) { + if (item.type === 'block') { + const block = item.block; + + if (block === undefined || block === null) { + continue; + } + + const allBlocks = block.getDescendants(false); + + for (let j = 0, child; (child = allBlocks[j]); j++) { + // Mark blocks as being inside a flyout. This is used to detect and + // prevent the closure of the flyout if the user right-clicks on such + // a block. + child.isInFlyout = true; + } + const root = block.getSvgRoot(); + const blockHW = block.getHeightWidth(); + // Figure out where to place the block. + const tab = block.outputConnection ? this.tabWidth_ : 0; + let moveX; + if (this.RTL) { + moveX = cursorX + blockHW.width; + } else { + moveX = cursorX - tab; + } + block.moveBy(moveX, cursorY); + + const rect = this.createRect_(block, moveX, cursorY, blockHW, i); + cursorX += blockHW.width + gaps[i]; + + this.addBlockListeners_(root, block, rect); + } else if (item.type === 'button') { + const button = item.button as FlyoutButton; + this.initFlyoutButton_(button, cursorX, cursorY); + cursorX += button.width + gaps[i]; + } + } + } + + /** + * Determine if a drag delta is toward the workspace, based on the position + * and orientation of the flyout. This is used in determineDragIntention_ to + * determine if a new block should be created or if the flyout should scroll. + * + * @param currentDragDeltaXY How far the pointer has moved from the position + * at mouse down, in pixel units. + * @returns True if the drag is toward the workspace. + */ + override isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean { + const dx = currentDragDeltaXY.x; + const dy = currentDragDeltaXY.y; + // Direction goes from -180 to 180, with 0 toward the right and 90 on top. + const dragDirection = (Math.atan2(dy, dx) / Math.PI) * 180; + + const range = this.dragAngleRange_; + // Check for up or down dragging. + if ( + (dragDirection < 90 + range && dragDirection > 90 - range) || + (dragDirection > -90 - range && dragDirection < -90 + range) + ) { + return true; + } + return false; + } + + /** + * Returns the bounding rectangle of the drag target area in pixel units + * relative to viewport. + * + * @returns The component's bounding box. Null if drag target area should be + * ignored. + */ + override getClientRect(): Rect | null { + if (!this.svgGroup_ || this.autoClose || !this.isVisible()) { + // The bounding rectangle won't compute correctly if the flyout is closed + // and auto-close flyouts aren't valid drag targets (or delete areas). + return null; + } + + const flyoutRect = this.svgGroup_.getBoundingClientRect(); + // BIG_NUM is offscreen padding so that blocks dragged beyond the shown + // flyout area are still deleted. Must be larger than the largest screen + // size, but be smaller than half Number.MAX_SAFE_INTEGER (not available on + // IE). + const BIG_NUM = 1000000000; + const top = flyoutRect.top; + + if (this.toolboxPosition_ === toolbox.Position.TOP) { + const height = flyoutRect.height; + return new Rect(-BIG_NUM, top + height, -BIG_NUM, BIG_NUM); + } else { + // Bottom. + return new Rect(top, BIG_NUM, -BIG_NUM, BIG_NUM); + } + } + + /** + * Compute height of flyout. toolbox.Position mat under each block. + * For RTL: Lay out the blocks right-aligned. + */ + protected override reflowInternal_() { + this.workspace_.scale = this.getFlyoutScale(); + let flyoutHeight = 0; + const blocks = this.workspace_.getTopBlocks(false); + for (let i = 0, block; (block = blocks[i]); i++) { + flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height); + } + const buttons = this.buttons_; + for (let i = 0, button; (button = buttons[i]); i++) { + flyoutHeight = Math.max(flyoutHeight, button.height); + } + flyoutHeight += this.MARGIN * 1.5; + flyoutHeight *= this.workspace_.scale; + flyoutHeight += Scrollbar.scrollbarThickness; + + if (this.height_ !== flyoutHeight) { + for (let i = 0, block; (block = blocks[i]); i++) { + if (this.rectMap_.has(block)) { + this.moveRectToBlock_(this.rectMap_.get(block)!, block); + } + } + + // TODO(#7689): Remove this. + // Workspace with no scrollbars where this is permanently open on the top. + // If scrollbars exist they properly update the metrics. + if ( + !this.targetWorkspace.scrollbar && + !this.autoClose && + this.targetWorkspace.getFlyout() === this && + this.toolboxPosition_ === toolbox.Position.TOP + ) { + this.targetWorkspace.translate( + this.targetWorkspace.scrollX, + this.targetWorkspace.scrollY + flyoutHeight, + ); + } + + this.height_ = flyoutHeight; + this.position(); + this.targetWorkspace.resizeContents(); + this.targetWorkspace.recordDragTargets(); + } + } +} + +registry.register( + registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX, + registry.DEFAULT, + HorizontalFlyout, +); diff --git a/core/flyout_metrics_manager.ts b/core/flyout_metrics_manager.ts new file mode 100644 index 00000000000..00f675caafa --- /dev/null +++ b/core/flyout_metrics_manager.ts @@ -0,0 +1,90 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Calculates and reports flyout workspace metrics. + * + * @class + */ +// Former goog.module ID: Blockly.FlyoutMetricsManager + +import type {IFlyout} from './interfaces/i_flyout.js'; +import {ContainerRegion, MetricsManager} from './metrics_manager.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Calculates metrics for a flyout's workspace. + * The metrics are mainly used to size scrollbars for the flyout. + */ +export class FlyoutMetricsManager extends MetricsManager { + /** The flyout that owns the workspace to calculate metrics for. */ + protected flyout_: IFlyout; + + /** + * @param workspace The flyout's workspace. + * @param flyout The flyout. + */ + constructor(workspace: WorkspaceSvg, flyout: IFlyout) { + super(workspace); + this.flyout_ = flyout; + } + + /** + * Gets the bounding box of the blocks on the flyout's workspace. + * This is in workspace coordinates. + * + * @returns The bounding box of the blocks on the workspace. + */ + private getBoundingBox(): + | SVGRect + | {height: number; y: number; width: number; x: number} { + let blockBoundingBox; + try { + blockBoundingBox = this.workspace_.getCanvas().getBBox(); + } catch { + // Firefox has trouble with hidden elements (Bug 528969). + // 2021 Update: It looks like this was fixed around Firefox 77 released in + // 2020. + blockBoundingBox = {height: 0, y: 0, width: 0, x: 0}; + } + return blockBoundingBox; + } + + override getContentMetrics(opt_getWorkspaceCoordinates?: boolean) { + // The bounding box is in workspace coordinates. + const blockBoundingBox = this.getBoundingBox(); + const scale = opt_getWorkspaceCoordinates ? 1 : this.workspace_.scale; + + return { + height: blockBoundingBox.height * scale, + width: blockBoundingBox.width * scale, + top: blockBoundingBox.y * scale, + left: blockBoundingBox.x * scale, + }; + } + + override getScrollMetrics( + opt_getWorkspaceCoordinates?: boolean, + opt_viewMetrics?: ContainerRegion, + opt_contentMetrics?: ContainerRegion, + ) { + const contentMetrics = opt_contentMetrics || this.getContentMetrics(); + const margin = this.flyout_.MARGIN * this.workspace_.scale; + const scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1; + + // The left padding isn't just the margin. Some blocks are also offset by + // tabWidth so that value and statement blocks line up. + // The contentMetrics.left value is equivalent to the variable left padding. + const leftPadding = contentMetrics.left; + + return { + height: (contentMetrics.height + 2 * margin) / scale, + width: (contentMetrics.width + leftPadding + margin) / scale, + top: 0, + left: 0, + }; + } +} diff --git a/core/flyout_vertical.js b/core/flyout_vertical.js deleted file mode 100644 index f1595a0a33a..00000000000 --- a/core/flyout_vertical.js +++ /dev/null @@ -1,452 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Layout code for a vertical variant of the flyout. - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -goog.provide('Blockly.VerticalFlyout'); - -goog.require('Blockly.Block'); -goog.require('Blockly.Events'); -goog.require('Blockly.Flyout'); -goog.require('Blockly.FlyoutButton'); -goog.require('Blockly.utils'); -goog.require('Blockly.WorkspaceSvg'); -goog.require('goog.dom'); -goog.require('goog.events'); -goog.require('goog.math.Rect'); -goog.require('goog.userAgent'); - - -/** - * Class for a flyout. - * @param {!Object} workspaceOptions Dictionary of options for the workspace. - * @extends {Blockly.Flyout} - * @constructor - */ -Blockly.VerticalFlyout = function(workspaceOptions) { - workspaceOptions.getMetrics = this.getMetrics_.bind(this); - workspaceOptions.setMetrics = this.setMetrics_.bind(this); - - Blockly.VerticalFlyout.superClass_.constructor.call(this, workspaceOptions); - /** - * Flyout should be laid out vertically. - * @type {boolean} - * @private - */ - this.horizontalLayout_ = false; -}; -goog.inherits(Blockly.VerticalFlyout, Blockly.Flyout); - -/** - * Return an object with all the metrics required to size scrollbars for the - * flyout. The following properties are computed: - * .viewHeight: Height of the visible rectangle, - * .viewWidth: Width of the visible rectangle, - * .contentHeight: Height of the contents, - * .contentWidth: Width of the contents, - * .viewTop: Offset of top edge of visible rectangle from parent, - * .contentTop: Offset of the top-most content from the y=0 coordinate, - * .absoluteTop: Top-edge of view. - * .viewLeft: Offset of the left edge of visible rectangle from parent, - * .contentLeft: Offset of the left-most content from the x=0 coordinate, - * .absoluteLeft: Left-edge of view. - * @return {Object} Contains size and position metrics of the flyout. - * @private - */ -Blockly.VerticalFlyout.prototype.getMetrics_ = function() { - if (!this.isVisible()) { - // Flyout is hidden. - return null; - } - - try { - var optionBox = this.workspace_.getCanvas().getBBox(); - } catch (e) { - // Firefox has trouble with hidden elements (Bug 528969). - var optionBox = {height: 0, y: 0, width: 0, x: 0}; - } - - // Padding for the end of the scrollbar. - var absoluteTop = this.SCROLLBAR_PADDING; - var absoluteLeft = 0; - - var viewHeight = this.height_ - 2 * this.SCROLLBAR_PADDING; - var viewWidth = this.width_; - if (!this.RTL) { - viewWidth -= this.SCROLLBAR_PADDING; - } - - var metrics = { - viewHeight: viewHeight, - viewWidth: viewWidth, - contentHeight: optionBox.height * this.workspace_.scale + 2 * this.MARGIN, - contentWidth: optionBox.width * this.workspace_.scale + 2 * this.MARGIN, - viewTop: -this.workspace_.scrollY + optionBox.y, - viewLeft: -this.workspace_.scrollX, - contentTop: optionBox.y, - contentLeft: optionBox.x, - absoluteTop: absoluteTop, - absoluteLeft: absoluteLeft - }; - return metrics; -}; - -/** - * Sets the translation of the flyout to match the scrollbars. - * @param {!Object} xyRatio Contains a y property which is a float - * between 0 and 1 specifying the degree of scrolling and a - * similar x property. - * @private - */ -Blockly.VerticalFlyout.prototype.setMetrics_ = function(xyRatio) { - var metrics = this.getMetrics_(); - // This is a fix to an apparent race condition. - if (!metrics) { - return; - } - if (goog.isNumber(xyRatio.y)) { - this.workspace_.scrollY = -metrics.contentHeight * xyRatio.y; - } - this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft, - this.workspace_.scrollY + metrics.absoluteTop); -}; - -/** - * Move the flyout to the edge of the workspace. - */ -Blockly.VerticalFlyout.prototype.position = function() { - if (!this.isVisible()) { - return; - } - var targetWorkspaceMetrics = this.targetWorkspace_.getMetrics(); - if (!targetWorkspaceMetrics) { - // Hidden components will return null. - return; - } - // Record the height for Blockly.Flyout.getMetrics_ - this.height_ = targetWorkspaceMetrics.viewHeight; - - var edgeWidth = this.width_ - this.CORNER_RADIUS; - var edgeHeight = targetWorkspaceMetrics.viewHeight - 2 * this.CORNER_RADIUS; - this.setBackgroundPath_(edgeWidth, edgeHeight); - - var y = targetWorkspaceMetrics.absoluteTop; - var x = targetWorkspaceMetrics.absoluteLeft; - if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) { - x += (targetWorkspaceMetrics.viewWidth - this.width_); - } - this.positionAt_(this.width_, this.height_, x, y); -}; - -/** - * Create and set the path for the visible boundaries of the flyout. - * @param {number} width The width of the flyout, not including the - * rounded corners. - * @param {number} height The height of the flyout, not including - * rounded corners. - * @private - */ -Blockly.VerticalFlyout.prototype.setBackgroundPath_ = function(width, height) { - var atRight = this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT; - var totalWidth = width + this.CORNER_RADIUS; - - // Decide whether to start on the left or right. - var path = ['M ' + (atRight ? totalWidth : 0) + ',0']; - // Top. - path.push('h', atRight ? -width : width); - // Rounded corner. - path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, - atRight ? 0 : 1, - atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS, - this.CORNER_RADIUS); - // Side closest to workspace. - path.push('v', Math.max(0, height)); - // Rounded corner. - path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, - atRight ? 0 : 1, - atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, - this.CORNER_RADIUS); - // Bottom. - path.push('h', atRight ? width : -width); - path.push('z'); - this.svgBackground_.setAttribute('d', path.join(' ')); -}; - -/** - * Scroll the flyout to the top. - */ -Blockly.VerticalFlyout.prototype.scrollToStart = function() { - this.scrollbar_.set(0); -}; - -/** - * Scroll the flyout. - * @param {!Event} e Mouse wheel scroll event. - * @private - */ -Blockly.VerticalFlyout.prototype.wheel_ = function(e) { - var delta = e.deltaY; - - if (delta) { - if (goog.userAgent.GECKO) { - // Firefox's deltas are a tenth that of Chrome/Safari. - delta *= 10; - } - var metrics = this.getMetrics_(); - var pos = metrics.viewTop + delta; - var limit = metrics.contentHeight - metrics.viewHeight; - pos = Math.min(pos, limit); - pos = Math.max(pos, 0); - this.scrollbar_.set(pos); - // When the flyout moves from a wheel event, hide WidgetDiv. - Blockly.WidgetDiv.hide(); - } - - // Don't scroll the page. - e.preventDefault(); - // Don't propagate mousewheel event (zooming). - e.stopPropagation(); -}; - -/** - * Lay out the blocks in the flyout. - * @param {!Array.} contents The blocks and buttons to lay out. - * @param {!Array.} gaps The visible gaps between blocks. - * @private - */ -Blockly.VerticalFlyout.prototype.layout_ = function(contents, gaps) { - this.workspace_.scale = this.targetWorkspace_.scale; - var margin = this.MARGIN; - var cursorX = this.RTL ? margin : margin + Blockly.BlockSvg.TAB_WIDTH; - var cursorY = margin; - - for (var i = 0, item; item = contents[i]; i++) { - if (item.type == 'block') { - var block = item.block; - var allBlocks = block.getDescendants(); - for (var j = 0, child; child = allBlocks[j]; j++) { - // Mark blocks as being inside a flyout. This is used to detect and - // prevent the closure of the flyout if the user right-clicks on such a - // block. - child.isInFlyout = true; - } - block.render(); - var root = block.getSvgRoot(); - var blockHW = block.getHeightWidth(); - block.moveBy(cursorX, cursorY); - - var rect = this.createRect_(block, - this.RTL ? cursorX - blockHW.width : cursorX, cursorY, blockHW, i); - - this.addBlockListeners_(root, block, rect); - - cursorY += blockHW.height + gaps[i]; - } else if (item.type == 'button') { - this.initFlyoutButton_(item.button, cursorX, cursorY); - cursorY += item.button.height + gaps[i]; - } - } -}; - -/** - * Determine if a drag delta is toward the workspace, based on the position - * and orientation of the flyout. This is used in determineDragIntention_ to - * determine if a new block should be created or if the flyout should scroll. - * @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has - * moved from the position at mouse down, in pixel units. - * @return {boolean} true if the drag is toward the workspace. - * @package - */ -Blockly.VerticalFlyout.prototype.isDragTowardWorkspace = function( - currentDragDeltaXY) { - var dx = currentDragDeltaXY.x; - var dy = currentDragDeltaXY.y; - // Direction goes from -180 to 180, with 0 toward the right and 90 on top. - var dragDirection = Math.atan2(dy, dx) / Math.PI * 180; - - var range = this.dragAngleRange_; - if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) { - // Vertical at left. - if (dragDirection < range && dragDirection > -range) { - return true; - } - } else { - // Vertical at right. - if (dragDirection < -180 + range || dragDirection > 180 - range) { - return true; - } - } - return false; -}; - -/** - * Copy a block from the flyout to the workspace and position it correctly. - * @param {!Blockly.Block} originBlock The flyout block to copy. - * @return {!Blockly.Block} The new block in the main workspace. - * @private - */ -Blockly.VerticalFlyout.prototype.placeNewBlock_ = function(originBlock) { - var targetWorkspace = this.targetWorkspace_; - var svgRootOld = originBlock.getSvgRoot(); - if (!svgRootOld) { - throw 'originBlock is not rendered.'; - } - // Figure out where the original block is on the screen, relative to the upper - // left corner of the main workspace. - if (targetWorkspace.isMutator) { - var xyOld = this.workspace_.getSvgXY(/** @type {!Element} */ (svgRootOld)); - } else { - var xyOld = Blockly.utils.getInjectionDivXY_(svgRootOld); - } - - // Take into account that the flyout might have been scrolled horizontally - // (separately from the main workspace). - // Generally a no-op in vertical mode but likely to happen in horizontal - // mode. - var scrollX = this.workspace_.scrollX; - var scale = this.workspace_.scale; - xyOld.x += scrollX / scale - scrollX; - - var targetMetrics = targetWorkspace.getMetrics(); - - // If the flyout is on the right side, (0, 0) in the flyout is offset to - // the right of (0, 0) in the main workspace. Add an offset to take that - // into account. - var scrollX = 0; - if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) { - scrollX = targetMetrics.viewWidth - this.width_; - // Scale the scroll (getSvgXY_ did not do this). - xyOld.x += scrollX / scale - scrollX; - } - - // Take into account that the flyout might have been scrolled vertically - // (separately from the main workspace). - // Generally a no-op in horizontal mode but likely to happen in vertical - // mode. - var scrollY = this.workspace_.scrollY; - scale = this.workspace_.scale; - xyOld.y += scrollY / scale - scrollY; - - // Create the new block by cloning the block in the flyout (via XML). - var xml = Blockly.Xml.blockToDom(originBlock); - var block = Blockly.Xml.domToBlock(xml, targetWorkspace); - var svgRootNew = block.getSvgRoot(); - if (!svgRootNew) { - throw 'block is not rendered.'; - } - // Figure out where the new block got placed on the screen, relative to the - // upper left corner of the workspace. This may not be the same as the - // original block because the flyout's origin may not be the same as the - // main workspace's origin. - if (targetWorkspace.isMutator) { - var xyNew = targetWorkspace.getSvgXY(/* @type {!Element} */(svgRootNew)); - } else { - var xyNew = Blockly.utils.getInjectionDivXY_(svgRootNew); - } - - // Scale the scroll (getSvgXY_ did not do this). - xyNew.x += - targetWorkspace.scrollX / targetWorkspace.scale - targetWorkspace.scrollX; - xyNew.y += - targetWorkspace.scrollY / targetWorkspace.scale - targetWorkspace.scrollY; - - // If the flyout is collapsible and the workspace can't be scrolled. - if (targetWorkspace.toolbox_ && !targetWorkspace.scrollbar) { - xyNew.x += targetWorkspace.toolbox_.getWidth() / targetWorkspace.scale; - xyNew.y += targetWorkspace.toolbox_.getHeight() / targetWorkspace.scale; - } - - // Move the new block to where the old block is. - block.moveBy(xyOld.x - xyNew.x, xyOld.y - xyNew.y); - return block; -}; - -/** - * Return the deletion rectangle for this flyout in viewport coordinates. - * @return {goog.math.Rect} Rectangle in which to delete. - */ -Blockly.VerticalFlyout.prototype.getClientRect = function() { - if (!this.svgGroup_) { - return null; - } - - var flyoutRect = this.svgGroup_.getBoundingClientRect(); - // BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout - // area are still deleted. Must be larger than the largest screen size, - // but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE). - var BIG_NUM = 1000000000; - var x = flyoutRect.left; - var width = flyoutRect.width; - - if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) { - return new goog.math.Rect(x - BIG_NUM, -BIG_NUM, BIG_NUM + width, - BIG_NUM * 2); - } else { // Right - return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width, BIG_NUM * 2); - } -}; - -/** - * Compute width of flyout. Position button under each block. - * For RTL: Lay out the blocks right-aligned. - * @param {!Array} blocks The blocks to reflow. - * @private - */ -Blockly.VerticalFlyout.prototype.reflowInternal_ = function(blocks) { - this.workspace_.scale = this.targetWorkspace_.scale; - var flyoutWidth = 0; - for (var i = 0, block; block = blocks[i]; i++) { - var width = block.getHeightWidth().width; - if (block.outputConnection) { - width -= Blockly.BlockSvg.TAB_WIDTH; - } - flyoutWidth = Math.max(flyoutWidth, width); - } - for (var i = 0, button; button = this.buttons_[i]; i++) { - flyoutWidth = Math.max(flyoutWidth, button.width); - } - flyoutWidth += this.MARGIN * 1.5 + Blockly.BlockSvg.TAB_WIDTH; - flyoutWidth *= this.workspace_.scale; - flyoutWidth += Blockly.Scrollbar.scrollbarThickness; - - if (this.width_ != flyoutWidth) { - for (var i = 0, block; block = blocks[i]; i++) { - if (this.RTL) { - // With the flyoutWidth known, right-align the blocks. - var oldX = block.getRelativeToSurfaceXY().x; - var newX = flyoutWidth / this.workspace_.scale - this.MARGIN; - newX -= Blockly.BlockSvg.TAB_WIDTH; - block.moveBy(newX - oldX, 0); - } - if (block.flyoutRect_) { - this.moveRectToBlock_(block.flyoutRect_, block); - } - } - // Record the width for .getMetrics_ and .position. - this.width_ = flyoutWidth; - // Call this since it is possible the trash and zoom buttons need - // to move. e.g. on a bottom positioned flyout when zoom is clicked. - this.targetWorkspace_.resize(); - } -}; diff --git a/core/flyout_vertical.ts b/core/flyout_vertical.ts new file mode 100644 index 00000000000..59682a390d2 --- /dev/null +++ b/core/flyout_vertical.ts @@ -0,0 +1,408 @@ +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Layout code for a vertical variant of the flyout. + * + * @class + */ +// Former goog.module ID: Blockly.VerticalFlyout + +import * as browserEvents from './browser_events.js'; +import * as dropDownDiv from './dropdowndiv.js'; +import {Flyout, FlyoutItem} from './flyout_base.js'; +import type {FlyoutButton} from './flyout_button.js'; +import type {Options} from './options.js'; +import * as registry from './registry.js'; +import {Scrollbar} from './scrollbar.js'; +import type {Coordinate} from './utils/coordinate.js'; +import {Rect} from './utils/rect.js'; +import * as toolbox from './utils/toolbox.js'; +import * as WidgetDiv from './widgetdiv.js'; + +/** + * Class for a flyout. + */ +export class VerticalFlyout extends Flyout { + /** The name of the vertical flyout in the registry. */ + static registryName = 'verticalFlyout'; + + /** @param workspaceOptions Dictionary of options for the workspace. */ + constructor(workspaceOptions: Options) { + super(workspaceOptions); + } + + /** + * Sets the translation of the flyout to match the scrollbars. + * + * @param xyRatio Contains a y property which is a float between 0 and 1 + * specifying the degree of scrolling and a similar x property. + */ + protected override setMetrics_(xyRatio: {x: number; y: number}) { + if (!this.isVisible()) { + return; + } + const metricsManager = this.workspace_.getMetricsManager(); + const scrollMetrics = metricsManager.getScrollMetrics(); + const viewMetrics = metricsManager.getViewMetrics(); + const absoluteMetrics = metricsManager.getAbsoluteMetrics(); + + if (typeof xyRatio.y === 'number') { + this.workspace_.scrollY = -( + scrollMetrics.top + + (scrollMetrics.height - viewMetrics.height) * xyRatio.y + ); + } + this.workspace_.translate( + this.workspace_.scrollX + absoluteMetrics.left, + this.workspace_.scrollY + absoluteMetrics.top, + ); + } + + /** + * Calculates the x coordinate for the flyout position. + * + * @returns X coordinate. + */ + override getX(): number { + if (!this.isVisible()) { + return 0; + } + const metricsManager = this.targetWorkspace!.getMetricsManager(); + const absoluteMetrics = metricsManager.getAbsoluteMetrics(); + const viewMetrics = metricsManager.getViewMetrics(); + const toolboxMetrics = metricsManager.getToolboxMetrics(); + let x = 0; + + // If this flyout is not the trashcan flyout (e.g. toolbox or mutator). + // Trashcan flyout is opposite the main flyout. + if (this.targetWorkspace!.toolboxPosition === this.toolboxPosition_) { + // If there is a category toolbox. + // Simple (flyout-only) toolbox. + if (this.targetWorkspace!.getToolbox()) { + if (this.toolboxPosition_ === toolbox.Position.LEFT) { + x = toolboxMetrics.width; + } else { + x = viewMetrics.width - this.width_; + } + } else { + if (this.toolboxPosition_ === toolbox.Position.LEFT) { + x = 0; + } else { + // The simple flyout does not cover the workspace. + x = viewMetrics.width; + } + } + } else { + if (this.toolboxPosition_ === toolbox.Position.LEFT) { + x = 0; + } else { + // Because the anchor point of the flyout is on the left, but we want + // to align the right edge of the flyout with the right edge of the + // blocklyDiv, we calculate the full width of the div minus the width + // of the flyout. + x = viewMetrics.width + absoluteMetrics.left - this.width_; + } + } + + return x; + } + + /** + * Calculates the y coordinate for the flyout position. + * + * @returns Y coordinate. + */ + override getY(): number { + // Y is always 0 since this is a vertical flyout. + return 0; + } + + /** Move the flyout to the edge of the workspace. */ + override position() { + if (!this.isVisible() || !this.targetWorkspace!.isVisible()) { + return; + } + const metricsManager = this.targetWorkspace!.getMetricsManager(); + const targetWorkspaceViewMetrics = metricsManager.getViewMetrics(); + this.height_ = targetWorkspaceViewMetrics.height; + + const edgeWidth = this.width_ - this.CORNER_RADIUS; + const edgeHeight = + targetWorkspaceViewMetrics.height - 2 * this.CORNER_RADIUS; + this.setBackgroundPath(edgeWidth, edgeHeight); + + const x = this.getX(); + const y = this.getY(); + + this.positionAt_(this.width_, this.height_, x, y); + } + + /** + * Create and set the path for the visible boundaries of the flyout. + * + * @param width The width of the flyout, not including the rounded corners. + * @param height The height of the flyout, not including rounded corners. + */ + private setBackgroundPath(width: number, height: number) { + const atRight = this.toolboxPosition_ === toolbox.Position.RIGHT; + const totalWidth = width + this.CORNER_RADIUS; + + // Decide whether to start on the left or right. + const path: Array = [ + 'M ' + (atRight ? totalWidth : 0) + ',0', + ]; + // Top. + path.push('h', atRight ? -width : width); + // Rounded corner. + path.push( + 'a', + this.CORNER_RADIUS, + this.CORNER_RADIUS, + 0, + 0, + atRight ? 0 : 1, + atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS, + this.CORNER_RADIUS, + ); + // Side closest to workspace. + path.push('v', Math.max(0, height)); + // Rounded corner. + path.push( + 'a', + this.CORNER_RADIUS, + this.CORNER_RADIUS, + 0, + 0, + atRight ? 0 : 1, + atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, + this.CORNER_RADIUS, + ); + // Bottom. + path.push('h', atRight ? width : -width); + path.push('z'); + this.svgBackground_!.setAttribute('d', path.join(' ')); + } + + /** Scroll the flyout to the top. */ + override scrollToStart() { + this.workspace_.scrollbar?.setY(0); + } + + /** + * Scroll the flyout. + * + * @param e Mouse wheel scroll event. + */ + protected override wheel_(e: WheelEvent) { + const scrollDelta = browserEvents.getScrollDeltaPixels(e); + + if (scrollDelta.y) { + const metricsManager = this.workspace_.getMetricsManager(); + const scrollMetrics = metricsManager.getScrollMetrics(); + const viewMetrics = metricsManager.getViewMetrics(); + const pos = viewMetrics.top - scrollMetrics.top + scrollDelta.y; + + this.workspace_.scrollbar?.setY(pos); + // When the flyout moves from a wheel event, hide WidgetDiv and + // dropDownDiv. + WidgetDiv.hideIfOwnerIsInWorkspace(this.workspace_); + dropDownDiv.hideWithoutAnimation(); + } + // Don't scroll the page. + e.preventDefault(); + // Don't propagate mousewheel event (zooming). + e.stopPropagation(); + } + + /** + * Lay out the blocks in the flyout. + * + * @param contents The blocks and buttons to lay out. + * @param gaps The visible gaps between blocks. + */ + protected override layout_(contents: FlyoutItem[], gaps: number[]) { + this.workspace_.scale = this.targetWorkspace!.scale; + const margin = this.MARGIN; + const cursorX = this.RTL ? margin : margin + this.tabWidth_; + let cursorY = margin; + + for (let i = 0, item; (item = contents[i]); i++) { + if (item.type === 'block') { + const block = item.block; + if (!block) { + continue; + } + const allBlocks = block.getDescendants(false); + for (let j = 0, child; (child = allBlocks[j]); j++) { + // Mark blocks as being inside a flyout. This is used to detect and + // prevent the closure of the flyout if the user right-clicks on such + // a block. + child.isInFlyout = true; + } + const root = block.getSvgRoot(); + const blockHW = block.getHeightWidth(); + const moveX = block.outputConnection + ? cursorX - this.tabWidth_ + : cursorX; + block.moveBy(moveX, cursorY); + + const rect = this.createRect_( + block, + this.RTL ? moveX - blockHW.width : moveX, + cursorY, + blockHW, + i, + ); + + this.addBlockListeners_(root, block, rect); + + cursorY += blockHW.height + gaps[i]; + } else if (item.type === 'button') { + const button = item.button as FlyoutButton; + this.initFlyoutButton_(button, cursorX, cursorY); + cursorY += button.height + gaps[i]; + } + } + } + + /** + * Determine if a drag delta is toward the workspace, based on the position + * and orientation of the flyout. This is used in determineDragIntention_ to + * determine if a new block should be created or if the flyout should scroll. + * + * @param currentDragDeltaXY How far the pointer has moved from the position + * at mouse down, in pixel units. + * @returns True if the drag is toward the workspace. + */ + override isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean { + const dx = currentDragDeltaXY.x; + const dy = currentDragDeltaXY.y; + // Direction goes from -180 to 180, with 0 toward the right and 90 on top. + const dragDirection = (Math.atan2(dy, dx) / Math.PI) * 180; + + const range = this.dragAngleRange_; + // Check for left or right dragging. + if ( + (dragDirection < range && dragDirection > -range) || + dragDirection < -180 + range || + dragDirection > 180 - range + ) { + return true; + } + return false; + } + + /** + * Returns the bounding rectangle of the drag target area in pixel units + * relative to viewport. + * + * @returns The component's bounding box. Null if drag target area should be + * ignored. + */ + override getClientRect(): Rect | null { + if (!this.svgGroup_ || this.autoClose || !this.isVisible()) { + // The bounding rectangle won't compute correctly if the flyout is closed + // and auto-close flyouts aren't valid drag targets (or delete areas). + return null; + } + + const flyoutRect = this.svgGroup_.getBoundingClientRect(); + // BIG_NUM is offscreen padding so that blocks dragged beyond the shown + // flyout area are still deleted. Must be larger than the largest screen + // size, but be smaller than half Number.MAX_SAFE_INTEGER (not available on + // IE). + const BIG_NUM = 1000000000; + const left = flyoutRect.left; + + if (this.toolboxPosition_ === toolbox.Position.LEFT) { + const width = flyoutRect.width; + return new Rect(-BIG_NUM, BIG_NUM, -BIG_NUM, left + width); + } else { + // Right + return new Rect(-BIG_NUM, BIG_NUM, left, BIG_NUM); + } + } + + /** + * Compute width of flyout. toolbox.Position mat under each block. + * For RTL: Lay out the blocks and buttons to be right-aligned. + */ + protected override reflowInternal_() { + this.workspace_.scale = this.getFlyoutScale(); + let flyoutWidth = 0; + const blocks = this.workspace_.getTopBlocks(false); + for (let i = 0, block; (block = blocks[i]); i++) { + let width = block.getHeightWidth().width; + if (block.outputConnection) { + width -= this.tabWidth_; + } + flyoutWidth = Math.max(flyoutWidth, width); + } + for (let i = 0, button; (button = this.buttons_[i]); i++) { + flyoutWidth = Math.max(flyoutWidth, button.width); + } + flyoutWidth += this.MARGIN * 1.5 + this.tabWidth_; + flyoutWidth *= this.workspace_.scale; + flyoutWidth += Scrollbar.scrollbarThickness; + + if (this.width_ !== flyoutWidth) { + for (let i = 0, block; (block = blocks[i]); i++) { + if (this.RTL) { + // With the flyoutWidth known, right-align the blocks. + const oldX = block.getRelativeToSurfaceXY().x; + let newX = flyoutWidth / this.workspace_.scale - this.MARGIN; + if (!block.outputConnection) { + newX -= this.tabWidth_; + } + block.moveBy(newX - oldX, 0); + } + if (this.rectMap_.has(block)) { + this.moveRectToBlock_(this.rectMap_.get(block)!, block); + } + } + if (this.RTL) { + // With the flyoutWidth known, right-align the buttons. + for (let i = 0, button; (button = this.buttons_[i]); i++) { + const y = button.getPosition().y; + const x = + flyoutWidth / this.workspace_.scale - + button.width - + this.MARGIN - + this.tabWidth_; + button.moveTo(x, y); + } + } + + // TODO(#7689): Remove this. + // Workspace with no scrollbars where this is permanently + // open on the left. + // If scrollbars exist they properly update the metrics. + if ( + !this.targetWorkspace.scrollbar && + !this.autoClose && + this.targetWorkspace.getFlyout() === this && + this.toolboxPosition_ === toolbox.Position.LEFT + ) { + this.targetWorkspace.translate( + this.targetWorkspace.scrollX + flyoutWidth, + this.targetWorkspace.scrollY, + ); + } + + this.width_ = flyoutWidth; + this.position(); + this.targetWorkspace.resizeContents(); + this.targetWorkspace.recordDragTargets(); + } + } +} + +registry.register( + registry.Type.FLYOUTS_VERTICAL_TOOLBOX, + registry.DEFAULT, + VerticalFlyout, +); diff --git a/core/generator.js b/core/generator.js deleted file mode 100644 index cba266f2787..00000000000 --- a/core/generator.js +++ /dev/null @@ -1,415 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Utility functions for generating executable code from - * Blockly code. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Generator'); - -goog.require('Blockly.Block'); -goog.require('goog.asserts'); - - -/** - * Class for a code generator that translates the blocks into a language. - * @param {string} name Language name of this generator. - * @constructor - */ -Blockly.Generator = function(name) { - this.name_ = name; - this.FUNCTION_NAME_PLACEHOLDER_REGEXP_ = - new RegExp(this.FUNCTION_NAME_PLACEHOLDER_, 'g'); -}; - -/** - * Category to separate generated function names from variables and procedures. - */ -Blockly.Generator.NAME_TYPE = 'generated_function'; - -/** - * Arbitrary code to inject into locations that risk causing infinite loops. - * Any instances of '%1' will be replaced by the block ID that failed. - * E.g. ' checkTimeout(%1);\n' - * @type {?string} - */ -Blockly.Generator.prototype.INFINITE_LOOP_TRAP = null; - -/** - * Arbitrary code to inject before every statement. - * Any instances of '%1' will be replaced by the block ID of the statement. - * E.g. 'highlight(%1);\n' - * @type {?string} - */ -Blockly.Generator.prototype.STATEMENT_PREFIX = null; - -/** - * The method of indenting. Defaults to two spaces, but language generators - * may override this to increase indent or change to tabs. - * @type {string} - */ -Blockly.Generator.prototype.INDENT = ' '; - -/** - * Maximum length for a comment before wrapping. Does not account for - * indenting level. - * @type {number} - */ -Blockly.Generator.prototype.COMMENT_WRAP = 60; - -/** - * List of outer-inner pairings that do NOT require parentheses. - * @type {!Array.>} - */ -Blockly.Generator.prototype.ORDER_OVERRIDES = []; - -/** - * Generate code for all blocks in the workspace to the specified language. - * @param {Blockly.Workspace} workspace Workspace to generate code from. - * @return {string} Generated code. - */ -Blockly.Generator.prototype.workspaceToCode = function(workspace) { - if (!workspace) { - // Backwards compatibility from before there could be multiple workspaces. - console.warn('No workspace specified in workspaceToCode call. Guessing.'); - workspace = Blockly.getMainWorkspace(); - } - var code = []; - this.init(workspace); - var blocks = workspace.getTopBlocks(true); - for (var x = 0, block; block = blocks[x]; x++) { - var line = this.blockToCode(block); - if (goog.isArray(line)) { - // Value blocks return tuples of code and operator order. - // Top-level blocks don't care about operator order. - line = line[0]; - } - if (line) { - if (block.outputConnection && this.scrubNakedValue) { - // This block is a naked value. Ask the language's code generator if - // it wants to append a semicolon, or something. - line = this.scrubNakedValue(line); - } - code.push(line); - } - } - code = code.join('\n'); // Blank line between each section. - code = this.finish(code); - // Final scrubbing of whitespace. - code = code.replace(/^\s+\n/, ''); - code = code.replace(/\n\s+$/, '\n'); - code = code.replace(/[ \t]+\n/g, '\n'); - return code; -}; - -// The following are some helpful functions which can be used by multiple -// languages. - -/** - * Prepend a common prefix onto each line of code. - * @param {string} text The lines of code. - * @param {string} prefix The common prefix. - * @return {string} The prefixed lines of code. - */ -Blockly.Generator.prototype.prefixLines = function(text, prefix) { - return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix); -}; - -/** - * Recursively spider a tree of blocks, returning all their comments. - * @param {!Blockly.Block} block The block from which to start spidering. - * @return {string} Concatenated list of comments. - */ -Blockly.Generator.prototype.allNestedComments = function(block) { - var comments = []; - var blocks = block.getDescendants(); - for (var i = 0; i < blocks.length; i++) { - var comment = blocks[i].getCommentText(); - if (comment) { - comments.push(comment); - } - } - // Append an empty string to create a trailing line break when joined. - if (comments.length) { - comments.push(''); - } - return comments.join('\n'); -}; - -/** - * Generate code for the specified block (and attached blocks). - * @param {Blockly.Block} block The block to generate code for. - * @return {string|!Array} For statement blocks, the generated code. - * For value blocks, an array containing the generated code and an - * operator order value. Returns '' if block is null. - */ -Blockly.Generator.prototype.blockToCode = function(block) { - if (!block) { - return ''; - } - if (block.disabled) { - // Skip past this block if it is disabled. - return this.blockToCode(block.getNextBlock()); - } - - var func = this[block.type]; - goog.asserts.assertFunction(func, - 'Language "%s" does not know how to generate code for block type "%s".', - this.name_, block.type); - // First argument to func.call is the value of 'this' in the generator. - // Prior to 24 September 2013 'this' was the only way to access the block. - // The current prefered method of accessing the block is through the second - // argument to func.call, which becomes the first parameter to the generator. - var code = func.call(block, block); - if (goog.isArray(code)) { - // Value blocks return tuples of code and operator order. - goog.asserts.assert(block.outputConnection, - 'Expecting string from statement block "%s".', block.type); - return [this.scrub_(block, code[0]), code[1]]; - } else if (goog.isString(code)) { - var id = block.id.replace(/\$/g, '$$$$'); // Issue 251. - if (this.STATEMENT_PREFIX) { - code = this.STATEMENT_PREFIX.replace(/%1/g, '\'' + id + '\'') + - code; - } - return this.scrub_(block, code); - } else if (code === null) { - // Block has handled code generation itself. - return ''; - } else { - goog.asserts.fail('Invalid code generated: %s', code); - } -}; - -/** - * Generate code representing the specified value input. - * @param {!Blockly.Block} block The block containing the input. - * @param {string} name The name of the input. - * @param {number} outerOrder The maximum binding strength (minimum order value) - * of any operators adjacent to "block". - * @return {string} Generated code or '' if no blocks are connected or the - * specified input does not exist. - */ -Blockly.Generator.prototype.valueToCode = function(block, name, outerOrder) { - if (isNaN(outerOrder)) { - goog.asserts.fail('Expecting valid order from block "%s".', block.type); - } - var targetBlock = block.getInputTargetBlock(name); - if (!targetBlock) { - return ''; - } - var tuple = this.blockToCode(targetBlock); - if (tuple === '') { - // Disabled block. - return ''; - } - // Value blocks must return code and order of operations info. - // Statement blocks must only return code. - goog.asserts.assertArray(tuple, 'Expecting tuple from value block "%s".', - targetBlock.type); - var code = tuple[0]; - var innerOrder = tuple[1]; - if (isNaN(innerOrder)) { - goog.asserts.fail('Expecting valid order from value block "%s".', - targetBlock.type); - } - if (!code) { - return ''; - } - - // Add parentheses if needed. - var parensNeeded = false; - var outerOrderClass = Math.floor(outerOrder); - var innerOrderClass = Math.floor(innerOrder); - if (outerOrderClass <= innerOrderClass) { - if (outerOrderClass == innerOrderClass && - (outerOrderClass == 0 || outerOrderClass == 99)) { - // Don't generate parens around NONE-NONE and ATOMIC-ATOMIC pairs. - // 0 is the atomic order, 99 is the none order. No parentheses needed. - // In all known languages multiple such code blocks are not order - // sensitive. In fact in Python ('a' 'b') 'c' would fail. - } else { - // The operators outside this code are stronger than the operators - // inside this code. To prevent the code from being pulled apart, - // wrap the code in parentheses. - parensNeeded = true; - // Check for special exceptions. - for (var i = 0; i < this.ORDER_OVERRIDES.length; i++) { - if (this.ORDER_OVERRIDES[i][0] == outerOrder && - this.ORDER_OVERRIDES[i][1] == innerOrder) { - parensNeeded = false; - break; - } - } - } - } - if (parensNeeded) { - // Technically, this should be handled on a language-by-language basis. - // However all known (sane) languages use parentheses for grouping. - code = '(' + code + ')'; - } - return code; -}; - -/** - * Generate code representing the statement. Indent the code. - * @param {!Blockly.Block} block The block containing the input. - * @param {string} name The name of the input. - * @return {string} Generated code or '' if no blocks are connected. - */ -Blockly.Generator.prototype.statementToCode = function(block, name) { - var targetBlock = block.getInputTargetBlock(name); - var code = this.blockToCode(targetBlock); - // Value blocks must return code and order of operations info. - // Statement blocks must only return code. - goog.asserts.assertString(code, 'Expecting code from statement block "%s".', - targetBlock && targetBlock.type); - if (code) { - code = this.prefixLines(/** @type {string} */ (code), this.INDENT); - } - return code; -}; - -/** - * Add an infinite loop trap to the contents of a loop. - * If loop is empty, add a statment prefix for the loop block. - * @param {string} branch Code for loop contents. - * @param {string} id ID of enclosing block. - * @return {string} Loop contents, with infinite loop trap added. - */ -Blockly.Generator.prototype.addLoopTrap = function(branch, id) { - id = id.replace(/\$/g, '$$$$'); // Issue 251. - if (this.INFINITE_LOOP_TRAP) { - branch = this.INFINITE_LOOP_TRAP.replace(/%1/g, '\'' + id + '\'') + branch; - } - if (this.STATEMENT_PREFIX) { - branch += this.prefixLines(this.STATEMENT_PREFIX.replace(/%1/g, - '\'' + id + '\''), this.INDENT); - } - return branch; -}; - -/** - * Comma-separated list of reserved words. - * @type {string} - * @private - */ -Blockly.Generator.prototype.RESERVED_WORDS_ = ''; - -/** - * Add one or more words to the list of reserved words for this language. - * @param {string} words Comma-separated list of words to add to the list. - * No spaces. Duplicates are ok. - */ -Blockly.Generator.prototype.addReservedWords = function(words) { - this.RESERVED_WORDS_ += words + ','; -}; - -/** - * This is used as a placeholder in functions defined using - * Blockly.Generator.provideFunction_. It must not be legal code that could - * legitimately appear in a function definition (or comment), and it must - * not confuse the regular expression parser. - * @type {string} - * @private - */ -Blockly.Generator.prototype.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}'; - -/** - * Define a function to be included in the generated code. - * The first time this is called with a given desiredName, the code is - * saved and an actual name is generated. Subsequent calls with the - * same desiredName have no effect but have the same return value. - * - * It is up to the caller to make sure the same desiredName is not - * used for different code values. - * - * The code gets output when Blockly.Generator.finish() is called. - * - * @param {string} desiredName The desired name of the function (e.g., isPrime). - * @param {!Array.} code A list of statements. Use ' ' for indents. - * @return {string} The actual name of the new function. This may differ - * from desiredName if the former has already been taken by the user. - * @private - */ -Blockly.Generator.prototype.provideFunction_ = function(desiredName, code) { - if (!this.definitions_[desiredName]) { - var functionName = this.variableDB_.getDistinctName(desiredName, - Blockly.Procedures.NAME_TYPE); - this.functionNames_[desiredName] = functionName; - var codeText = code.join('\n').replace( - this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName); - // Change all ' ' indents into the desired indent. - // To avoid an infinite loop of replacements, change all indents to '\0' - // character first, then replace them all with the indent. - // We are assuming that no provided functions contain a literal null char. - var oldCodeText; - while (oldCodeText != codeText) { - oldCodeText = codeText; - codeText = codeText.replace(/^(( )*) /gm, '$1\0'); - } - codeText = codeText.replace(/\0/g, this.INDENT); - this.definitions_[desiredName] = codeText; - } - return this.functionNames_[desiredName]; -}; - -/** - * Hook for code to run before code generation starts. - * Subclasses may override this, e.g. to initialise the database of variable - * names. - * @param {!Blockly.Workspace} workspace Workspace to generate code from. - */ -Blockly.Generator.prototype.init = undefined; - -/** - * Common tasks for generating code from blocks. This is called from - * blockToCode and is called on every block, not just top level blocks. - * Subclasses may override this, e.g. to generate code for statements following - * the block, or to handle comments for the specified block and any connected - * value blocks. - * @param {!Blockly.Block} block The current block. - * @param {string} code The JavaScript code created for this block. - * @return {string} JavaScript code with comments and subsequent blocks added. - * @private - */ -Blockly.Generator.prototype.scrub_ = undefined; - -/** - * Hook for code to run at end of code generation. - * Subclasses may override this, e.g. to prepend the generated code with the - * variable definitions. - * @param {string} code Generated code. - * @return {string} Completed code. - */ -Blockly.Generator.prototype.finish = undefined; - -/** - * Naked values are top-level blocks with outputs that aren't plugged into - * anything. - * Subclasses may override this, e.g. if their language does not allow - * naked values. - * @param {string} line Line of generated code. - * @return {string} Legal line of code. - */ -Blockly.Generator.prototype.scrubNakedValue = undefined; diff --git a/core/generator.ts b/core/generator.ts new file mode 100644 index 00000000000..5884b4e5449 --- /dev/null +++ b/core/generator.ts @@ -0,0 +1,612 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Utility functions for generating executable code from + * Blockly code. + * + * @class + */ +// Former goog.module ID: Blockly.CodeGenerator + +import type {Block} from './block.js'; +import * as common from './common.js'; +import {Names, NameType} from './names.js'; +import type {Workspace} from './workspace.js'; + +/** + * Deprecated, no-longer used type declaration for per-block-type generator + * functions. + * + * @deprecated + * @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/generating-code} + * @param block The Block instance to generate code for. + * @param generator The CodeGenerator calling the function. + * @returns A string containing the generated code (for statement blocks), + * or a [code, precedence] tuple (for value/expression blocks), or + * null if no code should be emitted for block. + */ +export type BlockGenerator = ( + block: Block, + generator: CodeGenerator, +) => [string, number] | string | null; + +/** + * Class for a code generator that translates the blocks into a language. + */ +export class CodeGenerator { + name_: string; + + /** + * A dictionary of block generator functions, keyed by block type. + * Each block generator function takes two parameters: + * + * - the Block to generate code for, and + * - the calling CodeGenerator (or subclass) instance, so the + * function can call methods defined below (e.g. blockToCode) or + * on the relevant subclass (e.g. JavascripGenerator), + * + * and returns: + * + * - a [code, precedence] tuple (for value/expression blocks), or + * - a string containing the generated code (for statement blocks), or + * - null if no code should be emitted for block. + */ + forBlock: Record< + string, + (block: Block, generator: this) => [string, number] | string | null + > = Object.create(null); + + /** + * This is used as a placeholder in functions defined using + * CodeGenerator.provideFunction_. It must not be legal code that could + * legitimately appear in a function definition (or comment), and it must + * not confuse the regular expression parser. + */ + FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}'; + FUNCTION_NAME_PLACEHOLDER_REGEXP_: RegExp; + + /** + * Arbitrary code to inject into locations that risk causing infinite loops. + * Any instances of '%1' will be replaced by the block ID that failed. + * E.g. ` checkTimeout(%1);\n` + */ + INFINITE_LOOP_TRAP: string | null = null; + + /** + * Arbitrary code to inject before every statement. + * Any instances of '%1' will be replaced by the block ID of the statement. + * E.g. `highlight(%1);\n` + */ + STATEMENT_PREFIX: string | null = null; + + /** + * Arbitrary code to inject after every statement. + * Any instances of '%1' will be replaced by the block ID of the statement. + * E.g. `highlight(%1);\n` + */ + STATEMENT_SUFFIX: string | null = null; + + /** + * The method of indenting. Defaults to two spaces, but language generators + * may override this to increase indent or change to tabs. + */ + INDENT = ' '; + + /** + * Maximum length for a comment before wrapping. Does not account for + * indenting level. + */ + COMMENT_WRAP = 60; + + /** List of outer-inner pairings that do NOT require parentheses. */ + ORDER_OVERRIDES: number[][] = []; + + /** + * Whether the init method has been called. + * Generators that set this flag to false after creation and true in init + * will cause blockToCode to emit a warning if the generator has not been + * initialized. If this flag is untouched, it will have no effect. + */ + isInitialized: boolean | null = null; + + /** Comma-separated list of reserved words. */ + protected RESERVED_WORDS_ = ''; + + /** A dictionary of definitions to be printed before the code. */ + protected definitions_: {[key: string]: string} = Object.create(null); + + /** + * A dictionary mapping desired function names in definitions_ to actual + * function names (to avoid collisions with user functions). + */ + protected functionNames_: {[key: string]: string} = Object.create(null); + + /** A database of variable and procedure names. */ + nameDB_?: Names = undefined; + + /** @param name Language name of this generator. */ + constructor(name: string) { + this.name_ = name; + + this.FUNCTION_NAME_PLACEHOLDER_REGEXP_ = new RegExp( + this.FUNCTION_NAME_PLACEHOLDER_, + 'g', + ); + } + + /** + * Generate code for all blocks in the workspace to the specified language. + * + * @param workspace Workspace to generate code from. + * @returns Generated code. + */ + workspaceToCode(workspace?: Workspace): string { + if (!workspace) { + // Backwards compatibility from before there could be multiple workspaces. + console.warn( + 'No workspace specified in workspaceToCode call. Guessing.', + ); + workspace = common.getMainWorkspace(); + } + const code = []; + this.init(workspace); + const blocks = workspace.getTopBlocks(true); + for (let i = 0, block; (block = blocks[i]); i++) { + let line = this.blockToCode(block); + if (Array.isArray(line)) { + // Value blocks return tuples of code and operator order. + // Top-level blocks don't care about operator order. + line = line[0]; + } + if (line) { + if (block.outputConnection) { + // This block is a naked value. Ask the language's code generator if + // it wants to append a semicolon, or something. + line = this.scrubNakedValue(line); + if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) { + line = this.injectId(this.STATEMENT_PREFIX, block) + line; + } + if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) { + line = line + this.injectId(this.STATEMENT_SUFFIX, block); + } + } + code.push(line); + } + } + // Blank line between each section. + let codeString = code.join('\n'); + codeString = this.finish(codeString); + // Final scrubbing of whitespace. + codeString = codeString.replace(/^\s+\n/, ''); + codeString = codeString.replace(/\n\s+$/, '\n'); + codeString = codeString.replace(/[ \t]+\n/g, '\n'); + return codeString; + } + + /** + * Prepend a common prefix onto each line of code. + * Intended for indenting code or adding comment markers. + * + * @param text The lines of code. + * @param prefix The common prefix. + * @returns The prefixed lines of code. + */ + prefixLines(text: string, prefix: string): string { + return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix); + } + + /** + * Recursively spider a tree of blocks, returning all their comments. + * + * @param block The block from which to start spidering. + * @returns Concatenated list of comments. + */ + allNestedComments(block: Block): string { + const comments = []; + const blocks = block.getDescendants(true); + for (let i = 0; i < blocks.length; i++) { + const comment = blocks[i].getCommentText(); + if (comment) { + comments.push(comment); + } + } + // Append an empty string to create a trailing line break when joined. + if (comments.length) { + comments.push(''); + } + return comments.join('\n'); + } + + /** + * Generate code for the specified block (and attached blocks). + * The generator must be initialized before calling this function. + * + * @param block The block to generate code for. + * @param opt_thisOnly True to generate code for only this statement. + * @returns For statement blocks, the generated code. + * For value blocks, an array containing the generated code and an + * operator order value. Returns '' if block is null. + */ + blockToCode( + block: Block | null, + opt_thisOnly?: boolean, + ): string | [string, number] { + if (this.isInitialized === false) { + console.warn( + 'CodeGenerator init was not called before blockToCode was called.', + ); + } + if (!block) { + return ''; + } + if (!block.isEnabled()) { + // Skip past this block if it is disabled. + return opt_thisOnly ? '' : this.blockToCode(block.getNextBlock()); + } + if (block.isInsertionMarker()) { + // Skip past insertion markers. + return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]); + } + + // Look up block generator function in dictionary - but fall back + // to looking up on this if not found, for backwards compatibility. + const func = this.forBlock[block.type]; + if (typeof func !== 'function') { + throw Error( + `${this.name_} generator does not know how to generate code ` + + `for block type "${block.type}".`, + ); + } + // First argument to func.call is the value of 'this' in the generator. + // Prior to 24 September 2013 'this' was the only way to access the block. + // The current preferred method of accessing the block is through the second + // argument to func.call, which becomes the first parameter to the + // generator. + let code = func.call(block, block, this); + if (Array.isArray(code)) { + // Value blocks return tuples of code and operator order. + if (!block.outputConnection) { + throw TypeError('Expecting string from statement block: ' + block.type); + } + return [this.scrub_(block, code[0], opt_thisOnly), code[1]]; + } else if (typeof code === 'string') { + if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) { + code = this.injectId(this.STATEMENT_PREFIX, block) + code; + } + if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) { + code = code + this.injectId(this.STATEMENT_SUFFIX, block); + } + return this.scrub_(block, code, opt_thisOnly); + } else if (code === null) { + // Block has handled code generation itself. + return ''; + } + throw SyntaxError('Invalid code generated: ' + code); + } + + /** + * Generate code representing the specified value input. + * + * @param block The block containing the input. + * @param name The name of the input. + * @param outerOrder The maximum binding strength (minimum order value) of any + * operators adjacent to "block". + * @returns Generated code or '' if no blocks are connected. + * @throws ReferenceError if the specified input does not exist. + */ + valueToCode(block: Block, name: string, outerOrder: number): string { + if (isNaN(outerOrder)) { + throw TypeError('Expecting valid order from block: ' + block.type); + } + const targetBlock = block.getInputTargetBlock(name); + if (!targetBlock && !block.getInput(name)) { + throw ReferenceError(`Input "${name}" doesn't exist on "${block.type}"`); + } + if (!targetBlock) { + return ''; + } + const tuple = this.blockToCode(targetBlock); + if (tuple === '') { + // Disabled block. + return ''; + } + // Value blocks must return code and order of operations info. + // Statement blocks must only return code. + if (!Array.isArray(tuple)) { + throw TypeError( + `Expecting tuple from value block: ${targetBlock.type} See ` + + `developers.google.com/blockly/guides/create-custom-blocks/generating-code ` + + `for more information`, + ); + } + let code = tuple[0]; + const innerOrder = tuple[1]; + if (isNaN(innerOrder)) { + throw TypeError( + 'Expecting valid order from value block: ' + targetBlock.type, + ); + } + if (!code) { + return ''; + } + + // Add parentheses if needed. + let parensNeeded = false; + const outerOrderClass = Math.floor(outerOrder); + const innerOrderClass = Math.floor(innerOrder); + if (outerOrderClass <= innerOrderClass) { + if ( + outerOrderClass === innerOrderClass && + (outerOrderClass === 0 || outerOrderClass === 99) + ) { + // Don't generate parens around NONE-NONE and ATOMIC-ATOMIC pairs. + // 0 is the atomic order, 99 is the none order. No parentheses needed. + // In all known languages multiple such code blocks are not order + // sensitive. In fact in Python ('a' 'b') 'c' would fail. + } else { + // The operators outside this code are stronger than the operators + // inside this code. To prevent the code from being pulled apart, + // wrap the code in parentheses. + parensNeeded = true; + // Check for special exceptions. + for (let i = 0; i < this.ORDER_OVERRIDES.length; i++) { + if ( + this.ORDER_OVERRIDES[i][0] === outerOrder && + this.ORDER_OVERRIDES[i][1] === innerOrder + ) { + parensNeeded = false; + break; + } + } + } + } + if (parensNeeded) { + // Technically, this should be handled on a language-by-language basis. + // However all known (sane) languages use parentheses for grouping. + code = '(' + code + ')'; + } + return code; + } + + /** + * Generate a code string representing the blocks attached to the named + * statement input. Indent the code. + * This is mainly used in generators. When trying to generate code to evaluate + * look at using workspaceToCode or blockToCode. + * + * @param block The block containing the input. + * @param name The name of the input. + * @returns Generated code or '' if no blocks are connected. + * @throws ReferenceError if the specified input does not exist. + */ + statementToCode(block: Block, name: string): string { + const targetBlock = block.getInputTargetBlock(name); + if (!targetBlock && !block.getInput(name)) { + throw ReferenceError(`Input "${name}" doesn't exist on "${block.type}"`); + } + let code = this.blockToCode(targetBlock); + // Value blocks must return code and order of operations info. + // Statement blocks must only return code. + if (typeof code !== 'string') { + throw TypeError( + 'Expecting code from statement block: ' + + (targetBlock && targetBlock.type), + ); + } + if (code) { + code = this.prefixLines(code, this.INDENT); + } + return code; + } + + /** + * Add an infinite loop trap to the contents of a loop. + * Add statement suffix at the start of the loop block (right after the loop + * statement executes), and a statement prefix to the end of the loop block + * (right before the loop statement executes). + * + * @param branch Code for loop contents. + * @param block Enclosing block. + * @returns Loop contents, with infinite loop trap added. + */ + addLoopTrap(branch: string, block: Block): string { + if (this.INFINITE_LOOP_TRAP) { + branch = + this.prefixLines( + this.injectId(this.INFINITE_LOOP_TRAP, block), + this.INDENT, + ) + branch; + } + if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) { + branch = + this.prefixLines( + this.injectId(this.STATEMENT_SUFFIX, block), + this.INDENT, + ) + branch; + } + if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) { + branch = + branch + + this.prefixLines( + this.injectId(this.STATEMENT_PREFIX, block), + this.INDENT, + ); + } + return branch; + } + + /** + * Inject a block ID into a message to replace '%1'. + * Used for STATEMENT_PREFIX, STATEMENT_SUFFIX, and INFINITE_LOOP_TRAP. + * + * @param msg Code snippet with '%1'. + * @param block Block which has an ID. + * @returns Code snippet with ID. + */ + injectId(msg: string, block: Block): string { + const id = block.id.replace(/\$/g, '$$$$'); // Issue 251. + return msg.replace(/%1/g, "'" + id + "'"); + } + + /** + * Add one or more words to the list of reserved words for this language. + * + * @param words Comma-separated list of words to add to the list. + * No spaces. Duplicates are ok. + */ + addReservedWords(words: string) { + this.RESERVED_WORDS_ += words + ','; + } + + /** + * Define a developer-defined function (not a user-defined procedure) to be + * included in the generated code. Used for creating private helper + * functions. The first time this is called with a given desiredName, the code + * is saved and an actual name is generated. Subsequent calls with the same + * desiredName have no effect but have the same return value. + * + * It is up to the caller to make sure the same desiredName is not + * used for different helper functions (e.g. use "colourRandom" and + * "listRandom", not "random"). There is no danger of colliding with reserved + * words, or user-defined variable or procedure names. + * + * The code gets output when CodeGenerator.finish() is called. + * + * @param desiredName The desired name of the function (e.g. mathIsPrime). + * @param code A list of statements or one multi-line code string. Use ' ' + * for indents (they will be replaced). + * @returns The actual name of the new function. This may differ from + * desiredName if the former has already been taken by the user. + */ + provideFunction_(desiredName: string, code: string[] | string): string { + if (!this.definitions_[desiredName]) { + const functionName = this.nameDB_!.getDistinctName( + desiredName, + NameType.PROCEDURE, + ); + this.functionNames_[desiredName] = functionName; + if (Array.isArray(code)) { + code = code.join('\n'); + } + let codeText = code + .trim() + .replace(this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName); + // Change all ' ' indents into the desired indent. + // To avoid an infinite loop of replacements, change all indents to '\0' + // character first, then replace them all with the indent. + // We are assuming that no provided functions contain a literal null char. + let oldCodeText; + while (oldCodeText !== codeText) { + oldCodeText = codeText; + codeText = codeText.replace(/^(( {2})*) {2}/gm, '$1\0'); + } + codeText = codeText.replace(/\0/g, this.INDENT); + this.definitions_[desiredName] = codeText; + } + return this.functionNames_[desiredName]; + } + + /** + * Gets a unique, legal name for a user-defined variable. + * Before calling this method, the `nameDB_` property of the class + * must have been initialized already. This is typically done in + * the `init` function of the code generator class. + * + * @param nameOrId The ID of the variable to get a name for, + * or the proposed name for a variable not associated with an id. + * @returns A unique, legal name for the variable. + */ + getVariableName(nameOrId: string): string { + return this.getName(nameOrId, NameType.VARIABLE); + } + + /** + * Gets a unique, legal name for a user-defined procedure. + * Before calling this method, the `nameDB_` property of the class + * must have been initialized already. This is typically done in + * the `init` function of the code generator class. + * + * @param name The proposed name for a procedure. + * @returns A unique, legal name for the procedure. + */ + getProcedureName(name: string): string { + return this.getName(name, NameType.PROCEDURE); + } + + private getName(nameOrId: string, type: NameType): string { + if (!this.nameDB_) { + throw new Error( + 'Name database is not defined. You must initialize `nameDB_` in your generator class and call `init` first.', + ); + } + return this.nameDB_.getName(nameOrId, type); + } + + /** + * Hook for code to run before code generation starts. + * Subclasses may override this, e.g. to initialise the database of variable + * names. + * + * @param _workspace Workspace to generate code from. + */ + init(_workspace: Workspace) { + // Optionally override + // Create a dictionary of definitions to be printed before the code. + this.definitions_ = Object.create(null); + // Create a dictionary mapping desired developer-defined function names in + // definitions_ to actual function names (to avoid collisions with + // user-defined procedures). + this.functionNames_ = Object.create(null); + } + + /** + * Common tasks for generating code from blocks. This is called from + * blockToCode and is called on every block, not just top level blocks. + * Subclasses may override this, e.g. to generate code for statements + * following the block, or to handle comments for the specified block and any + * connected value blocks. + * + * @param _block The current block. + * @param code The code created for this block. + * @param _opt_thisOnly True to generate code for only this statement. + * @returns Code with comments and subsequent blocks added. + */ + scrub_(_block: Block, code: string, _opt_thisOnly?: boolean): string { + // Optionally override + return code; + } + + /** + * Hook for code to run at end of code generation. + * Subclasses may override this, e.g. to prepend the generated code with + * import statements or variable definitions. + * + * @param code Generated code. + * @returns Completed code. + */ + finish(code: string): string { + // Optionally override + // Clean up temporary data. + this.definitions_ = Object.create(null); + this.functionNames_ = Object.create(null); + return code; + } + + /** + * Naked values are top-level blocks with outputs that aren't plugged into + * anything. + * Subclasses may override this, e.g. if their language does not allow + * naked values. + * + * @param line Line of generated code. + * @returns Legal line of code. + */ + scrubNakedValue(line: string): string { + // Optionally override + return line; + } +} diff --git a/core/gesture.js b/core/gesture.js deleted file mode 100644 index ba09192cf39..00000000000 --- a/core/gesture.js +++ /dev/null @@ -1,783 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 The class representing an in-progress gesture, usually a drag - * or a tap. - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -goog.provide('Blockly.Gesture'); - -goog.require('Blockly.BlockDragger'); -goog.require('Blockly.constants'); -goog.require('Blockly.FlyoutDragger'); -goog.require('Blockly.Tooltip'); -goog.require('Blockly.Touch'); -goog.require('Blockly.WorkspaceDragger'); - -goog.require('goog.asserts'); -goog.require('goog.math.Coordinate'); - - -/** - * NB: In this file "start" refers to touchstart, mousedown, and pointerstart - * events. "End" refers to touchend, mouseup, and pointerend events. - * TODO: Consider touchcancel/pointercancel. - */ - -/** - * Class for one gesture. - * @param {!Event} e The event that kicked off this gesture. - * @param {!Blockly.WorkspaceSvg} creatorWorkspace The workspace that created - * this gesture and has a reference to it. - * @constructor - */ -Blockly.Gesture = function(e, creatorWorkspace) { - - /** - * The position of the mouse when the gesture started. Units are css pixels, - * with (0, 0) at the top left of the browser window (mouseEvent clientX/Y). - * @type {goog.math.Coordinate} - */ - this.mouseDownXY_ = null; - - /** - * How far the mouse has moved during this drag, in pixel units. - * (0, 0) is at this.mouseDownXY_. - * @type {goog.math.Coordinate} - * private - */ - this.currentDragDeltaXY_ = 0; - - /** - * The field that the gesture started on, or null if it did not start on a - * field. - * @type {Blockly.Field} - * @private - */ - this.startField_ = null; - - /** - * The block that the gesture started on, or null if it did not start on a - * block. - * @type {Blockly.BlockSvg} - * @private - */ - this.startBlock_ = null; - - /** - * The block that this gesture targets. If the gesture started on a - * shadow block, this is the first non-shadow parent of the block. If the - * gesture started in the flyout, this is the root block of the block group - * that was clicked or dragged. - * @type {Blockly.BlockSvg} - * @private - */ - this.targetBlock_ = null; - - /** - * The workspace that the gesture started on. There may be multiple - * workspaces on a page; this is more accurate than using - * Blockly.getMainWorkspace(). - * @type {Blockly.WorkspaceSvg} - * @private - */ - this.startWorkspace_ = null; - - /** - * The workspace that created this gesture. This workspace keeps a reference - * to the gesture, which will need to be cleared at deletion. - * This may be different from the start workspace. For instance, a flyout is - * a workspace, but its parent workspace manages gestures for it. - * @type {Blockly.WorkspaceSvg} - * @private - */ - this.creatorWorkspace_ = creatorWorkspace; - - /** - * Whether the pointer has at any point moved out of the drag radius. - * A gesture that exceeds the drag radius is a drag even if it ends exactly at - * its start point. - * @type {boolean} - * @private - */ - this.hasExceededDragRadius_ = false; - - /** - * Whether the workspace is currently being dragged. - * @type {boolean} - * @private - */ - this.isDraggingWorkspace_ = false; - - /** - * Whether the block is currently being dragged. - * @type {boolean} - * @private - */ - this.isDraggingBlock_ = false; - - /** - * The event that most recently updated this gesture. - * @type {!Event} - * @private - */ - this.mostRecentEvent_ = e; - - /** - * A handle to use to unbind a mouse move listener at the end of a drag. - * Opaque data returned from Blockly.bindEventWithChecks_. - * @type {Array.} - * @private - */ - this.onMoveWrapper_ = null; - - /** - * A handle to use to unbind a mouse up listener at the end of a drag. - * Opaque data returned from Blockly.bindEventWithChecks_. - * @type {Array.} - * @private - */ - this.onUpWrapper_ = null; - - /** - * The object tracking a block drag, or null if none is in progress. - * @type {Blockly.BlockDragger} - * @private - */ - this.blockDragger_ = null; - - /** - * The object tracking a workspace or flyout workspace drag, or null if none - * is in progress. - * @type {Blockly.WorkspaceDragger} - * @private - */ - this.workspaceDragger_ = null; - - /** - * The flyout a gesture started in, if any. - * @type {Blockly.Flyout} - * @private - */ - this.flyout_ = null; - - /** - * Boolean for sanity-checking that some code is only called once. - * @type {boolean} - * @private - */ - this.calledUpdateIsDragging_ = false; - - /** - * Boolean for sanity-checking that some code is only called once. - * @type {boolean} - * @private - */ - this.hasStarted_ = false; - - /** - * Boolean used internally to break a cycle in disposal. - * @type {boolean} - * @private - */ - this.isEnding_ = false; -}; - -/** - * Sever all links from this object. - * @package - */ -Blockly.Gesture.prototype.dispose = function() { - Blockly.Touch.clearTouchIdentifier(); - Blockly.Tooltip.unblock(); - // Clear the owner's reference to this gesture. - this.creatorWorkspace_.clearGesture(); - - if (this.onMoveWrapper_) { - Blockly.unbindEvent_(this.onMoveWrapper_); - } - if (this.onUpWrapper_) { - Blockly.unbindEvent_(this.onUpWrapper_); - } - - - this.startField_ = null; - this.startBlock_ = null; - this.targetBlock_ = null; - this.startWorkspace_ = null; - this.flyout_ = null; - - if (this.blockDragger_) { - this.blockDragger_.dispose(); - this.blockDragger_ = null; - } - if (this.workspaceDragger_) { - this.workspaceDragger_.dispose(); - this.workspaceDragger_ = null; - } -}; - -/** - * Update internal state based on an event. - * @param {!Event} e The most recent mouse or touch event. - * @private - */ -Blockly.Gesture.prototype.updateFromEvent_ = function(e) { - var currentXY = new goog.math.Coordinate(e.clientX, e.clientY); - var changed = this.updateDragDelta_(currentXY); - // Exceeded the drag radius for the first time. - if (changed){ - this.updateIsDragging_(); - Blockly.longStop_(); - } - this.mostRecentEvent_ = e; -}; - -/** - * DO MATH to set currentDragDeltaXY_ based on the most recent mouse position. - * @param {!goog.math.Coordinate} currentXY The most recent mouse/pointer - * position, in pixel units, with (0, 0) at the window's top left corner. - * @return {boolean} True if the drag just exceeded the drag radius for the - * first time. - * @private - */ -Blockly.Gesture.prototype.updateDragDelta_ = function(currentXY) { - this.currentDragDeltaXY_ = goog.math.Coordinate.difference(currentXY, - this.mouseDownXY_); - - if (!this.hasExceededDragRadius_) { - var currentDragDelta = goog.math.Coordinate.magnitude( - this.currentDragDeltaXY_); - - // The flyout has a different drag radius from the rest of Blockly. - var limitRadius = this.flyout_ ? Blockly.FLYOUT_DRAG_RADIUS : - Blockly.DRAG_RADIUS; - - this.hasExceededDragRadius_ = currentDragDelta > limitRadius; - return this.hasExceededDragRadius_; - } - return false; -}; - -/** - * Update this gesture to record whether a block is being dragged from the - * flyout. - * This function should be called on a mouse/touch move event the first time the - * drag radius is exceeded. It should be called no more than once per gesture. - * If a block should be dragged from the flyout this function creates the new - * block on the main workspace and updates targetBlock_ and startWorkspace_. - * @return {boolean} True if a block is being dragged from the flyout. - * @private - */ -Blockly.Gesture.prototype.updateIsDraggingFromFlyout_ = function() { - // Disabled blocks may not be dragged from the flyout. - if (this.targetBlock_.disabled) { - return false; - } - if (!this.flyout_.isScrollable() || - this.flyout_.isDragTowardWorkspace(this.currentDragDeltaXY_)) { - this.startWorkspace_ = this.flyout_.targetWorkspace_; - this.startWorkspace_.updateScreenCalculationsIfScrolled(); - // Start the event group now, so that the same event group is used for block - // creation and block dragging. - if (!Blockly.Events.getGroup()) { - Blockly.Events.setGroup(true); - } - // The start block is no longer relevant, because this is a drag. - this.startBlock_ = null; - this.targetBlock_ = this.flyout_.createBlock(this.targetBlock_); - this.targetBlock_.select(); - return true; - } - return false; -}; - -/** - * Update this gesture to record whether a block is being dragged. - * This function should be called on a mouse/touch move event the first time the - * drag radius is exceeded. It should be called no more than once per gesture. - * If a block should be dragged, either from the flyout or in the workspace, - * this function creates the necessary BlockDragger and starts the drag. - * @return {boolean} true if a block is being dragged. - * @private - */ -Blockly.Gesture.prototype.updateIsDraggingBlock_ = function() { - if (!this.targetBlock_) { - return false; - } - - if (this.flyout_) { - this.isDraggingBlock_ = this.updateIsDraggingFromFlyout_(); - } else if (this.targetBlock_.isMovable()){ - this.isDraggingBlock_ = true; - } - - if (this.isDraggingBlock_) { - this.startDraggingBlock_(); - return true; - } - return false; -}; - -/** - * Update this gesture to record whether a workspace is being dragged. - * This function should be called on a mouse/touch move event the first time the - * drag radius is exceeded. It should be called no more than once per gesture. - * If a workspace is being dragged this function creates the necessary - * WorkspaceDragger or FlyoutDragger and starts the drag. - * @private - */ -Blockly.Gesture.prototype.updateIsDraggingWorkspace_ = function() { - var wsMovable = this.flyout_ ? this.flyout_.isScrollable() : - this.startWorkspace_ && this.startWorkspace_.isDraggable(); - - if (!wsMovable) { - return; - } - - if (this.flyout_) { - this.workspaceDragger_ = new Blockly.FlyoutDragger(this.flyout_); - } else { - this.workspaceDragger_ = new Blockly.WorkspaceDragger(this.startWorkspace_); - } - - this.isDraggingWorkspace_ = true; - this.workspaceDragger_.startDrag(); -}; - -/** - * Update this gesture to record whether anything is being dragged. - * This function should be called on a mouse/touch move event the first time the - * drag radius is exceeded. It should be called no more than once per gesture. - * @private - */ -Blockly.Gesture.prototype.updateIsDragging_ = function() { - // Sanity check. - goog.asserts.assert(!this.calledUpdateIsDragging_, - 'updateIsDragging_ should only be called once per gesture.'); - this.calledUpdateIsDragging_ = true; - - // First check if it was a block drag. - if (this.updateIsDraggingBlock_()) { - return; - } - // Then check if it's a workspace drag. - this.updateIsDraggingWorkspace_(); -}; - -/** - * Create a block dragger and start dragging the selected block. - * @private - */ -Blockly.Gesture.prototype.startDraggingBlock_ = function() { - this.blockDragger_ = new Blockly.BlockDragger(this.targetBlock_, - this.startWorkspace_); - this.blockDragger_.startBlockDrag(this.currentDragDeltaXY_); - this.blockDragger_.dragBlock(this.mostRecentEvent_, - this.currentDragDeltaXY_); -}; - -/** - * Start a gesture: update the workspace to indicate that a gesture is in - * progress and bind mousemove and mouseup handlers. - * @param {!Event} e A mouse down or touch start event. - * @package - */ -Blockly.Gesture.prototype.doStart = function(e) { - if (Blockly.utils.isTargetInput(e)) { - this.cancel(); - return; - } - this.hasStarted_ = true; - - Blockly.BlockSvg.disconnectUiStop_(); - this.startWorkspace_.updateScreenCalculationsIfScrolled(); - if (this.startWorkspace_.isMutator) { - // Mutator's coordinate system could be out of date because the bubble was - // dragged, the block was moved, the parent workspace zoomed, etc. - this.startWorkspace_.resize(); - } - this.startWorkspace_.markFocused(); - this.mostRecentEvent_ = e; - - // Hide chaff also hides the flyout, so don't do it if the click is in a flyout. - Blockly.hideChaff(!!this.flyout_); - Blockly.Tooltip.block(); - - if (this.targetBlock_) { - this.targetBlock_.select(); - } - - if (Blockly.utils.isRightButton(e)) { - this.handleRightClick(e); - return; - } - - if (goog.string.caseInsensitiveEquals(e.type, 'touchstart')) { - Blockly.longStart_(e, this); - } - - this.mouseDownXY_ = new goog.math.Coordinate(e.clientX, e.clientY); - - this.onMoveWrapper_ = Blockly.bindEventWithChecks_( - document, 'mousemove', null, this.handleMove.bind(this)); - this.onUpWrapper_ = Blockly.bindEventWithChecks_( - document, 'mouseup', null, this.handleUp.bind(this)); - - e.preventDefault(); - e.stopPropagation(); -}; - -/** - * Handle a mouse move or touch move event. - * @param {!Event} e A mouse move or touch move event. - * @package - */ -Blockly.Gesture.prototype.handleMove = function(e) { - this.updateFromEvent_(e); - if (this.isDraggingWorkspace_) { - this.workspaceDragger_.drag(this.currentDragDeltaXY_); - } else if (this.isDraggingBlock_) { - this.blockDragger_.dragBlock(this.mostRecentEvent_, - this.currentDragDeltaXY_); - } - e.preventDefault(); - e.stopPropagation(); -}; - -/** - * Handle a mouse up or touch end event. - * @param {!Event} e A mouse up or touch end event. - * @package - */ -Blockly.Gesture.prototype.handleUp = function(e) { - this.updateFromEvent_(e); - Blockly.longStop_(); - - if (this.isEnding_) { - console.log('Trying to end a gesture recursively.'); - return; - } - this.isEnding_ = true; - // The ordering of these checks is important: drags have higher priority than - // clicks. Fields have higher priority than blocks; blocks have higher - // priority than workspaces. - if (this.isDraggingBlock_) { - this.blockDragger_.endBlockDrag(e, this.currentDragDeltaXY_); - } else if (this.isDraggingWorkspace_) { - this.workspaceDragger_.endDrag(this.currentDragDeltaXY_); - } else if (this.isFieldClick_()) { - this.doFieldClick_(); - } else if (this.isBlockClick_()) { - this.doBlockClick_(); - } else if (this.isWorkspaceClick_()) { - this.doWorkspaceClick_(); - } - - e.preventDefault(); - e.stopPropagation(); - - this.dispose(); -}; - -/** - * Cancel an in-progress gesture. If a workspace or block drag is in progress, - * end the drag at the most recent location. - * @package - */ -Blockly.Gesture.prototype.cancel = function() { - // Disposing of a block cancels in-progress drags, but dragging to a delete - // area disposes of a block and leads to recursive disposal. Break that cycle. - if (this.isEnding_) { - return; - } - Blockly.longStop_(); - if (this.isDraggingBlock_) { - this.blockDragger_.endBlockDrag(this.mostRecentEvent_, - this.currentDragDeltaXY_); - } else if (this.isDraggingWorkspace_) { - this.workspaceDragger_.endDrag(this.currentDragDeltaXY_); - } - this.dispose(); -}; - -/** - * Handle a real or faked right-click event by showing a context menu. - * @param {!Event} e A mouse move or touch move event. - * @package - */ -Blockly.Gesture.prototype.handleRightClick = function(e) { - if (this.targetBlock_) { - this.bringBlockToFront_(); - Blockly.hideChaff(this.flyout_); - this.targetBlock_.showContextMenu_(e); - } else if (this.startWorkspace_ && !this.flyout_) { - Blockly.hideChaff(); - this.startWorkspace_.showContextMenu_(e); - } - - e.preventDefault(); - e.stopPropagation(); - - this.dispose(); -}; - -/** - * Handle a mousedown/touchstart event on a workspace. - * @param {!Event} e A mouse down or touch start event. - * @param {!Blockly.Workspace} ws The workspace the event hit. - * @package - */ -Blockly.Gesture.prototype.handleWsStart = function(e, ws) { - goog.asserts.assert(!this.hasStarted_, - 'Tried to call gesture.handleWsStart, but the gesture had already been ' + - 'started.'); - this.setStartWorkspace_(ws); - this.mostRecentEvent_ = e; - this.doStart(e); -}; - -/** - * Handle a mousedown/touchstart event on a flyout. - * @param {!Event} e A mouse down or touch start event. - * @param {!Blockly.Flyout} flyout The flyout the event hit. - * @package - */ -Blockly.Gesture.prototype.handleFlyoutStart = function(e, flyout) { - goog.asserts.assert(!this.hasStarted_, - 'Tried to call gesture.handleFlyoutStart, but the gesture had already been ' + - 'started.'); - this.setStartFlyout_(flyout); - this.handleWsStart(e, flyout.getWorkspace()); -}; - -/** - * Handle a mousedown/touchstart event on a block. - * @param {!Event} e A mouse down or touch start event. - * @param {!Blockly.BlockSvg} block The block the event hit. - * @package - */ -Blockly.Gesture.prototype.handleBlockStart = function(e, block) { - goog.asserts.assert(!this.hasStarted_, - 'Tried to call gesture.handleBlockStart, but the gesture had already been ' + - 'started.'); - this.setStartBlock(block); - this.mostRecentEvent_ = e; -}; - -/* Begin functions defining what actions to take to execute clicks on each type - * of target. Any developer wanting to add behaviour on clicks should modify - * only this code. */ - -/** - * Execute a field click. - * @private - */ -Blockly.Gesture.prototype.doFieldClick_ = function() { - this.startField_.showEditor_(); - this.bringBlockToFront_(); -}; - -/** - * Execute a block click. - * @private - */ -Blockly.Gesture.prototype.doBlockClick_ = function() { - // Block click in an autoclosing flyout. - if (this.flyout_ && this.flyout_.autoClose) { - if (!this.targetBlock_.disabled) { - if (!Blockly.Events.getGroup()) { - Blockly.Events.setGroup(true); - } - var newBlock = this.flyout_.createBlock(this.targetBlock_); - newBlock.scheduleSnapAndBump(); - } - } else { - // Clicks events are on the start block, even if it was a shadow. - Blockly.Events.fire( - new Blockly.Events.Ui(this.startBlock_, 'click', undefined, undefined)); - } - this.bringBlockToFront_(); - Blockly.Events.setGroup(false); -}; - -/** - * Execute a workspace click. - * @private - */ -Blockly.Gesture.prototype.doWorkspaceClick_ = function() { - if (Blockly.selected) { - Blockly.selected.unselect(); - } -}; - -/* End functions defining what actions to take to execute clicks on each type - * of target. */ - -/** - * Move the dragged/clicked block to the front of the workspace so that it is - * not occluded by other blocks. - * @private - */ -Blockly.Gesture.prototype.bringBlockToFront_ = function() { - // Blocks in the flyout don't overlap, so skip the work. - if (this.targetBlock_ && !this.flyout_) { - this.targetBlock_.bringToFront(); - } -}; - -/* Begin functions for populating a gesture at mouse down. */ - -/** - * Record the field that a gesture started on. - * @param {Blockly.Field} field The field the gesture started on. - * @package - */ -Blockly.Gesture.prototype.setStartField = function(field) { - goog.asserts.assert(!this.hasStarted_, - 'Tried to call gesture.setStartField, but the gesture had already been ' + - 'started.'); - if (!this.startField_) { - this.startField_ = field; - } -}; - -/** - * Record the block that a gesture started on, and set the target block - * appropriately. - * @param {Blockly.BlockSvg} block The block the gesture started on. - * @package - */ -Blockly.Gesture.prototype.setStartBlock = function(block) { - if (!this.startBlock_) { - this.startBlock_ = block; - if (block.isInFlyout && block != block.getRootBlock()) { - this.setTargetBlock_(block.getRootBlock()); - } else { - this.setTargetBlock_(block); - } - } -}; - -/** - * Record the block that a gesture targets, meaning the block that will be - * dragged if this turns into a drag. If this block is a shadow, that will be - * its first non-shadow parent. - * @param {Blockly.BlockSvg} block The block the gesture targets. - * @private - */ -Blockly.Gesture.prototype.setTargetBlock_ = function(block) { - if (block.isShadow()) { - this.setTargetBlock_(block.getParent()); - } else { - this.targetBlock_ = block; - } -}; - -/** - * Record the workspace that a gesture started on. - * @param {Blockly.WorkspaceSvg} ws The workspace the gesture started on. - * @private - */ -Blockly.Gesture.prototype.setStartWorkspace_ = function(ws) { - if (!this.startWorkspace_) { - this.startWorkspace_ = ws; - } -}; - -/** - * Record the flyout that a gesture started on. - * @param {Blockly.Flyout} flyout The flyout the gesture started on. - * @private - */ -Blockly.Gesture.prototype.setStartFlyout_ = function(flyout) { - if (!this.flyout_) { - this.flyout_ = flyout; - } -}; - -/* End functions for populating a gesture at mouse down. */ - -/* Begin helper functions defining types of clicks. Any developer wanting - * to change the definition of a click should modify only this code. */ - -/** - * Whether this gesture is a click on a block. This should only be called when - * ending a gesture (mouse up, touch end). - * @return {boolean} whether this gesture was a click on a block. - * @private - */ -Blockly.Gesture.prototype.isBlockClick_ = function() { - // A block click starts on a block, never escapes the drag radius, and is not - // a field click. - var hasStartBlock = !!this.startBlock_; - return hasStartBlock && !this.hasExceededDragRadius_ && !this.isFieldClick_(); -}; - -/** - * Whether this gesture is a click on a field. This should only be called when - * ending a gesture (mouse up, touch end). - * @return {boolean} whether this gesture was a click on a field. - * @private - */ -Blockly.Gesture.prototype.isFieldClick_ = function() { - var fieldEditable = this.startField_ ? - this.startField_.isCurrentlyEditable() : false; - return fieldEditable && !this.hasExceededDragRadius_ && (!this.flyout_ || - !this.flyout_.autoClose); -}; - -/** - * Whether this gesture is a click on a workspace. This should only be called - * when ending a gesture (mouse up, touch end). - * @return {boolean} whether this gesture was a click on a workspace. - * @private - */ -Blockly.Gesture.prototype.isWorkspaceClick_ = function() { - var onlyTouchedWorkspace = !this.startBlock_ && !this.startField_; - return onlyTouchedWorkspace && !this.hasExceededDragRadius_; -}; - -/* End helper functions defining types of clicks. */ - -/** - * Whether this gesture is a drag of either a workspace or block. - * This function is called externally to block actions that cannot be taken - * mid-drag (e.g. using the keyboard to delete the selected blocks). - * @return {boolean} true if this gesture is a drag of a workspace or block. - * @package - */ -Blockly.Gesture.prototype.isDragging = function() { - return this.isDraggingWorkspace_ || this.isDraggingBlock_; -}; - -/** - * Whether this gesture has already been started. In theory every mouse down - * has a corresponding mouse up, but in reality it is possible to lose a - * mouse up, leaving an in-process gesture hanging. - * @return {boolean} whether this gesture was a click on a workspace. - * @package - */ -Blockly.Gesture.prototype.hasStarted = function() { - return this.hasStarted_; -}; diff --git a/core/gesture.ts b/core/gesture.ts new file mode 100644 index 00000000000..0b65299e578 --- /dev/null +++ b/core/gesture.ts @@ -0,0 +1,1199 @@ +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The class representing an in-progress gesture, e.g. a drag, + * tap, or pinch to zoom. + * + * @class + */ +// Former goog.module ID: Blockly.Gesture + +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_click.js'; + +import * as blockAnimations from './block_animations.js'; +import type {BlockSvg} from './block_svg.js'; +import * as browserEvents from './browser_events.js'; +import {RenderedWorkspaceComment} from './comments.js'; +import * as common from './common.js'; +import {config} from './config.js'; +import * as dropDownDiv from './dropdowndiv.js'; +import {EventType} from './events/type.js'; +import * as eventUtils from './events/utils.js'; +import type {Field} from './field.js'; +import type {IBubble} from './interfaces/i_bubble.js'; +import {IDraggable, isDraggable} from './interfaces/i_draggable.js'; +import {IDragger} from './interfaces/i_dragger.js'; +import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IIcon} from './interfaces/i_icon.js'; +import * as registry from './registry.js'; +import * as Tooltip from './tooltip.js'; +import * as Touch from './touch.js'; +import {Coordinate} from './utils/coordinate.js'; +import {WorkspaceDragger} from './workspace_dragger.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Note: In this file "start" refers to pointerdown + * events. "End" refers to pointerup events. + */ + +/** A multiplier used to convert the gesture scale to a zoom in delta. */ +const ZOOM_IN_MULTIPLIER = 5; + +/** A multiplier used to convert the gesture scale to a zoom out delta. */ +const ZOOM_OUT_MULTIPLIER = 6; + +/** + * Class for one gesture. + */ +export class Gesture { + /** + * The position of the pointer when the gesture started. Units are CSS + * pixels, with (0, 0) at the top left of the browser window (pointer event + * clientX/Y). + */ + private mouseDownXY = new Coordinate(0, 0); + private currentDragDeltaXY: Coordinate; + + /** + * The bubble that the gesture started on, or null if it did not start on a + * bubble. + */ + private startBubble: IBubble | null = null; + + /** + * The field that the gesture started on, or null if it did not start on a + * field. + */ + private startField: Field | null = null; + + /** + * The icon that the gesture started on, or null if it did not start on an + * icon. + */ + private startIcon: IIcon | null = null; + + /** + * The block that the gesture started on, or null if it did not start on a + * block. + */ + private startBlock: BlockSvg | null = null; + + /** + * The comment that the gesture started on, or null if it did not start on a + * comment. + */ + private startComment: RenderedWorkspaceComment | null = null; + + /** + * The block that this gesture targets. If the gesture started on a + * shadow block, this is the first non-shadow parent of the block. If the + * gesture started in the flyout, this is the root block of the block group + * that was clicked or dragged. + */ + private targetBlock: BlockSvg | null = null; + + /** + * The workspace that the gesture started on. There may be multiple + * workspaces on a page; this is more accurate than using + * Blockly.common.getMainWorkspace(). + */ + protected startWorkspace_: WorkspaceSvg | null = null; + + /** + * Whether the pointer has at any point moved out of the drag radius. + * A gesture that exceeds the drag radius is a drag even if it ends exactly + * at its start point. + */ + private hasExceededDragRadius = false; + + /** + * Array holding info needed to unbind events. + * Used for disposing. + * Ex: [[node, name, func], [node, name, func]]. + */ + private boundEvents: browserEvents.Data[] = []; + + private dragger: IDragger | null = null; + + /** + * The object tracking a workspace or flyout workspace drag, or null if none + * is in progress. + */ + private workspaceDragger: WorkspaceDragger | null = null; + + /** Whether the gesture is dragging or not. */ + private dragging: boolean = false; + + /** The flyout a gesture started in, if any. */ + private flyout: IFlyout | null = null; + + /** Boolean for sanity-checking that some code is only called once. */ + private calledUpdateIsDragging = false; + + /** Boolean for sanity-checking that some code is only called once. */ + private gestureHasStarted = false; + + /** Boolean used internally to break a cycle in disposal. */ + protected isEnding_ = false; + + /** The event that most recently updated this gesture. */ + private mostRecentEvent: PointerEvent; + + /** Boolean for whether or not this gesture is a multi-touch gesture. */ + private multiTouch = false; + + /** A map of cached points used for tracking multi-touch gestures. */ + private cachedPoints = new Map(); + + /** + * This is the ratio between the starting distance between the touch points + * and the most recent distance between the touch points. + * Scales between 0 and 1 mean the most recent zoom was a zoom out. + * Scales above 1.0 mean the most recent zoom was a zoom in. + */ + private previousScale = 0; + + /** The starting distance between two touch points. */ + private startDistance = 0; + + /** Boolean for whether or not the workspace supports pinch-zoom. */ + private isPinchZoomEnabled: boolean | null = null; + + /** + * The owner of the dropdownDiv when this gesture first starts. + * Needed because we'll close the dropdown before fields get to + * act on their events, and some fields care about who owns + * the dropdown. + */ + currentDropdownOwner: Field | null = null; + + /** + * @param e The event that kicked off this gesture. + * @param creatorWorkspace The workspace that created this gesture and has a + * reference to it. + */ + constructor( + e: PointerEvent, + private readonly creatorWorkspace: WorkspaceSvg, + ) { + this.mostRecentEvent = e; + + /** + * How far the pointer has moved during this drag, in pixel units. + * (0, 0) is at this.mouseDownXY_. + */ + this.currentDragDeltaXY = new Coordinate(0, 0); + } + + /** + * Sever all links from this object. + * + * @internal + */ + dispose() { + Touch.clearTouchIdentifier(); + Tooltip.unblock(); + // Clear the owner's reference to this gesture. + this.creatorWorkspace.clearGesture(); + + for (const event of this.boundEvents) { + browserEvents.unbind(event); + } + this.boundEvents.length = 0; + + if (this.workspaceDragger) { + this.workspaceDragger.dispose(); + } + } + + /** + * Update internal state based on an event. + * + * @param e The most recent pointer event. + */ + private updateFromEvent(e: PointerEvent) { + const currentXY = new Coordinate(e.clientX, e.clientY); + const changed = this.updateDragDelta(currentXY); + // Exceeded the drag radius for the first time. + if (changed) { + this.updateIsDragging(e); + Touch.longStop(); + } + this.mostRecentEvent = e; + } + + /** + * DO MATH to set currentDragDeltaXY_ based on the most recent pointer + * position. + * + * @param currentXY The most recent pointer position, in pixel units, + * with (0, 0) at the window's top left corner. + * @returns True if the drag just exceeded the drag radius for the first time. + */ + private updateDragDelta(currentXY: Coordinate): boolean { + this.currentDragDeltaXY = Coordinate.difference( + currentXY, + this.mouseDownXY, + ); + + if (!this.hasExceededDragRadius) { + const currentDragDelta = Coordinate.magnitude(this.currentDragDeltaXY); + + // The flyout has a different drag radius from the rest of Blockly. + const limitRadius = this.flyout + ? config.flyoutDragRadius + : config.dragRadius; + + this.hasExceededDragRadius = currentDragDelta > limitRadius; + return this.hasExceededDragRadius; + } + return false; + } + + /** + * Update this gesture to record whether a block is being dragged from the + * flyout. + * This function should be called on a pointermove event the first time + * the drag radius is exceeded. It should be called no more than once per + * gesture. If a block should be dragged from the flyout this function creates + * the new block on the main workspace and updates targetBlock_ and + * startWorkspace_. + * + * @returns True if a block is being dragged from the flyout. + */ + private updateIsDraggingFromFlyout(): boolean { + if (!this.targetBlock || !this.flyout?.isBlockCreatable(this.targetBlock)) { + return false; + } + if (!this.flyout.targetWorkspace) { + throw new Error(`Cannot update dragging from the flyout because the ' + + 'flyout's target workspace is undefined`); + } + if ( + !this.flyout.isScrollable() || + this.flyout.isDragTowardWorkspace(this.currentDragDeltaXY) + ) { + this.startWorkspace_ = this.flyout.targetWorkspace; + this.startWorkspace_.updateScreenCalculationsIfScrolled(); + // Start the event group now, so that the same event group is used for + // block creation and block dragging. + if (!eventUtils.getGroup()) { + eventUtils.setGroup(true); + } + // The start block is no longer relevant, because this is a drag. + this.startBlock = null; + this.targetBlock = this.flyout.createBlock(this.targetBlock); + common.setSelected(this.targetBlock); + return true; + } + return false; + } + + /** + * Check whether to start a workspace drag. If a workspace is being dragged, + * create the necessary WorkspaceDragger and start the drag. + * + * This function should be called on a pointermove event the first time + * the drag radius is exceeded. It should be called no more than once per + * gesture. If a workspace is being dragged this function creates the + * necessary WorkspaceDragger and starts the drag. + */ + private updateIsDraggingWorkspace() { + if (!this.startWorkspace_) { + throw new Error( + 'Cannot update dragging the workspace because the ' + + 'start workspace is undefined', + ); + } + + const wsMovable = this.flyout + ? this.flyout.isScrollable() + : this.startWorkspace_ && this.startWorkspace_.isDraggable(); + if (!wsMovable) return; + + this.dragging = true; + this.workspaceDragger = new WorkspaceDragger(this.startWorkspace_); + + this.workspaceDragger.startDrag(); + } + + /** + * Update this gesture to record whether anything is being dragged. + * This function should be called on a pointermove event the first time + * the drag radius is exceeded. It should be called no more than once per + * gesture. + */ + private updateIsDragging(e: PointerEvent) { + if (!this.startWorkspace_) { + throw new Error( + 'Cannot update dragging because the start workspace is undefined', + ); + } + + if (this.calledUpdateIsDragging) { + throw Error('updateIsDragging_ should only be called once per gesture.'); + } + this.calledUpdateIsDragging = true; + + // If we drag a block out of the flyout, it updates `common.getSelected` + // to return the new block. + if (this.flyout) this.updateIsDraggingFromFlyout(); + + const selected = common.getSelected(); + if (selected && isDraggable(selected) && selected.isMovable()) { + this.dragging = true; + this.dragger = this.createDragger(selected, this.startWorkspace_); + this.dragger.onDragStart(e); + this.dragger.onDrag(e, this.currentDragDeltaXY); + } else { + this.updateIsDraggingWorkspace(); + } + } + + private createDragger( + draggable: IDraggable, + workspace: WorkspaceSvg, + ): IDragger { + const DraggerClass = registry.getClassFromOptions( + registry.Type.BLOCK_DRAGGER, + this.creatorWorkspace.options, + true, + ); + return new DraggerClass!(draggable, workspace); + } + + /** + * Start a gesture: update the workspace to indicate that a gesture is in + * progress and bind pointermove and pointerup handlers. + * + * @param e A pointerdown event. + * @internal + */ + doStart(e: PointerEvent) { + if (!this.startWorkspace_) { + throw new Error( + 'Cannot start the touch gesture becauase the start ' + + 'workspace is undefined', + ); + } + this.isPinchZoomEnabled = + this.startWorkspace_.options.zoomOptions && + this.startWorkspace_.options.zoomOptions.pinch; + + if (browserEvents.isTargetInput(e)) { + this.cancel(); + return; + } + + this.gestureHasStarted = true; + + blockAnimations.disconnectUiStop(); + + this.startWorkspace_.updateScreenCalculationsIfScrolled(); + if (this.startWorkspace_.isMutator) { + // Mutator's coordinate system could be out of date because the bubble was + // dragged, the block was moved, the parent workspace zoomed, etc. + this.startWorkspace_.resize(); + } + + // Keep track of which field owns the dropdown before we close it. + this.currentDropdownOwner = dropDownDiv.getOwner(); + // Hide chaff also hides the flyout, so don't do it if the click is in a + // flyout. + this.startWorkspace_.hideChaff(!!this.flyout); + + this.startWorkspace_.markFocused(); + this.mostRecentEvent = e; + + Tooltip.block(); + + if (browserEvents.isRightButton(e)) { + this.handleRightClick(e); + return; + } + + if (e.type.toLowerCase() === 'pointerdown' && e.pointerType !== 'mouse') { + Touch.longStart(e, this); + } + + this.mouseDownXY = new Coordinate(e.clientX, e.clientY); + + this.bindMouseEvents(e); + + if (!this.isEnding_) { + this.handleTouchStart(e); + } + } + + /** + * Bind gesture events. + * + * @param e A pointerdown event. + * @internal + */ + bindMouseEvents(e: PointerEvent) { + this.boundEvents.push( + browserEvents.conditionalBind( + document, + 'pointerdown', + null, + this.handleStart.bind(this), + /* opt_noCaptureIdentifier */ true, + ), + ); + this.boundEvents.push( + browserEvents.conditionalBind( + document, + 'pointermove', + null, + this.handleMove.bind(this), + /* opt_noCaptureIdentifier */ true, + ), + ); + this.boundEvents.push( + browserEvents.conditionalBind( + document, + 'pointerup', + null, + this.handleUp.bind(this), + /* opt_noCaptureIdentifier */ true, + ), + ); + + e.preventDefault(); + e.stopPropagation(); + } + + /** + * Handle a pointerdown event. + * + * @param e A pointerdown event. + * @internal + */ + handleStart(e: PointerEvent) { + if (this.isDragging()) { + // A drag has already started, so this can no longer be a pinch-zoom. + return; + } + this.handleTouchStart(e); + + if (this.isMultiTouch()) { + Touch.longStop(); + } + } + + /** + * Handle a pointermove event. + * + * @param e A pointermove event. + * @internal + */ + handleMove(e: PointerEvent) { + if ( + (this.isDragging() && Touch.shouldHandleEvent(e)) || + !this.isMultiTouch() + ) { + this.updateFromEvent(e); + if (this.workspaceDragger) { + this.workspaceDragger.drag(this.currentDragDeltaXY); + } else if (this.dragger) { + this.dragger.onDrag(this.mostRecentEvent, this.currentDragDeltaXY); + } + e.preventDefault(); + e.stopPropagation(); + } else if (this.isMultiTouch()) { + this.handleTouchMove(e); + Touch.longStop(); + } + } + + /** + * Handle a pointerup event. + * + * @param e A pointerup event. + * @internal + */ + handleUp(e: PointerEvent) { + if (!this.isDragging()) { + this.handleTouchEnd(e); + } + if (!this.isMultiTouch() || this.isDragging()) { + if (!Touch.shouldHandleEvent(e)) { + return; + } + this.updateFromEvent(e); + Touch.longStop(); + + if (this.isEnding_) { + console.log('Trying to end a gesture recursively.'); + return; + } + this.isEnding_ = true; + // The ordering of these checks is important: drags have higher priority + // than clicks. Fields and icons have higher priority than blocks; blocks + // have higher priority than workspaces. The ordering within drags does + // not matter, because the three types of dragging are exclusive. + if (this.dragger) { + this.dragger.onDragEnd(e, this.currentDragDeltaXY); + } else if (this.workspaceDragger) { + this.workspaceDragger.endDrag(this.currentDragDeltaXY); + } else if (this.isBubbleClick()) { + // Do nothing, bubbles don't currently respond to clicks. + } else if (this.isCommentClick()) { + // Do nothing, comments don't currently respond to clicks. + } else if (this.isFieldClick()) { + this.doFieldClick(); + } else if (this.isIconClick()) { + this.doIconClick(); + } else if (this.isBlockClick()) { + this.doBlockClick(); + } else if (this.isWorkspaceClick()) { + this.doWorkspaceClick(e); + } + + e.preventDefault(); + e.stopPropagation(); + + this.dispose(); + } else { + e.preventDefault(); + e.stopPropagation(); + + this.dispose(); + } + } + + /** + * Handle a pointerdown event and keep track of current + * pointers. + * + * @param e A pointerdown event. + * @internal + */ + handleTouchStart(e: PointerEvent) { + const pointerId = Touch.getTouchIdentifierFromEvent(e); + // store the pointerId in the current list of pointers + this.cachedPoints.set(pointerId, this.getTouchPoint(e)); + const pointers = Array.from(this.cachedPoints.keys()); + // If two pointers are down, store info + if (pointers.length === 2) { + const point0 = this.cachedPoints.get(pointers[0])!; + const point1 = this.cachedPoints.get(pointers[1])!; + this.startDistance = Coordinate.distance(point0, point1); + this.multiTouch = true; + e.preventDefault(); + } + } + + /** + * Handle a pointermove event and zoom in/out if two pointers + * are on the screen. + * + * @param e A pointermove event. + * @internal + */ + handleTouchMove(e: PointerEvent) { + const pointerId = Touch.getTouchIdentifierFromEvent(e); + this.cachedPoints.set(pointerId, this.getTouchPoint(e)); + + if (this.isPinchZoomEnabled && this.cachedPoints.size === 2) { + this.handlePinch(e); + } else { + // Handle the move directly instead of calling handleMove + this.updateFromEvent(e); + if (this.workspaceDragger) { + this.workspaceDragger.drag(this.currentDragDeltaXY); + } else if (this.dragger) { + this.dragger.onDrag(this.mostRecentEvent, this.currentDragDeltaXY); + } + e.preventDefault(); + e.stopPropagation(); + } + } + + /** + * Handle pinch zoom gesture. + * + * @param e A pointermove event. + */ + private handlePinch(e: PointerEvent) { + const pointers = Array.from(this.cachedPoints.keys()); + // Calculate the distance between the two pointers + const point0 = this.cachedPoints.get(pointers[0])!; + const point1 = this.cachedPoints.get(pointers[1])!; + const moveDistance = Coordinate.distance(point0, point1); + const scale = moveDistance / this.startDistance; + + if (this.previousScale > 0 && this.previousScale < Infinity) { + const gestureScale = scale - this.previousScale; + const delta = + gestureScale > 0 + ? gestureScale * ZOOM_IN_MULTIPLIER + : gestureScale * ZOOM_OUT_MULTIPLIER; + if (!this.startWorkspace_) { + throw new Error( + 'Cannot handle a pinch because the start workspace ' + 'is undefined', + ); + } + const workspace = this.startWorkspace_; + const position = browserEvents.mouseToSvg( + e, + workspace.getParentSvg(), + workspace.getInverseScreenCTM(), + ); + workspace.zoom(position.x, position.y, delta); + } + this.previousScale = scale; + e.preventDefault(); + } + + /** + * Handle a pointerup event and end the gesture. + * + * @param e A pointerup event. + * @internal + */ + handleTouchEnd(e: PointerEvent) { + const pointerId = Touch.getTouchIdentifierFromEvent(e); + if (this.cachedPoints.has(pointerId)) { + this.cachedPoints.delete(pointerId); + } + if (this.cachedPoints.size < 2) { + this.cachedPoints.clear(); + this.previousScale = 0; + } + } + + /** + * Helper function returning the current touch point coordinate. + * + * @param e A pointer event. + * @returns The current touch point coordinate + * @internal + */ + getTouchPoint(e: PointerEvent): Coordinate | null { + if (!this.startWorkspace_) { + return null; + } + return new Coordinate(e.pageX, e.pageY); + } + + /** + * Whether this gesture is part of a multi-touch gesture. + * + * @returns Whether this gesture is part of a multi-touch gesture. + * @internal + */ + isMultiTouch(): boolean { + return this.multiTouch; + } + + /** + * Cancel an in-progress gesture. If a workspace or block drag is in + * progress, end the drag at the most recent location. + * + * @internal + */ + cancel() { + // Disposing of a block cancels in-progress drags, but dragging to a delete + // area disposes of a block and leads to recursive disposal. Break that + // cycle. + if (this.isEnding_) { + return; + } + Touch.longStop(); + if (this.dragger) { + this.dragger.onDragEnd(this.mostRecentEvent, this.currentDragDeltaXY); + } else if (this.workspaceDragger) { + this.workspaceDragger.endDrag(this.currentDragDeltaXY); + } + this.dispose(); + } + + /** + * Handle a real or faked right-click event by showing a context menu. + * + * @param e A pointerdown event. + * @internal + */ + handleRightClick(e: PointerEvent) { + if (this.targetBlock) { + this.bringBlockToFront(); + this.targetBlock.workspace.hideChaff(!!this.flyout); + this.targetBlock.showContextMenu(e); + } else if (this.startBubble) { + this.startBubble.showContextMenu(e); + } else if (this.startComment) { + this.startComment.workspace.hideChaff(); + this.startComment.showContextMenu(e); + } else if (this.startWorkspace_ && !this.flyout) { + this.startWorkspace_.hideChaff(); + this.startWorkspace_.showContextMenu(e); + } + + // TODO: Handle right-click on a bubble. + e.preventDefault(); + e.stopPropagation(); + + this.dispose(); + } + + /** + * Handle a pointerdown event on a workspace. + * + * @param e A pointerdown event. + * @param ws The workspace the event hit. + * @internal + */ + handleWsStart(e: PointerEvent, ws: WorkspaceSvg) { + if (this.gestureHasStarted) { + throw Error( + 'Tried to call gesture.handleWsStart, ' + + 'but the gesture had already been started.', + ); + } + this.setStartWorkspace(ws); + this.mostRecentEvent = e; + + if (!this.startBlock && !this.startBubble && !this.startComment) { + // Selection determines what things start drags. So to drag the workspace, + // we need to deselect anything that was previously selected. + common.setSelected(null); + } + + this.doStart(e); + } + + /** + * Fires a workspace click event. + * + * @param ws The workspace that a user clicks on. + */ + private fireWorkspaceClick(ws: WorkspaceSvg) { + eventUtils.fire( + new (eventUtils.get(EventType.CLICK))(null, ws.id, 'workspace'), + ); + } + + /** + * Handle a pointerdown event on a flyout. + * + * @param e A pointerdown event. + * @param flyout The flyout the event hit. + * @internal + */ + handleFlyoutStart(e: PointerEvent, flyout: IFlyout) { + if (this.gestureHasStarted) { + throw Error( + 'Tried to call gesture.handleFlyoutStart, ' + + 'but the gesture had already been started.', + ); + } + this.setStartFlyout(flyout); + this.handleWsStart(e, flyout.getWorkspace()); + } + + /** + * Handle a pointerdown event on a block. + * + * @param e A pointerdown event. + * @param block The block the event hit. + * @internal + */ + handleBlockStart(e: PointerEvent, block: BlockSvg) { + if (this.gestureHasStarted) { + throw Error( + 'Tried to call gesture.handleBlockStart, ' + + 'but the gesture had already been started.', + ); + } + this.setStartBlock(block); + this.mostRecentEvent = e; + } + + /** + * Handle a pointerdown event on a bubble. + * + * @param e A pointerdown event. + * @param bubble The bubble the event hit. + * @internal + */ + handleBubbleStart(e: PointerEvent, bubble: IBubble) { + if (this.gestureHasStarted) { + throw Error( + 'Tried to call gesture.handleBubbleStart, ' + + 'but the gesture had already been started.', + ); + } + this.setStartBubble(bubble); + this.mostRecentEvent = e; + } + + /** + * Handle a pointerdown event on a workspace comment. + * + * @param e A pointerdown event. + * @param comment The comment the event hit. + * @internal + */ + handleCommentStart(e: PointerEvent, comment: RenderedWorkspaceComment) { + if (this.gestureHasStarted) { + throw Error( + 'Tried to call gesture.handleCommentStart, ' + + 'but the gesture had already been started.', + ); + } + this.setStartComment(comment); + this.mostRecentEvent = e; + } + + /* Begin functions defining what actions to take to execute clicks on each + * type of target. Any developer wanting to add behaviour on clicks should + * modify only this code. */ + + /** Execute a field click. */ + private doFieldClick() { + if (!this.startField) { + throw new Error( + 'Cannot do a field click because the start field is undefined', + ); + } + + // Only show the editor if the field's editor wasn't already open + // right before this gesture started. + const dropdownAlreadyOpen = this.currentDropdownOwner === this.startField; + if (!dropdownAlreadyOpen) { + this.startField.showEditor(this.mostRecentEvent); + } + this.bringBlockToFront(); + } + + /** Execute an icon click. */ + private doIconClick() { + if (!this.startIcon) { + throw new Error( + 'Cannot do an icon click because the start icon is undefined', + ); + } + this.bringBlockToFront(); + this.startIcon.onClick(); + } + + /** Execute a block click. */ + private doBlockClick() { + // Block click in an autoclosing flyout. + if (this.flyout && this.flyout.autoClose) { + if (!this.targetBlock) { + throw new Error( + 'Cannot do a block click because the target block is ' + 'undefined', + ); + } + if (this.targetBlock.isEnabled()) { + if (!eventUtils.getGroup()) { + eventUtils.setGroup(true); + } + const newBlock = this.flyout.createBlock(this.targetBlock); + newBlock.snapToGrid(); + newBlock.bumpNeighbours(); + } + } else { + if (!this.startWorkspace_) { + throw new Error( + 'Cannot do a block click because the start workspace ' + + 'is undefined', + ); + } + // Clicks events are on the start block, even if it was a shadow. + const event = new (eventUtils.get(EventType.CLICK))( + this.startBlock, + this.startWorkspace_.id, + 'block', + ); + eventUtils.fire(event); + } + this.bringBlockToFront(); + eventUtils.setGroup(false); + } + + /** + * Execute a workspace click. When in accessibility mode shift clicking will + * move the cursor. + * + * @param _e A pointerup event. + */ + private doWorkspaceClick(_e: PointerEvent) { + const ws = this.creatorWorkspace; + if (common.getSelected()) { + common.getSelected()!.unselect(); + } + this.fireWorkspaceClick(this.startWorkspace_ || ws); + } + + /* End functions defining what actions to take to execute clicks on each type + * of target. */ + + // TODO (fenichel): Move bubbles to the front. + + /** + * Move the dragged/clicked block to the front of the workspace so that it is + * not occluded by other blocks. + */ + private bringBlockToFront() { + // Blocks in the flyout don't overlap, so skip the work. + if (this.targetBlock && !this.flyout) { + this.targetBlock.bringToFront(); + } + } + + /* Begin functions for populating a gesture at pointerdown. */ + + /** + * Record the field that a gesture started on. + * + * @param field The field the gesture started on. + * @internal + */ + setStartField(field: Field) { + if (this.gestureHasStarted) { + throw Error( + 'Tried to call gesture.setStartField, ' + + 'but the gesture had already been started.', + ); + } + if (!this.startField) { + this.startField = field as Field; + } + } + + /** + * Record the icon that a gesture started on. + * + * @param icon The icon the gesture started on. + * @internal + */ + setStartIcon(icon: IIcon) { + if (this.gestureHasStarted) { + throw Error( + 'Tried to call gesture.setStartIcon, ' + + 'but the gesture had already been started.', + ); + } + + if (!this.startIcon) this.startIcon = icon; + } + + /** + * Record the bubble that a gesture started on + * + * @param bubble The bubble the gesture started on. + * @internal + */ + setStartBubble(bubble: IBubble) { + if (!this.startBubble) { + this.startBubble = bubble; + } + } + + /** + * Record the comment that a gesture started on + * + * @param comment The comment the gesture started on. + * @internal + */ + setStartComment(comment: RenderedWorkspaceComment) { + if (!this.startComment) { + this.startComment = comment; + } + } + + /** + * Record the block that a gesture started on, and set the target block + * appropriately. + * + * @param block The block the gesture started on. + * @internal + */ + setStartBlock(block: BlockSvg) { + // If the gesture already went through a bubble, don't set the start block. + if (!this.startBlock && !this.startBubble) { + this.startBlock = block; + common.setSelected(this.startBlock); + if (block.isInFlyout && block !== block.getRootBlock()) { + this.setTargetBlock(block.getRootBlock()); + } else { + this.setTargetBlock(block); + } + } + } + + /** + * Record the block that a gesture targets, meaning the block that will be + * dragged if this turns into a drag. If this block is a shadow, that will be + * its first non-shadow parent. + * + * @param block The block the gesture targets. + */ + private setTargetBlock(block: BlockSvg) { + if (block.isShadow()) { + // Non-null assertion is fine b/c it is an invariant that shadows always + // have parents. + this.setTargetBlock(block.getParent()!); + } else { + this.targetBlock = block; + } + } + + /** + * Record the workspace that a gesture started on. + * + * @param ws The workspace the gesture started on. + */ + private setStartWorkspace(ws: WorkspaceSvg) { + if (!this.startWorkspace_) { + this.startWorkspace_ = ws; + } + } + + /** + * Record the flyout that a gesture started on. + * + * @param flyout The flyout the gesture started on. + */ + private setStartFlyout(flyout: IFlyout) { + if (!this.flyout) { + this.flyout = flyout; + } + } + + /* End functions for populating a gesture at pointerdown. */ + + /* Begin helper functions defining types of clicks. Any developer wanting + * to change the definition of a click should modify only this code. */ + + /** + * Whether this gesture is a click on a bubble. This should only be called + * when ending a gesture (pointerup). + * + * @returns Whether this gesture was a click on a bubble. + */ + private isBubbleClick(): boolean { + // A bubble click starts on a bubble and never escapes the drag radius. + const hasStartBubble = !!this.startBubble; + return hasStartBubble && !this.hasExceededDragRadius; + } + + private isCommentClick(): boolean { + return !!this.startComment && !this.hasExceededDragRadius; + } + + /** + * Whether this gesture is a click on a block. This should only be called + * when ending a gesture (pointerup). + * + * @returns Whether this gesture was a click on a block. + */ + private isBlockClick(): boolean { + // A block click starts on a block, never escapes the drag radius, and is + // not a field click. + const hasStartBlock = !!this.startBlock; + return ( + hasStartBlock && + !this.hasExceededDragRadius && + !this.isFieldClick() && + !this.isIconClick() + ); + } + + /** + * Whether this gesture is a click on a field that should be handled. This should only be called + * when ending a gesture (pointerup). + * + * @returns Whether this gesture was a click on a field. + */ + private isFieldClick(): boolean { + if (!this.startField) return false; + return ( + this.startField.isClickable() && + !this.hasExceededDragRadius && + (!this.flyout || + this.startField.isClickableInFlyout(this.flyout.autoClose)) + ); + } + + /** @returns Whether this gesture is a click on an icon that should be handled. */ + private isIconClick(): boolean { + if (!this.startIcon) return false; + const handleInFlyout = + !this.flyout || + !this.startIcon.isClickableInFlyout || + this.startIcon.isClickableInFlyout(this.flyout.autoClose); + return !this.hasExceededDragRadius && handleInFlyout; + } + + /** + * Whether this gesture is a click on a workspace. This should only be called + * when ending a gesture (pointerup). + * + * @returns Whether this gesture was a click on a workspace. + */ + private isWorkspaceClick(): boolean { + const onlyTouchedWorkspace = + !this.startBlock && !this.startBubble && !this.startField; + return onlyTouchedWorkspace && !this.hasExceededDragRadius; + } + + /* End helper functions defining types of clicks. */ + + /** Returns the current dragger if the gesture is a drag. */ + getCurrentDragger(): WorkspaceDragger | IDragger | null { + return this.workspaceDragger ?? this.dragger ?? null; + } + + /** + * Whether this gesture is a drag of either a workspace or block. + * This function is called externally to block actions that cannot be taken + * mid-drag (e.g. using the keyboard to delete the selected blocks). + * + * @returns True if this gesture is a drag of a workspace or block. + * @internal + */ + isDragging(): boolean { + return this.dragging; + } + + /** + * Whether this gesture has already been started. In theory every pointerdown + * has a corresponding pointerup, but in reality it is possible to lose a + * pointerup, leaving an in-process gesture hanging. + * + * @returns Whether this gesture was a click on a workspace. + * @internal + */ + hasStarted(): boolean { + return this.gestureHasStarted; + } + + /** + * Is a drag or other gesture currently in progress on any workspace? + * + * @returns True if gesture is occurring. + */ + static inProgress(): boolean { + const workspaces = common.getAllWorkspaces(); + for (let i = 0, workspace; (workspace = workspaces[i]); i++) { + // Not actually necessarily a WorkspaceSvg, but it doesn't matter b/c + // we're just checking if the property exists. Theoretically we would + // want to use instanceof, but that causes a circular dependency. + if ((workspace as WorkspaceSvg).currentGesture_) { + return true; + } + } + return false; + } +} diff --git a/core/grid.js b/core/grid.js deleted file mode 100644 index e87df60218b..00000000000 --- a/core/grid.js +++ /dev/null @@ -1,222 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2017 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Object for configuring and updating a workspace grid in - * Blockly. - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -goog.provide('Blockly.Grid'); - -goog.require('Blockly.utils'); - -goog.require('goog.userAgent'); - - -/** - * Class for a workspace's grid. - * @param {!SVGElement} pattern The grid's SVG pattern, created during injection. - * @param {!Object} options A dictionary of normalized options for the grid. - * See grid documentation: - * https://developers.google.com/blockly/guides/configure/web/grid - * @constructor - */ -Blockly.Grid = function(pattern, options) { - /** - * The grid's SVG pattern, created during injection. - * @type {!SVGElement} - * @private - */ - this.gridPattern_ = pattern; - - /** - * The spacing of the grid lines (in px). - * @type {number} - * @private - */ - this.spacing_ = options['spacing']; - - /** - * How long the grid lines should be (in px). - * @type {number} - * @private - */ - this.length_ = options['length']; - - /** - * The horizontal grid line, if it exists. - * @type {SVGElement} - * @private - */ - this.line1_ = pattern.firstChild; - - /** - * The vertical grid line, if it exists. - * @type {SVGElement} - * @private - */ - this.line2_ = this.line1_ && this.line1_.nextSibling; - - /** - * Whether blocks should snap to the grid. - * @type {boolean} - * @private - */ - this.snapToGrid_ = options['snap']; -}; - -/** - * The scale of the grid, used to set stroke width on grid lines. - * This should always be the same as the workspace scale. - * @type {number} - * @private - */ -Blockly.Grid.prototype.scale_ = 1; - -/** - * Dispose of this grid and unlink from the DOM. - * @package - */ -Blockly.Grid.prototype.dispose = function() { - this.gridPattern_ = null; -}; - -/** - * Whether blocks should snap to the grid, based on the initial configuration. - * @return {boolean} True if blocks should snap, false otherwise. - * @package - */ -Blockly.Grid.prototype.shouldSnap = function() { - return this.snapToGrid_; -}; - -/** - * Get the spacing of the grid points (in px). - * @return {number} The spacing of the grid points. - * @package - */ -Blockly.Grid.prototype.getSpacing = function() { - return this.spacing_; -}; - -/** - * Get the id of the pattern element, which should be randomized to avoid - * conflicts with other Blockly instances on the page. - * @return {string} The pattern id. - * @package - */ -Blockly.Grid.prototype.getPatternId = function() { - return this.gridPattern_.id; -}; - -/** - * Update the grid with a new scale. - * @param {number} scale The new workspace scale. - * @package - */ -Blockly.Grid.prototype.update = function(scale) { - this.scale_ = scale; - // MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100. - var safeSpacing = (this.spacing_ * scale) || 100; - - this.gridPattern_.setAttribute('width', safeSpacing); - this.gridPattern_.setAttribute('height', safeSpacing); - - var half = Math.floor(this.spacing_ / 2) + 0.5; - var start = half - this.length_ / 2; - var end = half + this.length_ / 2; - - half *= scale; - start *= scale; - end *= scale; - - this.setLineAttributes_(this.line1_, scale, start, end, half, half); - this.setLineAttributes_(this.line2_, scale, half, half, start, end); -}; - -/** - * Set the attributes on one of the lines in the grid. Use this to update the - * length and stroke width of the grid lines. - * @param {!SVGElement} line Which line to update. - * @param {number} width The new stroke size (in px). - * @param {number} x1 The new x start position of the line (in px). - * @param {number} x2 The new x end position of the line (in px). - * @param {number} y1 The new y start position of the line (in px). - * @param {number} y2 The new y end position of the line (in px). - * @private - */ -Blockly.Grid.prototype.setLineAttributes_ = function(line, width, x1, x2, y1, y2) { - if (line) { - line.setAttribute('stroke-width', width); - line.setAttribute('x1', x1); - line.setAttribute('y1', y1); - line.setAttribute('x2', x2); - line.setAttribute('y2', y2); - } -}; - -/** - * Move the grid to a new x and y position, and make sure that change is visible. - * @param {number} x The new x position of the grid (in px). - * @param {number} y The new y position ofthe grid (in px). - * @package - */ -Blockly.Grid.prototype.moveTo = function(x, y) { - this.gridPattern_.setAttribute('x', x); - this.gridPattern_.setAttribute('y', y); - - if (goog.userAgent.IE || goog.userAgent.EDGE) { - // IE/Edge doesn't notice that the x/y offsets have changed. - // Force an update. - this.update(this.scale_); - } -}; - -/** - * Create the DOM for the grid described by options. - * @param {string} rnd A random ID to append to the pattern's ID. - * @param {!Object} gridOptions The object containing grid configuration. - * @param {!SVGElement} defs The root SVG element for this workspace's defs. - * @return {!SVGElement} The SVG element for the grid pattern. - * @package - */ -Blockly.Grid.createDom = function(rnd, gridOptions, defs) { - /* - - - - - */ - var gridPattern = Blockly.utils.createSvgElement('pattern', - {'id': 'blocklyGridPattern' + rnd, - 'patternUnits': 'userSpaceOnUse'}, defs); - if (gridOptions['length'] > 0 && gridOptions['spacing'] > 0) { - Blockly.utils.createSvgElement('line', - {'stroke': gridOptions['colour']}, gridPattern); - if (gridOptions['length'] > 1) { - Blockly.utils.createSvgElement('line', - {'stroke': gridOptions['colour']}, gridPattern); - } - // x1, y1, x1, x2 properties will be set later in update. - } - return gridPattern; -}; diff --git a/core/grid.ts b/core/grid.ts new file mode 100644 index 00000000000..e2fc054a262 --- /dev/null +++ b/core/grid.ts @@ -0,0 +1,252 @@ +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Object for configuring and updating a workspace grid in + * Blockly. + * + * @class + */ +// Former goog.module ID: Blockly.Grid + +import {GridOptions} from './options.js'; +import {Coordinate} from './utils/coordinate.js'; +import * as dom from './utils/dom.js'; +import {Svg} from './utils/svg.js'; + +/** + * Class for a workspace's grid. + */ +export class Grid { + private spacing: number; + private length: number; + private scale: number = 1; + private readonly line1: SVGElement; + private readonly line2: SVGElement; + private snapToGrid: boolean; + + /** + * @param pattern The grid's SVG pattern, created during injection. + * @param options A dictionary of normalized options for the grid. + * See grid documentation: + * https://developers.google.com/blockly/guides/configure/web/grid + */ + constructor( + private pattern: SVGElement, + options: GridOptions, + ) { + /** The spacing of the grid lines (in px). */ + this.spacing = options['spacing'] ?? 0; + + /** How long the grid lines should be (in px). */ + this.length = options['length'] ?? 1; + + /** The horizontal grid line, if it exists. */ + this.line1 = pattern.firstChild as SVGElement; + + /** The vertical grid line, if it exists. */ + this.line2 = this.line1 && (this.line1.nextSibling as SVGElement); + + /** Whether blocks should snap to the grid. */ + this.snapToGrid = options['snap'] ?? false; + } + + /** + * Sets the spacing between the centers of the grid lines. + * + * This does not trigger snapping to the newly spaced grid. If you want to + * snap blocks to the grid programmatically that needs to be triggered + * on individual top-level blocks. The next time a block is dragged and + * dropped it will snap to the grid if snapping to the grid is enabled. + */ + setSpacing(spacing: number) { + this.spacing = spacing; + this.update(this.scale); + } + + /** + * Get the spacing of the grid points (in px). + * + * @returns The spacing of the grid points. + */ + getSpacing(): number { + return this.spacing; + } + + /** Sets the length of the grid lines. */ + setLength(length: number) { + this.length = length; + this.update(this.scale); + } + + /** Get the length of the grid lines (in px). */ + getLength(): number { + return this.length; + } + + /** + * Sets whether blocks should snap to the grid or not. + * + * Setting this to true does not trigger snapping. If you want to snap blocks + * to the grid programmatically that needs to be triggered on individual + * top-level blocks. The next time a block is dragged and dropped it will + * snap to the grid. + */ + setSnapToGrid(snap: boolean) { + this.snapToGrid = snap; + } + + /** + * Whether blocks should snap to the grid. + * + * @returns True if blocks should snap, false otherwise. + */ + shouldSnap(): boolean { + return this.snapToGrid; + } + + /** + * Get the ID of the pattern element, which should be randomized to avoid + * conflicts with other Blockly instances on the page. + * + * @returns The pattern ID. + * @internal + */ + getPatternId(): string { + return this.pattern.id; + } + + /** + * Update the grid with a new scale. + * + * @param scale The new workspace scale. + * @internal + */ + update(scale: number) { + this.scale = scale; + const safeSpacing = this.spacing * scale; + + this.pattern.setAttribute('width', `${safeSpacing}`); + this.pattern.setAttribute('height', `${safeSpacing}`); + + let half = Math.floor(this.spacing / 2) + 0.5; + let start = half - this.length / 2; + let end = half + this.length / 2; + + half *= scale; + start *= scale; + end *= scale; + + this.setLineAttributes(this.line1, scale, start, end, half, half); + this.setLineAttributes(this.line2, scale, half, half, start, end); + } + + /** + * Set the attributes on one of the lines in the grid. Use this to update the + * length and stroke width of the grid lines. + * + * @param line Which line to update. + * @param width The new stroke size (in px). + * @param x1 The new x start position of the line (in px). + * @param x2 The new x end position of the line (in px). + * @param y1 The new y start position of the line (in px). + * @param y2 The new y end position of the line (in px). + */ + private setLineAttributes( + line: SVGElement, + width: number, + x1: number, + x2: number, + y1: number, + y2: number, + ) { + if (line) { + line.setAttribute('stroke-width', `${width}`); + line.setAttribute('x1', `${x1}`); + line.setAttribute('y1', `${y1}`); + line.setAttribute('x2', `${x2}`); + line.setAttribute('y2', `${y2}`); + } + } + + /** + * Move the grid to a new x and y position, and make sure that change is + * visible. + * + * @param x The new x position of the grid (in px). + * @param y The new y position of the grid (in px). + * @internal + */ + moveTo(x: number, y: number) { + this.pattern.setAttribute('x', `${x}`); + this.pattern.setAttribute('y', `${y}`); + } + + /** + * Given a coordinate, return the nearest coordinate aligned to the grid. + * + * @param xy A workspace coordinate. + * @returns Workspace coordinate of nearest grid point. + * If there's no change, return the same coordinate object. + */ + alignXY(xy: Coordinate): Coordinate { + const spacing = this.getSpacing(); + const half = spacing / 2; + const x = Math.round(Math.round((xy.x - half) / spacing) * spacing + half); + const y = Math.round(Math.round((xy.y - half) / spacing) * spacing + half); + if (x === xy.x && y === xy.y) { + // No change. + return xy; + } + return new Coordinate(x, y); + } + + /** + * Create the DOM for the grid described by options. + * + * @param rnd A random ID to append to the pattern's ID. + * @param gridOptions The object containing grid configuration. + * @param defs The root SVG element for this workspace's defs. + * @returns The SVG element for the grid pattern. + * @internal + */ + static createDom( + rnd: string, + gridOptions: GridOptions, + defs: SVGElement, + ): SVGElement { + /* + + + + + */ + const gridPattern = dom.createSvgElement( + Svg.PATTERN, + {'id': 'blocklyGridPattern' + rnd, 'patternUnits': 'userSpaceOnUse'}, + defs, + ); + // x1, y1, x1, x2 properties will be set later in update. + if ((gridOptions['length'] ?? 1) > 0 && (gridOptions['spacing'] ?? 0) > 0) { + dom.createSvgElement( + Svg.LINE, + {'stroke': gridOptions['colour']}, + gridPattern, + ); + if (gridOptions['length'] ?? 1 > 1) { + dom.createSvgElement( + Svg.LINE, + {'stroke': gridOptions['colour']}, + gridPattern, + ); + } + } else { + // Edge 16 doesn't handle empty patterns + dom.createSvgElement(Svg.LINE, {}, gridPattern); + } + return gridPattern; + } +} diff --git a/core/icon.js b/core/icon.js deleted file mode 100644 index 37dd6768d33..00000000000 --- a/core/icon.js +++ /dev/null @@ -1,205 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2013 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Object representing an icon on a block. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Icon'); - -goog.require('goog.dom'); -goog.require('goog.math.Coordinate'); - - -/** - * Class for an icon. - * @param {Blockly.Block} block The block associated with this icon. - * @constructor - */ -Blockly.Icon = function(block) { - this.block_ = block; -}; - -/** - * Does this icon get hidden when the block is collapsed. - */ -Blockly.Icon.prototype.collapseHidden = true; - -/** - * Height and width of icons. - */ -Blockly.Icon.prototype.SIZE = 17; - -/** - * Bubble UI (if visible). - * @type {Blockly.Bubble} - * @private - */ -Blockly.Icon.prototype.bubble_ = null; - -/** - * Absolute coordinate of icon's center. - * @type {goog.math.Coordinate} - * @private - */ -Blockly.Icon.prototype.iconXY_ = null; - -/** - * Create the icon on the block. - */ -Blockly.Icon.prototype.createIcon = function() { - if (this.iconGroup_) { - // Icon already exists. - return; - } - /* Here's the markup that will be generated: - - ... - - */ - this.iconGroup_ = Blockly.utils.createSvgElement('g', - {'class': 'blocklyIconGroup'}, null); - if (this.block_.isInFlyout) { - Blockly.utils.addClass(/** @type {!Element} */ (this.iconGroup_), - 'blocklyIconGroupReadonly'); - } - this.drawIcon_(this.iconGroup_); - - this.block_.getSvgRoot().appendChild(this.iconGroup_); - Blockly.bindEventWithChecks_(this.iconGroup_, 'mouseup', this, - this.iconClick_); - this.updateEditable(); -}; - -/** - * Dispose of this icon. - */ -Blockly.Icon.prototype.dispose = function() { - // Dispose of and unlink the icon. - goog.dom.removeNode(this.iconGroup_); - this.iconGroup_ = null; - // Dispose of and unlink the bubble. - this.setVisible(false); - this.block_ = null; -}; - -/** - * Add or remove the UI indicating if this icon may be clicked or not. - */ -Blockly.Icon.prototype.updateEditable = function() { -}; - -/** - * Is the associated bubble visible? - * @return {boolean} True if the bubble is visible. - */ -Blockly.Icon.prototype.isVisible = function() { - return !!this.bubble_; -}; - -/** - * Clicking on the icon toggles if the bubble is visible. - * @param {!Event} e Mouse click event. - * @private - */ -Blockly.Icon.prototype.iconClick_ = function(e) { - if (this.block_.workspace.isDragging()) { - // Drag operation is concluding. Don't open the editor. - return; - } - if (!this.block_.isInFlyout && !Blockly.utils.isRightButton(e)) { - this.setVisible(!this.isVisible()); - } -}; - -/** - * Change the colour of the associated bubble to match its block. - */ -Blockly.Icon.prototype.updateColour = function() { - if (this.isVisible()) { - this.bubble_.setColour(this.block_.getColour()); - } -}; - -/** - * Render the icon. - * @param {number} cursorX Horizontal offset at which to position the icon. - * @return {number} Horizontal offset for next item to draw. - */ -Blockly.Icon.prototype.renderIcon = function(cursorX) { - if (this.collapseHidden && this.block_.isCollapsed()) { - this.iconGroup_.setAttribute('display', 'none'); - return cursorX; - } - this.iconGroup_.setAttribute('display', 'block'); - - var TOP_MARGIN = 5; - var width = this.SIZE; - if (this.block_.RTL) { - cursorX -= width; - } - this.iconGroup_.setAttribute('transform', - 'translate(' + cursorX + ',' + TOP_MARGIN + ')'); - this.computeIconLocation(); - if (this.block_.RTL) { - cursorX -= Blockly.BlockSvg.SEP_SPACE_X; - } else { - cursorX += width + Blockly.BlockSvg.SEP_SPACE_X; - } - return cursorX; -}; - -/** - * Notification that the icon has moved. Update the arrow accordingly. - * @param {!goog.math.Coordinate} xy Absolute location in workspace coordinates. - */ -Blockly.Icon.prototype.setIconLocation = function(xy) { - this.iconXY_ = xy; - if (this.isVisible()) { - this.bubble_.setAnchorLocation(xy); - } -}; - -/** - * Notification that the icon has moved, but we don't really know where. - * Recompute the icon's location from scratch. - */ -Blockly.Icon.prototype.computeIconLocation = function() { - // Find coordinates for the centre of the icon and update the arrow. - var blockXY = this.block_.getRelativeToSurfaceXY(); - var iconXY = Blockly.utils.getRelativeXY(this.iconGroup_); - var newXY = new goog.math.Coordinate( - blockXY.x + iconXY.x + this.SIZE / 2, - blockXY.y + iconXY.y + this.SIZE / 2); - if (!goog.math.Coordinate.equals(this.getIconLocation(), newXY)) { - this.setIconLocation(newXY); - } -}; - -/** - * Returns the center of the block's icon relative to the surface. - * @return {!goog.math.Coordinate} Object with x and y properties in workspace - * coordinates. - */ -Blockly.Icon.prototype.getIconLocation = function() { - return this.iconXY_; -}; diff --git a/core/icons.ts b/core/icons.ts new file mode 100644 index 00000000000..fcc7c98c663 --- /dev/null +++ b/core/icons.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {CommentIcon, CommentState} from './icons/comment_icon.js'; +import * as exceptions from './icons/exceptions.js'; +import {Icon} from './icons/icon.js'; +import {IconType} from './icons/icon_types.js'; +import {MutatorIcon} from './icons/mutator_icon.js'; +import * as registry from './icons/registry.js'; +import {WarningIcon} from './icons/warning_icon.js'; + +export { + CommentIcon, + CommentState, + exceptions, + Icon, + IconType, + MutatorIcon, + registry, + WarningIcon, +}; diff --git a/core/icons/comment_icon.ts b/core/icons/comment_icon.ts new file mode 100644 index 00000000000..ea120ca1728 --- /dev/null +++ b/core/icons/comment_icon.ts @@ -0,0 +1,360 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.Comment + +import type {Block} from '../block.js'; +import type {BlockSvg} from '../block_svg.js'; +import {TextInputBubble} from '../bubbles/textinput_bubble.js'; +import {EventType} from '../events/type.js'; +import * as eventUtils from '../events/utils.js'; +import type {IHasBubble} from '../interfaces/i_has_bubble.js'; +import type {ISerializable} from '../interfaces/i_serializable.js'; +import * as renderManagement from '../render_management.js'; +import {Coordinate} from '../utils.js'; +import * as dom from '../utils/dom.js'; +import {Rect} from '../utils/rect.js'; +import {Size} from '../utils/size.js'; +import {Svg} from '../utils/svg.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import {Icon} from './icon.js'; +import {IconType} from './icon_types.js'; +import * as registry from './registry.js'; + +/** The size of the comment icon in workspace-scale units. */ +const SIZE = 17; + +/** The default width in workspace-scale units of the text input bubble. */ +const DEFAULT_BUBBLE_WIDTH = 160; + +/** The default height in workspace-scale units of the text input bubble. */ +const DEFAULT_BUBBLE_HEIGHT = 80; + +/** + * An icon which allows the user to add comment text to a block. + */ +export class CommentIcon extends Icon implements IHasBubble, ISerializable { + /** The type string used to identify this icon. */ + static readonly TYPE = IconType.COMMENT; + + /** + * The weight this icon has relative to other icons. Icons with more positive + * weight values are rendered farther toward the end of the block. + */ + static readonly WEIGHT = 3; + + /** The bubble used to show comment text to the user. */ + private textInputBubble: TextInputBubble | null = null; + + /** The text of this comment. */ + private text = ''; + + /** The size of this comment (which is applied to the editable bubble). */ + private bubbleSize = new Size(DEFAULT_BUBBLE_WIDTH, DEFAULT_BUBBLE_HEIGHT); + + /** + * The visibility of the bubble for this comment. + * + * This is used to track what the visibile state /should/ be, not necessarily + * what it currently /is/. E.g. sometimes this will be true, but the block + * hasn't been rendered yet, so the bubble will not currently be visible. + */ + private bubbleVisiblity = false; + + constructor(protected readonly sourceBlock: Block) { + super(sourceBlock); + } + + override getType(): IconType { + return CommentIcon.TYPE; + } + + override initView(pointerdownListener: (e: PointerEvent) => void): void { + if (this.svgRoot) return; // Already initialized. + + super.initView(pointerdownListener); + + // Circle. + dom.createSvgElement( + Svg.CIRCLE, + {'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'}, + this.svgRoot, + ); + // Can't use a real '?' text character since different browsers and + // operating systems render it differently. Body of question mark. + dom.createSvgElement( + Svg.PATH, + { + 'class': 'blocklyIconSymbol', + 'd': + 'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405' + + '0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25' + + '-1.201,0.998 -1.201,1.528 -1.204,2.19z', + }, + this.svgRoot, + ); + // Dot of question mark. + dom.createSvgElement( + Svg.RECT, + { + 'class': 'blocklyIconSymbol', + 'x': '6.8', + 'y': '10.78', + 'height': '2', + 'width': '2', + }, + this.svgRoot, + ); + dom.addClass(this.svgRoot!, 'blockly-icon-comment'); + } + + override dispose() { + super.dispose(); + this.textInputBubble?.dispose(); + } + + override getWeight(): number { + return CommentIcon.WEIGHT; + } + + override getSize(): Size { + return new Size(SIZE, SIZE); + } + + override applyColour(): void { + super.applyColour(); + const colour = (this.sourceBlock as BlockSvg).style.colourPrimary; + this.textInputBubble?.setColour(colour); + } + + /** + * Updates the state of the bubble (editable / noneditable) to reflect the + * state of the bubble if the bubble is currently shown. + */ + override async updateEditable(): Promise { + super.updateEditable(); + if (this.bubbleIsVisible()) { + // Close and reopen the bubble to display the correct UI. + await this.setBubbleVisible(false); + await this.setBubbleVisible(true); + } + } + + override onLocationChange(blockOrigin: Coordinate): void { + super.onLocationChange(blockOrigin); + const anchorLocation = this.getAnchorLocation(); + this.textInputBubble?.setAnchorLocation(anchorLocation); + } + + /** Sets the text of this comment. Updates any bubbles if they are visible. */ + setText(text: string) { + const oldText = this.text; + eventUtils.fire( + new (eventUtils.get(EventType.BLOCK_CHANGE))( + this.sourceBlock, + 'comment', + null, + oldText, + text, + ), + ); + this.text = text; + this.textInputBubble?.setText(this.text); + } + + /** Returns the text of this comment. */ + getText(): string { + return this.text; + } + + /** + * Sets the size of the editable bubble for this comment. Resizes the + * bubble if it is visible. + */ + setBubbleSize(size: Size) { + this.bubbleSize = size; + this.textInputBubble?.setSize(this.bubbleSize, true); + } + + /** @returns the size of the editable bubble for this comment. */ + getBubbleSize(): Size { + return this.bubbleSize; + } + + /** + * @returns the state of the comment as a JSON serializable value if the + * comment has text. Otherwise returns null. + */ + saveState(): CommentState | null { + if (this.text) { + return { + 'text': this.text, + 'pinned': this.bubbleIsVisible(), + 'height': this.bubbleSize.height, + 'width': this.bubbleSize.width, + }; + } + return null; + } + + /** Applies the given state to this comment. */ + loadState(state: CommentState) { + this.text = state['text'] ?? ''; + this.bubbleSize = new Size( + state['width'] ?? DEFAULT_BUBBLE_WIDTH, + state['height'] ?? DEFAULT_BUBBLE_HEIGHT, + ); + this.bubbleVisiblity = state['pinned'] ?? false; + this.setBubbleVisible(this.bubbleVisiblity); + } + + override onClick(): void { + super.onClick(); + this.setBubbleVisible(!this.bubbleIsVisible()); + } + + override isClickableInFlyout(): boolean { + return false; + } + + /** + * Updates the text of this comment in response to changes in the text of + * the input bubble. + */ + onTextChange(): void { + if (!this.textInputBubble) return; + + const newText = this.textInputBubble.getText(); + if (this.text === newText) return; + + eventUtils.fire( + new (eventUtils.get(EventType.BLOCK_CHANGE))( + this.sourceBlock, + 'comment', + null, + this.text, + newText, + ), + ); + this.text = newText; + } + + /** + * Updates the size of this icon in response to changes in the size of the + * input bubble. + */ + onSizeChange(): void { + if (this.textInputBubble) { + this.bubbleSize = this.textInputBubble.getSize(); + } + } + + bubbleIsVisible(): boolean { + return this.bubbleVisiblity; + } + + async setBubbleVisible(visible: boolean): Promise { + if (this.bubbleVisiblity === visible) return; + this.bubbleVisiblity = visible; + + await renderManagement.finishQueuedRenders(); + + if ( + !this.sourceBlock.rendered || + this.sourceBlock.isInFlyout || + this.sourceBlock.isInsertionMarker() + ) + return; + + if (visible) { + if (this.sourceBlock.isEditable()) { + this.showEditableBubble(); + } else { + this.showNonEditableBubble(); + } + this.applyColour(); + } else { + this.hideBubble(); + } + + eventUtils.fire( + new (eventUtils.get(EventType.BUBBLE_OPEN))( + this.sourceBlock, + visible, + 'comment', + ), + ); + } + + /** + * Shows the editable text bubble for this comment, and adds change listeners + * to update the state of this icon in response to changes in the bubble. + */ + private showEditableBubble() { + this.createBubble(); + this.textInputBubble?.addTextChangeListener(() => this.onTextChange()); + this.textInputBubble?.addSizeChangeListener(() => this.onSizeChange()); + } + + /** Shows the non editable text bubble for this comment. */ + private showNonEditableBubble() { + this.createBubble(); + this.textInputBubble?.setEditable(false); + } + + protected createBubble() { + this.textInputBubble = new TextInputBubble( + this.sourceBlock.workspace as WorkspaceSvg, + this.getAnchorLocation(), + this.getBubbleOwnerRect(), + ); + this.textInputBubble.setText(this.getText()); + this.textInputBubble.setSize(this.bubbleSize, true); + } + + /** Hides any open bubbles owned by this comment. */ + private hideBubble() { + this.textInputBubble?.dispose(); + this.textInputBubble = null; + } + + /** + * @returns the location the bubble should be anchored to. + * I.E. the middle of this icon. + */ + private getAnchorLocation(): Coordinate { + const midIcon = SIZE / 2; + return Coordinate.sum( + this.workspaceLocation, + new Coordinate(midIcon, midIcon), + ); + } + + /** + * @returns the rect the bubble should avoid overlapping. + * I.E. the block that owns this icon. + */ + private getBubbleOwnerRect(): Rect { + return (this.sourceBlock as BlockSvg).getBoundingRectangleWithoutChildren(); + } +} + +/** The save state format for a comment icon. */ +export interface CommentState { + /** The text of the comment. */ + text?: string; + + /** True if the comment is open, false otherwise. */ + pinned?: boolean; + + /** The height of the comment bubble. */ + height?: number; + + /** The width of the comment bubble. */ + width?: number; +} + +registry.register(CommentIcon.TYPE, CommentIcon); diff --git a/core/icons/exceptions.ts b/core/icons/exceptions.ts new file mode 100644 index 00000000000..26b48e7c88a --- /dev/null +++ b/core/icons/exceptions.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IIcon} from '../interfaces/i_icon.js'; + +/** + * Thrown when you add more than one icon of the same type to a block. + */ +export class DuplicateIconType extends Error { + /** + * @internal + */ + constructor(public icon: IIcon) { + super( + `Tried to append an icon of type ${icon.getType()} when an icon of ` + + `that type already exists on the block. ` + + `Use getIcon to access the existing icon.`, + ); + } +} diff --git a/core/icons/icon.ts b/core/icons/icon.ts new file mode 100644 index 00000000000..30a6b538f6e --- /dev/null +++ b/core/icons/icon.ts @@ -0,0 +1,147 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Block} from '../block.js'; +import type {BlockSvg} from '../block_svg.js'; +import * as browserEvents from '../browser_events.js'; +import {hasBubble} from '../interfaces/i_has_bubble.js'; +import type {IIcon} from '../interfaces/i_icon.js'; +import * as tooltip from '../tooltip.js'; +import {Coordinate} from '../utils/coordinate.js'; +import * as dom from '../utils/dom.js'; +import {Size} from '../utils/size.js'; +import {Svg} from '../utils/svg.js'; +import type {IconType} from './icon_types.js'; + +/** + * The abstract icon class. Icons are visual elements that live in the top-start + * corner of the block. Usually they provide more "meta" information about a + * block (such as warnings or comments) as opposed to fields, which provide + * "actual" information, related to how a block functions. + */ +export abstract class Icon implements IIcon { + /** + * The position of this icon relative to its blocks top-start, + * in workspace units. + */ + protected offsetInBlock: Coordinate = new Coordinate(0, 0); + + /** The position of this icon in workspace coordinates. */ + protected workspaceLocation: Coordinate = new Coordinate(0, 0); + + /** The root svg element visually representing this icon. */ + protected svgRoot: SVGGElement | null = null; + + /** The tooltip for this icon. */ + protected tooltip: tooltip.TipInfo; + + constructor(protected sourceBlock: Block) { + this.tooltip = sourceBlock; + } + + getType(): IconType { + throw new Error('Icons must implement getType'); + } + + initView(pointerdownListener: (e: PointerEvent) => void): void { + if (this.svgRoot) return; // The icon has already been initialized. + + const svgBlock = this.sourceBlock as BlockSvg; + this.svgRoot = dom.createSvgElement(Svg.G, {'class': 'blocklyIconGroup'}); + svgBlock.getSvgRoot().appendChild(this.svgRoot); + this.updateSvgRootOffset(); + browserEvents.conditionalBind( + this.svgRoot, + 'pointerdown', + this, + pointerdownListener, + ); + (this.svgRoot as any).tooltip = this; + tooltip.bindMouseEvents(this.svgRoot); + } + + dispose(): void { + tooltip.unbindMouseEvents(this.svgRoot); + dom.removeNode(this.svgRoot); + } + + getWeight(): number { + return -1; + } + + getSize(): Size { + return new Size(0, 0); + } + + /** + * Sets the tooltip for this icon to the given value. Null to show the + * tooltip of the block. + */ + setTooltip(tip: tooltip.TipInfo | null) { + this.tooltip = tip ?? this.sourceBlock; + } + + /** Returns the tooltip for this icon. */ + getTooltip(): tooltip.TipInfo { + return this.tooltip; + } + + applyColour(): void {} + + updateEditable(): void {} + + updateCollapsed(): void { + if (!this.svgRoot) return; + if (this.sourceBlock.isCollapsed()) { + this.svgRoot.style.display = 'none'; + } else { + this.svgRoot.style.display = 'block'; + } + if (hasBubble(this)) { + this.setBubbleVisible(false); + } + } + + hideForInsertionMarker(): void { + if (!this.svgRoot) return; + this.svgRoot.style.display = 'none'; + } + + isShownWhenCollapsed(): boolean { + return false; + } + + setOffsetInBlock(offset: Coordinate): void { + this.offsetInBlock = offset; + this.updateSvgRootOffset(); + } + + private updateSvgRootOffset(): void { + this.svgRoot?.setAttribute( + 'transform', + `translate(${this.offsetInBlock.x}, ${this.offsetInBlock.y})`, + ); + } + + onLocationChange(blockOrigin: Coordinate): void { + this.workspaceLocation = Coordinate.sum(blockOrigin, this.offsetInBlock); + } + + onClick(): void {} + + /** + * Check whether the icon should be clickable while the block is in a flyout. + * The default is that icons are clickable in all flyouts (auto-closing or not). + * Subclasses may override this function to change this behavior. + * + * @param autoClosingFlyout true if the containing flyout is an auto-closing one. + * @returns Whether the icon should be clickable while the block is in a flyout. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isClickableInFlyout(autoClosingFlyout: boolean): boolean { + return true; + } +} diff --git a/core/icons/icon_types.ts b/core/icons/icon_types.ts new file mode 100644 index 00000000000..c5edb0f7487 --- /dev/null +++ b/core/icons/icon_types.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {ICommentIcon} from '../interfaces/i_comment_icon.js'; +import {IIcon} from '../interfaces/i_icon.js'; +import {MutatorIcon} from './mutator_icon.js'; +import {WarningIcon} from './warning_icon.js'; + +/** + * Defines the type of an icon, so that it can be retrieved from block.getIcon + */ +export class IconType<_T extends IIcon> { + /** @param name The name of the registry type. */ + constructor(private readonly name: string) {} + + /** @returns the name of the type. */ + toString(): string { + return this.name; + } + + /** @returns true if this icon type is equivalent to the given icon type. */ + equals(type: IconType): boolean { + return this.name === type.toString(); + } + + static MUTATOR = new IconType('mutator'); + static WARNING = new IconType('warning'); + static COMMENT = new IconType('comment'); +} diff --git a/core/icons/mutator_icon.ts b/core/icons/mutator_icon.ts new file mode 100644 index 00000000000..d02c7e1871b --- /dev/null +++ b/core/icons/mutator_icon.ts @@ -0,0 +1,355 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.Mutator + +import type {BlockSvg} from '../block_svg.js'; +import type {BlocklyOptions} from '../blockly_options.js'; +import {MiniWorkspaceBubble} from '../bubbles/mini_workspace_bubble.js'; +import type {Abstract} from '../events/events_abstract.js'; +import {BlockChange} from '../events/events_block_change.js'; +import {isBlockChange, isBlockCreate} from '../events/predicates.js'; +import {EventType} from '../events/type.js'; +import * as eventUtils from '../events/utils.js'; +import type {IHasBubble} from '../interfaces/i_has_bubble.js'; +import * as renderManagement from '../render_management.js'; +import {Coordinate} from '../utils/coordinate.js'; +import * as dom from '../utils/dom.js'; +import {Rect} from '../utils/rect.js'; +import {Size} from '../utils/size.js'; +import {Svg} from '../utils/svg.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import {Icon} from './icon.js'; +import {IconType} from './icon_types.js'; + +/** The size of the mutator icon in workspace-scale units. */ +const SIZE = 17; + +/** + * The distance between the root block in the mini workspace and that + * workspace's edges. + */ +const WORKSPACE_MARGIN = 16; + +/** + * An icon that allows the user to change the shape of the block. + * + * For example, it could be used to add additional fields or inputs to + * the block. + */ +export class MutatorIcon extends Icon implements IHasBubble { + /** The type string used to identify this icon. */ + static readonly TYPE = IconType.MUTATOR; + + /** + * The weight this icon has relative to other icons. Icons with more positive + * weight values are rendered farther toward the end of the block. + */ + static readonly WEIGHT = 1; + + /** The bubble used to show the mini workspace to the user. */ + private miniWorkspaceBubble: MiniWorkspaceBubble | null = null; + + /** The root block in the mini workspace. */ + private rootBlock: BlockSvg | null = null; + + /** The PID tracking updating the workkspace in response to user events. */ + private updateWorkspacePid: ReturnType | null = null; + + /** + * The change listener in the main workspace that triggers the saveConnections + * method when anything in the main workspace changes. + * + * Only actually registered to listen for events while the mutator bubble is + * open. + */ + private saveConnectionsListener: (() => void) | null = null; + + constructor( + private readonly flyoutBlockTypes: string[], + protected readonly sourceBlock: BlockSvg, + ) { + super(sourceBlock); + } + + override getType(): IconType { + return MutatorIcon.TYPE; + } + + override initView(pointerdownListener: (e: PointerEvent) => void): void { + if (this.svgRoot) return; // Already initialized. + + super.initView(pointerdownListener); + + // Square with rounded corners. + dom.createSvgElement( + Svg.RECT, + { + 'class': 'blocklyIconShape', + 'rx': '4', + 'ry': '4', + 'height': '16', + 'width': '16', + }, + this.svgRoot, + ); + // Gear teeth. + dom.createSvgElement( + 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', + }, + this.svgRoot, + ); + // Axle hole. + dom.createSvgElement( + Svg.CIRCLE, + {'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'}, + this.svgRoot, + ); + dom.addClass(this.svgRoot!, 'blockly-icon-mutator'); + } + + override dispose(): void { + super.dispose(); + this.miniWorkspaceBubble?.dispose(); + } + + override getWeight(): number { + return MutatorIcon.WEIGHT; + } + + override getSize(): Size { + return new Size(SIZE, SIZE); + } + + override applyColour(): void { + super.applyColour(); + this.miniWorkspaceBubble?.setColour(this.sourceBlock.style.colourPrimary); + this.miniWorkspaceBubble?.updateBlockStyles(); + } + + override updateCollapsed(): void { + super.updateCollapsed(); + if (this.sourceBlock.isCollapsed()) this.setBubbleVisible(false); + } + + override onLocationChange(blockOrigin: Coordinate): void { + super.onLocationChange(blockOrigin); + this.miniWorkspaceBubble?.setAnchorLocation(this.getAnchorLocation()); + } + + override onClick(): void { + super.onClick(); + if (this.sourceBlock.isEditable()) { + this.setBubbleVisible(!this.bubbleIsVisible()); + } + } + + override isClickableInFlyout(): boolean { + return false; + } + + bubbleIsVisible(): boolean { + return !!this.miniWorkspaceBubble; + } + + async setBubbleVisible(visible: boolean): Promise { + if (this.bubbleIsVisible() === visible) return; + + await renderManagement.finishQueuedRenders(); + + if (visible) { + this.miniWorkspaceBubble = new MiniWorkspaceBubble( + this.getMiniWorkspaceConfig(), + this.sourceBlock.workspace, + this.getAnchorLocation(), + this.getBubbleOwnerRect(), + ); + this.applyColour(); + this.createRootBlock(); + this.addSaveConnectionsListener(); + this.miniWorkspaceBubble?.addWorkspaceChangeListener( + this.createMiniWorkspaceChangeListener(), + ); + } else { + this.miniWorkspaceBubble?.dispose(); + this.miniWorkspaceBubble = null; + if (this.saveConnectionsListener) { + this.sourceBlock.workspace.removeChangeListener( + this.saveConnectionsListener, + ); + } + this.saveConnectionsListener = null; + } + + eventUtils.fire( + new (eventUtils.get(EventType.BUBBLE_OPEN))( + this.sourceBlock, + visible, + 'mutator', + ), + ); + } + + /** @returns the configuration the mini workspace should have. */ + private getMiniWorkspaceConfig() { + const options: BlocklyOptions = { + 'disable': false, + 'media': this.sourceBlock.workspace.options.pathToMedia, + 'rtl': this.sourceBlock.RTL, + 'renderer': this.sourceBlock.workspace.options.renderer, + 'rendererOverrides': + this.sourceBlock.workspace.options.rendererOverrides ?? undefined, + }; + + if (this.flyoutBlockTypes.length) { + options.toolbox = { + 'kind': 'flyoutToolbox', + 'contents': this.flyoutBlockTypes.map((type) => ({ + 'kind': 'block', + 'type': type, + })), + }; + } + + return options; + } + + /** + * @returns the location the bubble should be anchored to. + * I.E. the middle of this icon. + */ + private getAnchorLocation(): Coordinate { + const midIcon = SIZE / 2; + return Coordinate.sum( + this.workspaceLocation, + new Coordinate(midIcon, midIcon), + ); + } + + /** + * @returns the rect the bubble should avoid overlapping. + * I.E. the block that owns this icon. + */ + private getBubbleOwnerRect(): Rect { + const bbox = this.sourceBlock.getSvgRoot().getBBox(); + return new Rect(bbox.y, bbox.y + bbox.height, bbox.x, bbox.x + bbox.width); + } + + /** Decomposes the source block to create blocks in the mini workspace. */ + private createRootBlock() { + if (!this.sourceBlock.decompose) { + throw new Error( + 'Blocks with mutator icons must include a decompose method', + ); + } + this.rootBlock = this.sourceBlock.decompose( + this.miniWorkspaceBubble!.getWorkspace(), + )!; + + for (const child of this.rootBlock.getDescendants(false)) { + child.queueRender(); + } + + this.rootBlock.setMovable(false); + this.rootBlock.setDeletable(false); + + const flyoutWidth = + this.miniWorkspaceBubble?.getWorkspace()?.getFlyout()?.getWidth() ?? 0; + this.rootBlock.moveBy( + this.rootBlock.RTL ? -(flyoutWidth + WORKSPACE_MARGIN) : WORKSPACE_MARGIN, + WORKSPACE_MARGIN, + ); + } + + /** Adds a listen to the source block that triggers saving connections. */ + private addSaveConnectionsListener() { + if (!this.sourceBlock.saveConnections || !this.rootBlock) return; + this.saveConnectionsListener = () => { + if (!this.sourceBlock.saveConnections || !this.rootBlock) return; + this.sourceBlock.saveConnections(this.rootBlock); + }; + this.saveConnectionsListener(); + this.sourceBlock.workspace.addChangeListener(this.saveConnectionsListener); + } + + /** + * Creates a change listener to add to the mini workspace which recomposes + * the block. + */ + private createMiniWorkspaceChangeListener() { + return (e: Abstract) => { + if (!MutatorIcon.isIgnorableMutatorEvent(e) && !this.updateWorkspacePid) { + this.updateWorkspacePid = setTimeout(() => { + this.updateWorkspacePid = null; + this.recomposeSourceBlock(); + }, 0); + } + }; + } + + /** + * Returns true if the given event is not one the mutator needs to + * care about. + * + * @internal + */ + static isIgnorableMutatorEvent(e: Abstract) { + return ( + e.isUiEvent || + isBlockCreate(e) || + (isBlockChange(e) && e.element === 'disabled') + ); + } + + /** Recomposes the source block based on changes to the mini workspace. */ + private recomposeSourceBlock() { + if (!this.rootBlock) return; + if (!this.sourceBlock.compose) { + throw new Error( + 'Blocks with mutator icons must include a compose method', + ); + } + + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) eventUtils.setGroup(true); + + const oldExtraState = BlockChange.getExtraBlockState_(this.sourceBlock); + this.sourceBlock.compose(this.rootBlock); + const newExtraState = BlockChange.getExtraBlockState_(this.sourceBlock); + + if (oldExtraState !== newExtraState) { + eventUtils.fire( + new (eventUtils.get(EventType.BLOCK_CHANGE))( + this.sourceBlock, + 'mutation', + null, + oldExtraState, + newExtraState, + ), + ); + } + + eventUtils.setGroup(existingGroup); + } + + /** + * @returns The workspace of the mini workspace bubble, if the bubble is + * currently open. + */ + getWorkspace(): WorkspaceSvg | undefined { + return this.miniWorkspaceBubble?.getWorkspace(); + } +} diff --git a/core/icons/registry.ts b/core/icons/registry.ts new file mode 100644 index 00000000000..ed6be0a004b --- /dev/null +++ b/core/icons/registry.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Block} from '../block.js'; +import type {IIcon} from '../interfaces/i_icon.js'; +import * as registry from '../registry.js'; +import {IconType} from './icon_types.js'; + +/** + * Registers the given icon so that it can be deserialized. + * + * @param type The type of the icon to register. This should be the same string + * that is returned from its `getType` method. + * @param iconConstructor The icon class/constructor to register. + */ +export function register( + type: IconType, + iconConstructor: new (block: Block) => IIcon, +) { + registry.register(registry.Type.ICON, type.toString(), iconConstructor); +} + +/** + * Unregisters the icon associated with the given type. + * + * @param type The type of the icon to unregister. + */ +export function unregister(type: string) { + registry.unregister(registry.Type.ICON, type); +} diff --git a/core/icons/warning_icon.ts b/core/icons/warning_icon.ts new file mode 100644 index 00000000000..2744195f98d --- /dev/null +++ b/core/icons/warning_icon.ts @@ -0,0 +1,220 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.Warning + +import type {BlockSvg} from '../block_svg.js'; +import {TextBubble} from '../bubbles/text_bubble.js'; +import {EventType} from '../events/type.js'; +import * as eventUtils from '../events/utils.js'; +import type {IHasBubble} from '../interfaces/i_has_bubble.js'; +import * as renderManagement from '../render_management.js'; +import {Size} from '../utils.js'; +import {Coordinate} from '../utils/coordinate.js'; +import * as dom from '../utils/dom.js'; +import {Rect} from '../utils/rect.js'; +import {Svg} from '../utils/svg.js'; +import {Icon} from './icon.js'; +import {IconType} from './icon_types.js'; + +/** The size of the warning icon in workspace-scale units. */ +const SIZE = 17; + +/** + * An icon that warns the user that something is wrong with their block. + * + * For example, this could be used to warn them about incorrect field values, + * or incorrect placement of the block (putting it somewhere it doesn't belong). + */ +export class WarningIcon extends Icon implements IHasBubble { + /** The type string used to identify this icon. */ + static readonly TYPE = IconType.WARNING; + + /** + * The weight this icon has relative to other icons. Icons with more positive + * weight values are rendered farther toward the end of the block. + */ + static readonly WEIGHT = 2; + + /** A map of warning IDs to warning text. */ + private textMap: Map = new Map(); + + /** The bubble used to display the warnings to the user. */ + private textBubble: TextBubble | null = null; + + /** @internal */ + constructor(protected readonly sourceBlock: BlockSvg) { + super(sourceBlock); + } + + override getType(): IconType { + return WarningIcon.TYPE; + } + + override initView(pointerdownListener: (e: PointerEvent) => void): void { + if (this.svgRoot) return; // Already initialized. + + super.initView(pointerdownListener); + + // Triangle with rounded corners. + dom.createSvgElement( + Svg.PATH, + { + 'class': 'blocklyIconShape', + 'd': 'M2,15Q-1,15 0.5,12L6.5,1.7Q8,-1 9.5,1.7L15.5,12Q17,15 14,15z', + }, + this.svgRoot, + ); + // Can't use a real '!' text character since different browsers and + // operating systems render it differently. Body of exclamation point. + dom.createSvgElement( + Svg.PATH, + { + 'class': 'blocklyIconSymbol', + 'd': 'm7,4.8v3.16l0.27,2.27h1.46l0.27,-2.27v-3.16z', + }, + this.svgRoot, + ); + // Dot of exclamation point. + dom.createSvgElement( + Svg.RECT, + { + 'class': 'blocklyIconSymbol', + 'x': '7', + 'y': '11', + 'height': '2', + 'width': '2', + }, + this.svgRoot, + ); + dom.addClass(this.svgRoot!, 'blockly-icon-warning'); + } + + override dispose() { + super.dispose(); + this.textBubble?.dispose(); + } + + override getWeight(): number { + return WarningIcon.WEIGHT; + } + + override getSize(): Size { + return new Size(SIZE, SIZE); + } + + override applyColour(): void { + super.applyColour(); + this.textBubble?.setColour(this.sourceBlock.style.colourPrimary); + } + + override updateCollapsed(): void { + // We are shown when collapsed, so do nothing! I.e. skip the default + // behavior of hiding. + } + + /** Tells blockly that this icon is shown when the block is collapsed. */ + override isShownWhenCollapsed(): boolean { + return true; + } + + /** Updates the location of the icon's bubble if it is open. */ + override onLocationChange(blockOrigin: Coordinate): void { + super.onLocationChange(blockOrigin); + this.textBubble?.setAnchorLocation(this.getAnchorLocation()); + } + + /** + * Adds a warning message to this warning icon. + * + * @param text The text of the message to add. + * @param id The id of the message to add. + * @internal + */ + addMessage(text: string, id: string): this { + if (this.textMap.get(id) === text) return this; + + if (text) { + this.textMap.set(id, text); + } else { + this.textMap.delete(id); + } + + this.textBubble?.setText(this.getText()); + return this; + } + + /** + * @returns the display text for this icon. Includes all warning messages + * concatenated together with newlines. + * @internal + */ + getText(): string { + return [...this.textMap.values()].join('\n'); + } + + /** Toggles the visibility of the bubble. */ + override onClick(): void { + super.onClick(); + this.setBubbleVisible(!this.bubbleIsVisible()); + } + + override isClickableInFlyout(): boolean { + return false; + } + + bubbleIsVisible(): boolean { + return !!this.textBubble; + } + + async setBubbleVisible(visible: boolean): Promise { + if (this.bubbleIsVisible() === visible) return; + + await renderManagement.finishQueuedRenders(); + + if (visible) { + this.textBubble = new TextBubble( + this.getText(), + this.sourceBlock.workspace, + this.getAnchorLocation(), + this.getBubbleOwnerRect(), + ); + this.applyColour(); + } else { + this.textBubble?.dispose(); + this.textBubble = null; + } + + eventUtils.fire( + new (eventUtils.get(EventType.BUBBLE_OPEN))( + this.sourceBlock, + visible, + 'warning', + ), + ); + } + + /** + * @returns the location the bubble should be anchored to. + * I.E. the middle of this icon. + */ + private getAnchorLocation(): Coordinate { + const midIcon = SIZE / 2; + return Coordinate.sum( + this.workspaceLocation, + new Coordinate(midIcon, midIcon), + ); + } + + /** + * @returns the rect the bubble should avoid overlapping. + * I.E. the block that owns this icon. + */ + private getBubbleOwnerRect(): Rect { + const bbox = this.sourceBlock.getSvgRoot().getBBox(); + return new Rect(bbox.y, bbox.y + bbox.height, bbox.x, bbox.x + bbox.width); + } +} diff --git a/core/inject.js b/core/inject.js deleted file mode 100644 index ff196202ece..00000000000 --- a/core/inject.js +++ /dev/null @@ -1,393 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2011 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Functions for injecting Blockly into a web page. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.inject'); - -goog.require('Blockly.BlockDragSurfaceSvg'); -goog.require('Blockly.Css'); -goog.require('Blockly.Grid'); -goog.require('Blockly.Options'); -goog.require('Blockly.WorkspaceSvg'); -goog.require('Blockly.WorkspaceDragSurfaceSvg'); -goog.require('goog.dom'); -goog.require('goog.ui.Component'); -goog.require('goog.userAgent'); - - -/** - * Inject a Blockly editor into the specified container element (usually a div). - * @param {!Element|string} container Containing element, or its ID, - * or a CSS selector. - * @param {Object=} opt_options Optional dictionary of options. - * @return {!Blockly.Workspace} Newly created main workspace. - */ -Blockly.inject = function(container, opt_options) { - if (goog.isString(container)) { - container = document.getElementById(container) || - document.querySelector(container); - } - // Verify that the container is in document. - if (!goog.dom.contains(document, container)) { - throw 'Error: container is not in current document.'; - } - var options = new Blockly.Options(opt_options || {}); - var subContainer = goog.dom.createDom('div', 'injectionDiv'); - container.appendChild(subContainer); - var svg = Blockly.createDom_(subContainer, options); - - // Create surfaces for dragging things. These are optimizations - // so that the broowser does not repaint during the drag. - var blockDragSurface = new Blockly.BlockDragSurfaceSvg(subContainer); - var workspaceDragSurface = new Blockly.WorkspaceDragSurfaceSvg(subContainer); - - var workspace = Blockly.createMainWorkspace_(svg, options, blockDragSurface, - workspaceDragSurface); - Blockly.init_(workspace); - Blockly.mainWorkspace = workspace; - - Blockly.svgResize(workspace); - return workspace; -}; - -/** - * Create the SVG image. - * @param {!Element} container Containing element. - * @param {!Blockly.Options} options Dictionary of options. - * @return {!Element} Newly created SVG image. - * @private - */ -Blockly.createDom_ = function(container, options) { - // Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying - // out content in RTL mode. Therefore Blockly forces the use of LTR, - // then manually positions content in RTL as needed. - container.setAttribute('dir', 'LTR'); - // Closure can be trusted to create HTML widgets with the proper direction. - goog.ui.Component.setDefaultRightToLeft(options.RTL); - - // Load CSS. - Blockly.Css.inject(options.hasCss, options.pathToMedia); - - // Build the SVG DOM. - /* - - ... - - */ - var svg = Blockly.utils.createSvgElement('svg', { - 'xmlns': 'http://www.w3.org/2000/svg', - 'xmlns:html': 'http://www.w3.org/1999/xhtml', - 'xmlns:xlink': 'http://www.w3.org/1999/xlink', - 'version': '1.1', - 'class': 'blocklySvg' - }, container); - /* - - ... filters go here ... - - */ - var defs = Blockly.utils.createSvgElement('defs', {}, svg); - // Each filter/pattern needs a unique ID for the case of multiple Blockly - // instances on a page. Browser behaviour becomes undefined otherwise. - // https://neil.fraser.name/news/2015/11/01/ - var rnd = String(Math.random()).substring(2); - /* - - - - - - - - - */ - var embossFilter = Blockly.utils.createSvgElement('filter', - {'id': 'blocklyEmbossFilter' + rnd}, defs); - Blockly.utils.createSvgElement('feGaussianBlur', - {'in': 'SourceAlpha', 'stdDeviation': 1, 'result': 'blur'}, embossFilter); - var feSpecularLighting = Blockly.utils.createSvgElement('feSpecularLighting', - {'in': 'blur', 'surfaceScale': 1, 'specularConstant': 0.5, - 'specularExponent': 10, 'lighting-color': 'white', 'result': 'specOut'}, - embossFilter); - Blockly.utils.createSvgElement('fePointLight', - {'x': -5000, 'y': -10000, 'z': 20000}, feSpecularLighting); - Blockly.utils.createSvgElement('feComposite', - {'in': 'specOut', 'in2': 'SourceAlpha', 'operator': 'in', - 'result': 'specOut'}, embossFilter); - Blockly.utils.createSvgElement('feComposite', - {'in': 'SourceGraphic', 'in2': 'specOut', 'operator': 'arithmetic', - 'k1': 0, 'k2': 1, 'k3': 1, 'k4': 0}, embossFilter); - options.embossFilterId = embossFilter.id; - /* - - - - - */ - var disabledPattern = Blockly.utils.createSvgElement('pattern', - {'id': 'blocklyDisabledPattern' + rnd, - 'patternUnits': 'userSpaceOnUse', - 'width': 10, 'height': 10}, defs); - Blockly.utils.createSvgElement('rect', - {'width': 10, 'height': 10, 'fill': '#aaa'}, disabledPattern); - Blockly.utils.createSvgElement('path', - {'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, disabledPattern); - options.disabledPatternId = disabledPattern.id; - - options.gridPattern = Blockly.Grid.createDom(rnd, options.gridOptions, defs); - return svg; -}; - -/** - * Create a main workspace and add it to the SVG. - * @param {!Element} svg SVG element with pattern defined. - * @param {!Blockly.Options} options Dictionary of options. - * @param {!Blockly.BlockDragSurfaceSvg} blockDragSurface Drag surface SVG - * for the blocks. - * @param {!Blockly.WorkspaceDragSurfaceSvg} workspaceDragSurface Drag surface - * SVG for the workspace. - * @return {!Blockly.Workspace} Newly created main workspace. - * @private - */ -Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, workspaceDragSurface) { - options.parentWorkspace = null; - var mainWorkspace = new Blockly.WorkspaceSvg(options, blockDragSurface, workspaceDragSurface); - mainWorkspace.scale = options.zoomOptions.startScale; - svg.appendChild(mainWorkspace.createDom('blocklyMainBackground')); - - if (!options.hasCategories && options.languageTree) { - // Add flyout as an that is a sibling of the workspace svg. - var flyout = mainWorkspace.addFlyout_('svg'); - Blockly.utils.insertAfter_(flyout, svg); - } - - // A null translation will also apply the correct initial scale. - mainWorkspace.translate(0, 0); - Blockly.mainWorkspace = mainWorkspace; - - if (!options.readOnly && !options.hasScrollbars) { - var workspaceChanged = function() { - if (!mainWorkspace.isDragging()) { - var metrics = mainWorkspace.getMetrics(); - var edgeLeft = metrics.viewLeft + metrics.absoluteLeft; - var edgeTop = metrics.viewTop + metrics.absoluteTop; - if (metrics.contentTop < edgeTop || - metrics.contentTop + metrics.contentHeight > - metrics.viewHeight + edgeTop || - metrics.contentLeft < - (options.RTL ? metrics.viewLeft : edgeLeft) || - metrics.contentLeft + metrics.contentWidth > (options.RTL ? - metrics.viewWidth : metrics.viewWidth + edgeLeft)) { - // One or more blocks may be out of bounds. Bump them back in. - var MARGIN = 25; - var blocks = mainWorkspace.getTopBlocks(false); - for (var b = 0, block; block = blocks[b]; b++) { - var blockXY = block.getRelativeToSurfaceXY(); - var blockHW = block.getHeightWidth(); - // Bump any block that's above the top back inside. - var overflowTop = edgeTop + MARGIN - blockHW.height - blockXY.y; - if (overflowTop > 0) { - block.moveBy(0, overflowTop); - } - // Bump any block that's below the bottom back inside. - var overflowBottom = - edgeTop + metrics.viewHeight - MARGIN - blockXY.y; - if (overflowBottom < 0) { - block.moveBy(0, overflowBottom); - } - // Bump any block that's off the left back inside. - var overflowLeft = MARGIN + edgeLeft - - blockXY.x - (options.RTL ? 0 : blockHW.width); - if (overflowLeft > 0) { - block.moveBy(overflowLeft, 0); - } - // Bump any block that's off the right back inside. - var overflowRight = edgeLeft + metrics.viewWidth - MARGIN - - blockXY.x + (options.RTL ? blockHW.width : 0); - if (overflowRight < 0) { - block.moveBy(overflowRight, 0); - } - } - } - } - }; - mainWorkspace.addChangeListener(workspaceChanged); - } - // The SVG is now fully assembled. - Blockly.svgResize(mainWorkspace); - Blockly.WidgetDiv.createDom(); - Blockly.Tooltip.createDom(); - return mainWorkspace; -}; - -/** - * Initialize Blockly with various handlers. - * @param {!Blockly.Workspace} mainWorkspace Newly created main workspace. - * @private - */ -Blockly.init_ = function(mainWorkspace) { - var options = mainWorkspace.options; - var svg = mainWorkspace.getParentSvg(); - - // Suppress the browser's context menu. - Blockly.bindEventWithChecks_(svg.parentNode, 'contextmenu', null, - function(e) { - if (!Blockly.utils.isTargetInput(e)) { - e.preventDefault(); - } - }); - - var workspaceResizeHandler = Blockly.bindEventWithChecks_(window, 'resize', - null, - function() { - Blockly.hideChaff(true); - Blockly.svgResize(mainWorkspace); - }); - mainWorkspace.setResizeHandlerWrapper(workspaceResizeHandler); - - Blockly.inject.bindDocumentEvents_(); - - if (options.languageTree) { - if (mainWorkspace.toolbox_) { - mainWorkspace.toolbox_.init(mainWorkspace); - } else if (mainWorkspace.flyout_) { - // Build a fixed flyout with the root blocks. - mainWorkspace.flyout_.init(mainWorkspace); - mainWorkspace.flyout_.show(options.languageTree.childNodes); - mainWorkspace.flyout_.scrollToStart(); - // Translate the workspace sideways to avoid the fixed flyout. - mainWorkspace.scrollX = mainWorkspace.flyout_.width_; - if (options.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { - mainWorkspace.scrollX *= -1; - } - mainWorkspace.translate(mainWorkspace.scrollX, 0); - } - } - - if (options.hasScrollbars) { - mainWorkspace.scrollbar = new Blockly.ScrollbarPair(mainWorkspace); - mainWorkspace.scrollbar.resize(); - } - - // Load the sounds. - if (options.hasSounds) { - Blockly.inject.loadSounds_(options.pathToMedia, mainWorkspace); - } -}; - -/** - * Bind document events, but only once. Destroying and reinjecting Blockly - * should not bind again. - * Bind events for scrolling the workspace. - * Most of these events should be bound to the SVG's surface. - * However, 'mouseup' has to be on the whole document so that a block dragged - * out of bounds and released will know that it has been released. - * Also, 'keydown' has to be on the whole document since the browser doesn't - * understand a concept of focus on the SVG image. - * @private - */ -Blockly.inject.bindDocumentEvents_ = function() { - if (!Blockly.documentEventsBound_) { - Blockly.bindEventWithChecks_(document, 'keydown', null, Blockly.onKeyDown_); - // longStop needs to run to stop the context menu from showing up. It - // should run regardless of what other touch event handlers have run. - Blockly.bindEvent_(document, 'touchend', null, Blockly.longStop_); - Blockly.bindEvent_(document, 'touchcancel', null, Blockly.longStop_); - // Some iPad versions don't fire resize after portrait to landscape change. - if (goog.userAgent.IPAD) { - Blockly.bindEventWithChecks_(window, 'orientationchange', document, - function() { - // TODO(#397): Fix for multiple blockly workspaces. - Blockly.svgResize(Blockly.getMainWorkspace()); - }); - } - } - Blockly.documentEventsBound_ = true; -}; - -/** - * Load sounds for the given workspace. - * @param {string} pathToMedia The path to the media directory. - * @param {!Blockly.Workspace} workspace The workspace to load sounds for. - * @private - */ -Blockly.inject.loadSounds_ = function(pathToMedia, workspace) { - var audioMgr = workspace.getAudioManager(); - audioMgr.load( - [pathToMedia + 'click.mp3', - pathToMedia + 'click.wav', - pathToMedia + 'click.ogg'], 'click'); - audioMgr.load( - [pathToMedia + 'disconnect.wav', - pathToMedia + 'disconnect.mp3', - pathToMedia + 'disconnect.ogg'], 'disconnect'); - audioMgr.load( - [pathToMedia + 'delete.mp3', - pathToMedia + 'delete.ogg', - pathToMedia + 'delete.wav'], 'delete'); - - // Bind temporary hooks that preload the sounds. - var soundBinds = []; - var unbindSounds = function() { - while (soundBinds.length) { - Blockly.unbindEvent_(soundBinds.pop()); - } - audioMgr.preload(); - }; - - // These are bound on mouse/touch events with Blockly.bindEventWithChecks_, so - // they restrict the touch identifier that will be recognized. But this is - // really something that happens on a click, not a drag, so that's not - // necessary. - - // Android ignores any sound not loaded as a result of a user action. - soundBinds.push( - Blockly.bindEventWithChecks_(document, 'mousemove', null, unbindSounds, - true)); - soundBinds.push( - Blockly.bindEventWithChecks_(document, 'touchstart', null, unbindSounds, - true)); -}; - -/** - * Modify the block tree on the existing toolbox. - * @param {Node|string} tree DOM tree of blocks, or text representation of same. - * @deprecated April 2015 - */ -Blockly.updateToolbox = function(tree) { - console.warn('Deprecated call to Blockly.updateToolbox, ' + - 'use workspace.updateToolbox instead.'); - Blockly.getMainWorkspace().updateToolbox(tree); -}; diff --git a/core/inject.ts b/core/inject.ts new file mode 100644 index 00000000000..b425d77b74b --- /dev/null +++ b/core/inject.ts @@ -0,0 +1,415 @@ +/** + * @license + * Copyright 2011 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.inject + +import type {BlocklyOptions} from './blockly_options.js'; +import * as browserEvents from './browser_events.js'; +import * as bumpObjects from './bump_objects.js'; +import * as common from './common.js'; +import * as Css from './css.js'; +import * as dropDownDiv from './dropdowndiv.js'; +import {Grid} from './grid.js'; +import {Msg} from './msg.js'; +import {Options} from './options.js'; +import {ScrollbarPair} from './scrollbar_pair.js'; +import {ShortcutRegistry} from './shortcut_registry.js'; +import * as Tooltip from './tooltip.js'; +import * as Touch from './touch.js'; +import * as aria from './utils/aria.js'; +import * as dom from './utils/dom.js'; +import {Svg} from './utils/svg.js'; +import * as WidgetDiv from './widgetdiv.js'; +import {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Inject a Blockly editor into the specified container element (usually a div). + * + * @param container Containing element, or its ID, or a CSS selector. + * @param opt_options Optional dictionary of options. + * @returns Newly created main workspace. + */ +export function inject( + container: Element | string, + opt_options?: BlocklyOptions, +): WorkspaceSvg { + let containerElement: Element | null = null; + if (typeof container === 'string') { + containerElement = + document.getElementById(container) || document.querySelector(container); + } else { + containerElement = container; + } + // Verify that the container is in document. + if ( + !document.contains(containerElement) && + document !== containerElement?.ownerDocument + ) { + throw Error('Error: container is not in current document'); + } + const options = new Options(opt_options || ({} as BlocklyOptions)); + const subContainer = document.createElement('div'); + dom.addClass(subContainer, 'injectionDiv'); + if (opt_options?.rtl) { + dom.addClass(subContainer, 'blocklyRTL'); + } + subContainer.tabIndex = 0; + aria.setState(subContainer, aria.State.LABEL, Msg['WORKSPACE_ARIA_LABEL']); + + containerElement!.appendChild(subContainer); + const svg = createDom(subContainer, options); + + const workspace = createMainWorkspace(subContainer, svg, options); + + init(workspace); + + // Keep focus on the first workspace so entering keyboard navigation looks + // correct. + common.setMainWorkspace(workspace); + + common.svgResize(workspace); + + subContainer.addEventListener('focusin', function () { + common.setMainWorkspace(workspace); + }); + + browserEvents.conditionalBind(subContainer, 'keydown', null, onKeyDown); + + return workspace; +} + +/** + * Create the SVG image. + * + * @param container Containing element. + * @param options Dictionary of options. + * @returns Newly created SVG image. + */ +function createDom(container: Element, options: Options): SVGElement { + // Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying + // out content in RTL mode. Therefore Blockly forces the use of LTR, + // then manually positions content in RTL as needed. + container.setAttribute('dir', 'LTR'); + + // Load CSS. + Css.inject(options.hasCss, options.pathToMedia); + + // Build the SVG DOM. + /* + + ... + + */ + const svg = dom.createSvgElement( + Svg.SVG, + { + 'xmlns': dom.SVG_NS, + 'xmlns:html': dom.HTML_NS, + 'xmlns:xlink': dom.XLINK_NS, + 'version': '1.1', + 'class': 'blocklySvg', + 'tabindex': '0', + }, + container, + ); + /* + + ... filters go here ... + + */ + const defs = dom.createSvgElement(Svg.DEFS, {}, svg); + // Each filter/pattern needs a unique ID for the case of multiple Blockly + // instances on a page. Browser behaviour becomes undefined otherwise. + // https://neil.fraser.name/news/2015/11/01/ + const rnd = String(Math.random()).substring(2); + + options.gridPattern = Grid.createDom(rnd, options.gridOptions, defs); + return svg; +} + +/** + * Create a main workspace and add it to the SVG. + * + * @param svg SVG element with pattern defined. + * @param options Dictionary of options. + * @returns Newly created main workspace. + */ +function createMainWorkspace( + injectionDiv: Element, + svg: SVGElement, + options: Options, +): WorkspaceSvg { + options.parentWorkspace = null; + const mainWorkspace = new WorkspaceSvg(options); + const wsOptions = mainWorkspace.options; + mainWorkspace.scale = wsOptions.zoomOptions.startScale; + svg.appendChild( + mainWorkspace.createDom('blocklyMainBackground', injectionDiv), + ); + + // Set the theme name and renderer name onto the injection div. + const rendererClassName = mainWorkspace.getRenderer().getClassName(); + if (rendererClassName) { + dom.addClass(injectionDiv, rendererClassName); + } + const themeClassName = mainWorkspace.getTheme().getClassName(); + if (themeClassName) { + dom.addClass(injectionDiv, themeClassName); + } + + if (!wsOptions.hasCategories && wsOptions.languageTree) { + // Add flyout as an that is a sibling of the workspace SVG. + const flyout = mainWorkspace.addFlyout(Svg.SVG); + dom.insertAfter(flyout, svg); + } + if (wsOptions.hasTrashcan) { + mainWorkspace.addTrashcan(); + } + if (wsOptions.zoomOptions && wsOptions.zoomOptions.controls) { + mainWorkspace.addZoomControls(); + } + // Register the workspace svg as a UI component. + mainWorkspace + .getThemeManager() + .subscribe(svg, 'workspaceBackgroundColour', 'background-color'); + + // A null translation will also apply the correct initial scale. + mainWorkspace.translate(0, 0); + + mainWorkspace.addChangeListener( + bumpObjects.bumpIntoBoundsHandler(mainWorkspace), + ); + + // The SVG is now fully assembled. + common.svgResize(mainWorkspace); + WidgetDiv.createDom(); + dropDownDiv.createDom(); + Tooltip.createDom(); + return mainWorkspace; +} + +/** + * Initialize Blockly with various handlers. + * + * @param mainWorkspace Newly created main workspace. + */ +function init(mainWorkspace: WorkspaceSvg) { + const options = mainWorkspace.options; + const svg = mainWorkspace.getParentSvg(); + + // Suppress the browser's context menu. + browserEvents.conditionalBind( + svg.parentNode as Element, + 'contextmenu', + null, + function (e: Event) { + if (!browserEvents.isTargetInput(e)) { + e.preventDefault(); + } + }, + ); + + const workspaceResizeHandler = browserEvents.conditionalBind( + window, + 'resize', + null, + function () { + // Don't hide all the chaff. Leave the dropdown and widget divs open if + // possible. + Tooltip.hide(); + mainWorkspace.hideComponents(true); + dropDownDiv.repositionForWindowResize(); + WidgetDiv.repositionForWindowResize(); + common.svgResize(mainWorkspace); + bumpObjects.bumpTopObjectsIntoBounds(mainWorkspace); + }, + ); + mainWorkspace.setResizeHandlerWrapper(workspaceResizeHandler); + + bindDocumentEvents(); + + if (options.languageTree) { + const toolbox = mainWorkspace.getToolbox(); + const flyout = mainWorkspace.getFlyout(true); + if (toolbox) { + toolbox.init(); + } else if (flyout) { + // Build a fixed flyout with the root blocks. + flyout.init(mainWorkspace); + flyout.show(options.languageTree); + if (typeof flyout.scrollToStart === 'function') { + flyout.scrollToStart(); + } + } + } + + if (options.hasTrashcan) { + mainWorkspace.trashcan!.init(); + } + if (options.zoomOptions && options.zoomOptions.controls) { + mainWorkspace.zoomControls_!.init(); + } + + if (options.moveOptions && options.moveOptions.scrollbars) { + const horizontalScroll = + options.moveOptions.scrollbars === true || + !!options.moveOptions.scrollbars.horizontal; + const verticalScroll = + options.moveOptions.scrollbars === true || + !!options.moveOptions.scrollbars.vertical; + mainWorkspace.scrollbar = new ScrollbarPair( + mainWorkspace, + horizontalScroll, + verticalScroll, + 'blocklyMainWorkspaceScrollbar', + ); + mainWorkspace.scrollbar.resize(); + } else { + mainWorkspace.setMetrics({x: 0.5, y: 0.5}); + } + + // Load the sounds. + if (options.hasSounds) { + loadSounds(options.pathToMedia, mainWorkspace); + } +} + +/** + * Handle a key-down on SVG drawing surface. Does nothing if the main workspace + * is not visible. + * + * @param e Key down event. + */ +// TODO (https://github.com/google/blockly/issues/1998) handle cases where there +// are multiple workspaces and non-main workspaces are able to accept input. +function onKeyDown(e: KeyboardEvent) { + const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg; + if (!mainWorkspace) { + return; + } + + if ( + browserEvents.isTargetInput(e) || + (mainWorkspace.rendered && !mainWorkspace.isVisible()) + ) { + // When focused on an HTML text input widget, don't trap any keys. + // Ignore keypresses on rendered workspaces that have been explicitly + // hidden. + return; + } + ShortcutRegistry.registry.onKeyDown(mainWorkspace, e); +} + +/** + * Whether event handlers have been bound. Document event handlers will only + * be bound once, even if Blockly is destroyed and reinjected. + */ +let documentEventsBound = false; + +/** + * Bind document events, but only once. Destroying and reinjecting Blockly + * should not bind again. + * Bind events for scrolling the workspace. + * Most of these events should be bound to the SVG's surface. + * However, 'mouseup' has to be on the whole document so that a block dragged + * out of bounds and released will know that it has been released. + */ +function bindDocumentEvents() { + if (!documentEventsBound) { + browserEvents.conditionalBind(document, 'scroll', null, function () { + const workspaces = common.getAllWorkspaces(); + for (let i = 0, workspace; (workspace = workspaces[i]); i++) { + if (workspace instanceof WorkspaceSvg) { + workspace.updateInverseScreenCTM(); + } + } + }); + // longStop needs to run to stop the context menu from showing up. It + // should run regardless of what other touch event handlers have run. + browserEvents.bind(document, 'touchend', null, Touch.longStop); + browserEvents.bind(document, 'touchcancel', null, Touch.longStop); + } + documentEventsBound = true; +} + +/** + * Load sounds for the given workspace. + * + * @param pathToMedia The path to the media directory. + * @param workspace The workspace to load sounds for. + */ +function loadSounds(pathToMedia: string, workspace: WorkspaceSvg) { + const audioMgr = workspace.getAudioManager(); + audioMgr.load( + [ + pathToMedia + 'click.mp3', + pathToMedia + 'click.wav', + pathToMedia + 'click.ogg', + ], + 'click', + ); + audioMgr.load( + [ + pathToMedia + 'disconnect.wav', + pathToMedia + 'disconnect.mp3', + pathToMedia + 'disconnect.ogg', + ], + 'disconnect', + ); + audioMgr.load( + [ + pathToMedia + 'delete.mp3', + pathToMedia + 'delete.ogg', + pathToMedia + 'delete.wav', + ], + 'delete', + ); + + // Bind temporary hooks that preload the sounds. + const soundBinds: browserEvents.Data[] = []; + /** + * + */ + function unbindSounds() { + while (soundBinds.length) { + const oldSoundBinding = soundBinds.pop(); + if (oldSoundBinding) { + browserEvents.unbind(oldSoundBinding); + } + } + audioMgr.preload(); + } + + // These are bound on mouse/touch events with + // Blockly.browserEvents.conditionalBind, so they restrict the touch + // identifier that will be recognized. But this is really something that + // happens on a click, not a drag, so that's not necessary. + + // Android ignores any sound not loaded as a result of a user action. + soundBinds.push( + browserEvents.conditionalBind( + document, + 'pointermove', + null, + unbindSounds, + true, + ), + ); + soundBinds.push( + browserEvents.conditionalBind( + document, + 'touchstart', + null, + unbindSounds, + true, + ), + ); +} diff --git a/core/input.js b/core/input.js deleted file mode 100644 index c2c006e5235..00000000000 --- a/core/input.js +++ /dev/null @@ -1,251 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Object representing an input (value, statement, or dummy). - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.Input'); - -goog.require('Blockly.Connection'); -goog.require('Blockly.FieldLabel'); -goog.require('goog.asserts'); - - -/** - * Class for an input with an optional field. - * @param {number} type The type of the input. - * @param {string} name Language-neutral identifier which may used to find this - * input again. - * @param {!Blockly.Block} block The block containing this input. - * @param {Blockly.Connection} connection Optional connection for this input. - * @constructor - */ -Blockly.Input = function(type, name, block, connection) { - if (type != Blockly.DUMMY_INPUT && !name) { - throw 'Value inputs and statement inputs must have non-empty name.'; - } - /** @type {number} */ - this.type = type; - /** @type {string} */ - this.name = name; - /** - * @type {!Blockly.Block} - * @private - */ - this.sourceBlock_ = block; - /** @type {Blockly.Connection} */ - this.connection = connection; - /** @type {!Array.} */ - this.fieldRow = []; -}; - -/** - * Alignment of input's fields (left, right or centre). - * @type {number} - */ -Blockly.Input.prototype.align = Blockly.ALIGN_LEFT; - -/** - * Is the input visible? - * @type {boolean} - * @private - */ -Blockly.Input.prototype.visible_ = true; - -/** - * Add a field (or label from string), and all prefix and suffix fields, to the - * end of the input's field row. - * @param {string|!Blockly.Field} field Something to add as a field. - * @param {string=} opt_name Language-neutral identifier which may used to find - * this field again. Should be unique to the host block. - * @return {!Blockly.Input} The input being append to (to allow chaining). - */ -Blockly.Input.prototype.appendField = function(field, opt_name) { - this.insertFieldAt(this.fieldRow.length, field, opt_name); - return this; -}; - -/** - * Inserts a field (or label from string), and all prefix and suffix fields, at - * the location of the input's field row. - * @param {number} index The index at which to insert field. - * @param {string|!Blockly.Field} field Something to add as a field. - * @param {string=} opt_name Language-neutral identifier which may used to find - * this field again. Should be unique to the host block. - * @return {number} The index following the last inserted field. - */ -Blockly.Input.prototype.insertFieldAt = function(index, field, opt_name) { - if (index < 0 || index > this.fieldRow.length) { - throw new Error('index ' + index + ' out of bounds.'); - } - - // Empty string, Null or undefined generates no field, unless field is named. - if (!field && !opt_name) { - return this; - } - // Generate a FieldLabel when given a plain text field. - if (goog.isString(field)) { - field = new Blockly.FieldLabel(/** @type {string} */ (field)); - } - field.setSourceBlock(this.sourceBlock_); - if (this.sourceBlock_.rendered) { - field.init(); - } - field.name = opt_name; - - if (field.prefixField) { - // Add any prefix. - index = this.insertFieldAt(index, field.prefixField); - } - // Add the field to the field row. - this.fieldRow.splice(index, 0, field); - ++index; - if (field.suffixField) { - // Add any suffix. - index = this.insertFieldAt(index, field.suffixField); - } - - if (this.sourceBlock_.rendered) { - this.sourceBlock_.render(); - // Adding a field will cause the block to change shape. - this.sourceBlock_.bumpNeighbours_(); - } - return index; -}; - -/** - * Remove a field from this input. - * @param {string} name The name of the field. - * @throws {goog.asserts.AssertionError} if the field is not present. - */ -Blockly.Input.prototype.removeField = function(name) { - for (var i = 0, field; field = this.fieldRow[i]; i++) { - if (field.name === name) { - field.dispose(); - this.fieldRow.splice(i, 1); - if (this.sourceBlock_.rendered) { - this.sourceBlock_.render(); - // Removing a field will cause the block to change shape. - this.sourceBlock_.bumpNeighbours_(); - } - return; - } - } - goog.asserts.fail('Field "%s" not found.', name); -}; - -/** - * Gets whether this input is visible or not. - * @return {boolean} True if visible. - */ -Blockly.Input.prototype.isVisible = function() { - return this.visible_; -}; - -/** - * Sets whether this input is visible or not. - * Used to collapse/uncollapse a block. - * @param {boolean} visible True if visible. - * @return {!Array.} List of blocks to render. - */ -Blockly.Input.prototype.setVisible = function(visible) { - var renderList = []; - if (this.visible_ == visible) { - return renderList; - } - this.visible_ = visible; - - var display = visible ? 'block' : 'none'; - for (var y = 0, field; field = this.fieldRow[y]; y++) { - field.setVisible(visible); - } - if (this.connection) { - // Has a connection. - if (visible) { - renderList = this.connection.unhideAll(); - } else { - this.connection.hideAll(); - } - var child = this.connection.targetBlock(); - if (child) { - child.getSvgRoot().style.display = display; - if (!visible) { - child.rendered = false; - } - } - } - return renderList; -}; - -/** - * Change a connection's compatibility. - * @param {string|Array.|null} check Compatible value type or - * list of value types. Null if all types are compatible. - * @return {!Blockly.Input} The input being modified (to allow chaining). - */ -Blockly.Input.prototype.setCheck = function(check) { - if (!this.connection) { - throw 'This input does not have a connection.'; - } - this.connection.setCheck(check); - return this; -}; - -/** - * Change the alignment of the connection's field(s). - * @param {number} align One of Blockly.ALIGN_LEFT, ALIGN_CENTRE, ALIGN_RIGHT. - * In RTL mode directions are reversed, and ALIGN_RIGHT aligns to the left. - * @return {!Blockly.Input} The input being modified (to allow chaining). - */ -Blockly.Input.prototype.setAlign = function(align) { - this.align = align; - if (this.sourceBlock_.rendered) { - this.sourceBlock_.render(); - } - return this; -}; - -/** - * Initialize the fields on this input. - */ -Blockly.Input.prototype.init = function() { - if (!this.sourceBlock_.workspace.rendered) { - return; // Headless blocks don't need fields initialized. - } - for (var i = 0; i < this.fieldRow.length; i++) { - this.fieldRow[i].init(); - } -}; - -/** - * Sever all links to this input. - */ -Blockly.Input.prototype.dispose = function() { - for (var i = 0, field; field = this.fieldRow[i]; i++) { - field.dispose(); - } - if (this.connection) { - this.connection.dispose(); - } - this.sourceBlock_ = null; -}; diff --git a/core/inputs.ts b/core/inputs.ts new file mode 100644 index 00000000000..064d37530a5 --- /dev/null +++ b/core/inputs.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {Align} from './inputs/align.js'; +import {DummyInput} from './inputs/dummy_input.js'; +import {EndRowInput} from './inputs/end_row_input.js'; +import {Input} from './inputs/input.js'; +import {inputTypes} from './inputs/input_types.js'; +import {StatementInput} from './inputs/statement_input.js'; +import {ValueInput} from './inputs/value_input.js'; + +export { + Align, + DummyInput, + EndRowInput, + Input, + inputTypes, + StatementInput, + ValueInput, +}; diff --git a/core/inputs/align.ts b/core/inputs/align.ts new file mode 100644 index 00000000000..b62846f5d84 --- /dev/null +++ b/core/inputs/align.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Enum for alignment of inputs. + */ +export enum Align { + LEFT = -1, + CENTRE = 0, + RIGHT = 1, +} diff --git a/core/inputs/dummy_input.ts b/core/inputs/dummy_input.ts new file mode 100644 index 00000000000..afb4b375b5f --- /dev/null +++ b/core/inputs/dummy_input.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Block} from '../block.js'; +import {Input} from './input.js'; +import {inputTypes} from './input_types.js'; + +/** Represents an input on a block with no connection. */ +export class DummyInput extends Input { + readonly type = inputTypes.DUMMY; + + /** + * @param name Language-neutral identifier which may used to find this input + * again. + * @param block The block containing this input. + */ + constructor( + public name: string, + block: Block, + ) { + super(name, block); + } +} diff --git a/core/inputs/end_row_input.ts b/core/inputs/end_row_input.ts new file mode 100644 index 00000000000..58227a09457 --- /dev/null +++ b/core/inputs/end_row_input.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Block} from '../block.js'; +import {Input} from './input.js'; +import {inputTypes} from './input_types.js'; + +/** + * Represents an input on a block that is always the last input in the row. Any + * following input will be rendered on the next row even if the block has inline + * inputs. Any newline character in a JSON block definition's message will + * automatically be parsed as an end-row input. + */ +export class EndRowInput extends Input { + readonly type = inputTypes.END_ROW; + + /** + * @param name Language-neutral identifier which may used to find this input + * again. + * @param block The block containing this input. + */ + constructor( + public name: string, + block: Block, + ) { + super(name, block); + } +} diff --git a/core/inputs/input.ts b/core/inputs/input.ts new file mode 100644 index 00000000000..0907bf44939 --- /dev/null +++ b/core/inputs/input.ts @@ -0,0 +1,318 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Object representing an input (value, statement, or dummy). + * + * @class + */ +// Former goog.module ID: Blockly.Input + +// Unused import preserved for side-effects. Remove if unneeded. +import '../field_label.js'; + +import type {Block} from '../block.js'; +import type {BlockSvg} from '../block_svg.js'; +import type {Connection} from '../connection.js'; +import type {ConnectionType} from '../connection_type.js'; +import type {Field} from '../field.js'; +import * as fieldRegistry from '../field_registry.js'; +import type {RenderedConnection} from '../rendered_connection.js'; +import {Align} from './align.js'; +import {inputTypes} from './input_types.js'; + +/** Class for an input with optional fields. */ +export class Input { + fieldRow: Field[] = []; + /** Alignment of input's fields (left, right or centre). */ + align = Align.LEFT; + + /** Is the input visible? */ + private visible = true; + + public readonly type: inputTypes = inputTypes.CUSTOM; + + public connection: Connection | null = null; + + /** + * @param name Language-neutral identifier which may used to find this input + * again. + * @param sourceBlock The block containing this input. + */ + constructor( + public name: string, + private sourceBlock: Block, + ) {} + + /** + * Get the source block for this input. + * + * @returns The block this input is part of. + */ + getSourceBlock(): Block { + return this.sourceBlock; + } + + /** + * Add a field (or label from string), and all prefix and suffix fields, to + * the end of the input's field row. + * + * @param field Something to add as a field. + * @param opt_name Language-neutral identifier which may used to find this + * field again. Should be unique to the host block. + * @returns The input being append to (to allow chaining). + */ + appendField(field: string | Field, opt_name?: string): Input { + this.insertFieldAt(this.fieldRow.length, field, opt_name); + return this; + } + + /** + * Inserts a field (or label from string), and all prefix and suffix fields, + * at the location of the input's field row. + * + * @param index The index at which to insert field. + * @param field Something to add as a field. + * @param opt_name Language-neutral identifier which may used to find this + * field again. Should be unique to the host block. + * @returns The index following the last inserted field. + */ + insertFieldAt( + index: number, + field: string | Field, + opt_name?: string, + ): number { + if (index < 0 || index > this.fieldRow.length) { + throw Error('index ' + index + ' out of bounds.'); + } + // Falsy field values don't generate a field, unless the field is an empty + // string and named. + if (!field && !(field === '' && opt_name)) { + return index; + } + + // Generate a FieldLabel when given a plain text field. + if (typeof field === 'string') { + field = fieldRegistry.fromJson({ + type: 'field_label', + text: field, + })!; + } + + field.setSourceBlock(this.sourceBlock); + if (this.sourceBlock.initialized) this.initField(field); + field.name = opt_name; + field.setVisible(this.isVisible()); + + if (field.prefixField) { + // Add any prefix. + index = this.insertFieldAt(index, field.prefixField); + } + // Add the field to the field row. + this.fieldRow.splice(index, 0, field as Field); + index++; + if (field.suffixField) { + // Add any suffix. + index = this.insertFieldAt(index, field.suffixField); + } + + if (this.sourceBlock.rendered) { + (this.sourceBlock as BlockSvg).queueRender(); + } + return index; + } + + /** + * Remove a field from this input. + * + * @param name The name of the field. + * @param opt_quiet True to prevent an error if field is not present. + * @returns True if operation succeeds, false if field is not present and + * opt_quiet is true. + * @throws {Error} if the field is not present and opt_quiet is false. + */ + removeField(name: string, opt_quiet?: boolean): boolean { + for (let i = 0, field; (field = this.fieldRow[i]); i++) { + if (field.name === name) { + field.dispose(); + this.fieldRow.splice(i, 1); + if (this.sourceBlock.rendered) { + (this.sourceBlock as BlockSvg).queueRender(); + } + return true; + } + } + if (opt_quiet) { + return false; + } + throw Error('Field "' + name + '" not found.'); + } + + /** + * Gets whether this input is visible or not. + * + * @returns True if visible. + */ + isVisible(): boolean { + return this.visible; + } + + /** + * Sets whether this input is visible or not. + * Should only be used to collapse/uncollapse a block. + * + * @param visible True if visible. + * @returns List of blocks to render. + * @internal + */ + setVisible(visible: boolean): BlockSvg[] { + // Note: Currently there are only unit tests for block.setCollapsed() + // because this function is package. If this function goes back to being a + // public API tests (lots of tests) should be added. + let renderList: AnyDuringMigration[] = []; + if (this.visible === visible) { + return renderList; + } + this.visible = visible; + + for (let y = 0, field; (field = this.fieldRow[y]); y++) { + field.setVisible(visible); + } + if (this.connection) { + const renderedConnection = this.connection as RenderedConnection; + // Has a connection. + if (visible) { + renderList = renderedConnection.startTrackingAll(); + } else { + renderedConnection.stopTrackingAll(); + } + const child = renderedConnection.targetBlock(); + if (child) { + child.getSvgRoot().style.display = visible ? 'block' : 'none'; + } + } + return renderList; + } + + /** + * Mark all fields on this input as dirty. + * + * @internal + */ + markDirty() { + for (let y = 0, field; (field = this.fieldRow[y]); y++) { + field.markDirty(); + } + } + + /** + * Change a connection's compatibility. + * + * @param check Compatible value type or list of value types. Null if all + * types are compatible. + * @returns The input being modified (to allow chaining). + */ + setCheck(check: string | string[] | null): Input { + if (!this.connection) { + throw Error('This input does not have a connection.'); + } + this.connection.setCheck(check); + return this; + } + + /** + * Change the alignment of the connection's field(s). + * + * @param align One of the values of Align. In RTL mode directions + * are reversed, and Align.RIGHT aligns to the left. + * @returns The input being modified (to allow chaining). + */ + setAlign(align: Align): Input { + this.align = align; + if (this.sourceBlock.rendered) { + const sourceBlock = this.sourceBlock as BlockSvg; + sourceBlock.queueRender(); + } + return this; + } + + /** + * Changes the connection's shadow block. + * + * @param shadow DOM representation of a block or null. + * @returns The input being modified (to allow chaining). + */ + setShadowDom(shadow: Element | null): Input { + if (!this.connection) { + throw Error('This input does not have a connection.'); + } + this.connection.setShadowDom(shadow); + return this; + } + + /** + * Returns the XML representation of the connection's shadow block. + * + * @returns Shadow DOM representation of a block or null. + */ + getShadowDom(): Element | null { + if (!this.connection) { + throw Error('This input does not have a connection.'); + } + return this.connection.getShadowDom(); + } + + /** Initialize the fields on this input. */ + init() { + for (const field of this.fieldRow) { + field.init(); + } + } + + /** + * Initializes the fields on this input for a headless block. + * + * @internal + */ + public initModel() { + for (const field of this.fieldRow) { + field.initModel(); + } + } + + /** Initializes the given field. */ + private initField(field: Field) { + if (this.sourceBlock.rendered) { + field.init(); + } else { + field.initModel(); + } + } + + /** + * Sever all links to this input. + */ + dispose() { + for (let i = 0, field; (field = this.fieldRow[i]); i++) { + field.dispose(); + } + if (this.connection) { + this.connection.dispose(); + } + } + + /** + * Constructs a connection based on the type of this input's source block. + * Properly handles constructing headless connections for headless blocks + * and rendered connections for rendered blocks. + * + * @returns a connection of the given type, which is either a headless + * or rendered connection, based on the type of this input's source block. + */ + protected makeConnection(type: ConnectionType): Connection { + return this.sourceBlock.makeConnection_(type); + } +} diff --git a/core/inputs/input_types.ts b/core/inputs/input_types.ts new file mode 100644 index 00000000000..cdae653dee1 --- /dev/null +++ b/core/inputs/input_types.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.inputTypes + +import {ConnectionType} from '../connection_type.js'; + +/** + * Enum for the type of a connection or input. + */ +export enum inputTypes { + // A right-facing value input. E.g. 'set item to' or 'return'. + VALUE = ConnectionType.INPUT_VALUE, + // A down-facing block stack. E.g. 'if-do' or 'else'. + STATEMENT = ConnectionType.NEXT_STATEMENT, + // A dummy input. Used to add field(s) with no input. + DUMMY = 5, + // An unknown type of input defined by an external developer. + CUSTOM = 6, + // An input with no connections that is always the last input of a row. Any + // subsequent input will be rendered on the next row. Any newline character in + // a JSON block definition's message will be parsed as an end-row input. + END_ROW = 7, +} diff --git a/core/inputs/statement_input.ts b/core/inputs/statement_input.ts new file mode 100644 index 00000000000..cf97de2343c --- /dev/null +++ b/core/inputs/statement_input.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Block} from '../block.js'; +import type {Connection} from '../connection.js'; +import {ConnectionType} from '../connection_type.js'; +import {Input} from './input.js'; +import {inputTypes} from './input_types.js'; + +/** Represents an input on a block with a statement connection. */ +export class StatementInput extends Input { + readonly type = inputTypes.STATEMENT; + + public connection: Connection; + + /** + * @param name Language-neutral identifier which may used to find this input + * again. + * @param block The block containing this input. + */ + constructor( + public name: string, + block: Block, + ) { + // Errors are maintained for people not using typescript. + if (!name) throw new Error('Statement inputs must have a non-empty name'); + + super(name, block); + this.connection = this.makeConnection(ConnectionType.NEXT_STATEMENT); + } +} diff --git a/core/inputs/value_input.ts b/core/inputs/value_input.ts new file mode 100644 index 00000000000..e8049b471f4 --- /dev/null +++ b/core/inputs/value_input.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Block} from '../block.js'; +import {ConnectionType} from '../connection_type.js'; +import {Input} from './input.js'; +import {inputTypes} from './input_types.js'; + +/** Represents an input on a block with a value connection. */ +export class ValueInput extends Input { + readonly type = inputTypes.VALUE; + + /** + * @param name Language-neutral identifier which may used to find this input + * again. + * @param block The block containing this input. + */ + constructor( + public name: string, + block: Block, + ) { + // Errors are maintained for people not using typescript. + if (!name) throw new Error('Value inputs must have a non-empty name'); + super(name, block); + this.connection = this.makeConnection(ConnectionType.INPUT_VALUE); + } +} diff --git a/core/insertion_marker_manager.ts b/core/insertion_marker_manager.ts new file mode 100644 index 00000000000..13d63042002 --- /dev/null +++ b/core/insertion_marker_manager.ts @@ -0,0 +1,742 @@ +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class that controls updates to connections during drags. + * + * @class + */ +// Former goog.module ID: Blockly.InsertionMarkerManager + +import * as blockAnimations from './block_animations.js'; +import type {BlockSvg} from './block_svg.js'; +import * as common from './common.js'; +import {ComponentManager} from './component_manager.js'; +import {config} from './config.js'; +import * as eventUtils from './events/utils.js'; +import type {IDeleteArea} from './interfaces/i_delete_area.js'; +import type {IDragTarget} from './interfaces/i_drag_target.js'; +import * as renderManagement from './render_management.js'; +import {finishQueuedRenders} from './render_management.js'; +import type {RenderedConnection} from './rendered_connection.js'; +import * as blocks from './serialization/blocks.js'; +import type {Coordinate} from './utils/coordinate.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** Represents a nearby valid connection. */ +interface CandidateConnection { + /** + * A nearby valid connection that is compatible with local. + * This is not on any of the blocks that are being dragged. + */ + closest: RenderedConnection; + /** + * A connection on the dragging stack that is compatible with closest. This is + * on the top block that is being dragged or the last block in the dragging + * stack. + */ + local: RenderedConnection; + radius: number; +} + +/** + * Class that controls updates to connections during drags. It is primarily + * responsible for finding the closest eligible connection and highlighting or + * unhighlighting it as needed during a drag. + * + * @deprecated v10 - Use an IConnectionPreviewer instead. + */ +export class InsertionMarkerManager { + /** + * The top block in the stack being dragged. + * Does not change during a drag. + */ + private readonly topBlock: BlockSvg; + + /** + * The workspace on which these connections are being dragged. + * Does not change during a drag. + */ + private readonly workspace: WorkspaceSvg; + + /** + * The last connection on the stack, if it's not the last connection on the + * first block. + * Set in initAvailableConnections, if at all. + */ + private lastOnStack: RenderedConnection | null = null; + + /** + * The insertion marker corresponding to the last block in the stack, if + * that's not the same as the first block in the stack. + * Set in initAvailableConnections, if at all + */ + private lastMarker: BlockSvg | null = null; + + /** + * The insertion marker that shows up between blocks to show where a block + * would go if dropped immediately. + */ + private firstMarker: BlockSvg; + + /** + * Information about the connection that would be made if the dragging block + * were released immediately. Updated on every mouse move. + */ + private activeCandidate: CandidateConnection | null = null; + + /** + * Whether the block would be deleted if it were dropped immediately. + * Updated on every mouse move. + * + * @internal + */ + public wouldDeleteBlock = false; + + /** + * Connection on the insertion marker block that corresponds to + * the active candidate's local connection on the currently dragged block. + */ + private markerConnection: RenderedConnection | null = null; + + /** The block that currently has an input being highlighted, or null. */ + private highlightedBlock: BlockSvg | null = null; + + /** The block being faded to indicate replacement, or null. */ + private fadedBlock: BlockSvg | null = null; + + /** + * The connections on the dragging blocks that are available to connect to + * other blocks. This includes all open connections on the top block, as + * well as the last connection on the block stack. + */ + private availableConnections: RenderedConnection[]; + + /** @param block The top block in the stack being dragged. */ + constructor(block: BlockSvg) { + common.setSelected(block); + this.topBlock = block; + + this.workspace = block.workspace; + + this.firstMarker = this.createMarkerBlock(this.topBlock); + + this.availableConnections = this.initAvailableConnections(); + + if (this.lastOnStack) { + this.lastMarker = this.createMarkerBlock( + this.lastOnStack.getSourceBlock(), + ); + } + } + + /** + * Sever all links from this object. + * + * @internal + */ + dispose() { + this.availableConnections.length = 0; + this.disposeInsertionMarker(this.firstMarker); + this.disposeInsertionMarker(this.lastMarker); + } + + /** + * Update the available connections for the top block. These connections can + * change if a block is unplugged and the stack is healed. + * + * @internal + */ + updateAvailableConnections() { + this.availableConnections = this.initAvailableConnections(); + } + + /** + * Return whether the block would be connected if dropped immediately, based + * on information from the most recent move event. + * + * @returns True if the block would be connected if dropped immediately. + * @internal + */ + wouldConnectBlock(): boolean { + return !!this.activeCandidate; + } + + /** + * Connect to the closest connection and render the results. + * This should be called at the end of a drag. + * + * @internal + */ + applyConnections() { + if (!this.activeCandidate) return; + eventUtils.disable(); + this.hidePreview(); + eventUtils.enable(); + const {local, closest} = this.activeCandidate; + local.connect(closest); + const inferiorConnection = local.isSuperior() ? closest : local; + const rootBlock = this.topBlock.getRootBlock(); + + finishQueuedRenders().then(() => { + blockAnimations.connectionUiEffect(inferiorConnection.getSourceBlock()); + // bringToFront is incredibly expensive. Delay until the next frame. + setTimeout(() => { + rootBlock.bringToFront(); + }, 0); + }); + } + + /** + * Update connections based on the most recent move location. + * + * @param dxy Position relative to drag start, in workspace units. + * @param dragTarget The drag target that the block is currently over. + * @internal + */ + update(dxy: Coordinate, dragTarget: IDragTarget | null) { + const newCandidate = this.getCandidate(dxy); + + this.wouldDeleteBlock = this.shouldDelete(!!newCandidate, dragTarget); + + const shouldUpdate = + this.wouldDeleteBlock || this.shouldUpdatePreviews(newCandidate, dxy); + + if (shouldUpdate) { + // Don't fire events for insertion marker creation or movement. + eventUtils.disable(); + this.maybeHidePreview(newCandidate); + this.maybeShowPreview(newCandidate); + eventUtils.enable(); + } + } + + /** + * Create an insertion marker that represents the given block. + * + * @param sourceBlock The block that the insertion marker will represent. + * @returns The insertion marker that represents the given block. + */ + private createMarkerBlock(sourceBlock: BlockSvg): BlockSvg { + eventUtils.disable(); + let result: BlockSvg; + try { + const blockJson = blocks.save(sourceBlock, { + addCoordinates: false, + addInputBlocks: false, + addNextBlocks: false, + doFullSerialization: false, + }); + + if (!blockJson) { + throw new Error( + `Failed to serialize source block. ${sourceBlock.toDevString()}`, + ); + } + + result = blocks.append(blockJson, this.workspace) as BlockSvg; + + // Turn shadow blocks that are created programmatically during + // initalization to insertion markers too. + for (const block of result.getDescendants(false)) { + block.setInsertionMarker(true); + } + + result.initSvg(); + result.getSvgRoot().setAttribute('visibility', 'hidden'); + } finally { + eventUtils.enable(); + } + + return result; + } + + /** + * Populate the list of available connections on this block stack. If the + * stack has more than one block, this function will also update lastOnStack. + * + * @returns A list of available connections. + */ + private initAvailableConnections(): RenderedConnection[] { + const available = this.topBlock.getConnections_(false); + // Also check the last connection on this stack + const lastOnStack = this.topBlock.lastConnectionInStack(true); + if (lastOnStack && lastOnStack !== this.topBlock.nextConnection) { + available.push(lastOnStack); + this.lastOnStack = lastOnStack; + } + return available; + } + + /** + * Whether the previews (insertion marker and replacement marker) should be + * updated based on the closest candidate and the current drag distance. + * + * @param newCandidate A new candidate connection that may replace the current + * best candidate. + * @param dxy Position relative to drag start, in workspace units. + * @returns Whether the preview should be updated. + */ + private shouldUpdatePreviews( + newCandidate: CandidateConnection | null, + dxy: Coordinate, + ): boolean { + // Only need to update if we were showing a preview before. + if (!newCandidate) return !!this.activeCandidate; + + // We weren't showing a preview before, but we should now. + if (!this.activeCandidate) return true; + + // We're already showing an insertion marker. + // Decide whether the new connection has higher priority. + const {local: activeLocal, closest: activeClosest} = this.activeCandidate; + if ( + activeClosest === newCandidate.closest && + activeLocal === newCandidate.local + ) { + // The connection was the same as the current connection. + return false; + } + + const xDiff = activeLocal.x + dxy.x - activeClosest.x; + const yDiff = activeLocal.y + dxy.y - activeClosest.y; + const curDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff); + // Slightly prefer the existing preview over a new preview. + return ( + newCandidate.radius < curDistance - config.currentConnectionPreference + ); + } + + /** + * Find the nearest valid connection, which may be the same as the current + * closest connection. + * + * @param dxy Position relative to drag start, in workspace units. + * @returns An object containing a local connection, a closest connection, and + * a radius. + */ + private getCandidate(dxy: Coordinate): CandidateConnection | null { + // It's possible that a block has added or removed connections during a + // drag, (e.g. in a drag/move event handler), so let's update the available + // connections. Note that this will be called on every move while dragging, + // so it might cause slowness, especially if the block stack is large. If + // so, maybe it could be made more efficient. Also note that we won't update + // the connections if we've already connected the insertion marker to a + // block. + if (!this.markerConnection || !this.markerConnection.isConnected()) { + this.updateAvailableConnections(); + } + + let radius = this.getStartRadius(); + let candidate = null; + for (let i = 0; i < this.availableConnections.length; i++) { + const myConnection = this.availableConnections[i]; + const neighbour = myConnection.closest(radius, dxy); + if (neighbour.connection) { + candidate = { + closest: neighbour.connection, + local: myConnection, + radius: neighbour.radius, + }; + radius = neighbour.radius; + } + } + return candidate; + } + + /** + * Decide the radius at which to start searching for the closest connection. + * + * @returns The radius at which to start the search for the closest + * connection. + */ + private getStartRadius(): number { + // If there is already a connection highlighted, + // increase the radius we check for making new connections. + // When a connection is highlighted, blocks move around when the + // insertion marker is created, which could cause the connection became out + // of range. By increasing radiusConnection when a connection already + // exists, we never "lose" the connection from the offset. + return this.activeCandidate + ? config.connectingSnapRadius + : config.snapRadius; + } + + /** + * Whether ending the drag would delete the block. + * + * @param newCandidate Whether there is a candidate connection that the + * block could connect to if the drag ended immediately. + * @param dragTarget The drag target that the block is currently over. + * @returns Whether dropping the block immediately would delete the block. + */ + private shouldDelete( + newCandidate: boolean, + dragTarget: IDragTarget | null, + ): boolean { + if (dragTarget) { + const componentManager = this.workspace.getComponentManager(); + const isDeleteArea = componentManager.hasCapability( + dragTarget.id, + ComponentManager.Capability.DELETE_AREA, + ); + if (isDeleteArea) { + return (dragTarget as IDeleteArea).wouldDelete(this.topBlock); + } + } + return false; + } + + /** + * Show an insertion marker or replacement highlighting during a drag, if + * needed. + * At the beginning of this function, this.activeConnection should be null. + * + * @param newCandidate A new candidate connection that may replace the current + * best candidate. + */ + private maybeShowPreview(newCandidate: CandidateConnection | null) { + if (this.wouldDeleteBlock) return; // Nope, don't add a marker. + if (!newCandidate) return; // Nothing to connect to. + + const closest = newCandidate.closest; + + // Something went wrong and we're trying to connect to an invalid + // connection. + if ( + closest === this.activeCandidate?.closest || + closest.getSourceBlock().isInsertionMarker() + ) { + console.log('Trying to connect to an insertion marker'); + return; + } + this.activeCandidate = newCandidate; + // Add an insertion marker or replacement marker. + this.showPreview(this.activeCandidate); + } + + /** + * A preview should be shown. This function figures out if it should be a + * block highlight or an insertion marker, and shows the appropriate one. + * + * @param activeCandidate The connection that will be made if the drag ends + * immediately. + */ + private showPreview(activeCandidate: CandidateConnection) { + const renderer = this.workspace.getRenderer(); + const method = renderer.getConnectionPreviewMethod( + activeCandidate.closest, + activeCandidate.local, + this.topBlock, + ); + + switch (method) { + case InsertionMarkerManager.PREVIEW_TYPE.INPUT_OUTLINE: + this.showInsertionInputOutline(activeCandidate); + break; + case InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER: + this.showInsertionMarker(activeCandidate); + break; + case InsertionMarkerManager.PREVIEW_TYPE.REPLACEMENT_FADE: + this.showReplacementFade(activeCandidate); + break; + } + + // Optionally highlight the actual connection, as a nod to previous + // behaviour. + if (renderer.shouldHighlightConnection(activeCandidate.closest)) { + activeCandidate.closest.highlight(); + } + } + + /** + * Hide an insertion marker or replacement highlighting during a drag, if + * needed. + * At the end of this function, this.activeCandidate will be null. + * + * @param newCandidate A new candidate connection that may replace the current + * best candidate. + */ + private maybeHidePreview(newCandidate: CandidateConnection | null) { + // If there's no new preview, remove the old one but don't bother deleting + // it. We might need it later, and this saves disposing of it and recreating + // it. + if (!newCandidate) { + this.hidePreview(); + } else { + if (this.activeCandidate) { + const closestChanged = + this.activeCandidate.closest !== newCandidate.closest; + const localChanged = this.activeCandidate.local !== newCandidate.local; + + // If there's a new preview and there was a preview before, and either + // connection has changed, remove the old preview. + // Also hide if we had a preview before but now we're going to delete + // instead. + if (closestChanged || localChanged || this.wouldDeleteBlock) { + this.hidePreview(); + } + } + } + + // Either way, clear out old state. + this.markerConnection = null; + this.activeCandidate = null; + } + + /** + * A preview should be hidden. Loop through all possible preview modes + * and hide everything. + */ + private hidePreview() { + const closest = this.activeCandidate?.closest; + if ( + closest && + closest.targetBlock() && + this.workspace.getRenderer().shouldHighlightConnection(closest) + ) { + closest.unhighlight(); + } + this.hideReplacementFade(); + this.hideInsertionInputOutline(); + this.hideInsertionMarker(); + } + + /** + * Shows an insertion marker connected to the appropriate blocks (based on + * manager state). + * + * @param activeCandidate The connection that will be made if the drag ends + * immediately. + */ + private showInsertionMarker(activeCandidate: CandidateConnection) { + const {local, closest} = activeCandidate; + + const isLastInStack = this.lastOnStack && local === this.lastOnStack; + let insertionMarker = isLastInStack ? this.lastMarker : this.firstMarker; + if (!insertionMarker) { + throw new Error( + 'Cannot show the insertion marker because there is no insertion ' + + 'marker block', + ); + } + let imConn; + try { + imConn = insertionMarker.getMatchingConnection( + local.getSourceBlock(), + local, + ); + } catch { + // It's possible that the number of connections on the local block has + // changed since the insertion marker was originally created. Let's + // recreate the insertion marker and try again. In theory we could + // probably recreate the marker block (e.g. in getCandidate_), which is + // called more often during the drag, but creating a block that often + // might be too slow, so we only do it if necessary. + if (isLastInStack && this.lastOnStack) { + this.disposeInsertionMarker(this.lastMarker); + this.lastMarker = this.createMarkerBlock( + this.lastOnStack.getSourceBlock(), + ); + insertionMarker = this.lastMarker; + } else { + this.disposeInsertionMarker(this.firstMarker); + this.firstMarker = this.createMarkerBlock(this.topBlock); + insertionMarker = this.firstMarker; + } + + if (!insertionMarker) { + throw new Error( + 'Cannot show the insertion marker because there is no insertion ' + + 'marker block', + ); + } + imConn = insertionMarker.getMatchingConnection( + local.getSourceBlock(), + local, + ); + } + + if (!imConn) { + throw new Error( + 'Cannot show the insertion marker because there is no ' + + 'associated connection', + ); + } + + if (imConn === this.markerConnection) { + throw new Error( + "Made it to showInsertionMarker_ even though the marker isn't " + + 'changing', + ); + } + + // Render disconnected from everything else so that we have a valid + // connection location. + insertionMarker.queueRender(); + renderManagement.triggerQueuedRenders(); + + // Connect() also renders the insertion marker. + imConn.connect(closest); + + const originalOffsetToTarget = { + x: closest.x - imConn.x, + y: closest.y - imConn.y, + }; + const originalOffsetInBlock = imConn.getOffsetInBlock().clone(); + const imConnConst = imConn; + renderManagement.finishQueuedRenders().then(() => { + // Position so that the existing block doesn't move. + insertionMarker?.positionNearConnection( + imConnConst, + originalOffsetToTarget, + originalOffsetInBlock, + ); + insertionMarker?.getSvgRoot().setAttribute('visibility', 'visible'); + }); + + this.markerConnection = imConn; + } + + /** + * Disconnects and hides the current insertion marker. Should return the + * blocks to their original state. + */ + private hideInsertionMarker() { + if (!this.markerConnection) return; + + const markerConn = this.markerConnection; + const imBlock = markerConn.getSourceBlock(); + const markerPrev = imBlock.previousConnection; + const markerOutput = imBlock.outputConnection; + + if (!markerPrev?.targetConnection && !markerOutput?.targetConnection) { + // If we are the top block, unplugging doesn't do anything. + // The marker connection may not have a target block if we are hiding + // as part of applying connections. + markerConn.targetBlock()?.unplug(false); + } else { + imBlock.unplug(true); + } + + if (markerConn.targetConnection) { + throw Error( + 'markerConnection still connected at the end of ' + + 'disconnectInsertionMarker', + ); + } + + this.markerConnection = null; + const svg = imBlock.getSvgRoot(); + if (svg) { + svg.setAttribute('visibility', 'hidden'); + } + } + + /** + * Shows an outline around the input the closest connection belongs to. + * + * @param activeCandidate The connection that will be made if the drag ends + * immediately. + */ + private showInsertionInputOutline(activeCandidate: CandidateConnection) { + const closest = activeCandidate.closest; + this.highlightedBlock = closest.getSourceBlock(); + this.highlightedBlock.highlightShapeForInput(closest, true); + } + + /** Hides any visible input outlines. */ + private hideInsertionInputOutline() { + if (!this.highlightedBlock) return; + + if (!this.activeCandidate) { + throw new Error( + 'Cannot hide the insertion marker outline because ' + + 'there is no active candidate', + ); + } + this.highlightedBlock.highlightShapeForInput( + this.activeCandidate.closest, + false, + ); + this.highlightedBlock = null; + } + + /** + * Shows a replacement fade affect on the closest connection's target block + * (the block that is currently connected to it). + * + * @param activeCandidate The connection that will be made if the drag ends + * immediately. + */ + private showReplacementFade(activeCandidate: CandidateConnection) { + this.fadedBlock = activeCandidate.closest.targetBlock(); + if (!this.fadedBlock) { + throw new Error( + 'Cannot show the replacement fade because the ' + + 'closest connection does not have a target block', + ); + } + this.fadedBlock.fadeForReplacement(true); + } + + /** + * Hides/Removes any visible fade affects. + */ + private hideReplacementFade() { + if (!this.fadedBlock) return; + + this.fadedBlock.fadeForReplacement(false); + this.fadedBlock = null; + } + + /** + * Get a list of the insertion markers that currently exist. Drags have 0, 1, + * or 2 insertion markers. + * + * @returns A possibly empty list of insertion marker blocks. + * @internal + */ + getInsertionMarkers(): BlockSvg[] { + const result = []; + if (this.firstMarker) { + result.push(this.firstMarker); + } + if (this.lastMarker) { + result.push(this.lastMarker); + } + return result; + } + + /** + * Safely disposes of an insertion marker. + */ + private disposeInsertionMarker(marker: BlockSvg | null) { + if (marker) { + eventUtils.disable(); + try { + marker.dispose(); + } finally { + eventUtils.enable(); + } + } + } +} + +export namespace InsertionMarkerManager { + /** + * An enum describing different kinds of previews the InsertionMarkerManager + * could display. + */ + export enum PREVIEW_TYPE { + INSERTION_MARKER = 0, + INPUT_OUTLINE = 1, + REPLACEMENT_FADE = 2, + } +} + +export type PreviewType = InsertionMarkerManager.PREVIEW_TYPE; +export const PreviewType = InsertionMarkerManager.PREVIEW_TYPE; diff --git a/core/insertion_marker_previewer.ts b/core/insertion_marker_previewer.ts new file mode 100644 index 00000000000..2343b9adc76 --- /dev/null +++ b/core/insertion_marker_previewer.ts @@ -0,0 +1,254 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {BlockSvg} from './block_svg.js'; +import {ConnectionType} from './connection_type.js'; +import * as eventUtils from './events/utils.js'; +import {IConnectionPreviewer} from './interfaces/i_connection_previewer.js'; +import * as registry from './registry.js'; +import * as renderManagement from './render_management.js'; +import {RenderedConnection} from './rendered_connection.js'; +import {Renderer as ZelosRenderer} from './renderers/zelos/renderer.js'; +import * as blocks from './serialization/blocks.js'; +import {WorkspaceSvg} from './workspace_svg.js'; + +export class InsertionMarkerPreviewer implements IConnectionPreviewer { + private readonly workspace: WorkspaceSvg; + + private fadedBlock: BlockSvg | null = null; + + private markerConn: RenderedConnection | null = null; + + private draggedConn: RenderedConnection | null = null; + + private staticConn: RenderedConnection | null = null; + + constructor(draggedBlock: BlockSvg) { + this.workspace = draggedBlock.workspace; + } + + /** + * Display a connection preview where the draggedCon connects to the + * staticCon, replacing the replacedBlock (currently connected to the + * staticCon). + * + * @param draggedConn The connection on the block stack being dragged. + * @param staticConn The connection not being dragged that we are + * connecting to. + * @param replacedBlock The block currently connected to the staticCon that + * is being replaced. + */ + previewReplacement( + draggedConn: RenderedConnection, + staticConn: RenderedConnection, + replacedBlock: BlockSvg, + ) { + eventUtils.disable(); + try { + this.hidePreview(); + this.fadedBlock = replacedBlock; + replacedBlock.fadeForReplacement(true); + if (this.workspace.getRenderer().shouldHighlightConnection(staticConn)) { + staticConn.highlight(); + this.staticConn = staticConn; + } + } finally { + eventUtils.enable(); + } + } + + /** + * Display a connection preview where the draggedCon connects to the + * staticCon, and no block is being relaced. + * + * @param draggedConn The connection on the block stack being dragged. + * @param staticConn The connection not being dragged that we are + * connecting to. + */ + previewConnection( + draggedConn: RenderedConnection, + staticConn: RenderedConnection, + ) { + if (draggedConn === this.draggedConn && staticConn === this.staticConn) { + return; + } + + eventUtils.disable(); + try { + this.hidePreview(); + + // TODO(7898): Instead of special casing, we should change the dragger to + // track the change in distance between the dragged connection and the + // static connection, so that it doesn't disconnect unless that + // (+ a bit) has been exceeded. + if (this.shouldUseMarkerPreview(draggedConn, staticConn)) { + this.markerConn = this.previewMarker(draggedConn, staticConn); + } + + if (this.workspace.getRenderer().shouldHighlightConnection(staticConn)) { + staticConn.highlight(); + } + + this.draggedConn = draggedConn; + this.staticConn = staticConn; + } finally { + eventUtils.enable(); + } + } + + private shouldUseMarkerPreview( + _draggedConn: RenderedConnection, + staticConn: RenderedConnection, + ): boolean { + return ( + staticConn.type === ConnectionType.PREVIOUS_STATEMENT || + staticConn.type === ConnectionType.NEXT_STATEMENT || + !(this.workspace.getRenderer() instanceof ZelosRenderer) + ); + } + + private previewMarker( + draggedConn: RenderedConnection, + staticConn: RenderedConnection, + ): RenderedConnection | null { + const dragged = draggedConn.getSourceBlock(); + const marker = this.createInsertionMarker(dragged); + const markerConn = this.getMatchingConnection(dragged, marker, draggedConn); + if (!markerConn) return null; + + // Render disconnected from everything else so that we have a valid + // connection location. + marker.queueRender(); + renderManagement.triggerQueuedRenders(); + + // Connect() also renders the insertion marker. + markerConn.connect(staticConn); + + const originalOffsetToTarget = { + x: staticConn.x - markerConn.x, + y: staticConn.y - markerConn.y, + }; + const originalOffsetInBlock = markerConn.getOffsetInBlock().clone(); + renderManagement.finishQueuedRenders().then(() => { + if (marker.isDeadOrDying()) return; + eventUtils.disable(); + try { + // Position so that the existing block doesn't move. + marker?.positionNearConnection( + markerConn, + originalOffsetToTarget, + originalOffsetInBlock, + ); + marker?.getSvgRoot().setAttribute('visibility', 'visible'); + } finally { + eventUtils.enable(); + } + }); + return markerConn; + } + + private createInsertionMarker(origBlock: BlockSvg) { + const blockJson = blocks.save(origBlock, { + addCoordinates: false, + addInputBlocks: false, + addNextBlocks: false, + doFullSerialization: false, + }); + + if (!blockJson) { + throw new Error( + `Failed to serialize source block. ${origBlock.toDevString()}`, + ); + } + + const result = blocks.append(blockJson, this.workspace) as BlockSvg; + + // Turn shadow blocks that are created programmatically during + // initalization to insertion markers too. + for (const block of result.getDescendants(false)) { + block.setInsertionMarker(true); + } + + result.initSvg(); + result.getSvgRoot().setAttribute('visibility', 'hidden'); + return result; + } + + /** + * Gets the connection on the marker block that matches the original + * connection on the original block. + * + * @param orig The original block. + * @param marker The marker block (where we want to find the matching + * connection). + * @param origConn The original connection. + */ + private getMatchingConnection( + orig: BlockSvg, + marker: BlockSvg, + origConn: RenderedConnection, + ): RenderedConnection | null { + const origConns = orig.getConnections_(true); + const markerConns = marker.getConnections_(true); + if (origConns.length !== markerConns.length) return null; + for (let i = 0; i < origConns.length; i++) { + if (origConns[i] === origConn) { + return markerConns[i]; + } + } + return null; + } + + /** Hide any previews that are currently displayed. */ + hidePreview() { + eventUtils.disable(); + try { + if (this.staticConn) { + this.staticConn.unhighlight(); + this.staticConn = null; + } + if (this.fadedBlock) { + this.fadedBlock.fadeForReplacement(false); + this.fadedBlock = null; + } + if (this.markerConn) { + this.hideInsertionMarker(this.markerConn); + this.markerConn = null; + this.draggedConn = null; + } + } finally { + eventUtils.enable(); + } + } + + private hideInsertionMarker(markerConn: RenderedConnection) { + const marker = markerConn.getSourceBlock(); + const markerPrev = marker.previousConnection; + const markerOutput = marker.outputConnection; + + if (!markerPrev?.targetConnection && !markerOutput?.targetConnection) { + // If we are the top block, unplugging doesn't do anything. + // The marker connection may not have a target block if we are hiding + // as part of applying connections. + markerConn.targetBlock()?.unplug(false); + } else { + marker.unplug(true); + } + + marker.dispose(); + } + + /** Dispose of any references held by this connection previewer. */ + dispose() { + this.hidePreview(); + } +} + +registry.register( + registry.Type.CONNECTION_PREVIEWER, + registry.DEFAULT, + InsertionMarkerPreviewer, +); diff --git a/core/interfaces/i_ast_node_location.ts b/core/interfaces/i_ast_node_location.ts new file mode 100644 index 00000000000..cc90bbc4065 --- /dev/null +++ b/core/interfaces/i_ast_node_location.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IASTNodeLocation + +/** + * An AST node location interface. + */ +export interface IASTNodeLocation {} diff --git a/core/interfaces/i_ast_node_location_svg.ts b/core/interfaces/i_ast_node_location_svg.ts new file mode 100644 index 00000000000..729e5f09543 --- /dev/null +++ b/core/interfaces/i_ast_node_location_svg.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IASTNodeLocationSvg + +import type {IASTNodeLocation} from './i_ast_node_location.js'; + +/** + * An AST node location SVG interface. + */ +export interface IASTNodeLocationSvg extends IASTNodeLocation { + /** + * Add the marker SVG to this node's SVG group. + * + * @param markerSvg The SVG root of the marker to be added to the SVG group. + */ + setMarkerSvg(markerSvg: SVGElement | null): void; + + /** + * Add the cursor SVG to this node's SVG group. + * + * @param cursorSvg The SVG root of the cursor to be added to the SVG group. + */ + setCursorSvg(cursorSvg: SVGElement | null): void; +} diff --git a/core/interfaces/i_ast_node_location_with_block.ts b/core/interfaces/i_ast_node_location_with_block.ts new file mode 100644 index 00000000000..b04234fd4a8 --- /dev/null +++ b/core/interfaces/i_ast_node_location_with_block.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IASTNodeLocationWithBlock + +import type {Block} from '../block.js'; +import type {IASTNodeLocation} from './i_ast_node_location.js'; + +/** + * An AST node location that has an associated block. + */ +export interface IASTNodeLocationWithBlock extends IASTNodeLocation { + /** + * Get the source block associated with this node. + * + * @returns The source block. + */ + getSourceBlock(): Block | null; +} diff --git a/core/interfaces/i_autohideable.ts b/core/interfaces/i_autohideable.ts new file mode 100644 index 00000000000..ecdec8595a6 --- /dev/null +++ b/core/interfaces/i_autohideable.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IAutoHideable + +import type {IComponent} from './i_component.js'; + +/** + * Interface for a component that can be automatically hidden. + */ +export interface IAutoHideable extends IComponent { + /** + * Hides the component. Called in WorkspaceSvg.hideChaff. + * + * @param onlyClosePopups Whether only popups should be closed. + * Flyouts should not be closed if this is true. + */ + autoHide(onlyClosePopups: boolean): void; +} diff --git a/core/interfaces/i_bounded_element.ts b/core/interfaces/i_bounded_element.ts new file mode 100644 index 00000000000..aac26855bd6 --- /dev/null +++ b/core/interfaces/i_bounded_element.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IBoundedElement + +import type {Rect} from '../utils/rect.js'; + +/** + * A bounded element interface. + */ +export interface IBoundedElement { + /** + * Returns the coordinates of a bounded element describing the dimensions of + * the element. Coordinate system: workspace coordinates. + * + * @returns Object with coordinates of the bounded element. + */ + getBoundingRectangle(): Rect; + + /** + * Move the element by a relative offset. + * + * @param dx Horizontal offset in workspace units. + * @param dy Vertical offset in workspace units. + * @param reason Why is this move happening? 'user', 'bump', 'snap'... + */ + moveBy(dx: number, dy: number, reason?: string[]): void; +} diff --git a/core/interfaces/i_bubble.ts b/core/interfaces/i_bubble.ts new file mode 100644 index 00000000000..d31ce9c9dce --- /dev/null +++ b/core/interfaces/i_bubble.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IBubble + +import type {Coordinate} from '../utils/coordinate.js'; +import type {IContextMenu} from './i_contextmenu.js'; +import type {IDraggable} from './i_draggable.js'; + +/** + * A bubble interface. + */ +export interface IBubble extends IDraggable, IContextMenu { + /** + * Return the coordinates of the top-left corner of this bubble's body + * relative to the drawing surface's origin (0,0), in workspace units. + * + * @returns Object with .x and .y properties. + */ + getRelativeToSurfaceXY(): Coordinate; + + /** + * Return the root node of the bubble's SVG group. + * + * @returns The root SVG node of the bubble's group. + */ + getSvgRoot(): SVGElement; + + /** + * Sets whether or not this bubble is being dragged. + * + * @param adding True if dragging, false otherwise. + */ + setDragging(dragging: boolean): void; + + /** + * Move this bubble during a drag. + * + * @param newLoc The location to translate to, in workspace coordinates. + */ + moveDuringDrag(newLoc: Coordinate): void; + + /** + * Move the bubble to the specified location in workspace coordinates. + * + * @param x The x position to move to. + * @param y The y position to move to. + */ + moveTo(x: number, y: number): void; + + /** + * Update the style of this bubble when it is dragged over a delete area. + * + * @param enable True if the bubble is about to be deleted, false otherwise. + */ + setDeleteStyle(enable: boolean): void; + + /** Dispose of this bubble. */ + dispose(): void; +} diff --git a/core/interfaces/i_collapsible_toolbox_item.ts b/core/interfaces/i_collapsible_toolbox_item.ts new file mode 100644 index 00000000000..0b591b4a6ff --- /dev/null +++ b/core/interfaces/i_collapsible_toolbox_item.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.ICollapsibleToolboxItem + +import type {ISelectableToolboxItem} from './i_selectable_toolbox_item.js'; +import type {IToolboxItem} from './i_toolbox_item.js'; + +/** + * Interface for an item in the toolbox that can be collapsed. + */ +export interface ICollapsibleToolboxItem extends ISelectableToolboxItem { + /** + * Gets any children toolbox items. (ex. Gets the subcategories) + * + * @returns The child toolbox items. + */ + getChildToolboxItems(): IToolboxItem[]; + + /** + * Whether the toolbox item is expanded to show its child subcategories. + * + * @returns True if the toolbox item shows its children, false if it is + * collapsed. + */ + isExpanded(): boolean; + + /** Toggles whether or not the toolbox item is expanded. */ + toggleExpanded(): void; +} diff --git a/core/interfaces/i_comment_icon.ts b/core/interfaces/i_comment_icon.ts new file mode 100644 index 00000000000..9801a8d6e11 --- /dev/null +++ b/core/interfaces/i_comment_icon.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {CommentState} from '../icons/comment_icon.js'; +import {IconType} from '../icons/icon_types.js'; +import {Size} from '../utils/size.js'; +import {IHasBubble, hasBubble} from './i_has_bubble.js'; +import {IIcon, isIcon} from './i_icon.js'; +import {ISerializable, isSerializable} from './i_serializable.js'; + +export interface ICommentIcon extends IIcon, IHasBubble, ISerializable { + setText(text: string): void; + + getText(): string; + + setBubbleSize(size: Size): void; + + getBubbleSize(): Size; + + saveState(): CommentState; + + loadState(state: CommentState): void; +} + +/** Checks whether the given object is an ICommentIcon. */ +export function isCommentIcon(obj: object): obj is ICommentIcon { + return ( + isIcon(obj) && + hasBubble(obj) && + isSerializable(obj) && + (obj as any)['setText'] !== undefined && + (obj as any)['getText'] !== undefined && + (obj as any)['setBubbleSize'] !== undefined && + (obj as any)['getBubbleSize'] !== undefined && + obj.getType() === IconType.COMMENT + ); +} diff --git a/core/interfaces/i_component.ts b/core/interfaces/i_component.ts new file mode 100644 index 00000000000..03f4b1fd2bf --- /dev/null +++ b/core/interfaces/i_component.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IComponent + +/** + * The interface for a workspace component that can be registered with the + * ComponentManager. + */ +export interface IComponent { + /** + * The unique ID for this component that is used to register with the + * ComponentManager. + */ + id: string; +} diff --git a/core/interfaces/i_connection_checker.ts b/core/interfaces/i_connection_checker.ts new file mode 100644 index 00000000000..352b719d665 --- /dev/null +++ b/core/interfaces/i_connection_checker.ts @@ -0,0 +1,101 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IConnectionChecker + +import type {Connection} from '../connection.js'; +import type {RenderedConnection} from '../rendered_connection.js'; + +/** + * Class for connection type checking logic. + */ +export interface IConnectionChecker { + /** + * Check whether the current connection can connect with the target + * connection. + * + * @param a Connection to check compatibility with. + * @param b Connection to check compatibility with. + * @param isDragging True if the connection is being made by dragging a block. + * @param opt_distance The max allowable distance between the connections for + * drag checks. + * @returns Whether the connection is legal. + */ + canConnect( + a: Connection | null, + b: Connection | null, + isDragging: boolean, + opt_distance?: number, + ): boolean; + + /** + * Checks whether the current connection can connect with the target + * connection, and return an error code if there are problems. + * + * @param a Connection to check compatibility with. + * @param b Connection to check compatibility with. + * @param isDragging True if the connection is being made by dragging a block. + * @param opt_distance The max allowable distance between the connections for + * drag checks. + * @returns Connection.CAN_CONNECT if the connection is legal, an error code + * otherwise. + */ + canConnectWithReason( + a: Connection | null, + b: Connection | null, + isDragging: boolean, + opt_distance?: number, + ): number; + + /** + * Helper method that translates a connection error code into a string. + * + * @param errorCode The error code. + * @param a One of the two connections being checked. + * @param b The second of the two connections being checked. + * @returns A developer-readable error string. + */ + getErrorMessage( + errorCode: number, + a: Connection | null, + b: Connection | null, + ): string; + + /** + * Check that connecting the given connections is safe, meaning that it would + * not break any of Blockly's basic assumptions (e.g. no self connections). + * + * @param a The first of the connections to check. + * @param b The second of the connections to check. + * @returns An enum with the reason this connection is safe or unsafe. + */ + doSafetyChecks(a: Connection | null, b: Connection | null): number; + + /** + * Check whether this connection is compatible with another connection with + * respect to the value type system. E.g. square_root("Hello") is not + * compatible. + * + * @param a Connection to compare. + * @param b Connection to compare against. + * @returns True if the connections share a type. + */ + doTypeChecks(a: Connection, b: Connection): boolean; + + /** + * Check whether this connection can be made by dragging. + * + * @param a Connection to compare. + * @param b Connection to compare against. + * @param distance The maximum allowable distance between connections. + * @returns True if the connection is allowed during a drag. + */ + doDragChecks( + a: RenderedConnection, + b: RenderedConnection, + distance: number, + ): boolean; +} diff --git a/core/interfaces/i_connection_previewer.ts b/core/interfaces/i_connection_previewer.ts new file mode 100644 index 00000000000..df7906a29dc --- /dev/null +++ b/core/interfaces/i_connection_previewer.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {BlockSvg} from '../block_svg'; +import type {RenderedConnection} from '../rendered_connection'; + +/** + * Displays visual "previews" of where a block will be connected if it is + * dropped. + */ +export interface IConnectionPreviewer { + /** + * Display a connection preview where the draggedCon connects to the + * staticCon, replacing the replacedBlock (currently connected to the + * staticCon). + * + * @param draggedCon The connection on the block stack being dragged. + * @param staticCon The connection not being dragged that we are + * connecting to. + * @param replacedBlock The block currently connected to the staticCon that + * is being replaced. + */ + previewReplacement( + draggedConn: RenderedConnection, + staticConn: RenderedConnection, + replacedBlock: BlockSvg, + ): void; + + /** + * Display a connection preview where the draggedCon connects to the + * staticCon, and no block is being relaced. + * + * @param draggedCon The connection on the block stack being dragged. + * @param staticCon The connection not being dragged that we are + * connecting to. + */ + previewConnection( + draggedConn: RenderedConnection, + staticConn: RenderedConnection, + ): void; + + /** Hide any previews that are currently displayed. */ + hidePreview(): void; + + /** Dispose of any references held by this connection previewer. */ + dispose(): void; +} diff --git a/core/interfaces/i_contextmenu.ts b/core/interfaces/i_contextmenu.ts new file mode 100644 index 00000000000..cba71259fa1 --- /dev/null +++ b/core/interfaces/i_contextmenu.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IContextMenu + +export interface IContextMenu { + /** + * Show the context menu for this object. + * + * @param e Mouse event. + */ + showContextMenu(e: Event): void; +} diff --git a/core/interfaces/i_copyable.ts b/core/interfaces/i_copyable.ts new file mode 100644 index 00000000000..b653bd20a10 --- /dev/null +++ b/core/interfaces/i_copyable.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.ICopyable + +import type {ISelectable} from './i_selectable.js'; + +export interface ICopyable extends ISelectable { + /** + * Encode for copying. + * + * @returns Copy metadata. + */ + toCopyData(): T | null; +} + +export namespace ICopyable { + export interface ICopyData { + paster: string; + } +} + +export type ICopyData = ICopyable.ICopyData; + +/** @returns true if the given object is copyable. */ +export function isCopyable(obj: any): obj is ICopyable { + return obj.toCopyData !== undefined; +} diff --git a/core/interfaces/i_deletable.ts b/core/interfaces/i_deletable.ts new file mode 100644 index 00000000000..0467709409a --- /dev/null +++ b/core/interfaces/i_deletable.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IDeletable + +/** + * The interface for an object that can be deleted. + */ +export interface IDeletable { + /** + * Get whether this object is deletable or not. + * + * @returns True if deletable. + */ + isDeletable(): boolean; + + /** Disposes of this object, cleaning up any references or DOM elements. */ + dispose(): void; + + /** Visually indicates that the object is pending deletion. */ + setDeleteStyle(wouldDelete: boolean): void; +} + +/** Returns whether the given object is an IDeletable. */ +export function isDeletable(obj: any): obj is IDeletable { + return ( + obj['isDeletable'] !== undefined && + obj['dispose'] !== undefined && + obj['setDeleteStyle'] !== undefined + ); +} diff --git a/core/interfaces/i_delete_area.ts b/core/interfaces/i_delete_area.ts new file mode 100644 index 00000000000..86d2673bbf8 --- /dev/null +++ b/core/interfaces/i_delete_area.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IDeleteArea + +import type {IDragTarget} from './i_drag_target.js'; +import type {IDraggable} from './i_draggable.js'; + +/** + * Interface for a component that can delete a block or bubble that is dropped + * on top of it. + */ +export interface IDeleteArea extends IDragTarget { + /** + * Returns whether the provided block or bubble would be deleted if dropped on + * this area. + * This method should check if the element is deletable and is always called + * before onDragEnter/onDragOver/onDragExit. + * + * @param element The block or bubble currently being dragged. + * @returns Whether the element provided would be deleted if dropped on this + * area. + */ + wouldDelete(element: IDraggable): boolean; +} diff --git a/core/interfaces/i_drag_target.ts b/core/interfaces/i_drag_target.ts new file mode 100644 index 00000000000..395b2345123 --- /dev/null +++ b/core/interfaces/i_drag_target.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IDragTarget + +import {Rect} from '../utils/rect.js'; +import type {IComponent} from './i_component.js'; +import {IDraggable} from './i_draggable.js'; + +/** + * Interface for a component with custom behaviour when a block or bubble is + * dragged over or dropped on top of it. + */ +export interface IDragTarget extends IComponent { + /** + * Returns the bounding rectangle of the drag target area in pixel units + * relative to viewport. + * + * @returns The component's bounding box. Null if drag target area should be + * ignored. + */ + getClientRect(): Rect | null; + + /** + * Handles when a cursor with a block or bubble enters this drag target. + * + * @param dragElement The block or bubble currently being dragged. + */ + onDragEnter(dragElement: IDraggable): void; + + /** + * Handles when a cursor with a block or bubble is dragged over this drag + * target. + * + * @param dragElement The block or bubble currently being dragged. + */ + onDragOver(dragElement: IDraggable): void; + + /** + * Handles when a cursor with a block or bubble exits this drag target. + * + * @param dragElement The block or bubble currently being dragged. + */ + onDragExit(dragElement: IDraggable): void; + + /** + * Handles when a block or bubble is dropped on this component. + * Should not handle delete here. + * + * @param dragElement The block or bubble currently being dragged. + */ + onDrop(dragElement: IDraggable): void; + + /** + * Returns whether the provided block or bubble should not be moved after + * being dropped on this component. If true, the element will return to where + * it was when the drag started. + * + * @param dragElement The block or bubble currently being dragged. + * @returns Whether the block or bubble provided should be returned to drag + * start. + */ + shouldPreventMove(dragElement: IDraggable): boolean; +} diff --git a/core/interfaces/i_draggable.ts b/core/interfaces/i_draggable.ts new file mode 100644 index 00000000000..cb723e7b88b --- /dev/null +++ b/core/interfaces/i_draggable.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {Coordinate} from '../utils/coordinate'; + +/** + * Represents an object that can be dragged. + */ +export interface IDraggable extends IDragStrategy { + /** + * Returns the current location of the draggable in workspace + * coordinates. + * + * @returns Coordinate of current location on workspace. + */ + getRelativeToSurfaceXY(): Coordinate; +} + +export interface IDragStrategy { + /** Returns true iff the element is currently movable. */ + isMovable(): boolean; + + /** + * Handles any drag startup (e.g moving elements to the front of the + * workspace). + * + * @param e PointerEvent that started the drag; can be used to + * check modifier keys, etc. May be missing when dragging is + * triggered programatically rather than by user. + */ + startDrag(e?: PointerEvent): void; + + /** + * Handles moving elements to the new location, and updating any + * visuals based on that (e.g connection previews for blocks). + * + * @param newLoc Workspace coordinate to which the draggable has + * been dragged. + * @param e PointerEvent that continued the drag. Can be + * used to check modifier keys, etc. + */ + drag(newLoc: Coordinate, e?: PointerEvent): void; + + /** + * Handles any drag cleanup, including e.g. connecting or deleting + * blocks. + * + * @param newLoc Workspace coordinate at which the drag finished. + * been dragged. + * @param e PointerEvent that finished the drag. Can be + * used to check modifier keys, etc. + */ + endDrag(e?: PointerEvent): void; + + /** Moves the draggable back to where it was at the start of the drag. */ + revertDrag(): void; +} + +/** Returns whether the given object is an IDraggable or not. */ +export function isDraggable(obj: any): obj is IDraggable { + return ( + obj.getRelativeToSurfaceXY !== undefined && + obj.isMovable !== undefined && + obj.startDrag !== undefined && + obj.drag !== undefined && + obj.endDrag !== undefined && + obj.revertDrag !== undefined + ); +} diff --git a/core/interfaces/i_dragger.ts b/core/interfaces/i_dragger.ts new file mode 100644 index 00000000000..1e8ad0ab6c4 --- /dev/null +++ b/core/interfaces/i_dragger.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {Coordinate} from '../utils/coordinate'; + +export interface IDragger { + /** + * Handles any drag startup. + * + * @param e PointerEvent that started the drag. + */ + onDragStart(e: PointerEvent): void; + + /** + * Handles dragging, including calculating where the element should + * actually be moved to. + * + * @param e PointerEvent that continued the drag. + * @param totalDelta The total distance, in pixels, that the mouse + * has moved since the start of the drag. + */ + onDrag(e: PointerEvent, totalDelta: Coordinate): void; + + /** + * Handles any drag cleanup. + * + * @param e PointerEvent that finished the drag. + * @param totalDelta The total distance, in pixels, that the mouse + * has moved since the start of the drag. + */ + onDragEnd(e: PointerEvent, totalDelta: Coordinate): void; +} diff --git a/core/interfaces/i_flyout.ts b/core/interfaces/i_flyout.ts new file mode 100644 index 00000000000..c79be344c5a --- /dev/null +++ b/core/interfaces/i_flyout.ts @@ -0,0 +1,189 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IFlyout + +import type {BlockSvg} from '../block_svg.js'; +import {FlyoutItem} from '../flyout_base.js'; +import type {Coordinate} from '../utils/coordinate.js'; +import type {Svg} from '../utils/svg.js'; +import type {FlyoutDefinition} from '../utils/toolbox.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import type {IRegistrable} from './i_registrable.js'; + +/** + * Interface for a flyout. + */ +export interface IFlyout extends IRegistrable { + /** Whether the flyout is laid out horizontally or not. */ + horizontalLayout: boolean; + + /** Is RTL vs LTR. */ + RTL: boolean; + + /** The target workspace */ + targetWorkspace: WorkspaceSvg | null; + + /** Margin around the edges of the blocks in the flyout. */ + readonly MARGIN: number; + + /** Does the flyout automatically close when a block is created? */ + autoClose: boolean; + + /** Corner radius of the flyout background. */ + readonly CORNER_RADIUS: number; + + /** + * Creates the flyout's DOM. Only needs to be called once. The flyout can + * either exist as its own svg element or be a g element nested inside a + * separate svg element. + * + * @param tagName The type of tag to put the flyout in. This should be + * or . + * @returns The flyout's SVG group. + */ + createDom( + tagName: string | Svg | Svg, + ): SVGElement; + + /** + * Initializes the flyout. + * + * @param targetWorkspace The workspace in which to create new blocks. + */ + init(targetWorkspace: WorkspaceSvg): void; + + /** + * Dispose of this flyout. + * Unlink from all DOM elements to prevent memory leaks. + */ + dispose(): void; + + /** + * Get the width of the flyout. + * + * @returns The width of the flyout. + */ + getWidth(): number; + + /** + * Get the height of the flyout. + * + * @returns The height of the flyout. + */ + getHeight(): number; + + /** + * Get the workspace inside the flyout. + * + * @returns The workspace inside the flyout. + */ + getWorkspace(): WorkspaceSvg; + + /** + * Is the flyout visible? + * + * @returns True if visible. + */ + isVisible(): boolean; + + /** + * Set whether the flyout is visible. A value of true does not necessarily + * mean that the flyout is shown. It could be hidden because its container is + * hidden. + * + * @param visible True if visible. + */ + setVisible(visible: boolean): void; + + /** + * Set whether this flyout's container is visible. + * + * @param visible Whether the container is visible. + */ + setContainerVisible(visible: boolean): void; + + /** Hide and empty the flyout. */ + hide(): void; + + /** + * Show and populate the flyout. + * + * @param flyoutDef Contents to display in the flyout. This is either an array + * of Nodes, a NodeList, a toolbox definition, or a string with the name + * of the dynamic category. + */ + show(flyoutDef: FlyoutDefinition | string): void; + + /** + * Returns the list of flyout items currently present in the flyout. + * The `show` method parses the flyout definition into a list of actual + * flyout items. This method should return those concrete items, which + * may be used for e.g. keyboard navigation. + * + * @returns List of flyout items. + */ + getContents(): FlyoutItem[]; + + /** + * Create a copy of this block on the workspace. + * + * @param originalBlock The block to copy from the flyout. + * @returns The newly created block. + * @throws {Error} if something went wrong with deserialization. + */ + createBlock(originalBlock: BlockSvg): BlockSvg; + + /** Reflow blocks and their mats. */ + reflow(): void; + + /** + * @returns True if this flyout may be scrolled with a scrollbar or by + * dragging. + */ + isScrollable(): boolean; + + /** + * Calculates the x coordinate for the flyout position. + * + * @returns X coordinate. + */ + getX(): number; + + /** + * Calculates the y coordinate for the flyout position. + * + * @returns Y coordinate. + */ + getY(): number; + + /** Position the flyout. */ + position(): void; + + /** + * Determine if a drag delta is toward the workspace, based on the position + * and orientation of the flyout. This is used in determineDragIntention_ to + * determine if a new block should be created or if the flyout should scroll. + * + * @param currentDragDeltaXY How far the pointer has moved from the position + * at mouse down, in pixel units. + * @returns True if the drag is toward the workspace. + */ + isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean; + + /** + * Does this flyout allow you to create a new instance of the given block? + * Used for deciding if a block can be "dragged out of" the flyout. + * + * @param block The block to copy from the flyout. + * @returns True if you can create a new instance of the block, false + * otherwise. + */ + isBlockCreatable(block: BlockSvg): boolean; + + /** Scroll the flyout to the beginning of its contents. */ + scrollToStart(): void; +} diff --git a/core/interfaces/i_has_bubble.ts b/core/interfaces/i_has_bubble.ts new file mode 100644 index 00000000000..276feff21e2 --- /dev/null +++ b/core/interfaces/i_has_bubble.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface IHasBubble { + /** @returns True if the bubble is currently open, false otherwise. */ + bubbleIsVisible(): boolean; + + /** Sets whether the bubble is open or not. */ + setBubbleVisible(visible: boolean): Promise; +} + +/** Type guard that checks whether the given object is a IHasBubble. */ +export function hasBubble(obj: any): obj is IHasBubble { + return ( + obj.bubbleIsVisible !== undefined && obj.setBubbleVisible !== undefined + ); +} diff --git a/core/interfaces/i_icon.ts b/core/interfaces/i_icon.ts new file mode 100644 index 00000000000..a6159985f91 --- /dev/null +++ b/core/interfaces/i_icon.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IconType} from '../icons/icon_types.js'; +import type {Coordinate} from '../utils/coordinate.js'; +import type {Size} from '../utils/size.js'; + +export interface IIcon { + /** + * @returns the IconType representing the type of the icon. This value should + * also be used to register the icon via `Blockly.icons.registry.register`. + */ + getType(): IconType; + + /** + * Creates the SVG elements for the icon that will live on the block. + * + * @param pointerdownListener An event listener that must be attached to the + * root SVG element by the implementation of `initView`. Used by Blockly's + * gesture system to properly handle clicks and drags. + */ + initView(pointerdownListener: (e: PointerEvent) => void): void; + + /** + * Disposes of any elements of the icon. + * + * @remarks + * + * In particular, if this icon is currently showing a bubble, this should be + * used to hide it. + */ + dispose(): void; + + /** + * @returns the "weight" of the icon, which determines the static order which + * icons should be rendered in. More positive numbers are rendered farther + * toward the end of the block. + */ + getWeight(): number; + + /** @returns The dimensions of the icon for use in rendering. */ + getSize(): Size; + + /** Updates the icon's color when the block's color changes.. */ + applyColour(): void; + + /** Hides the icon when it is part of an insertion marker. */ + hideForInsertionMarker(): void; + + /** Updates the icon's editability when the block's editability changes. */ + updateEditable(): void; + + /** + * Updates the icon's collapsed-ness/view when the block's collapsed-ness + * changes. + */ + updateCollapsed(): void; + + /** + * @returns Whether this icon is shown when the block is collapsed. Used + * to allow renderers to account for padding. + */ + isShownWhenCollapsed(): boolean; + + /** + * Notifies the icon where it is relative to its block's top-start, in + * workspace units. + */ + setOffsetInBlock(offset: Coordinate): void; + + /** + * Notifies the icon that it has changed locations. + * + * @param blockOrigin The location of this icon's block's top-start corner + * in workspace coordinates. + */ + onLocationChange(blockOrigin: Coordinate): void; + + /** + * Notifies the icon that it has been clicked. + */ + onClick(): void; + + /** + * Check whether the icon should be clickable while the block is in a flyout. + * If this function is not defined, the icon will be clickable in all flyouts. + * + * @param autoClosingFlyout true if the containing flyout is an auto-closing one. + * @returns Whether the icon should be clickable while the block is in a flyout. + */ + isClickableInFlyout?(autoClosingFlyout: boolean): boolean; +} + +/** Type guard that checks whether the given object is an IIcon. */ +export function isIcon(obj: any): obj is IIcon { + return ( + obj.getType !== undefined && + obj.initView !== undefined && + obj.dispose !== undefined && + obj.getWeight !== undefined && + obj.getSize !== undefined && + obj.applyColour !== undefined && + obj.hideForInsertionMarker !== undefined && + obj.updateEditable !== undefined && + obj.updateCollapsed !== undefined && + obj.isShownWhenCollapsed !== undefined && + obj.setOffsetInBlock !== undefined && + obj.onLocationChange !== undefined && + obj.onClick !== undefined + ); +} diff --git a/core/interfaces/i_keyboard_accessible.ts b/core/interfaces/i_keyboard_accessible.ts new file mode 100644 index 00000000000..4d04e9d4f08 --- /dev/null +++ b/core/interfaces/i_keyboard_accessible.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IKeyboardAccessible + +import {KeyboardShortcut} from '../shortcut_registry.js'; + +/** + * An interface for an object that handles keyboard shortcuts. + */ +export interface IKeyboardAccessible { + /** + * Handles the given keyboard shortcut. + * + * @param shortcut The shortcut to be handled. + * @returns True if the shortcut has been handled, false otherwise. + */ + onShortcut(shortcut: KeyboardShortcut): boolean; +} diff --git a/core/interfaces/i_legacy_procedure_blocks.ts b/core/interfaces/i_legacy_procedure_blocks.ts new file mode 100644 index 00000000000..d74eaec220a --- /dev/null +++ b/core/interfaces/i_legacy_procedure_blocks.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Legacy means of representing a procedure signature. The elements are + * respectively: name, parameter names, and whether it has a return value. + */ +export type ProcedureTuple = [string, string[], boolean]; + +/** + * Procedure block type. + * + * @internal + */ +export interface ProcedureBlock { + getProcedureCall: () => string; + renameProcedure: (p1: string, p2: string) => void; + getProcedureDef: () => ProcedureTuple; +} + +/** @internal */ +export interface LegacyProcedureDefBlock { + getProcedureDef: () => ProcedureTuple; +} + +/** @internal */ +export function isLegacyProcedureDefBlock( + block: object, +): block is LegacyProcedureDefBlock { + return (block as any).getProcedureDef !== undefined; +} + +/** @internal */ +export interface LegacyProcedureCallBlock { + getProcedureCall: () => string; + renameProcedure: (p1: string, p2: string) => void; +} + +/** @internal */ +export function isLegacyProcedureCallBlock( + block: object, +): block is LegacyProcedureCallBlock { + return ( + (block as any).getProcedureCall !== undefined && + (block as any).renameProcedure !== undefined + ); +} diff --git a/core/interfaces/i_metrics_manager.ts b/core/interfaces/i_metrics_manager.ts new file mode 100644 index 00000000000..bb4d54da440 --- /dev/null +++ b/core/interfaces/i_metrics_manager.ts @@ -0,0 +1,150 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IMetricsManager + +import type { + AbsoluteMetrics, + ContainerRegion, + ToolboxMetrics, + UiMetrics, +} from '../metrics_manager.js'; +import type {Metrics} from '../utils/metrics.js'; +import type {Size} from '../utils/size.js'; + +/** + * Interface for a metrics manager. + */ +export interface IMetricsManager { + /** + * Returns whether the scroll area has fixed edges. + * + * @returns Whether the scroll area has fixed edges. + * @internal + */ + hasFixedEdges(): boolean; + + /** + * Returns the metrics for the scroll area of the workspace. + * + * @param opt_getWorkspaceCoordinates True to get the scroll metrics in + * workspace coordinates, false to get them in pixel coordinates. + * @param opt_viewMetrics The view metrics if they have been previously + * computed. Passing in null may cause the view metrics to be computed + * again, if it is needed. + * @param opt_contentMetrics The content metrics if they have been previously + * computed. Passing in null may cause the content metrics to be computed + * again, if it is needed. + * @returns The metrics for the scroll container + */ + getScrollMetrics( + opt_getWorkspaceCoordinates?: boolean, + opt_viewMetrics?: ContainerRegion, + opt_contentMetrics?: ContainerRegion, + ): ContainerRegion; + + /** + * Gets the width and the height of the flyout in pixel + * coordinates. By default, will get metrics for either a simple flyout (owned + * directly by the workspace) or for the flyout owned by the toolbox. If you + * pass `opt_own` as `true` then only metrics for the simple flyout will be + * returned, and it will return 0 for the width and height if the workspace + * has a category toolbox instead of a simple toolbox. + * + * @param opt_own Whether to only return the workspace's own flyout metrics. + * @returns The width and height of the flyout. + */ + getFlyoutMetrics(opt_own?: boolean): ToolboxMetrics; + + /** + * Gets the width, height and position of the toolbox on the workspace in + * pixel coordinates. Returns 0 for the width and height if the workspace has + * a simple toolbox instead of a category toolbox. To get the width and height + * of a simple toolbox, see {@link IMetricsManager#getFlyoutMetrics}. + * + * @returns The object with the width, height and position of the toolbox. + */ + getToolboxMetrics(): ToolboxMetrics; + + /** + * Gets the width and height of the workspace's parent SVG element in pixel + * coordinates. This area includes the toolbox and the visible workspace area. + * + * @returns The width and height of the workspace's parent SVG element. + */ + getSvgMetrics(): Size; + + /** + * Gets the absolute left and absolute top in pixel coordinates. + * This is where the visible workspace starts in relation to the SVG + * container. + * + * @returns The absolute metrics for the workspace. + */ + getAbsoluteMetrics(): AbsoluteMetrics; + + /** + * Gets the metrics for the visible workspace in either pixel or workspace + * coordinates. The visible workspace does not include the toolbox or flyout. + * + * @param opt_getWorkspaceCoordinates True to get the view metrics in + * workspace coordinates, false to get them in pixel coordinates. + * @returns The width, height, top and left of the viewport in either + * workspace coordinates or pixel coordinates. + */ + getViewMetrics(opt_getWorkspaceCoordinates?: boolean): ContainerRegion; + + /** + * Gets content metrics in either pixel or workspace coordinates. + * The content area is a rectangle around all the top bounded elements on the + * workspace (workspace comments and blocks). + * + * @param opt_getWorkspaceCoordinates True to get the content metrics in + * workspace coordinates, false to get them in pixel coordinates. + * @returns The metrics for the content container. + */ + getContentMetrics(opt_getWorkspaceCoordinates?: boolean): ContainerRegion; + + /** + * Returns an object with all the metrics required to size scrollbars for a + * top level workspace. The following properties are computed: + * Coordinate system: pixel coordinates, -left, -up, +right, +down + * .viewHeight: Height of the visible portion of the workspace. + * .viewWidth: Width of the visible portion of the workspace. + * .contentHeight: Height of the content. + * .contentWidth: Width of the content. + * .svgHeight: Height of the Blockly div (the view + the toolbox, + * simple or otherwise), + * .svgWidth: Width of the Blockly div (the view + the toolbox, + * simple or otherwise), + * .viewTop: Top-edge of the visible portion of the workspace, relative to + * the workspace origin. + * .viewLeft: Left-edge of the visible portion of the workspace, relative to + * the workspace origin. + * .contentTop: Top-edge of the content, relative to the workspace origin. + * .contentLeft: Left-edge of the content relative to the workspace origin. + * .absoluteTop: Top-edge of the visible portion of the workspace, relative + * to the blocklyDiv. + * .absoluteLeft: Left-edge of the visible portion of the workspace, relative + * to the blocklyDiv. + * .toolboxWidth: Width of the toolbox, if it exists. Otherwise zero. + * .toolboxHeight: Height of the toolbox, if it exists. Otherwise zero. + * .flyoutWidth: Width of the flyout if it is always open. Otherwise zero. + * .flyoutHeight: Height of the flyout if it is always open. Otherwise zero. + * .toolboxPosition: Top, bottom, left or right. Use TOOLBOX_AT constants to + * compare. + * + * @returns Contains size and position metrics of a top level workspace. + */ + getMetrics(): Metrics; + + /** + * Returns common metrics used by UI elements. + * + * @returns The UI metrics. + */ + getUiMetrics(): UiMetrics; +} diff --git a/core/interfaces/i_movable.ts b/core/interfaces/i_movable.ts new file mode 100644 index 00000000000..cc2a2b727c3 --- /dev/null +++ b/core/interfaces/i_movable.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IMovable + +/** + * The interface for an object that is movable. + */ +export interface IMovable { + /** + * Get whether this is movable or not. + * + * @returns True if movable. + */ + isMovable(): boolean; +} diff --git a/core/interfaces/i_observable.ts b/core/interfaces/i_observable.ts new file mode 100644 index 00000000000..96a2a0bc4e8 --- /dev/null +++ b/core/interfaces/i_observable.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * An object that fires events optionally. + * + * @internal + */ +export interface IObservable { + startPublishing(): void; + stopPublishing(): void; +} + +/** + * Type guard for checking if an object fulfills IObservable. + * + * @internal + */ +export function isObservable(obj: any): obj is IObservable { + return obj.startPublishing !== undefined && obj.stopPublishing !== undefined; +} diff --git a/core/interfaces/i_parameter_model.ts b/core/interfaces/i_parameter_model.ts new file mode 100644 index 00000000000..6b351b6b3a7 --- /dev/null +++ b/core/interfaces/i_parameter_model.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {ParameterState} from '../serialization/procedures'; +import {IProcedureModel} from './i_procedure_model'; + +/** + * A data model for a procedure. + */ +export interface IParameterModel { + /** + * Sets the name of this parameter to the given name. + */ + setName(name: string): this; + + /** + * Sets the types of this parameter to the given type. + */ + setTypes(types: string[]): this; + + /** + * Returns the name of this parameter. + */ + getName(): string; + + /** + * Return the types of this parameter. + */ + getTypes(): string[]; + + /** + * Returns the unique language-neutral ID for the parameter. + * + * This represents the identify of the variable model which does not change + * over time. + */ + getId(): string; + + /** Sets the procedure model this parameter is associated with. */ + setProcedureModel(model: IProcedureModel): this; + + /** + * Serializes the state of the parameter to JSON. + * + * @returns JSON serializable state of the parameter. + */ + saveState(): ParameterState; +} diff --git a/core/interfaces/i_paster.ts b/core/interfaces/i_paster.ts new file mode 100644 index 00000000000..321ff118f70 --- /dev/null +++ b/core/interfaces/i_paster.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {Coordinate} from '../utils/coordinate.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; +import {ICopyable, ICopyData} from './i_copyable.js'; + +/** An object that can paste data into a workspace. */ +export interface IPaster> { + paste( + copyData: U, + workspace: WorkspaceSvg, + coordinate?: Coordinate, + ): T | null; +} + +/** @returns True if the given object is a paster. */ +export function isPaster( + obj: any, +): obj is IPaster> { + return obj.paste !== undefined; +} diff --git a/core/interfaces/i_positionable.ts b/core/interfaces/i_positionable.ts new file mode 100644 index 00000000000..4ea7dafa08d --- /dev/null +++ b/core/interfaces/i_positionable.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IPositionable + +import type {UiMetrics} from '../metrics_manager.js'; +import type {Rect} from '../utils/rect.js'; +import type {IComponent} from './i_component.js'; + +/** + * Interface for a component that is positioned on top of the workspace. + */ +export interface IPositionable extends IComponent { + /** + * Positions the element. Called when the window is resized. + * + * @param metrics The workspace metrics. + * @param savedPositions List of rectangles that are already on the workspace. + */ + position(metrics: UiMetrics, savedPositions: Rect[]): void; + + /** + * Returns the bounding rectangle of the UI element in pixel units relative to + * the Blockly injection div. + * + * @returns The UI elements's bounding box. Null if bounding box should be + * ignored by other UI elements. + */ + getBoundingRectangle(): Rect | null; +} diff --git a/core/interfaces/i_procedure_block.ts b/core/interfaces/i_procedure_block.ts new file mode 100644 index 00000000000..f8538052749 --- /dev/null +++ b/core/interfaces/i_procedure_block.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.procedures.IProcedureBlock + +import type {Block} from '../block.js'; +import {IProcedureModel} from './i_procedure_model.js'; + +/** The interface for a block which models a procedure. */ +export interface IProcedureBlock { + getProcedureModel(): IProcedureModel; + doProcedureUpdate(): void; + isProcedureDef(): boolean; +} + +/** A type guard which checks if the given block is a procedure block. */ +export function isProcedureBlock( + block: Block | IProcedureBlock, +): block is IProcedureBlock { + return ( + (block as IProcedureBlock).getProcedureModel !== undefined && + (block as IProcedureBlock).doProcedureUpdate !== undefined && + (block as IProcedureBlock).isProcedureDef !== undefined + ); +} diff --git a/core/interfaces/i_procedure_map.ts b/core/interfaces/i_procedure_map.ts new file mode 100644 index 00000000000..e14d53a3384 --- /dev/null +++ b/core/interfaces/i_procedure_map.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {IProcedureModel} from './i_procedure_model.js'; + +export interface IProcedureMap extends Map { + /** + * Adds the given ProcedureModel to the map of procedure models, so that + * blocks can find it. + */ + add(proc: IProcedureModel): this; + + /** Returns all of the procedures stored in this map. */ + getProcedures(): IProcedureModel[]; +} diff --git a/core/interfaces/i_procedure_model.ts b/core/interfaces/i_procedure_model.ts new file mode 100644 index 00000000000..61026adaeca --- /dev/null +++ b/core/interfaces/i_procedure_model.ts @@ -0,0 +1,71 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {State} from '../serialization/procedures.js'; +import {IParameterModel} from './i_parameter_model.js'; + +/** + * A data model for a procedure. + */ +export interface IProcedureModel { + /** Sets the human-readable name of the procedure. */ + setName(name: string): this; + + /** + * Inserts a parameter into the list of parameters. + * + * To move a parameter, first delete it, and then re-insert. + */ + insertParameter(parameterModel: IParameterModel, index: number): this; + + /** Removes the parameter at the given index from the parameter list. */ + deleteParameter(index: number): this; + + /** + * Sets the return type(s) of the procedure. + * + * Pass null to represent a procedure that does not return. + */ + setReturnTypes(types: string[] | null): this; + + /** + * Sets whether this procedure is enabled/disabled. If a procedure is disabled + * all procedure caller blocks should be disabled as well. + */ + setEnabled(enabled: boolean): this; + + /** Returns the unique language-neutral ID for the procedure. */ + getId(): string; + + /** Returns the human-readable name of the procedure. */ + getName(): string; + + /** Returns the parameter at the given index in the parameter list. */ + getParameter(index: number): IParameterModel; + + /** Returns an array of all of the parameters in the parameter list. */ + getParameters(): IParameterModel[]; + + /** + * Returns the return type(s) of the procedure. + * + * Null represents a procedure that does not return a value. + */ + getReturnTypes(): string[] | null; + + /** + * Returns whether the procedure is enabled/disabled. If a procedure is + * disabled, all procedure caller blocks should be disabled as well. + */ + getEnabled(): boolean; + + /** + * Serializes the state of the procedure to JSON. + * + * @returns JSON serializable state of the procedure. + */ + saveState(): State; +} diff --git a/core/interfaces/i_registrable.ts b/core/interfaces/i_registrable.ts new file mode 100644 index 00000000000..7fb469eee7d --- /dev/null +++ b/core/interfaces/i_registrable.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IRegistrable + +/** + * The interface for a Blockly component that can be registered. + */ +export interface IRegistrable {} diff --git a/core/interfaces/i_rendered_element.ts b/core/interfaces/i_rendered_element.ts new file mode 100644 index 00000000000..7e6981ca6b1 --- /dev/null +++ b/core/interfaces/i_rendered_element.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** @internal */ +export interface IRenderedElement { + /** + * @returns The root SVG element of htis rendered element. + */ + getSvgRoot(): SVGElement; +} + +/** + * @returns True if the given object is an IRenderedElement. + * + * @internal + */ +export function isRenderedElement(obj: any): obj is IRenderedElement { + return obj['getSvgRoot'] !== undefined; +} diff --git a/core/interfaces/i_selectable.ts b/core/interfaces/i_selectable.ts new file mode 100644 index 00000000000..972b0adb107 --- /dev/null +++ b/core/interfaces/i_selectable.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.ISelectable + +import type {Workspace} from '../workspace.js'; + +/** + * The interface for an object that is selectable. + */ +export interface ISelectable { + id: string; + + workspace: Workspace; + + /** Select this. Highlight it visually. */ + select(): void; + + /** Unselect this. Unhighlight it visually. */ + unselect(): void; +} + +/** Checks whether the given object is an ISelectable. */ +export function isSelectable(obj: object): obj is ISelectable { + return ( + typeof (obj as any).id === 'string' && + (obj as any).workspace !== undefined && + (obj as any).select !== undefined && + (obj as any).unselect !== undefined + ); +} diff --git a/core/interfaces/i_selectable_toolbox_item.ts b/core/interfaces/i_selectable_toolbox_item.ts new file mode 100644 index 00000000000..890d4e370af --- /dev/null +++ b/core/interfaces/i_selectable_toolbox_item.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.ISelectableToolboxItem + +import type {FlyoutItemInfoArray} from '../utils/toolbox'; +import type {IToolboxItem} from './i_toolbox_item.js'; + +/** + * Interface for an item in the toolbox that can be selected. + */ +export interface ISelectableToolboxItem extends IToolboxItem { + /** + * Gets the name of the toolbox item. Used for emitting events. + * + * @returns The name of the toolbox item. + */ + getName(): string; + + /** + * Gets the contents of the toolbox item. These are items that are meant to be + * displayed in the flyout. + * + * @returns The definition of items to be displayed in the flyout. + */ + getContents(): FlyoutItemInfoArray | string; + + /** + * Sets the current toolbox item as selected. + * + * @param _isSelected True if this category is selected, false otherwise. + */ + setSelected(_isSelected: boolean): void; + + /** + * Gets the HTML element that is clickable. + * The parent toolbox element receives clicks. The parent toolbox will add an + * ID to this element so it can pass the onClick event to the correct + * toolboxItem. + * + * @returns The HTML element that receives clicks. + */ + getClickTarget(): Element; + + /** + * Handles when the toolbox item is clicked. + * + * @param _e Click event to handle. + */ + onClick(_e: Event): void; +} + +/** + * Type guard that checks whether an IToolboxItem is an ISelectableToolboxItem. + */ +export function isSelectableToolboxItem( + toolboxItem: IToolboxItem, +): toolboxItem is ISelectableToolboxItem { + return toolboxItem.isSelectable(); +} diff --git a/core/interfaces/i_serializable.ts b/core/interfaces/i_serializable.ts new file mode 100644 index 00000000000..380a277095d --- /dev/null +++ b/core/interfaces/i_serializable.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface ISerializable { + /** + * @param doFullSerialization If true, this signals that any backing data + * structures used by this ISerializable should also be serialized. This + * is used for copy-paste. + * @returns a JSON serializable value that records the ISerializable's state. + */ + saveState(doFullSerialization: boolean): any; + + /** + * Takes in a JSON serializable value and sets the ISerializable's state + * based on that. + * + * @param state The state to apply to the ISerializable. + */ + loadState(state: any): void; +} + +/** Type guard that checks whether the given object is a ISerializable. */ +export function isSerializable(obj: any): obj is ISerializable { + return obj.saveState !== undefined && obj.loadState !== undefined; +} diff --git a/core/interfaces/i_serializer.ts b/core/interfaces/i_serializer.ts new file mode 100644 index 00000000000..f5fbb67d100 --- /dev/null +++ b/core/interfaces/i_serializer.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.serialization.ISerializer + +import type {Workspace} from '../workspace.js'; + +/** + * Serializes and deserializes a plugin or system. + */ +export interface ISerializer { + /** + * A priority value used to determine the order of deserializing state. + * More positive priorities are deserialized before less positive + * priorities. Eg if you have priorities (0, -10, 10, 100) the order of + * deserialiation will be (100, 10, 0, -10). + * If two serializers have the same priority, they are deserialized in an + * arbitrary order relative to each other. + */ + priority: number; + + /** + * Saves the state of the plugin or system. + * + * @param workspace The workspace the system to serialize is associated with. + * @returns A JS object containing the system's state, or null if there is no + * state to record. + */ + save(workspace: Workspace): object | null; + + /** + * Loads the state of the plugin or system. + * + * @param state The state of the system to deserialize. This will always be + * non-null. + * @param workspace The workspace the system to deserialize is associated + * with. + */ + load(state: object, workspace: Workspace): void; + + /** + * Clears the state of the plugin or system. + * + * @param workspace The workspace the system to clear the state of is + * associated with. + */ + clear(workspace: Workspace): void; +} diff --git a/core/interfaces/i_styleable.ts b/core/interfaces/i_styleable.ts new file mode 100644 index 00000000000..cb043b213f9 --- /dev/null +++ b/core/interfaces/i_styleable.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IStyleable + +/** + * Interface for an object that a style can be added to. + */ +export interface IStyleable { + /** + * Adds a style on the toolbox. Usually used to change the cursor. + * + * @param style The name of the class to add. + */ + addStyle(style: string): void; + + /** + * Removes a style from the toolbox. Usually used to change the cursor. + * + * @param style The name of the class to remove. + */ + removeStyle(style: string): void; +} diff --git a/core/interfaces/i_toolbox.ts b/core/interfaces/i_toolbox.ts new file mode 100644 index 00000000000..2756099ec34 --- /dev/null +++ b/core/interfaces/i_toolbox.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IToolbox + +import type {ToolboxInfo} from '../utils/toolbox.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import type {IFlyout} from './i_flyout.js'; +import type {IRegistrable} from './i_registrable.js'; +import type {IToolboxItem} from './i_toolbox_item.js'; + +/** + * Interface for a toolbox. + */ +export interface IToolbox extends IRegistrable { + /** Initializes the toolbox. */ + init(): void; + + /** + * Fills the toolbox with new toolbox items and removes any old contents. + * + * @param toolboxDef Object holding information for creating a toolbox. + */ + render(toolboxDef: ToolboxInfo): void; + + /** + * Gets the width of the toolbox. + * + * @returns The width of the toolbox. + */ + getWidth(): number; + + /** + * Gets the height of the toolbox. + * + * @returns The height of the toolbox. + */ + getHeight(): number; + + /** + * Gets the toolbox flyout. + * + * @returns The toolbox flyout. + */ + getFlyout(): IFlyout | null; + + /** + * Gets the workspace for the toolbox. + * + * @returns The parent workspace for the toolbox. + */ + getWorkspace(): WorkspaceSvg; + + /** + * Gets whether or not the toolbox is horizontal. + * + * @returns True if the toolbox is horizontal, false if the toolbox is + * vertical. + */ + isHorizontal(): boolean; + + /** + * Positions the toolbox based on whether it is a horizontal toolbox and + * whether the workspace is in rtl. + */ + position(): void; + + /** Handles resizing the toolbox when a toolbox item resizes. */ + handleToolboxItemResize(): void; + + /** Unhighlights any previously selected item. */ + clearSelection(): void; + + /** + * Updates the category colours and background colour of selected categories. + */ + refreshTheme(): void; + + /** + * Updates the flyout's content without closing it. Should be used in + * response to a change in one of the dynamic categories, such as variables or + * procedures. + */ + refreshSelection(): void; + + /** + * Sets the visibility of the toolbox. + * + * @param isVisible True if toolbox should be visible. + */ + setVisible(isVisible: boolean): void; + + /** + * Selects the toolbox item by it's position in the list of toolbox items. + * + * @param position The position of the item to select. + */ + selectItemByPosition(position: number): void; + + /** + * Gets the selected item. + * + * @returns The selected item, or null if no item is currently selected. + */ + getSelectedItem(): IToolboxItem | null; + + /** Disposes of this toolbox. */ + dispose(): void; +} diff --git a/core/interfaces/i_toolbox_item.ts b/core/interfaces/i_toolbox_item.ts new file mode 100644 index 00000000000..e3c9864f0c0 --- /dev/null +++ b/core/interfaces/i_toolbox_item.ts @@ -0,0 +1,81 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.IToolboxItem + +/** + * Interface for an item in the toolbox. + */ +export interface IToolboxItem { + /** + * Initializes the toolbox item. + * This includes creating the DOM and updating the state of any items based + * on the info object. + */ + init(): void; + + /** + * Gets the div for the toolbox item. + * + * @returns The div for the toolbox item. + */ + getDiv(): Element | null; + + /** + * Gets a unique identifier for this toolbox item. + * + * @returns The ID for the toolbox item. + */ + getId(): string; + + /** + * Gets the parent if the toolbox item is nested. + * + * @returns The parent toolbox item, or null if this toolbox item is not + * nested. + */ + getParent(): IToolboxItem | null; + + /** + * Gets the nested level of the category. + * + * @returns The nested level of the category. + * @internal + */ + getLevel(): number; + + /** + * Whether the toolbox item is selectable. + * + * @returns True if the toolbox item can be selected. + */ + isSelectable(): boolean; + + /** + * Whether the toolbox item is collapsible. + * + * @returns True if the toolbox item is collapsible. + */ + isCollapsible(): boolean; + + /** Dispose of this toolbox item. No-op by default. */ + dispose(): void; + + /** + * Gets the HTML element that is clickable. + * + * @returns The HTML element that receives clicks. + */ + getClickTarget(): Element | null; + + /** + * Sets whether the category is visible or not. + * For a category to be visible its parent category must also be expanded. + * + * @param isVisible True if category should be visible. + */ + setVisible_(isVisible: boolean): void; +} diff --git a/core/interfaces/i_variable_backed_parameter_model.ts b/core/interfaces/i_variable_backed_parameter_model.ts new file mode 100644 index 00000000000..b2042bfb2f5 --- /dev/null +++ b/core/interfaces/i_variable_backed_parameter_model.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {VariableModel} from '../variable_model.js'; +import {IParameterModel} from './i_parameter_model.js'; + +/** Interface for a parameter model that holds a variable model. */ +export interface IVariableBackedParameterModel extends IParameterModel { + /** Returns the variable model held by this type. */ + getVariableModel(): VariableModel; +} + +/** + * Returns whether the given object is a variable holder or not. + */ +export function isVariableBackedParameterModel( + param: IParameterModel, +): param is IVariableBackedParameterModel { + return (param as any).getVariableModel !== undefined; +} diff --git a/core/internal_constants.ts b/core/internal_constants.ts new file mode 100644 index 00000000000..27c945dc08b --- /dev/null +++ b/core/internal_constants.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.internalConstants + +import {ConnectionType} from './connection_type.js'; + +/** + * Number of characters to truncate a collapsed block to. + * + * @internal + */ +export const COLLAPSE_CHARS = 30; + +/** + * Lookup table for determining the opposite type of a connection. + * + * @internal + */ +export const OPPOSITE_TYPE: number[] = []; +OPPOSITE_TYPE[ConnectionType.INPUT_VALUE] = ConnectionType.OUTPUT_VALUE; +OPPOSITE_TYPE[ConnectionType.OUTPUT_VALUE] = ConnectionType.INPUT_VALUE; +OPPOSITE_TYPE[ConnectionType.NEXT_STATEMENT] = + ConnectionType.PREVIOUS_STATEMENT; +OPPOSITE_TYPE[ConnectionType.PREVIOUS_STATEMENT] = + ConnectionType.NEXT_STATEMENT; + +/** + * String for use in the dropdown created in field_variable. + * This string indicates that this option in the dropdown is 'Rename + * variable...' and if selected, should trigger the prompt to rename a variable. + * + * @internal + */ +export const RENAME_VARIABLE_ID = 'RENAME_VARIABLE_ID'; + +/** + * String for use in the dropdown created in field_variable. + * This string indicates that this option in the dropdown is 'Delete the "%1" + * variable' and if selected, should trigger the prompt to delete a variable. + * + * @internal + */ +export const DELETE_VARIABLE_ID = 'DELETE_VARIABLE_ID'; diff --git a/core/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts new file mode 100644 index 00000000000..3b0efae3fb3 --- /dev/null +++ b/core/keyboard_nav/ast_node.ts @@ -0,0 +1,880 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The class representing an AST node. + * Used to traverse the Blockly AST. + * + * @class + */ +// Former goog.module ID: Blockly.ASTNode + +import {Block} from '../block.js'; +import type {Connection} from '../connection.js'; +import {ConnectionType} from '../connection_type.js'; +import type {Field} from '../field.js'; +import {FlyoutItem} from '../flyout_base.js'; +import {FlyoutButton} from '../flyout_button.js'; +import type {Input} from '../inputs/input.js'; +import type {IASTNodeLocation} from '../interfaces/i_ast_node_location.js'; +import type {IASTNodeLocationWithBlock} from '../interfaces/i_ast_node_location_with_block.js'; +import {Coordinate} from '../utils/coordinate.js'; +import type {Workspace} from '../workspace.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; + +/** + * Class for an AST node. + * It is recommended that you use one of the createNode methods instead of + * creating a node directly. + */ +export class ASTNode { + /** + * True to navigate to all fields. False to only navigate to clickable fields. + */ + static NAVIGATE_ALL_FIELDS = false; + + /** + * The default y offset to use when moving the cursor from a stack to the + * workspace. + */ + private static readonly DEFAULT_OFFSET_Y: number = -20; + private readonly type: string; + private readonly isConnectionLocation: boolean; + private readonly location: IASTNodeLocation; + + /** The coordinate on the workspace. */ + // AnyDuringMigration because: Type 'null' is not assignable to type + // 'Coordinate'. + private wsCoordinate: Coordinate = null as AnyDuringMigration; + + /** + * @param type The type of the location. + * Must be in ASTNode.types. + * @param location The position in the AST. + * @param opt_params Optional dictionary of options. + */ + constructor(type: string, location: IASTNodeLocation, opt_params?: Params) { + if (!location) { + throw Error('Cannot create a node without a location.'); + } + + /** + * The type of the location. + * One of ASTNode.types + */ + this.type = type; + + /** Whether the location points to a connection. */ + this.isConnectionLocation = ASTNode.isConnectionType(type); + + /** The location of the AST node. */ + this.location = location; + + this.processParams(opt_params || null); + } + + /** + * Parse the optional parameters. + * + * @param params The user specified parameters. + */ + private processParams(params: Params | null) { + if (!params) { + return; + } + if (params.wsCoordinate) { + this.wsCoordinate = params.wsCoordinate; + } + } + + /** + * Gets the value pointed to by this node. + * It is the callers responsibility to check the node type to figure out what + * type of object they get back from this. + * + * @returns The current field, connection, workspace, or block the cursor is + * on. + */ + getLocation(): IASTNodeLocation { + return this.location; + } + + /** + * The type of the current location. + * One of ASTNode.types + * + * @returns The type of the location. + */ + getType(): string { + return this.type; + } + + /** + * The coordinate on the workspace. + * + * @returns The workspace coordinate or null if the location is not a + * workspace. + */ + getWsCoordinate(): Coordinate { + return this.wsCoordinate; + } + + /** + * Whether the node points to a connection. + * + * @returns [description] + * @internal + */ + isConnection(): boolean { + return this.isConnectionLocation; + } + + /** + * Given an input find the next editable field or an input with a non null + * connection in the same block. The current location must be an input + * connection. + * + * @returns The AST node holding the next field or connection or null if there + * is no editable field or input connection after the given input. + */ + private findNextForInput(): ASTNode | null { + const location = this.location as Connection; + const parentInput = location.getParentInput(); + const block = parentInput!.getSourceBlock(); + // AnyDuringMigration because: Argument of type 'Input | null' is not + // assignable to parameter of type 'Input'. + const curIdx = block!.inputList.indexOf(parentInput as AnyDuringMigration); + for (let i = curIdx + 1; i < block!.inputList.length; i++) { + const input = block!.inputList[i]; + const fieldRow = input.fieldRow; + for (let j = 0; j < fieldRow.length; j++) { + const field = fieldRow[j]; + if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { + return ASTNode.createFieldNode(field); + } + } + if (input.connection) { + return ASTNode.createInputNode(input); + } + } + return null; + } + + /** + * Given a field find the next editable field or an input with a non null + * connection in the same block. The current location must be a field. + * + * @returns The AST node pointing to the next field or connection or null if + * there is no editable field or input connection after the given input. + */ + private findNextForField(): ASTNode | null { + const location = this.location as Field; + const input = location.getParentInput(); + const block = location.getSourceBlock(); + if (!block) { + throw new Error( + 'The current AST location is not associated with a block', + ); + } + const curIdx = block.inputList.indexOf(input); + let fieldIdx = input.fieldRow.indexOf(location) + 1; + for (let i = curIdx; i < block.inputList.length; i++) { + const newInput = block.inputList[i]; + const fieldRow = newInput.fieldRow; + while (fieldIdx < fieldRow.length) { + if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { + return ASTNode.createFieldNode(fieldRow[fieldIdx]); + } + fieldIdx++; + } + fieldIdx = 0; + if (newInput.connection) { + return ASTNode.createInputNode(newInput); + } + } + return null; + } + + /** + * Given an input find the previous editable field or an input with a non null + * connection in the same block. The current location must be an input + * connection. + * + * @returns The AST node holding the previous field or connection. + */ + private findPrevForInput(): ASTNode | null { + const location = this.location as Connection; + const parentInput = location.getParentInput(); + const block = parentInput!.getSourceBlock(); + // AnyDuringMigration because: Argument of type 'Input | null' is not + // assignable to parameter of type 'Input'. + const curIdx = block!.inputList.indexOf(parentInput as AnyDuringMigration); + for (let i = curIdx; i >= 0; i--) { + const input = block!.inputList[i]; + if (input.connection && input !== parentInput) { + return ASTNode.createInputNode(input); + } + const fieldRow = input.fieldRow; + for (let j = fieldRow.length - 1; j >= 0; j--) { + const field = fieldRow[j]; + if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { + return ASTNode.createFieldNode(field); + } + } + } + return null; + } + + /** + * Given a field find the previous editable field or an input with a non null + * connection in the same block. The current location must be a field. + * + * @returns The AST node holding the previous input or field. + */ + private findPrevForField(): ASTNode | null { + const location = this.location as Field; + const parentInput = location.getParentInput(); + const block = location.getSourceBlock(); + if (!block) { + throw new Error( + 'The current AST location is not associated with a block', + ); + } + const curIdx = block.inputList.indexOf(parentInput); + let fieldIdx = parentInput.fieldRow.indexOf(location) - 1; + for (let i = curIdx; i >= 0; i--) { + const input = block.inputList[i]; + if (input.connection && input !== parentInput) { + return ASTNode.createInputNode(input); + } + const fieldRow = input.fieldRow; + while (fieldIdx > -1) { + if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { + return ASTNode.createFieldNode(fieldRow[fieldIdx]); + } + fieldIdx--; + } + // Reset the fieldIdx to the length of the field row of the previous + // input. + if (i - 1 >= 0) { + fieldIdx = block.inputList[i - 1].fieldRow.length - 1; + } + } + return null; + } + + /** + * Navigate between stacks of blocks on the workspace. + * + * @param forward True to go forward. False to go backwards. + * @returns The first block of the next stack or null if there are no blocks + * on the workspace. + */ + private navigateBetweenStacks(forward: boolean): ASTNode | null { + let curLocation = this.getLocation(); + // TODO(#6097): Use instanceof checks to exit early for values of + // curLocation that don't make sense. + if ((curLocation as IASTNodeLocationWithBlock).getSourceBlock) { + const block = (curLocation as IASTNodeLocationWithBlock).getSourceBlock(); + if (block) { + curLocation = block; + } + } + // TODO(#6097): Use instanceof checks to exit early for values of + // curLocation that don't make sense. + const curLocationAsBlock = curLocation as Block; + if (!curLocationAsBlock || curLocationAsBlock.isDeadOrDying()) { + return null; + } + if (curLocationAsBlock.workspace.isFlyout) { + return this.navigateFlyoutContents(forward); + } + const curRoot = curLocationAsBlock.getRootBlock(); + const topBlocks = curRoot.workspace.getTopBlocks(true); + for (let i = 0; i < topBlocks.length; i++) { + const topBlock = topBlocks[i]; + if (curRoot.id === topBlock.id) { + const offset = forward ? 1 : -1; + const resultIndex = i + offset; + if (resultIndex === -1 || resultIndex === topBlocks.length) { + return null; + } + return ASTNode.createStackNode(topBlocks[resultIndex]); + } + } + throw Error( + "Couldn't find " + (forward ? 'next' : 'previous') + ' stack?!', + ); + } + + /** + * Navigate between buttons and stacks of blocks on the flyout workspace. + * + * @param forward True to go forward. False to go backwards. + * @returns The next button, or next stack's first block, or null + */ + private navigateFlyoutContents(forward: boolean): ASTNode | null { + const nodeType = this.getType(); + let location; + let targetWorkspace; + + switch (nodeType) { + case ASTNode.types.STACK: { + location = this.getLocation() as Block; + const workspace = location.workspace as WorkspaceSvg; + targetWorkspace = workspace.targetWorkspace as WorkspaceSvg; + break; + } + case ASTNode.types.BUTTON: { + location = this.getLocation() as FlyoutButton; + targetWorkspace = location.getTargetWorkspace() as WorkspaceSvg; + break; + } + default: + return null; + } + + const flyout = targetWorkspace.getFlyout(); + if (!flyout) return null; + + const nextItem = this.findNextLocationInFlyout( + flyout.getContents(), + location, + forward, + ); + if (!nextItem) return null; + + if (nextItem.type === 'button' && nextItem.button) { + return ASTNode.createButtonNode(nextItem.button); + } else if (nextItem.type === 'block' && nextItem.block) { + return ASTNode.createStackNode(nextItem.block); + } + + return null; + } + + /** + * Finds the next (or previous if navigating backward) item in the flyout that should be navigated to. + * + * @param flyoutContents Contents of the current flyout. + * @param currentLocation Current ASTNode location. + * @param forward True if we're navigating forward, else false. + * @returns The next (or previous) FlyoutItem, or null if there is none. + */ + private findNextLocationInFlyout( + flyoutContents: FlyoutItem[], + currentLocation: IASTNodeLocation, + forward: boolean, + ): FlyoutItem | null { + const currentIndex = flyoutContents.findIndex((item: FlyoutItem) => { + if (currentLocation instanceof Block && item.block === currentLocation) { + return true; + } + if ( + currentLocation instanceof FlyoutButton && + item.button === currentLocation + ) { + return true; + } + return false; + }); + + if (currentIndex < 0) return null; + + const resultIndex = forward ? currentIndex + 1 : currentIndex - 1; + if (resultIndex === -1 || resultIndex === flyoutContents.length) { + return null; + } + + return flyoutContents[resultIndex]; + } + + /** + * Finds the top most AST node for a given block. + * This is either the previous connection, output connection or block + * depending on what kind of connections the block has. + * + * @param block The block that we want to find the top connection on. + * @returns The AST node containing the top connection. + */ + private findTopASTNodeForBlock(block: Block): ASTNode | null { + const topConnection = getParentConnection(block); + if (topConnection) { + return ASTNode.createConnectionNode(topConnection); + } else { + return ASTNode.createBlockNode(block); + } + } + + /** + * Get the AST node pointing to the input that the block is nested under or if + * the block is not nested then get the stack AST node. + * + * @param block The source block of the current location. + * @returns The AST node pointing to the input connection or the top block of + * the stack this block is in. + */ + private getOutAstNodeForBlock(block: Block): ASTNode | null { + if (!block) { + return null; + } + // If the block doesn't have a previous connection then it is the top of the + // substack. + const topBlock = block.getTopStackBlock(); + const topConnection = getParentConnection(topBlock); + // If the top connection has a parentInput, create an AST node pointing to + // that input. + if ( + topConnection && + topConnection.targetConnection && + topConnection.targetConnection.getParentInput() + ) { + // AnyDuringMigration because: Argument of type 'Input | null' is not + // assignable to parameter of type 'Input'. + return ASTNode.createInputNode( + topConnection.targetConnection.getParentInput() as AnyDuringMigration, + ); + } else { + // Go to stack level if you are not underneath an input. + return ASTNode.createStackNode(topBlock); + } + } + + /** + * Find the first editable field or input with a connection on a given block. + * + * @param block The source block of the current location. + * @returns An AST node pointing to the first field or input. + * Null if there are no editable fields or inputs with connections on the + * block. + */ + private findFirstFieldOrInput(block: Block): ASTNode | null { + const inputs = block.inputList; + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + const fieldRow = input.fieldRow; + for (let j = 0; j < fieldRow.length; j++) { + const field = fieldRow[j]; + if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { + return ASTNode.createFieldNode(field); + } + } + if (input.connection) { + return ASTNode.createInputNode(input); + } + } + return null; + } + + /** + * Finds the source block of the location of this node. + * + * @returns The source block of the location, or null if the node is of type + * workspace or button. + */ + getSourceBlock(): Block | null { + if (this.getType() === ASTNode.types.BLOCK) { + return this.getLocation() as Block; + } else if (this.getType() === ASTNode.types.STACK) { + return this.getLocation() as Block; + } else if (this.getType() === ASTNode.types.WORKSPACE) { + return null; + } else if (this.getType() === ASTNode.types.BUTTON) { + return null; + } else { + return (this.getLocation() as IASTNodeLocationWithBlock).getSourceBlock(); + } + } + + /** + * Find the element to the right of the current element in the AST. + * + * @returns An AST node that wraps the next field, connection, block, or + * workspace. Or null if there is no node to the right. + */ + next(): ASTNode | null { + switch (this.type) { + case ASTNode.types.STACK: + return this.navigateBetweenStacks(true); + + case ASTNode.types.OUTPUT: { + const connection = this.location as Connection; + return ASTNode.createBlockNode(connection.getSourceBlock()); + } + case ASTNode.types.FIELD: + return this.findNextForField(); + + case ASTNode.types.INPUT: + return this.findNextForInput(); + + case ASTNode.types.BLOCK: { + const block = this.location as Block; + const nextConnection = block.nextConnection; + if (!nextConnection) return null; + return ASTNode.createConnectionNode(nextConnection); + } + case ASTNode.types.PREVIOUS: { + const connection = this.location as Connection; + return ASTNode.createBlockNode(connection.getSourceBlock()); + } + case ASTNode.types.NEXT: { + const connection = this.location as Connection; + const targetConnection = connection.targetConnection; + return ASTNode.createConnectionNode(targetConnection!); + } + case ASTNode.types.BUTTON: + return this.navigateFlyoutContents(true); + } + + return null; + } + + /** + * Find the element one level below and all the way to the left of the current + * location. + * + * @returns An AST node that wraps the next field, connection, workspace, or + * block. Or null if there is nothing below this node. + */ + in(): ASTNode | null { + switch (this.type) { + case ASTNode.types.WORKSPACE: { + const workspace = this.location as Workspace; + const topBlocks = workspace.getTopBlocks(true); + if (topBlocks.length > 0) { + return ASTNode.createStackNode(topBlocks[0]); + } + break; + } + case ASTNode.types.STACK: { + const block = this.location as Block; + return this.findTopASTNodeForBlock(block); + } + case ASTNode.types.BLOCK: { + const block = this.location as Block; + return this.findFirstFieldOrInput(block); + } + case ASTNode.types.INPUT: { + const connection = this.location as Connection; + const targetConnection = connection.targetConnection; + return ASTNode.createConnectionNode(targetConnection!); + } + } + + return null; + } + + /** + * Find the element to the left of the current element in the AST. + * + * @returns An AST node that wraps the previous field, connection, workspace + * or block. Or null if no node exists to the left. null. + */ + prev(): ASTNode | null { + switch (this.type) { + case ASTNode.types.STACK: + return this.navigateBetweenStacks(false); + + case ASTNode.types.OUTPUT: + return null; + + case ASTNode.types.FIELD: + return this.findPrevForField(); + + case ASTNode.types.INPUT: + return this.findPrevForInput(); + + case ASTNode.types.BLOCK: { + const block = this.location as Block; + const topConnection = getParentConnection(block); + if (!topConnection) return null; + return ASTNode.createConnectionNode(topConnection); + } + case ASTNode.types.PREVIOUS: { + const connection = this.location as Connection; + const targetConnection = connection.targetConnection; + if (targetConnection && !targetConnection.getParentInput()) { + return ASTNode.createConnectionNode(targetConnection); + } + break; + } + case ASTNode.types.NEXT: { + const connection = this.location as Connection; + return ASTNode.createBlockNode(connection.getSourceBlock()); + } + case ASTNode.types.BUTTON: + return this.navigateFlyoutContents(false); + } + + return null; + } + + /** + * Find the next element that is one position above and all the way to the + * left of the current location. + * + * @returns An AST node that wraps the next field, connection, workspace or + * block. Or null if we are at the workspace level. + */ + out(): ASTNode | null { + switch (this.type) { + case ASTNode.types.STACK: { + const block = this.location as Block; + const blockPos = block.getRelativeToSurfaceXY(); + // TODO: Make sure this is in the bounds of the workspace. + const wsCoordinate = new Coordinate( + blockPos.x, + blockPos.y + ASTNode.DEFAULT_OFFSET_Y, + ); + return ASTNode.createWorkspaceNode(block.workspace, wsCoordinate); + } + case ASTNode.types.OUTPUT: { + const connection = this.location as Connection; + const target = connection.targetConnection; + if (target) { + return ASTNode.createConnectionNode(target); + } + return ASTNode.createStackNode(connection.getSourceBlock()); + } + case ASTNode.types.FIELD: { + const field = this.location as Field; + const block = field.getSourceBlock(); + if (!block) { + throw new Error( + 'The current AST location is not associated with a block', + ); + } + return ASTNode.createBlockNode(block); + } + case ASTNode.types.INPUT: { + const connection = this.location as Connection; + return ASTNode.createBlockNode(connection.getSourceBlock()); + } + case ASTNode.types.BLOCK: { + const block = this.location as Block; + return this.getOutAstNodeForBlock(block); + } + case ASTNode.types.PREVIOUS: { + const connection = this.location as Connection; + return this.getOutAstNodeForBlock(connection.getSourceBlock()); + } + case ASTNode.types.NEXT: { + const connection = this.location as Connection; + return this.getOutAstNodeForBlock(connection.getSourceBlock()); + } + } + + return null; + } + + /** + * Whether an AST node of the given type points to a connection. + * + * @param type The type to check. One of ASTNode.types. + * @returns True if a node of the given type points to a connection. + */ + private static isConnectionType(type: string): boolean { + switch (type) { + case ASTNode.types.PREVIOUS: + case ASTNode.types.NEXT: + case ASTNode.types.INPUT: + case ASTNode.types.OUTPUT: + return true; + } + return false; + } + + /** + * Create an AST node pointing to a field. + * + * @param field The location of the AST node. + * @returns An AST node pointing to a field. + */ + static createFieldNode(field: Field): ASTNode | null { + if (!field) { + return null; + } + return new ASTNode(ASTNode.types.FIELD, field); + } + + /** + * Creates an AST node pointing to a connection. If the connection has a + * parent input then create an AST node of type input that will hold the + * connection. + * + * @param connection This is the connection the node will point to. + * @returns An AST node pointing to a connection. + */ + static createConnectionNode(connection: Connection): ASTNode | null { + if (!connection) { + return null; + } + const type = connection.type; + if (type === ConnectionType.INPUT_VALUE) { + // AnyDuringMigration because: Argument of type 'Input | null' is not + // assignable to parameter of type 'Input'. + return ASTNode.createInputNode( + connection.getParentInput() as AnyDuringMigration, + ); + } else if ( + type === ConnectionType.NEXT_STATEMENT && + connection.getParentInput() + ) { + // AnyDuringMigration because: Argument of type 'Input | null' is not + // assignable to parameter of type 'Input'. + return ASTNode.createInputNode( + connection.getParentInput() as AnyDuringMigration, + ); + } else if (type === ConnectionType.NEXT_STATEMENT) { + return new ASTNode(ASTNode.types.NEXT, connection); + } else if (type === ConnectionType.OUTPUT_VALUE) { + return new ASTNode(ASTNode.types.OUTPUT, connection); + } else if (type === ConnectionType.PREVIOUS_STATEMENT) { + return new ASTNode(ASTNode.types.PREVIOUS, connection); + } + return null; + } + + /** + * Creates an AST node pointing to an input. Stores the input connection as + * the location. + * + * @param input The input used to create an AST node. + * @returns An AST node pointing to a input. + */ + static createInputNode(input: Input): ASTNode | null { + if (!input || !input.connection) { + return null; + } + return new ASTNode(ASTNode.types.INPUT, input.connection); + } + + /** + * Creates an AST node pointing to a block. + * + * @param block The block used to create an AST node. + * @returns An AST node pointing to a block. + */ + static createBlockNode(block: Block): ASTNode | null { + if (!block) { + return null; + } + return new ASTNode(ASTNode.types.BLOCK, block); + } + + /** + * Create an AST node of type stack. A stack, represented by its top block, is + * the set of all blocks connected to a top block, including the top + * block. + * + * @param topBlock A top block has no parent and can be found in the list + * returned by workspace.getTopBlocks(). + * @returns An AST node of type stack that points to the top block on the + * stack. + */ + static createStackNode(topBlock: Block): ASTNode | null { + if (!topBlock) { + return null; + } + return new ASTNode(ASTNode.types.STACK, topBlock); + } + + /** + * Create an AST node of type button. A button in this case refers + * specifically to a button in a flyout. + * + * @param button A top block has no parent and can be found in the list + * returned by workspace.getTopBlocks(). + * @returns An AST node of type stack that points to the top block on the + * stack. + */ + static createButtonNode(button: FlyoutButton): ASTNode | null { + if (!button) { + return null; + } + return new ASTNode(ASTNode.types.BUTTON, button); + } + + /** + * Creates an AST node pointing to a workspace. + * + * @param workspace The workspace that we are on. + * @param wsCoordinate The position on the workspace for this node. + * @returns An AST node pointing to a workspace and a position on the + * workspace. + */ + static createWorkspaceNode( + workspace: Workspace | null, + wsCoordinate: Coordinate | null, + ): ASTNode | null { + if (!wsCoordinate || !workspace) { + return null; + } + const params = {wsCoordinate}; + return new ASTNode(ASTNode.types.WORKSPACE, workspace, params); + } + + /** + * Creates an AST node for the top position on a block. + * This is either an output connection, previous connection, or block. + * + * @param block The block to find the top most AST node on. + * @returns The AST node holding the top most position on the block. + */ + static createTopNode(block: Block): ASTNode | null { + let astNode; + const topConnection = getParentConnection(block); + if (topConnection) { + astNode = ASTNode.createConnectionNode(topConnection); + } else { + astNode = ASTNode.createBlockNode(block); + } + return astNode; + } +} + +export namespace ASTNode { + export interface Params { + wsCoordinate: Coordinate; + } + + export enum types { + FIELD = 'field', + BLOCK = 'block', + INPUT = 'input', + OUTPUT = 'output', + NEXT = 'next', + PREVIOUS = 'previous', + STACK = 'stack', + WORKSPACE = 'workspace', + BUTTON = 'button', + } +} + +export type Params = ASTNode.Params; +// No need to export ASTNode.types from the module at this time because (1) it +// wasn't automatically converted by the automatic migration script, (2) the +// name doesn't follow the styleguide. + +/** + * Gets the parent connection on a block. + * This is either an output connection, previous connection or undefined. + * If both connections exist return the one that is actually connected + * to another block. + * + * @param block The block to find the parent connection on. + * @returns The connection connecting to the parent of the block. + */ +function getParentConnection(block: Block): Connection | null { + let topConnection = block.outputConnection; + if ( + !topConnection || + (block.previousConnection && block.previousConnection.isConnected()) + ) { + topConnection = block.previousConnection; + } + return topConnection; +} diff --git a/core/keyboard_nav/basic_cursor.ts b/core/keyboard_nav/basic_cursor.ts new file mode 100644 index 00000000000..7526141529e --- /dev/null +++ b/core/keyboard_nav/basic_cursor.ts @@ -0,0 +1,222 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The class representing a basic cursor. + * Used to demo switching between different cursors. + * + * @class + */ +// Former goog.module ID: Blockly.BasicCursor + +import * as registry from '../registry.js'; +import {ASTNode} from './ast_node.js'; +import {Cursor} from './cursor.js'; + +/** + * Class for a basic cursor. + * This will allow the user to get to all nodes in the AST by hitting next or + * previous. + */ +export class BasicCursor extends Cursor { + /** Name used for registering a basic cursor. */ + static readonly registrationName = 'basicCursor'; + + constructor() { + super(); + } + + /** + * Find the next node in the pre order traversal. + * + * @returns The next node, or null if the current node is not set or there is + * no next value. + */ + override next(): ASTNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + const newNode = this.getNextNode_(curNode, this.validNode_); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + + /** + * For a basic cursor we only have the ability to go next and previous, so + * in will also allow the user to get to the next node in the pre order + * traversal. + * + * @returns The next node, or null if the current node is not set or there is + * no next value. + */ + override in(): ASTNode | null { + return this.next(); + } + + /** + * Find the previous node in the pre order traversal. + * + * @returns The previous node, or null if the current node is not set or there + * is no previous value. + */ + override prev(): ASTNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + const newNode = this.getPreviousNode_(curNode, this.validNode_); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + + /** + * For a basic cursor we only have the ability to go next and previous, so + * out will allow the user to get to the previous node in the pre order + * traversal. + * + * @returns The previous node, or null if the current node is not set or there + * is no previous value. + */ + override out(): ASTNode | null { + return this.prev(); + } + + /** + * Uses pre order traversal to navigate the Blockly AST. This will allow + * a user to easily navigate the entire Blockly AST without having to go in + * and out levels on the tree. + * + * @param node The current position in the AST. + * @param isValid A function true/false depending on whether the given node + * should be traversed. + * @returns The next node in the traversal. + */ + protected getNextNode_( + node: ASTNode | null, + isValid: (p1: ASTNode | null) => boolean, + ): ASTNode | null { + if (!node) { + return null; + } + const newNode = node.in() || node.next(); + if (isValid(newNode)) { + return newNode; + } else if (newNode) { + return this.getNextNode_(newNode, isValid); + } + const siblingOrParent = this.findSiblingOrParent(node.out()); + if (isValid(siblingOrParent)) { + return siblingOrParent; + } else if (siblingOrParent) { + return this.getNextNode_(siblingOrParent, isValid); + } + return null; + } + + /** + * Reverses the pre order traversal in order to find the previous node. This + * will allow a user to easily navigate the entire Blockly AST without having + * to go in and out levels on the tree. + * + * @param node The current position in the AST. + * @param isValid A function true/false depending on whether the given node + * should be traversed. + * @returns The previous node in the traversal or null if no previous node + * exists. + */ + protected getPreviousNode_( + node: ASTNode | null, + isValid: (p1: ASTNode | null) => boolean, + ): ASTNode | null { + if (!node) { + return null; + } + let newNode: ASTNode | null = node.prev(); + + if (newNode) { + newNode = this.getRightMostChild(newNode); + } else { + newNode = node.out(); + } + if (isValid(newNode)) { + return newNode; + } else if (newNode) { + return this.getPreviousNode_(newNode, isValid); + } + return null; + } + + /** + * Decides what nodes to traverse and which ones to skip. Currently, it + * skips output, stack and workspace nodes. + * + * @param node The AST node to check whether it is valid. + * @returns True if the node should be visited, false otherwise. + */ + protected validNode_(node: ASTNode | null): boolean { + let isValid = false; + const type = node && node.getType(); + if ( + type === ASTNode.types.OUTPUT || + type === ASTNode.types.INPUT || + type === ASTNode.types.FIELD || + type === ASTNode.types.NEXT || + type === ASTNode.types.PREVIOUS || + type === ASTNode.types.WORKSPACE + ) { + isValid = true; + } + return isValid; + } + + /** + * From the given node find either the next valid sibling or parent. + * + * @param node The current position in the AST. + * @returns The parent AST node or null if there are no valid parents. + */ + private findSiblingOrParent(node: ASTNode | null): ASTNode | null { + if (!node) { + return null; + } + const nextNode = node.next(); + if (nextNode) { + return nextNode; + } + return this.findSiblingOrParent(node.out()); + } + + /** + * Get the right most child of a node. + * + * @param node The node to find the right most child of. + * @returns The right most child of the given node, or the node if no child + * exists. + */ + private getRightMostChild(node: ASTNode | null): ASTNode | null { + if (!node!.in()) { + return node; + } + let newNode = node!.in(); + while (newNode && newNode.next()) { + newNode = newNode.next(); + } + return this.getRightMostChild(newNode); + } +} + +registry.register( + registry.Type.CURSOR, + BasicCursor.registrationName, + BasicCursor, +); diff --git a/core/keyboard_nav/cursor.ts b/core/keyboard_nav/cursor.ts new file mode 100644 index 00000000000..92279da562d --- /dev/null +++ b/core/keyboard_nav/cursor.ts @@ -0,0 +1,137 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The class representing a cursor. + * Used primarily for keyboard navigation. + * + * @class + */ +// Former goog.module ID: Blockly.Cursor + +import * as registry from '../registry.js'; +import {ASTNode} from './ast_node.js'; +import {Marker} from './marker.js'; + +/** + * Class for a cursor. + * A cursor controls how a user navigates the Blockly AST. + */ +export class Cursor extends Marker { + override type = 'cursor'; + + constructor() { + super(); + } + + /** + * Find the next connection, field, or block. + * + * @returns The next element, or null if the current node is not set or there + * is no next value. + */ + next(): ASTNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + + let newNode = curNode.next(); + while ( + newNode && + newNode.next() && + (newNode.getType() === ASTNode.types.NEXT || + newNode.getType() === ASTNode.types.BLOCK) + ) { + newNode = newNode.next(); + } + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + + /** + * Find the in connection or field. + * + * @returns The in element, or null if the current node is not set or there is + * no in value. + */ + in(): ASTNode | null { + let curNode: ASTNode | null = this.getCurNode(); + if (!curNode) { + return null; + } + // If we are on a previous or output connection, go to the block level + // before performing next operation. + if ( + curNode.getType() === ASTNode.types.PREVIOUS || + curNode.getType() === ASTNode.types.OUTPUT + ) { + curNode = curNode.next(); + } + const newNode = curNode?.in() ?? null; + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + + /** + * Find the previous connection, field, or block. + * + * @returns The previous element, or null if the current node is not set or + * there is no previous value. + */ + prev(): ASTNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + let newNode = curNode.prev(); + + while ( + newNode && + newNode.prev() && + (newNode.getType() === ASTNode.types.NEXT || + newNode.getType() === ASTNode.types.BLOCK) + ) { + newNode = newNode.prev(); + } + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + + /** + * Find the out connection, field, or block. + * + * @returns The out element, or null if the current node is not set or there + * is no out value. + */ + out(): ASTNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + let newNode = curNode.out(); + + if (newNode && newNode.getType() === ASTNode.types.BLOCK) { + newNode = newNode.prev() || newNode; + } + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } +} + +registry.register(registry.Type.CURSOR, registry.DEFAULT, Cursor); diff --git a/core/keyboard_nav/marker.ts b/core/keyboard_nav/marker.ts new file mode 100644 index 00000000000..e3b438e6efe --- /dev/null +++ b/core/keyboard_nav/marker.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The class representing a marker. + * Used primarily for keyboard navigation to show a marked location. + * + * @class + */ +// Former goog.module ID: Blockly.Marker + +import type {MarkerSvg} from '../renderers/common/marker_svg.js'; +import type {ASTNode} from './ast_node.js'; + +/** + * Class for a marker. + * This is used in keyboard navigation to save a location in the Blockly AST. + */ +export class Marker { + /** The colour of the marker. */ + colour: string | null = null; + + /** The current location of the marker. */ + // AnyDuringMigration because: Type 'null' is not assignable to type + // 'ASTNode'. + private curNode: ASTNode = null as AnyDuringMigration; + + /** + * The object in charge of drawing the visual representation of the current + * node. + */ + // AnyDuringMigration because: Type 'null' is not assignable to type + // 'MarkerSvg'. + private drawer: MarkerSvg = null as AnyDuringMigration; + + /** The type of the marker. */ + type = 'marker'; + + /** Constructs a new Marker instance. */ + constructor() {} + + /** + * Sets the object in charge of drawing the marker. + * + * @param drawer The object in charge of drawing the marker. + */ + setDrawer(drawer: MarkerSvg) { + this.drawer = drawer; + } + + /** + * Get the current drawer for the marker. + * + * @returns The object in charge of drawing the marker. + */ + getDrawer(): MarkerSvg { + return this.drawer; + } + + /** + * Gets the current location of the marker. + * + * @returns The current field, connection, or block the marker is on. + */ + getCurNode(): ASTNode { + return this.curNode; + } + + /** + * Set the location of the marker and call the update method. + * Setting isStack to true will only work if the newLocation is the top most + * output or previous connection on a stack. + * + * @param newNode The new location of the marker. + */ + setCurNode(newNode: ASTNode) { + const oldNode = this.curNode; + this.curNode = newNode; + if (this.drawer) { + this.drawer.draw(oldNode, this.curNode); + } + } + + /** + * Redraw the current marker. + * + * @internal + */ + draw() { + if (this.drawer) { + this.drawer.draw(this.curNode, this.curNode); + } + } + + /** Hide the marker SVG. */ + hide() { + if (this.drawer) { + this.drawer.hide(); + } + } + + /** Dispose of this marker. */ + dispose() { + if (this.getDrawer()) { + this.getDrawer().dispose(); + } + } +} diff --git a/core/keyboard_nav/tab_navigate_cursor.ts b/core/keyboard_nav/tab_navigate_cursor.ts new file mode 100644 index 00000000000..0392887a1fd --- /dev/null +++ b/core/keyboard_nav/tab_navigate_cursor.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The class representing a cursor that is used to navigate + * between tab navigable fields. + * + * @class + */ +// Former goog.module ID: Blockly.TabNavigateCursor + +import type {Field} from '../field.js'; +import {ASTNode} from './ast_node.js'; +import {BasicCursor} from './basic_cursor.js'; + +/** + * A cursor for navigating between tab navigable fields. + */ +export class TabNavigateCursor extends BasicCursor { + /** + * Skip all nodes except for tab navigable fields. + * + * @param node The AST node to check whether it is valid. + * @returns True if the node should be visited, false otherwise. + */ + override validNode_(node: ASTNode | null): boolean { + let isValid = false; + const type = node && node.getType(); + if (node) { + const location = node.getLocation() as Field; + if ( + type === ASTNode.types.FIELD && + location && + location.isTabNavigable() && + location.isClickable() + ) { + isValid = true; + } + } + return isValid; + } +} diff --git a/core/layer_manager.ts b/core/layer_manager.ts new file mode 100644 index 00000000000..e7663b1b7ee --- /dev/null +++ b/core/layer_manager.ts @@ -0,0 +1,186 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {IRenderedElement} from './interfaces/i_rendered_element.js'; +import * as layerNums from './layers.js'; +import {Coordinate} from './utils/coordinate.js'; +import * as dom from './utils/dom.js'; +import {Svg} from './utils/svg.js'; +import {WorkspaceSvg} from './workspace_svg.js'; + +/** @internal */ +export class LayerManager { + /** The layer elements being dragged are appended to. */ + private dragLayer: SVGGElement | undefined; + /** The layer elements being animated are appended to. */ + private animationLayer: SVGGElement | undefined; + /** The layers elements not being dragged are appended to. */ + private layers = new Map(); + + /** @internal */ + constructor(private workspace: WorkspaceSvg) { + const injectionDiv = workspace.getInjectionDiv(); + // `getInjectionDiv` is actually nullable. We hit this if the workspace + // is part of a flyout and the workspace the flyout is attached to hasn't + // been appended yet. + if (injectionDiv) { + this.dragLayer = this.createDragLayer(injectionDiv); + this.animationLayer = this.createAnimationLayer(injectionDiv); + } + + // We construct these manually so we can add the css class for backwards + // compatibility. + const blockLayer = this.createLayer(layerNums.BLOCK); + dom.addClass(blockLayer, 'blocklyBlockCanvas'); + const bubbleLayer = this.createLayer(layerNums.BUBBLE); + dom.addClass(bubbleLayer, 'blocklyBubbleCanvas'); + } + + private createDragLayer(injectionDiv: Element) { + const svg = dom.createSvgElement(Svg.SVG, { + 'class': 'blocklyBlockDragSurface', + 'xmlns': dom.SVG_NS, + 'xmlns:html': dom.HTML_NS, + 'xmlns:xlink': dom.XLINK_NS, + 'version': '1.1', + }); + injectionDiv.append(svg); + return dom.createSvgElement(Svg.G, {}, svg); + } + + private createAnimationLayer(injectionDiv: Element) { + const svg = dom.createSvgElement(Svg.SVG, { + 'class': 'blocklyAnimationLayer', + 'xmlns': dom.SVG_NS, + 'xmlns:html': dom.HTML_NS, + 'xmlns:xlink': dom.XLINK_NS, + 'version': '1.1', + }); + injectionDiv.append(svg); + return dom.createSvgElement(Svg.G, {}, svg); + } + + /** + * Appends the element to the animation layer. The animation layer doesn't + * move when the workspace moves, so e.g. delete animations don't move + * when a block delete triggers a workspace resize. + * + * @internal + */ + appendToAnimationLayer(elem: IRenderedElement) { + const currentTransform = this.dragLayer?.getAttribute('transform'); + // Only update the current transform when appending, so animations don't + // move if the workspace moves. + if (currentTransform) { + this.animationLayer?.setAttribute('transform', currentTransform); + } + this.animationLayer?.appendChild(elem.getSvgRoot()); + } + + /** + * Translates layers when the workspace is dragged or zoomed. + * + * @internal + */ + translateLayers(newCoord: Coordinate, newScale: number) { + const translation = `translate(${newCoord.x}, ${newCoord.y}) scale(${newScale})`; + this.dragLayer?.setAttribute('transform', translation); + for (const [_, layer] of this.layers) { + layer.setAttribute('transform', translation); + } + } + + /** + * Moves the given element to the drag layer, which exists on top of all other + * layers, and the drag surface. + * + * @internal + */ + moveToDragLayer(elem: IRenderedElement) { + this.dragLayer?.appendChild(elem.getSvgRoot()); + } + + /** + * Moves the given element off of the drag layer. + * + * @internal + */ + moveOffDragLayer(elem: IRenderedElement, layerNum: number) { + this.append(elem, layerNum); + } + + /** + * Appends the given element to a layer. If the layer does not exist, it is + * created. + * + * @internal + */ + append(elem: IRenderedElement, layerNum: number) { + if (!this.layers.has(layerNum)) { + this.createLayer(layerNum); + } + this.layers.get(layerNum)?.appendChild(elem.getSvgRoot()); + } + + /** + * Creates a layer and inserts it at the proper place given the layer number. + * + * More positive layers exist later in the dom and are rendered ontop of + * less positive layers. Layers are added to the layer map as a side effect. + */ + private createLayer(layerNum: number): SVGGElement { + const parent = this.workspace.getSvgGroup(); + const layer = dom.createSvgElement(Svg.G, {}); + + let inserted = false; + const sortedLayers = [...this.layers].sort((a, b) => a[0] - b[0]); + for (const [num, sib] of sortedLayers) { + if (layerNum < num) { + parent.insertBefore(layer, sib); + inserted = true; + break; + } + } + if (!inserted) { + parent.appendChild(layer); + } + this.layers.set(layerNum, layer); + return layer; + } + + /** + * Returns true if the given element is a layer managed by the layer manager. + * False otherwise. + * + * @internal + */ + hasLayer(elem: SVGElement) { + return ( + elem === this.dragLayer || + new Set(this.layers.values()).has(elem as SVGGElement) + ); + } + + /** + * We need to be able to access this layer explicitly for backwards + * compatibility. + * + * @internal + */ + getBlockLayer(): SVGGElement { + return this.layers.get(layerNums.BLOCK)!; + } + + /** + * We need to be able to access this layer explicitly for backwards + * compatibility. + * + * @internal + */ + getBubbleLayer(): SVGGElement { + return this.layers.get(layerNums.BUBBLE)!; + } +} diff --git a/core/layers.ts b/core/layers.ts new file mode 100644 index 00000000000..c62c40f3e53 --- /dev/null +++ b/core/layers.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The layer to place blocks on. + * + */ +export const BLOCK = 50; + +/** + * The layer to place bubbles on. + * + */ +export const BUBBLE = 100; diff --git a/core/main.ts b/core/main.ts new file mode 100644 index 00000000000..c301e989034 --- /dev/null +++ b/core/main.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file The entrypoint for blockly_compressed.js. Provides various + * backwards-compatibility hacks. Not used when loading in + * uncompressed mode. + */ + +// Former goog.module ID: Blockly.main + +import * as Msg from './msg.js'; + +// If Blockly is compiled with ADVANCED_COMPILATION and/or loaded as a +// CJS or ES module there will not be a Blockly global variable +// created. This can cause problems because a very common way of +// loading translations is to use a - - - - - - - - - - - - - - - - -

    - Blockly > - Demos > Accessible Blockly -

    - -

    - This is a demo of a version of Blockly designed for screen readers, - optimized for NVDA on Firefox. It allows users to create programs in a - workspace by manipulating groups of blocks. -

      -
    • To explore a group of blocks, use the arrow keys.
    • -
    • To navigate between groups, use Tab or Shift-Tab.
    • -
    • To add new blocks, use the buttons in the menu on the right.
    • -
    • To delete or add links to existing blocks, press Enter while you're on that block.
    • -
    -

    - - - - - - - - - - - diff --git a/demos/blockfactory/analytics.js b/demos/blockfactory/analytics.js new file mode 100644 index 00000000000..5611c09a958 --- /dev/null +++ b/demos/blockfactory/analytics.js @@ -0,0 +1,195 @@ +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Stubbed interface functions for analytics integration. + */ + +var BlocklyDevTools = BlocklyDevTools || Object.create(null); +BlocklyDevTools.Analytics = BlocklyDevTools.Analytics || Object.create(null); + +/** + * Whether these stub methods should log analytics calls to the console. + * @private + * @const + */ +BlocklyDevTools.Analytics.LOG_TO_CONSOLE_ = false; + +/** + * An import/export type id for a library of BlockFactory's original block + * save files (each a serialized workspace of block definition blocks). + * @package + * @const + */ +BlocklyDevTools.Analytics.BLOCK_FACTORY_LIBRARY = "Block Factory library"; +/** + * An import/export type id for a standard Blockly library of block + * definitions. + * @package + * @const + */ +BlocklyDevTools.Analytics.BLOCK_DEFINITIONS = "Block definitions"; +/** + * An import/export type id for a code generation function, or a + * boilerplate stub of the same. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.GENERATOR = "Generator"; +/** + * An import/export type id for a Blockly Toolbox. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.TOOLBOX = "Toolbox"; +/** + * An import/export type id for the serialized contents of a workspace. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.WORKSPACE_CONTENTS = "Workspace contents"; + +/** + * Format id for imported/exported JavaScript resources. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.FORMAT_JS = "JavaScript"; +/** + * Format id for imported/exported JSON resources. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.FORMAT_JSON = "JSON"; +/** + * Format id for imported/exported XML resources. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.FORMAT_XML = "XML"; + +/** + * Platform id for resources exported for use in Android projects. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.PLATFORM_ANDROID = "Android"; +/** + * Platform id for resources exported for use in iOS projects. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.PLATFORM_IOS = "iOS"; +/** + * Platform id for resources exported for use in web projects. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.PLATFORM_WEB = "web"; + +/** + * Initializes the analytics framework, including noting that the page/app was + * opened. + * @package + */ +BlocklyDevTools.Analytics.init = function() { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.init'); +}; + +/** + * Event noting the user navigated to a specific view. + * + * @package + * @param viewId {string} An identifier for the view state. + */ +BlocklyDevTools.Analytics.onNavigateTo = function(viewId) { + // stub + this.LOG_TO_CONSOLE_ && + console.log('Analytics.onNavigateTo(' + viewId + ')'); +}; + +/** + * Event noting a project resource was saved. In the web Block Factory, this + * means saved to localStorage. + * + * @package + * @param typeId {string} An identifying string for the saved type. + */ +BlocklyDevTools.Analytics.onSave = function(typeId) { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.onSave(' + typeId + ')'); +}; + +/** + * Event noting the user attempted to import a resource file. + * + * @package + * @param typeId {string} An identifying string for the imported type. + * @param optMetadata {Object} Metadata about the import, such as format and + * platform. + */ +BlocklyDevTools.Analytics.onImport = function(typeId, optMetadata) { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.onImport(' + typeId + + (optMetadata ? '): ' + JSON.stringify(optMetadata) : ')')); +}; + +/** + * Event noting a project resource was saved. In the web Block Factory, this + * means downloaded to the user's system. + * + * @package + * @param typeId {string} An identifying string for the exported object type. + * @param optMetadata {Object} Metadata about the import, such as format and + * platform. + */ +BlocklyDevTools.Analytics.onExport = function(typeId, optMetadata) { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.onExport(' + typeId + + (optMetadata ? '): ' + JSON.stringify(optMetadata) : ')')); +}; + +/** + * Event noting the system encountered an error. It should attempt to send + * immediately. + * + * @package + * @param e {!Object} A value representing or describing the error. + */ +BlocklyDevTools.Analytics.onError = function(e) { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.onError("' + e + '")'); +}; + +/** + * Event noting the user was notified with a warning. + * + * @package + * @param msg {string} The warning message, or a description thereof. + */ +BlocklyDevTools.Analytics.onWarning = function(msg) { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.onWarning("' + msg + '")'); +}; + +/** + * Request the analytics framework to send any queued events to the server. + * @package + */ +BlocklyDevTools.Analytics.sendQueued = function() { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.sendQueued'); +}; diff --git a/demos/blockfactory/app_controller.js b/demos/blockfactory/app_controller.js index ac69780b264..3626eccf641 100644 --- a/demos/blockfactory/app_controller.js +++ b/demos/blockfactory/app_controller.js @@ -1,40 +1,14 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview The AppController Class brings together the Block * Factory, Block Library, and Block Exporter functionality into a single web * app. - * - * @author quachtina96 (Tina Quach) */ -goog.provide('AppController'); - -goog.require('BlockFactory'); -goog.require('FactoryUtils'); -goog.require('BlockLibraryController'); -goog.require('BlockExporterController'); -goog.require('goog.dom.classlist'); -goog.require('goog.ui.PopupColorPicker'); -goog.require('goog.ui.ColorPicker'); - /** * Controller for the Blockly Factory @@ -85,6 +59,10 @@ AppController.prototype.importBlockLibraryFromFile = function() { var files = document.getElementById('files'); // If the file list is empty, the user likely canceled in the dialog. if (files.files.length > 0) { + BlocklyDevTools.Analytics.onImport( + BlocklyDevTools.Analytics.BLOCK_FACTORY_LIBRARY, + { format: BlocklyDevTools.Analytics.FORMAT_XML }); + // The input tag doesn't have the "multiple" attribute // so the user can only choose 1 file. var file = files.files[0]; @@ -137,10 +115,25 @@ AppController.prototype.exportBlockLibraryToFile = function() { // Download file if all necessary parameters are provided. if (filename) { FactoryUtils.createAndDownloadFile(blockLibText, filename, 'xml'); + BlocklyDevTools.Analytics.onExport( + BlocklyDevTools.Analytics.BLOCK_FACTORY_LIBRARY, + { format: BlocklyDevTools.Analytics.FORMAT_XML }); } else { - alert('Could not export Block Library without file name under which to ' + - 'save library.'); + var msg = 'Could not export Block Library without file name under which ' + + 'to save library.'; + BlocklyDevTools.Analytics.onWarning(msg); + alert(msg); + } +}; + +AppController.prototype.exportBlockLibraryAsJson = function() { + const blockJson = this.blockLibraryController.getBlockLibraryAsJson(); + if (blockJson.length === 0) { + alert('No blocks in library to export'); + return; } + const filename = 'legacy_block_factory_export.txt'; + FactoryUtils.createAndDownloadFile(JSON.stringify(blockJson), filename, 'plain'); }; /** @@ -151,13 +144,11 @@ AppController.prototype.exportBlockLibraryToFile = function() { */ AppController.prototype.formatBlockLibraryForExport_ = function(blockXmlMap) { // Create DOM for XML. - var xmlDom = goog.dom.createDom('xml', { - 'xmlns':"http://www.w3.org/1999/xhtml" - }); + var xmlDom = Blockly.utils.xml.createElement('xml'); // Append each block node to XML DOM. for (var blockType in blockXmlMap) { - var blockXmlDom = Blockly.Xml.textToDom(blockXmlMap[blockType]); + var blockXmlDom = Blockly.utils.xml.textToDom(blockXmlMap[blockType]); var blockNode = blockXmlDom.firstElementChild; xmlDom.appendChild(blockNode); } @@ -174,34 +165,29 @@ AppController.prototype.formatBlockLibraryForExport_ = function(blockXmlMap) { * @private */ AppController.prototype.formatBlockLibraryForImport_ = function(xmlText) { - var xmlDom = Blockly.Xml.textToDom(xmlText); - - // Get array of XMLs. Use an asterisk (*) instead of a tag name for the XPath - // selector, to match all elements at that level and get all factory_base - // blocks. - var blockNodes = goog.dom.xml.selectNodes(xmlDom, '*'); + var inputXml = Blockly.utils.xml.textToDom(xmlText); + // Convert the live HTMLCollection of child Elements into a static array, + // since the addition to editorWorkspaceXml below removes it from inputXml. + var inputChildren = Array.from(inputXml.children); - // Create empty map. The line below creates a truly empy object. It doesn't + // Create empty map. The line below creates a truly empty object. It doesn't // have built-in attributes/functions such as length or toString. var blockXmlTextMap = Object.create(null); // Populate map. - for (var i = 0, blockNode; blockNode = blockNodes[i]; i++) { - + for (var i = 0, blockNode; blockNode = inputChildren[i]; i++) { // Add outer XML tag to the block for proper injection in to the // main workspace. // Create DOM for XML. - var xmlDom = goog.dom.createDom('xml', { - 'xmlns':"http://www.w3.org/1999/xhtml" - }); - xmlDom.appendChild(blockNode); + var editorWorkspaceXml = Blockly.utils.xml.createElement('xml'); + editorWorkspaceXml.appendChild(blockNode); - xmlText = Blockly.Xml.domToText(xmlDom); + xmlText = Blockly.Xml.domToText(editorWorkspaceXml); // All block types should be lowercase. var blockType = this.getBlockTypeFromXml_(xmlText).toLowerCase(); // Some names are invalid so fix them up. blockType = FactoryUtils.cleanBlockType(blockType); - + blockXmlTextMap[blockType] = xmlText; } @@ -216,14 +202,14 @@ AppController.prototype.formatBlockLibraryForImport_ = function(xmlText) { * @private */ AppController.prototype.getBlockTypeFromXml_ = function(xmlText) { - var xmlDom = Blockly.Xml.textToDom(xmlText); + var xmlDom = Blockly.utils.xml.textToDom(xmlText); // Find factory base block. var factoryBaseBlockXml = xmlDom.getElementsByTagName('block')[0]; // Get field elements from factory base. var fields = factoryBaseBlockXml.getElementsByTagName('field'); for (var i = 0; i < fields.length; i++) { // The field whose name is 'NAME' holds the block type as its value. - if (fields[i].getAttribute('name') == 'NAME') { + if (fields[i].getAttribute('name') === 'NAME') { return fields[i].childNodes[0].nodeValue; } } @@ -280,30 +266,36 @@ AppController.prototype.onTab = function() { var workspaceFactoryTab = this.tabMap[AppController.WORKSPACE_FACTORY]; // Warn user if they have unsaved changes when leaving Block Factory. - if (this.lastSelectedTab == AppController.BLOCK_FACTORY && - this.selectedTab != AppController.BLOCK_FACTORY) { + if (this.lastSelectedTab === AppController.BLOCK_FACTORY && + this.selectedTab !== AppController.BLOCK_FACTORY) { var hasUnsavedChanges = !FactoryUtils.savedBlockChanges(this.blockLibraryController); - if (hasUnsavedChanges && - !confirm('You have unsaved changes in Block Factory.')) { - // If the user doesn't want to switch tabs with unsaved changes, - // stay on Block Factory Tab. - this.setSelected_(AppController.BLOCK_FACTORY); - this.lastSelectedTab = AppController.BLOCK_FACTORY; - return; + if (hasUnsavedChanges) { + var msg = 'You have unsaved changes in Block Factory.'; + var continueAnyway = confirm(msg); + BlocklyDevTools.Analytics.onWarning(msg); + if (!continueAnyway) { + // If the user doesn't want to switch tabs with unsaved changes, + // stay on Block Factory Tab. + this.setSelected_(AppController.BLOCK_FACTORY); + this.lastSelectedTab = AppController.BLOCK_FACTORY; + return; + } } } // Only enable key events in workspace factory if workspace factory tab is // selected. this.workspaceFactoryController.keyEventsEnabled = - this.selectedTab == AppController.WORKSPACE_FACTORY; + this.selectedTab === AppController.WORKSPACE_FACTORY; // Turn selected tab on and other tabs off. this.styleTabs_(); - if (this.selectedTab == AppController.EXPORTER) { + if (this.selectedTab === AppController.EXPORTER) { + BlocklyDevTools.Analytics.onNavigateTo('Exporter'); + // Hide other tabs. FactoryUtils.hide('workspaceFactoryContent'); FactoryUtils.hide('blockFactoryContent'); @@ -324,14 +316,19 @@ AppController.prototype.onTab = function() { // Update the exporter's preview to reflect any changes made to the blocks. this.exporter.updatePreview(); - } else if (this.selectedTab == AppController.BLOCK_FACTORY) { + } else if (this.selectedTab === AppController.BLOCK_FACTORY) { + BlocklyDevTools.Analytics.onNavigateTo('BlockFactory'); + // Hide other tabs. FactoryUtils.hide('blockLibraryExporter'); FactoryUtils.hide('workspaceFactoryContent'); // Show Block Factory. FactoryUtils.show('blockFactoryContent'); - } else if (this.selectedTab == AppController.WORKSPACE_FACTORY) { + } else if (this.selectedTab === AppController.WORKSPACE_FACTORY) { + // TODO: differentiate Workspace and Toolbox editor, based on the other tab state. + BlocklyDevTools.Analytics.onNavigateTo('WorkspaceFactory'); + // Hide other tabs. FactoryUtils.hide('blockLibraryExporter'); FactoryUtils.hide('blockFactoryContent'); @@ -354,10 +351,10 @@ AppController.prototype.onTab = function() { */ AppController.prototype.styleTabs_ = function() { for (var tabName in this.tabMap) { - if (this.selectedTab == tabName) { - goog.dom.classlist.addRemove(this.tabMap[tabName], 'taboff', 'tabon'); + if (this.selectedTab === tabName) { + this.tabMap[tabName].classList.replace('taboff', 'tabon'); } else { - goog.dom.classlist.addRemove(this.tabMap[tabName], 'tabon', 'taboff'); + this.tabMap[tabName].classList.replace('tabon', 'taboff'); } } }; @@ -443,7 +440,7 @@ AppController.prototype.assignExporterChangeListeners = function() { /** * If given checkbox is checked, enable the given elements. Otherwise, disable. * @param {boolean} enabled True if enabled, false otherwise. - * @param {!Array.} idArray Array of element IDs to enable when + * @param {!Array} idArray Array of element IDs to enable when * checkbox is checked. */ AppController.prototype.ifCheckedEnable = function(enabled, idArray) { @@ -504,9 +501,13 @@ AppController.prototype.assignBlockFactoryClickHandlers = function() { self.exportBlockLibraryToFile(); }); + document.getElementById('exportAsJson').addEventListener('click', function() { + self.exportBlockLibraryAsJson(); + }); + document.getElementById('helpButton').addEventListener('click', function() { - open('https://developers.google.com/blockly/custom-blocks/block-factory', + open('https://developers.google.com/blockly/guides/create-custom-blocks/legacy-blockly-developer-tools', 'BlockFactoryHelp'); }); @@ -563,9 +564,9 @@ AppController.prototype.addBlockFactoryEventListeners = function() { document.getElementById('direction') .addEventListener('change', BlockFactory.updatePreview); document.getElementById('languageTA') - .addEventListener('change', BlockFactory.updatePreview); + .addEventListener('change', BlockFactory.manualEdit); document.getElementById('languageTA') - .addEventListener('keyup', BlockFactory.updatePreview); + .addEventListener('keyup', BlockFactory.manualEdit); document.getElementById('format') .addEventListener('change', BlockFactory.formatChange); document.getElementById('language') @@ -579,7 +580,7 @@ AppController.prototype.initializeBlocklyStorage = function() { BlocklyStorage.HTTPREQUEST_ERROR = 'There was a problem with the request.\n'; BlocklyStorage.LINK_ALERT = - 'Share your blocks with this link:\n\n%1'; + 'Share your blocks with this public link. We\'ll delete them if not used for a year. They are not associated with your account and handled as per Google\'s Privacy Policy. Please be sure not to include any private information.:\n\n%1'; BlocklyStorage.HASH_ERROR = 'Sorry, "%1" doesn\'t correspond with any saved Blockly file.'; BlocklyStorage.XML_ERROR = 'Could not load your saved file.\n' + @@ -596,7 +597,7 @@ AppController.prototype.initializeBlocklyStorage = function() { * Handle resizing of elements. */ AppController.prototype.onresize = function(event) { - if (this.selectedTab == AppController.BLOCK_FACTORY) { + if (this.selectedTab === AppController.BLOCK_FACTORY) { // Handle resizing of Block Factory elements. var expandList = [ document.getElementById('blocklyPreviewContainer'), @@ -611,7 +612,7 @@ AppController.prototype.onresize = function(event) { expand.style.width = (expand.parentNode.offsetWidth - 2) + 'px'; expand.style.height = (expand.parentNode.offsetHeight - 2) + 'px'; } - } else if (this.selectedTab == AppController.EXPORTER) { + } else if (this.selectedTab === AppController.EXPORTER) { // Handle resize of Exporter block options. this.exporter.view.centerPreviewBlocks(); } @@ -624,12 +625,14 @@ AppController.prototype.onresize = function(event) { * @param {!Event} e beforeunload event. */ AppController.prototype.confirmLeavePage = function(e) { + BlocklyDevTools.Analytics.sendQueued(); if ((!BlockFactory.isStarterBlock() && !FactoryUtils.savedBlockChanges(blocklyFactory.blockLibraryController)) || blocklyFactory.workspaceFactoryController.hasUnsavedChanges()) { var confirmationMessage = 'You will lose any unsaved changes. ' + 'Are you sure you want to exit this page?'; + BlocklyDevTools.Analytics.onWarning(confirmationMessage); e.returnValue = confirmationMessage; return confirmationMessage; } @@ -640,7 +643,7 @@ AppController.prototype.confirmLeavePage = function(e) { * @param {string} id ID of element to show. */ AppController.prototype.openModal = function(id) { - Blockly.hideChaff(); + Blockly.common.getMainWorkspace().hideChaff(); this.modalName_ = id; document.getElementById(id).style.display = 'block'; document.getElementById('modalShadow').style.display = 'block'; @@ -670,20 +673,6 @@ AppController.prototype.modalName_ = null; * Initialize Blockly and layout. Called on page load. */ AppController.prototype.init = function() { - // Block Factory has a dependency on bits of Closure that core Blockly - // doesn't have. When you run this from file:// without a copy of Closure, - // it breaks it non-obvious ways. Warning about this for now until the - // dependency is broken. - // TODO: #668. - if (!window.goog.dom.xml) { - alert('Sorry: Closure dependency not found. We are working on removing ' + - 'this dependency. In the meantime, you can use our hosted demo\n ' + - 'https://blockly-demo.appspot.com/static/demos/blockfactory/index.html' + - '\nor use these instructions to continue running locally:\n' + - 'https://developers.google.com/blockly/guides/modify/web/closure'); - return; - } - var self = this; // Handle Blockly Storage with App Engine. if ('BlocklyStorage' in window) { @@ -710,6 +699,8 @@ AppController.prototype.init = function() { BlockFactory.mainWorkspace = Blockly.inject('blockly', {collapse: false, toolbox: toolbox, + comments: false, + disable: false, media: '../../media/'}); // Add tab handlers for switching between Block Factory and Block Exporter. diff --git a/demos/blockfactory/block_definition_extractor.js b/demos/blockfactory/block_definition_extractor.js new file mode 100644 index 00000000000..fa1aae7750e --- /dev/null +++ b/demos/blockfactory/block_definition_extractor.js @@ -0,0 +1,742 @@ +/** + * Copyright 2017 Juan Carlos Orozco Arena + * Apache License Version 2.0 + */ + +/** + * @fileoverview + * The BlockDefinitionExtractor is a class that generates a workspace DOM + * suitable for the BlockFactory's block editor, derived from an example + * Blockly.Block. + * + * + * var workspaceDom = new BlockDefinitionExtractor() + * .buildBlockFactoryWorkspace(exampleBlocklyBlock); + * Blockly.Xml.domToWorkspace(workspaceDom, BlockFactory.mainWorkspace); + * + * + * The exampleBlocklyBlock is usually the block loaded into the + * preview workspace after manually entering the block definition. + */ +'use strict'; + +/** + * Namespace to contain all functions needed to extract block definition from + * the block preview data structure. + * @namespace + */ +var BlockDefinitionExtractor = BlockDefinitionExtractor || Object.create(null); + +/** + * Builds a BlockFactory workspace that reflects the block structure of the + * example block. + * + * @param {!Blockly.Block} block The reference block from which the definition + * will be extracted. + * @return {!Element} Returns the root workspace DOM for the block editor + * workspace. + */ +BlockDefinitionExtractor.buildBlockFactoryWorkspace = function(block) { + var workspaceXml = Blockly.utils.xml.createElement('xml'); + workspaceXml.append(BlockDefinitionExtractor.factoryBase_(block, block.type)); + return workspaceXml; +}; + +/** + * Helper function to create a new Element with the provided attributes and + * inner text. + * + * @param {string} name New element tag name. + * @param {!Object=} opt_attrs Optional list of attributes. + * @param {string=} opt_text Optional inner text. + * @return {!Element} The newly created element. + * @private + */ +BlockDefinitionExtractor.newDomElement_ = function(name, opt_attrs, opt_text) { + // Avoid createDom(..)'s attributes argument for being too HTML specific. + var elem = Blockly.utils.xml.createElement(name); + if (opt_attrs) { + for (var key in opt_attrs) { + elem.setAttribute(key, opt_attrs[key]); + } + } + if (opt_text) { + elem.append(opt_text); + } + return elem; +}; + +/** + * Creates an connection type constraint Element representing the + * requested type. + * + * @param {string} type Type name of desired connection constraint. + * @return {!Element} The representing the constraint type. + * @private + */ +BlockDefinitionExtractor.buildBlockForType_ = function(type) { + switch (type) { + case 'Null': + return BlockDefinitionExtractor.typeNull_(); + case 'Boolean': + return BlockDefinitionExtractor.typeBoolean_(); + case 'Number': + return BlockDefinitionExtractor.typeNumber_(); + case 'String': + return BlockDefinitionExtractor.typeString_(); + case 'Array': + return BlockDefinitionExtractor.typeList_(); + default: + return BlockDefinitionExtractor.typeOther_(type); + } +}; + +/** + * Constructs a element representing the type constraints of the + * provided connection. + * + * @param {!Blockly.Connection} connection The connection with desired + * connection constraints. + * @return {!Element} The root element of the constraint definition. + * @private + */ +BlockDefinitionExtractor.buildTypeConstraintBlockForConnection_ = + function(connection) +{ + var typeBlock; + if (connection.check_) { + if (connection.check_.length < 1) { + typeBlock = BlockDefinitionExtractor.typeNullShadow_(); + } else if (connection.check_.length === 1) { + typeBlock = BlockDefinitionExtractor.buildBlockForType_( + connection.check_[0]); + } else if (connection.check_.length > 1) { + typeBlock = BlockDefinitionExtractor.typeGroup_(connection.check_); + } + } else { + typeBlock = BlockDefinitionExtractor.typeNullShadow_(); + } + return typeBlock; +}; + +/** + * Creates the root "factory_base" element for the block definition. + * + * @param {!Blockly.Block} block The example block from which to extract the + * definition. + * @param {string} name Block name. + * @return {!Element} The factory_base block element. + * @private + */ +BlockDefinitionExtractor.factoryBase_ = function(block, name) { + BlockDefinitionExtractor.src = {root: block, current: block}; + var factoryBaseEl = + BlockDefinitionExtractor.newDomElement_('block', {type: 'factory_base'}); + factoryBaseEl.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'NAME'}, name)); + factoryBaseEl.append(BlockDefinitionExtractor.buildInlineField_(block)); + + BlockDefinitionExtractor.buildConnections_(block, factoryBaseEl); + + var inputsStatement = BlockDefinitionExtractor.newDomElement_( + 'statement', {name: 'INPUTS'}); + inputsStatement.append(BlockDefinitionExtractor.parseInputs_(block)); + factoryBaseEl.append(inputsStatement); + + var tooltipValue = + BlockDefinitionExtractor.newDomElement_('value', {name: 'TOOLTIP'}); + tooltipValue.append(BlockDefinitionExtractor.text_(block.tooltip)); + factoryBaseEl.append(tooltipValue); + + var helpUrlValue = + BlockDefinitionExtractor.newDomElement_('value', {name: 'HELPURL'}); + helpUrlValue.append(BlockDefinitionExtractor.text_(block.helpUrl)); + factoryBaseEl.append(helpUrlValue); + + // Convert colour_ to hue value 0-360 degrees + var colour_hue = block.getHue(); // May be null if not set via hue. + if (colour_hue) { + var colourBlock = BlockDefinitionExtractor.colourBlockFromHue_(colour_hue); + var colourInputValue = + BlockDefinitionExtractor.newDomElement_('value', {name: 'COLOUR'}); + colourInputValue.append(colourBlock); + factoryBaseEl.append(colourInputValue); + } else { + // Editor will not have a colour block and preview will render black. + // TODO: Support RGB colours in the block editor. + } + return factoryBaseEl; +}; + +/** + * Generates the appropriate element for the block definition's + * CONNECTIONS field, which determines the next, previous, and output + * connections. + * + * @param {!Blockly.Block} block The example block from which to extract the + * definition. + * @param {!Element} factoryBaseEl The root of the block definition. + * @private + */ +BlockDefinitionExtractor.buildConnections_ = function(block, factoryBaseEl) { + var connections = 'NONE'; + if (block.outputConnection) { + connections = 'LEFT'; + } else { + if (block.previousConnection) { + if (block.nextConnection) { + connections = 'BOTH'; + } else { + connections = 'TOP'; + } + } else if (block.nextConnection) { + connections = 'BOTTOM'; + } + } + factoryBaseEl.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'CONNECTIONS'}, connections)); + + if (connections === 'LEFT') { + var inputValue = + BlockDefinitionExtractor.newDomElement_('value', {name: 'OUTPUTTYPE'}); + inputValue.append( + BlockDefinitionExtractor.buildTypeConstraintBlockForConnection_( + block.outputConnection)); + factoryBaseEl.append(inputValue); + } else { + if (connections === 'UP' || connections === 'BOTH') { + var inputValue = + BlockDefinitionExtractor.newDomElement_('value', {name: 'TOPTYPE'}); + inputValue.append( + BlockDefinitionExtractor.buildTypeConstraintBlockForConnection_( + block.previousConnection)); + factoryBaseEl.append(inputValue); + } + if (connections === 'DOWN' || connections === 'BOTH') { + var inputValue = BlockDefinitionExtractor.newDomElement_( + 'value', {name: 'BOTTOMTYPE'}); + inputValue.append( + BlockDefinitionExtractor.buildTypeConstraintBlockForConnection_( + block.nextConnection)); + factoryBaseEl.append(inputValue); + } + } +}; + +/** + * Generates the appropriate element for the block definition's INLINE + * field. + * + * @param {!Blockly.Block} block The example block from which to extract the + * definition. + * @return {Element} The INLINE with value 'AUTO', 'INT' (internal) or + * 'EXT' (external). + * @private + */ +BlockDefinitionExtractor.buildInlineField_ = function(block) { + var inline = 'AUTO'; // When block.inputsInlineDefault === undefined + if (block.inputsInlineDefault === true) { + inline = 'INT'; + } else if (block.inputsInlineDefault === false) { + inline = 'EXT'; + } + return BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'INLINE'}, inline); +}; + +/** + * Constructs a sequence of elements that represent the inputs of the + * provided block. + * + * @param {!Blockly.Block} block The source block to copy the inputs of. + * @return {Element} The fist element of the sequence + * (and the root of the constructed DOM). + * @private + */ +BlockDefinitionExtractor.parseInputs_ = function(block) { + var firstInputDefElement = null; + var lastInputDefElement = null; + for (var i = 0; i < block.inputList.length; i++) { + var input = block.inputList[i]; + var align = 'LEFT'; // Left alignment is the default. + if (input.align === Blockly.ALIGN_CENTRE) { + align = 'CENTRE'; + } else if (input.align === Blockly.ALIGN_RIGHT) { + align = 'RIGHT'; + } + + var inputDefElement = BlockDefinitionExtractor.input_(input, align); + if (lastInputDefElement) { + var next = BlockDefinitionExtractor.newDomElement_('next'); + next.append(inputDefElement); + lastInputDefElement.append(next); + } else { + firstInputDefElement = inputDefElement; + } + lastInputDefElement = inputDefElement; + } + return firstInputDefElement; +}; + +/** + * Creates a element representing a block input. + * + * @param {!Blockly.Input} input The input object. + * @param {string} align Can be left, right or centre. + * @return {!Element} The element that defines the input. + * @private + */ +BlockDefinitionExtractor.input_ = function(input, align) { + var hasConnector = (input.type === Blockly.inputs.inputTypes.VALUE || input.type === Blockly.inputs.inputTypes.STATEMENT); + var inputTypeAttr = + input.type === Blockly.inputs.inputTypes.DUMMY ? 'input_dummy' : + input.type === Blockly.inputs.inputTypes.END_ROW ? 'input_end_row' : + input.type === Blockly.inputs.inputTypes.VALUE ? 'input_value' : + 'input_statement'; + var inputDefBlock = + BlockDefinitionExtractor.newDomElement_('block', {type: inputTypeAttr}); + + if (hasConnector) { + inputDefBlock.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'INPUTNAME'}, input.name)); + } + inputDefBlock.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'ALIGN'}, align)); + + var fieldsDef = BlockDefinitionExtractor.newDomElement_( + 'statement', {name: 'FIELDS'}); + var fieldsXml = BlockDefinitionExtractor.buildFields_(input.fieldRow); + fieldsDef.append(fieldsXml); + inputDefBlock.append(fieldsDef); + + if (hasConnector) { + var typeValue = BlockDefinitionExtractor.newDomElement_( + 'value', {name: 'TYPE'}); + typeValue.append( + BlockDefinitionExtractor.buildTypeConstraintBlockForConnection_( + input.connection)); + inputDefBlock.append(typeValue); + } + + return inputDefBlock; +}; + +/** + * Constructs a sequence elements representing the field definition. + * @param {Array} fieldRow A list of fields in a Blockly.Input. + * @return {Element} The fist element of the sequence + * (and the root of the constructed DOM). + * @private + */ +BlockDefinitionExtractor.buildFields_ = function(fieldRow) { + var firstFieldDefElement = null; + var lastFieldDefElement = null; + + for (var i = 0; i < fieldRow.length; i++) { + var field = fieldRow[i]; + var fieldDefElement = BlockDefinitionExtractor.buildFieldElement_(field); + + if (lastFieldDefElement) { + var next = BlockDefinitionExtractor.newDomElement_('next'); + next.append(fieldDefElement); + lastFieldDefElement.append(next); + } else { + firstFieldDefElement = fieldDefElement; + } + lastFieldDefElement = fieldDefElement; + } + + return firstFieldDefElement; +}; + +/** + * Constructs a element that describes the provided Blockly.Field. + * @param {!Blockly.Field} field The field from which the definition is copied. + * @param {!Element} A for the Field definition. + * @private + */ +BlockDefinitionExtractor.buildFieldElement_ = function(field) { + if (field instanceof Blockly.FieldLabel) { + return BlockDefinitionExtractor.buildFieldLabel_(field.text_); + } else if (field instanceof Blockly.FieldTextInput) { + return BlockDefinitionExtractor.buildFieldInput_(field.name, field.text_); + } else if (field instanceof Blockly.FieldNumber) { + return BlockDefinitionExtractor.buildFieldNumber_( + field.name, field.text_, field.min_, field.max_, field.presicion_); + } else if (field instanceof Blockly.FieldAngle) { + return BlockDefinitionExtractor.buildFieldAngle_(field.name, field.text_); + } else if (field instanceof Blockly.FieldCheckbox) { + return BlockDefinitionExtractor.buildFieldCheckbox_(field.name, field.state_); + } else if (field instanceof Blockly.FieldColour) { + return BlockDefinitionExtractor.buildFieldColour_(field.name, field.colour_); + } else if (field instanceof Blockly.FieldImage) { + return BlockDefinitionExtractor.buildFieldImage_( + field.src_, field.width_, field.height_, field.text_); + } else if (field instanceof Blockly.FieldVariable) { + // FieldVariable must be before FieldDropdown, because FieldVariable is a + // subclass. + return BlockDefinitionExtractor.buildFieldVariable_(field.name, field.text_); + } else if (field instanceof Blockly.FieldDropdown) { + return BlockDefinitionExtractor.buildFieldDropdown_(field); + } + throw Error('Unrecognized field class: ' + field.constructor.name); +}; + + +/** + * Creates a element representing a FieldLabel definition. + * @param {string} text + * @return {Element} The XML for FieldLabel definition. + * @private + */ +BlockDefinitionExtractor.buildFieldLabel_ = function(text) { + var fieldBlock = + BlockDefinitionExtractor.newDomElement_('block', {type: 'field_static'}); + fieldBlock.append( + BlockDefinitionExtractor.newDomElement_('field', {name: 'TEXT'}, text)); + return fieldBlock; +}; + +/** + * Creates a element representing a FieldInput (text input) definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {string} text The default text string. + * @return {Element} The XML for FieldInput definition. + * @private + */ +BlockDefinitionExtractor.buildFieldInput_ = function(fieldName, text) { + var fieldInput = + BlockDefinitionExtractor.newDomElement_('block', {type: 'field_input'}); + fieldInput.append( + BlockDefinitionExtractor.newDomElement_('field', {name: 'TEXT'}, text)); + fieldInput.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + return fieldInput; +}; + +/** + * Creates a element representing a FieldNumber definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {number} value The field's default value. + * @param {number} min The minimum allowed value, or negative infinity. + * @param {number} max The maximum allowed value, or positive infinity. + * @param {number} precision The precision allowed for the number. + * @return {Element} The XML for FieldNumber definition. + * @private + */ +BlockDefinitionExtractor.buildFieldNumber_ = + function(fieldName, value, min, max, precision) +{ + var fieldNumber = + BlockDefinitionExtractor.newDomElement_('block', {type: 'field_number'}); + fieldNumber.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'VALUE'}, value)); + fieldNumber.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + fieldNumber.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'MIN'}, min)); + fieldNumber.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'MAX'}, max)); + fieldNumber.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'PRECISION'}, precision)); + return fieldNumber; +}; + +/** + * Creates a element representing a FieldAngle definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {number} angle The field's default value. + * @return {Element} The XML for FieldAngle definition. + * @private + */ +BlockDefinitionExtractor.buildFieldAngle_ = function(angle, fieldName) { + var fieldAngle = + BlockDefinitionExtractor.newDomElement_('block', {type: 'field_angle'}); + fieldAngle.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'ANGLE'}, angle)); + fieldAngle.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + return fieldAngle; +}; + +/** + * Creates a element representing a FieldDropdown definition. + * + * @param {Blockly.FieldDropdown} dropdown + * @return {Element} The element representing a similar FieldDropdown. + * @private + */ +BlockDefinitionExtractor.buildFieldDropdown_ = function(dropdown) { + var menuGenerator = dropdown.menuGenerator_; + if (typeof menuGenerator === 'function') { + var options = menuGenerator(); + } else if (Array.isArray(menuGenerator)) { + var options = menuGenerator; + } else { + throw Error('Unrecognized type of menuGenerator: ' + menuGenerator); + } + + var fieldDropdown = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'field_dropdown'}); + var optionsStr = '['; + + var mutation = BlockDefinitionExtractor.newDomElement_('mutation'); + fieldDropdown.append(mutation); + fieldDropdown.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, dropdown.name)); + for (var i=0; i element representing a FieldCheckbox definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {string} checked The field's default value, true or false. + * @return {Element} The XML for FieldCheckbox definition. + * @private + */ +BlockDefinitionExtractor.buildFieldCheckbox_ = + function(fieldName, checked) +{ + var fieldCheckbox = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'field_checkbox'}); + fieldCheckbox.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'CHECKED'}, checked)); + fieldCheckbox.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + return fieldCheckbox; +}; + +/** + * Creates a element representing a FieldColour definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {string} colour The field's default value as a string. + * @return {Element} The XML for FieldColour definition. + * @private + */ +BlockDefinitionExtractor.buildFieldColour_ = + function(fieldName, colour) +{ + var fieldColour = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'field_colour'}); + fieldColour.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'COLOUR'}, colour)); + fieldColour.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + return fieldColour; +}; + +/** + * Creates a element representing a FieldVariable definition. + * + * @param {string} fieldName The identifying name of the field. + * @param {string} varName The variables + * @return {Element} The element representing the FieldVariable. + * @private + */ +BlockDefinitionExtractor.buildFieldVariable_ = function(fieldName, varName) { + var fieldVar = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'field_variable'}); + fieldVar.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'FIELDNAME'}, fieldName)); + fieldVar.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'TEXT'}, varName)); + return fieldVar; +}; + +/** + * Creates a element representing a FieldImage definition. + * + * @param {string} src The URL of the field image. + * @param {number} width The pixel width of the source image + * @param {number} height The pixel height of the source image. + * @param {string} alt Alternate text to describe image. + * @private + */ +BlockDefinitionExtractor.buildFieldImage_ = + function(src, width, height, alt) +{ + var block1 = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'field_image'}); + block1.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'SRC'}, src)); + block1.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'WIDTH'}, width)); + block1.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'HEIGHT'}, height)); + block1.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'ALT'}, alt)); +}; + +/** + * Creates a element a group of allowed connection constraint types. + * + * @param {Array} types List of type names in this group. + * @return {Element} The element representing the group, with child + * types attached. + * @private + */ +BlockDefinitionExtractor.typeGroup_ = function(types) { + var typeGroupBlock = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'type_group'}); + typeGroupBlock.append(BlockDefinitionExtractor.newDomElement_( + 'mutation', {types:types.length})); + for (var i=0; i block element representing the default null connection + * constraint. + * @return {Element} The element representing the "null" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeNullShadow_ = function() { + return BlockDefinitionExtractor.newDomElement_( + 'shadow', {type: 'type_null'}); +}; + +/** + * Creates a element representing null in a connection constraint. + * @return {Element} The element representing the "null" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeNull_ = function() { + return BlockDefinitionExtractor.newDomElement_('block', {type: 'type_null'}); +}; + +/** + * Creates a element representing the a boolean in a connection + * constraint. + * @return {Element} The element representing the "boolean" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeBoolean_ = function() { + return BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'type_boolean'}); +}; + +/** + * Creates a element representing the a number in a connection + * constraint. + * @return {Element} The element representing the "number" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeNumber_ = function() { + return BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'type_number'}); +}; + +/** + * Creates a element representing the a string in a connection + * constraint. + * @return {Element} The element representing the "string" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeString_ = function() { + return BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'type_string'}); +}; + +/** + * Creates a element representing the a list in a connection + * constraint. + * @return {Element} The element representing the "list" type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeList_ = function() { + return BlockDefinitionExtractor.newDomElement_('block', {type: 'type_list'}); +}; + +/** + * Creates a element representing the given custom connection + * constraint type name. + * + * @param {string} type The connection constraint type name. + * @return {Element} The element representing a custom input type + * constraint. + * @private + */ +BlockDefinitionExtractor.typeOther_ = function(type) { + var block = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'type_other'}); + block.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'TYPE'}, type)); + return block; +}; + +/** + * Creates a block Element for the colour_hue block, with the given hue. + * @param hue {number} The hue value, from 0 to 360. + * @return {Element} The Element representing a colour_hue block + * with the given hue. + * @private + */ +BlockDefinitionExtractor.colourBlockFromHue_ = function(hue) { + var colourBlock = BlockDefinitionExtractor.newDomElement_( + 'block', {type: 'colour_hue'}); + colourBlock.append(BlockDefinitionExtractor.newDomElement_('mutation', { + colour: Blockly.utils.colour.hueToHex(hue) + })); + colourBlock.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'HUE'}, hue.toString())); + return colourBlock; +}; + +/** + * Creates a block Element for a text block with the given text. + * + * @param text {string} The text value of the block. + * @return {Element} The element representing a "text" block. + * @private + */ +BlockDefinitionExtractor.text_ = function(text) { + var textBlock = + BlockDefinitionExtractor.newDomElement_('block', {type: 'text'}); + if (text) { + textBlock.append(BlockDefinitionExtractor.newDomElement_( + 'field', {name: 'TEXT'}, text)); + } // Else, use empty string default. + return textBlock; +}; diff --git a/demos/blockfactory/block_exporter_controller.js b/demos/blockfactory/block_exporter_controller.js index b237f48a70f..bccd8087de0 100644 --- a/demos/blockfactory/block_exporter_controller.js +++ b/demos/blockfactory/block_exporter_controller.js @@ -1,21 +1,7 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** @@ -23,27 +9,16 @@ * users to export block definitions and generator stubs of their saved blocks * easily using a visual interface. Depends on Block Exporter View and Block * Exporter Tools classes. Interacts with Export Settings in the index.html. - * - * @author quachtina96 (Tina Quach) */ 'use strict'; -goog.provide('BlockExporterController'); - -goog.require('FactoryUtils'); -goog.require('StandardCategories'); -goog.require('BlockExporterView'); -goog.require('BlockExporterTools'); -goog.require('goog.dom.xml'); - - /** * BlockExporter Controller Class * @param {!BlockLibrary.Storage} blockLibStorage Block Library Storage. * @constructor */ -BlockExporterController = function(blockLibStorage) { +function BlockExporterController(blockLibStorage) { // BlockLibrary.Storage object containing user's saved blocks. this.blockLibStorage = blockLibStorage; // Utils for generating code to export. @@ -103,7 +78,9 @@ BlockExporterController.prototype.export = function() { // User wants to export selected blocks' definitions. if (!blockDef_filename) { // User needs to enter filename. - alert('Please enter a filename for your block definition(s) download.'); + var msg = 'Please enter a filename for your block definition(s) download.'; + BlocklyDevTools.Analytics.onWarning(msg); + alert(msg); } else { // Get block definition code in the selected format for the blocks. var blockDefs = this.tools.getBlockDefinitions(blockXmlMap, @@ -111,6 +88,13 @@ BlockExporterController.prototype.export = function() { // Download the file, using .js file ending for JSON or Javascript. FactoryUtils.createAndDownloadFile( blockDefs, blockDef_filename, 'javascript'); + BlocklyDevTools.Analytics.onExport( + BlocklyDevTools.Analytics.BLOCK_DEFINITIONS, + { + format: (definitionFormat === 'JSON' ? + BlocklyDevTools.Analytics.FORMAT_JSON : + BlocklyDevTools.Analytics.FORMAT_JS) + }); } } @@ -118,16 +102,20 @@ BlockExporterController.prototype.export = function() { // User wants to export selected blocks' generator stubs. if (!generatorStub_filename) { // User needs to enter filename. - alert('Please enter a filename for your generator stub(s) download.'); + var msg = 'Please enter a filename for your generator stub(s) download.'; + BlocklyDevTools.Analytics.onWarning(msg); + alert(msg); } else { + // Get generator stub code in the selected language for the blocks. var genStubs = this.tools.getGeneratorCode(blockXmlMap, language); - // Get the correct file extension. - var fileType = (language == 'JavaScript') ? 'javascript' : 'plain'; + // Download the file. FactoryUtils.createAndDownloadFile( - genStubs, generatorStub_filename, fileType); + genStubs, generatorStub_filename + '.js', 'javascript'); + BlocklyDevTools.Analytics.onExport( + BlocklyDevTools.Analytics.GENERATOR, { format: BlocklyDevTools.Analytics.FORMAT_JS }); } } @@ -236,9 +224,9 @@ BlockExporterController.prototype.selectUsedBlocks = function() { var unstoredCustomBlockTypes = []; for (var i = 0, blockType; blockType = this.usedBlockTypes[i]; i++) { - if (storedBlockTypes.indexOf(blockType) != -1) { + if (storedBlockTypes.includes(blockType)) { sharedBlockTypes.push(blockType); - } else if (StandardCategories.coreBlockTypes.indexOf(blockType) == -1) { + } else if (!StandardCategories.coreBlockTypes.includes(blockType)) { unstoredCustomBlockTypes.push(blockType); } } @@ -249,8 +237,8 @@ BlockExporterController.prototype.selectUsedBlocks = function() { } this.view.listSelectedBlocks(); - if (unstoredCustomBlockTypes.length > 0){ - // Warn user to import block defifnitions and generator code for blocks + if (unstoredCustomBlockTypes.length > 0) { + // Warn user to import block definitions and generator code for blocks // not in their Block Library nor Blockly's standard library. var blockTypesText = unstoredCustomBlockTypes.join(', '); var customWarning = 'Custom blocks used in workspace factory but not ' + @@ -263,7 +251,7 @@ BlockExporterController.prototype.selectUsedBlocks = function() { /** * Set the array that holds the block types used in workspace factory. - * @param {!Array.} usedBlockTypes Block types used in + * @param {!Array} usedBlockTypes Block types used in */ BlockExporterController.prototype.setUsedBlockTypes = function(usedBlockTypes) { diff --git a/demos/blockfactory/block_exporter_tools.js b/demos/blockfactory/block_exporter_tools.js index 4d6d9bec65a..58da9be1a3b 100644 --- a/demos/blockfactory/block_exporter_tools.js +++ b/demos/blockfactory/block_exporter_tools.js @@ -1,21 +1,7 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** @@ -23,30 +9,18 @@ * block definitions and generator stubs for given block types. Also generates * toolbox XML for the exporter's workspace. Depends on the FactoryUtils for * its code generation functions. - * - * @author quachtina96 (Tina Quach) */ 'use strict'; -goog.provide('BlockExporterTools'); - -goog.require('FactoryUtils'); -goog.require('BlockOption'); -goog.require('goog.dom'); -goog.require('goog.dom.xml'); - - /** -* Block Exporter Tools Class -* @constructor -*/ -BlockExporterTools = function() { + * Block Exporter Tools Class + * @constructor + */ +function BlockExporterTools() { // Create container for hidden workspace. - this.container = goog.dom.createDom('div', { - 'id': 'blockExporterTools_hiddenWorkspace' - }, ''); // Empty quotes for empty div. - // Hide hidden workspace. - this.container.style.display = 'none'; + this.container = document.createElement('div'); + this.container.id = 'blockExporterTools_hiddenWorkspace'; + this.container.style.display = 'none'; // Hide the hidden workspace. document.body.appendChild(this.container); /** * Hidden workspace for the Block Exporter that holds pieces that make @@ -114,7 +88,7 @@ BlockExporterTools.prototype.getBlockDefinitions = } // Surround json with [] and comma separate items. - if (definitionFormat == "JSON") { + if (definitionFormat === "JSON") { return "[" + blockCode.join(",\n") + "]"; } return blockCode.join("\n\n"); @@ -167,45 +141,6 @@ BlockExporterTools.prototype.addBlockDefinitions = function(blockXmlMap) { eval(blockDefs); }; -/** - * Pulls information about all blocks in the block library to generate XML - * for the selector workpace's toolbox. - * @param {!BlockLibraryStorage} blockLibStorage Block Library Storage object. - * @return {!Element} XML representation of the toolbox. - */ -BlockExporterTools.prototype.generateToolboxFromLibrary - = function(blockLibStorage) { - // Create DOM for XML. - var xmlDom = goog.dom.createDom('xml', { - 'id' : 'blockExporterTools_toolbox', - 'style' : 'display:none' - }); - - var allBlockTypes = blockLibStorage.getBlockTypes(); - // Object mapping block type to XML. - var blockXmlMap = blockLibStorage.getBlockXmlMap(allBlockTypes); - - // Define the custom blocks in order to be able to create instances of - // them in the exporter workspace. - this.addBlockDefinitions(blockXmlMap); - - for (var blockType in blockXmlMap) { - // Get block. - var block = FactoryUtils.getDefinedBlock(blockType, this.hiddenWorkspace); - var category = FactoryUtils.generateCategoryXml([block], blockType); - xmlDom.appendChild(category); - } - - // If there are no blocks in library and the map is empty, append dummy - // category. - if (Object.keys(blockXmlMap).length == 0) { - var category = goog.dom.createDom('category'); - category.setAttribute('name','Next Saved Block'); - xmlDom.appendChild(category); - } - return xmlDom; -}; - /** * Generate XML for the workspace factory's category from imported block * definitions. @@ -233,7 +168,7 @@ BlockExporterTools.prototype.generateCategoryFromBlockLib = }; /** - * Generate selector dom from block library storage. For each block in the + * Generate selector DOM from block library storage. For each block in the * library, it has a block option, which consists of a checkbox, a label, * and a fixed size preview workspace. * @param {!BlockLibraryStorage} blockLibStorage Block Library Storage object. diff --git a/demos/blockfactory/block_exporter_view.js b/demos/blockfactory/block_exporter_view.js index 198598c1418..aa840e59dd6 100644 --- a/demos/blockfactory/block_exporter_view.js +++ b/demos/blockfactory/block_exporter_view.js @@ -1,45 +1,22 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Javascript for the Block Exporter View class. Reads from and * manages a block selector through which users select blocks to export. - * - * @author quachtina96 (Tina Quach) */ 'use strict'; -goog.provide('BlockExporterView'); - -goog.require('BlockExporterTools'); -goog.require('BlockOption'); -goog.require('goog.dom'); - - /** * BlockExporter View Class * @param {!Object} blockOptions Map of block types to BlockOption objects. * @constructor */ -BlockExporterView = function(blockOptions) { +function BlockExporterView(blockOptions) { // Map of block types to BlockOption objects to select from. this.blockOptions = blockOptions; }; @@ -72,7 +49,7 @@ BlockExporterView.prototype.select = function(blockType) { /** * Deselects a block in the selector. - * @param {!Blockly.Block} block Type of block to add to selector workspce. + * @param {!Blockly.Block} block Type of block to add to selector workspace. */ BlockExporterView.prototype.deselect = function(blockType) { this.blockOptions[blockType].setSelected(false); @@ -91,7 +68,7 @@ BlockExporterView.prototype.deselectAllBlocks = function() { /** * Given an array of selected blocks, selects these blocks in the view, marking * the checkboxes accordingly. - * @param {Array.} blockTypes Array of block types to select. + * @param {Array} blockTypes Array of block types to select. */ BlockExporterView.prototype.setSelectedBlockTypes = function(blockTypes) { for (var i = 0, blockType; blockType = blockTypes[i]; i++) { @@ -101,7 +78,7 @@ BlockExporterView.prototype.setSelectedBlockTypes = function(blockTypes) { /** * Returns array of selected blocks. - * @return {!Array.} Array of all selected block types. + * @return {!Array} Array of all selected block types. */ BlockExporterView.prototype.getSelectedBlockTypes = function() { var selectedTypes = []; diff --git a/demos/blockfactory/block_library_controller.js b/demos/blockfactory/block_library_controller.js index 1066ab127d7..8eed54db02c 100644 --- a/demos/blockfactory/block_library_controller.js +++ b/demos/blockfactory/block_library_controller.js @@ -1,21 +1,7 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** @@ -27,27 +13,18 @@ * - delete blocks * - clear their block library * Depends on BlockFactory functions defined in factory.js. - * - * @author quachtina96 (Tina Quach) */ 'use strict'; -goog.provide('BlockLibraryController'); - -goog.require('BlockLibraryStorage'); -goog.require('BlockLibraryView'); -goog.require('BlockFactory'); - - /** * Block Library Controller Class * @param {string} blockLibraryName Desired name of Block Library, also used * to create the key for where it's stored in local storage. - * @param {!BlockLibraryStorage} opt_blockLibraryStorage Optional storage + * @param {!BlockLibraryStorage=} opt_blockLibraryStorage Optional storage * object that allows user to import a block library. * @constructor */ -BlockLibraryController = function(blockLibraryName, opt_blockLibraryStorage) { +function BlockLibraryController(blockLibraryName, opt_blockLibraryStorage) { this.name = blockLibraryName; // Create a new, empty Block Library Storage object, or load existing one. this.storage = opt_blockLibraryStorage || new BlockLibraryStorage(this.name); @@ -110,8 +87,9 @@ BlockLibraryController.prototype.getSelectedBlockType = function() { * updating the dropdown and displaying the starter block (factory_base). */ BlockLibraryController.prototype.clearBlockLibrary = function() { - var check = confirm('Delete all blocks from library?'); - if (check) { + var msg = 'Delete all blocks from library?'; + BlocklyDevTools.Analytics.onWarning(msg); + if (confirm(msg)) { // Clear Block Library Storage. this.storage.clear(); this.storage.saveToLocalStorage(); @@ -131,16 +109,18 @@ BlockLibraryController.prototype.clearBlockLibrary = function() { BlockLibraryController.prototype.saveToBlockLibrary = function() { var blockType = this.getCurrentBlockType(); // If user has not changed the name of the starter block. - if (blockType == 'block_type') { + if (reservedBlockFactoryBlocks.has(blockType) || blockType === 'block_type') { // Do not save block if it has the default type, 'block_type'. - alert('You cannot save a block under the name "block_type". Try changing ' + - 'the name before saving. Then, click on the "Block Library" button ' + - 'to view your saved blocks.'); + var msg = `You cannot save a block under the name "${blockType}". Try ` + + 'changing the name before saving. Then, click on the "Block Library"' + + ' button to view your saved blocks.'; + alert(msg); + BlocklyDevTools.Analytics.onWarning(msg); return; } // Create block XML. - var xmlElement = goog.dom.createDom('xml'); + var xmlElement = Blockly.utils.xml.createElement('xml'); var block = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); xmlElement.appendChild(Blockly.Xml.blockToDomWithXY(block)); @@ -159,6 +139,7 @@ BlockLibraryController.prototype.saveToBlockLibrary = function() { // Add select handler to the new option. this.addOptionSelectHandler(blockType); + BlocklyDevTools.Analytics.onSave('Block'); }; /** @@ -168,7 +149,7 @@ BlockLibraryController.prototype.saveToBlockLibrary = function() { */ BlockLibraryController.prototype.has = function(blockType) { var blockLibrary = this.storage.blocks; - return (blockType in blockLibrary && blockLibrary[blockType] != null); + return (blockType in blockLibrary && blockLibrary[blockType] !== null); }; /** @@ -192,6 +173,29 @@ BlockLibraryController.prototype.getBlockLibrary = function() { return this.storage.getBlockXmlTextMap(); }; +/** + * @return {Object[]} Array of JSON data, where each item is the data for one block type. + */ +BlockLibraryController.prototype.getBlockLibraryAsJson = function() { + const xmlBlocks = this.storage.getBlockXmlMap(this.storage.getBlockTypes()); + const jsonBlocks = []; + const headlessWorkspace = new Blockly.Workspace(); + + for (const blockName in xmlBlocks) { + // Load the block XML into a workspace so we can save it as JSON + headlessWorkspace.clear(); + const blockXml = xmlBlocks[blockName]; + Blockly.Xml.domToWorkspace(blockXml, headlessWorkspace); + const block = headlessWorkspace.getBlocksByType('factory_base', false)[0]; + + if (!block) continue; + + const json = Blockly.serialization.blocks.save(block, {addCoordinates: false, saveIds: false}); + jsonBlocks.push(json); + } + return jsonBlocks; +} + /** * Return stored XML of a given block type. * @param {string} blockType The type of block. @@ -229,7 +233,7 @@ BlockLibraryController.prototype.hasEmptyBlockLibrary = function() { /** * Get all block types stored in block library. - * @return {!Array.} Array of block types. + * @return {!Array} Array of block types. */ BlockLibraryController.prototype.getStoredBlockTypes = function() { return this.storage.getBlockTypes(); diff --git a/demos/blockfactory/block_library_storage.js b/demos/blockfactory/block_library_storage.js index 750717752f0..c843ae542af 100644 --- a/demos/blockfactory/block_library_storage.js +++ b/demos/blockfactory/block_library_storage.js @@ -1,49 +1,30 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Javascript for Block Library's Storage Class. * Depends on Block Library for its namespace. - * - * @author quachtina96 (Tina Quach) */ 'use strict'; -goog.provide('BlockLibraryStorage'); - - /** * Represents a block library's storage. * @param {string} blockLibraryName Desired name of Block Library, also used * to create the key for where it's stored in local storage. - * @param {Object} opt_blocks Object mapping block type to XML. + * @param {!Object=} opt_blocks Object mapping block type to XML. * @constructor */ -BlockLibraryStorage = function(blockLibraryName, opt_blocks) { +function BlockLibraryStorage(blockLibraryName, opt_blocks) { // Add prefix to this.name to avoid collisions in local storage. this.name = 'BlockLibraryStorage.' + blockLibraryName; if (!opt_blocks) { // Initialize this.blocks by loading from local storage. this.loadFromLocalStorage(); - if (this.blocks == null) { + if (this.blocks === null) { this.blocks = Object.create(null); // The line above is equivalent of {} except that this object is TRULY // empty. It doesn't have built-in attributes/functions such as length or @@ -60,9 +41,7 @@ BlockLibraryStorage = function(blockLibraryName, opt_blocks) { * Reads the named block library from local storage and saves it in this.blocks. */ BlockLibraryStorage.prototype.loadFromLocalStorage = function() { - // goog.global is synonymous to window, and allows for flexibility - // between browsers. - var object = goog.global.localStorage[this.name]; + var object = localStorage[this.name]; this.blocks = object ? JSON.parse(object) : null; }; @@ -70,7 +49,7 @@ BlockLibraryStorage.prototype.loadFromLocalStorage = function() { * Writes the current block library (this.blocks) to local storage. */ BlockLibraryStorage.prototype.saveToLocalStorage = function() { - goog.global.localStorage[this.name] = JSON.stringify(this.blocks); + localStorage[this.name] = JSON.stringify(this.blocks); }; /** @@ -110,7 +89,7 @@ BlockLibraryStorage.prototype.removeBlock = function(blockType) { BlockLibraryStorage.prototype.getBlockXml = function(blockType) { var xml = this.blocks[blockType] || null; if (xml) { - var xml = Blockly.Xml.textToDom(xml); + var xml = Blockly.utils.xml.textToDom(xml); } return xml; }; @@ -119,11 +98,11 @@ BlockLibraryStorage.prototype.getBlockXml = function(blockType) { /** * Returns map of each block type to its corresponding XML stored in current * block library (this.blocks). - * @param {!Array.} blockTypes Types of blocks. + * @param {!Array} blockTypes Types of blocks. * @return {!Object} Map of block type to corresponding XML. */ BlockLibraryStorage.prototype.getBlockXmlMap = function(blockTypes) { - var blockXmlMap = {}; + var blockXmlMap = Object.create(null); for (var i = 0; i < blockTypes.length; i++) { var blockType = blockTypes[i]; var xml = this.getBlockXml(blockType); @@ -134,7 +113,7 @@ BlockLibraryStorage.prototype.getBlockXmlMap = function(blockTypes) { /** * Returns array of all block types stored in current block library. - * @return {!Array.} Array of block types stored in library. + * @return {!Array} Array of block types stored in library. */ BlockLibraryStorage.prototype.getBlockTypes = function() { return Object.keys(this.blocks); @@ -153,7 +132,7 @@ BlockLibraryStorage.prototype.isEmpty = function() { /** * Returns array of all block types stored in current block library. - * @return {!Array.} Map of block type to corresponding XML text. + * @return {!Array} Map of block type to corresponding XML text. */ BlockLibraryStorage.prototype.getBlockXmlTextMap = function() { return this.blocks; diff --git a/demos/blockfactory/block_library_view.js b/demos/blockfactory/block_library_view.js index 16181f290c6..2c91ce3782e 100644 --- a/demos/blockfactory/block_library_view.js +++ b/demos/blockfactory/block_library_view.js @@ -1,38 +1,16 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Javascript for BlockLibraryView class. It manages the display * of the Block Library dropdown, save, and delete buttons. - * - * @author quachtina96 (Tina Quach) */ 'use strict'; -goog.provide('BlockLibraryView'); - -goog.require('goog.dom'); -goog.require('goog.dom.classlist'); - - /** * BlockLibraryView Class * @constructor @@ -59,10 +37,10 @@ var BlockLibraryView = function() { */ BlockLibraryView.prototype.addOption = function(blockType, selected) { // Create option. - var option = goog.dom.createDom('a', { - 'id': 'dropdown_' + blockType, - 'class': 'blockLibOpt' - }, blockType); + var option = document.createElement('a'); + option.id ='dropdown_' + blockType; + option.classList.add('blockLibOpt'); + option.textContent = blockType; // Add option to dropdown. this.dropdown.appendChild(option); @@ -84,7 +62,7 @@ BlockLibraryView.prototype.setSelectedBlockType = function(blockTypeToSelect) { // if null or invalid block type selected. for (var blockType in this.optionMap) { var option = this.optionMap[blockType]; - if (blockType == blockTypeToSelect) { + if (blockType === blockTypeToSelect) { this.selectOption_(option); } else { this.deselectOption_(option); @@ -99,7 +77,7 @@ BlockLibraryView.prototype.setSelectedBlockType = function(blockTypeToSelect) { * @private */ BlockLibraryView.prototype.selectOption_ = function(option) { - goog.dom.classlist.add(option, 'dropdown-content-selected'); + option.classList.add('dropdown-content-selected'); }; /** @@ -109,7 +87,7 @@ BlockLibraryView.prototype.selectOption_ = function(option) { * @private */ BlockLibraryView.prototype.deselectOption_ = function(option) { - goog.dom.classlist.remove(option, 'dropdown-content-selected'); + option.classList.remove('dropdown-content-selected'); }; /** @@ -126,37 +104,36 @@ BlockLibraryView.prototype.updateButtons = // User is editing a block. if (!isInLibrary) { - // Block type has not been saved to library yet. Disable the delete button - // and allow user to save. + // Block type has not been saved to the library yet. + // Disable the delete button. this.saveButton.textContent = 'Save "' + blockType + '"'; - this.saveButton.disabled = false; this.deleteButton.disabled = true; } else { - // Block type has already been saved. Disable the save button unless the - // there are unsaved changes (checked below). + // A version of the block type has already been saved. + // Enable the delete button. this.saveButton.textContent = 'Update "' + blockType + '"'; - this.saveButton.disabled = true; this.deleteButton.disabled = false; } this.deleteButton.textContent = 'Delete "' + blockType + '"'; - // If changes to block have been made and are not saved, make button - // green to encourage user to save the block. + this.saveButton.classList.remove('button_alert', 'button_warn'); if (!savedChanges) { - var buttonFormatClass = 'button_warn'; + var buttonFormatClass; - // If block type is the default, 'block_type', make button red to alert - // user. - if (blockType == 'block_type') { + var isReserved = reservedBlockFactoryBlocks.has(blockType); + if (isReserved || blockType === 'block_type') { + // Make button red to alert user that the block type can't be saved. buttonFormatClass = 'button_alert'; + } else { + // Block type has not been saved to library yet or has unsaved changes. + // Make the button green to encourage the user to save the block. + buttonFormatClass = 'button_warn'; } - goog.dom.classlist.add(this.saveButton, buttonFormatClass); + this.saveButton.classList.add(buttonFormatClass); this.saveButton.disabled = false; } else { // No changes to save. - var classesToRemove = ['button_alert', 'button_warn']; - goog.dom.classlist.removeAll(this.saveButton, classesToRemove); this.saveButton.disabled = true; } diff --git a/demos/blockfactory/block_option.js b/demos/blockfactory/block_option.js index 8bc1a2fd411..184c3c23517 100644 --- a/demos/blockfactory/block_option.js +++ b/demos/blockfactory/block_option.js @@ -1,36 +1,17 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** - * @fileoverview Javascript for the BlockOption class, used to represent each of - * the various blocks that you may select. Each block option has a checkbox, - * a label, and a preview workspace through which to view the block. - * - * @author quachtina96 (Tina Quach) + * @fileoverview Javascript for the BlockOption class, used to represent each + * of the various blocks that you may select in the Block Selector. Each block + * option has a checkbox, a label, and a preview workspace through which to + * view the block. */ 'use strict'; -goog.provide('BlockOption'); -goog.require('goog.dom'); - - /** * BlockOption Class * A block option includes checkbox, label, and div element that shows a preview @@ -63,55 +44,49 @@ var BlockOption = function(blockSelector, blockType, previewBlockXml) { }; /** - * Creates the dom for a single block option. Includes checkbox, label, and div + * Creates the DOM for a single block option. Includes checkbox, label, and div * in which to inject the preview block. - * @return {!Element} Root node of the selector dom which consists of a + * @return {!Element} Root node of the selector DOM which consists of a * checkbox, a label, and a fixed size preview workspace per block. */ BlockOption.prototype.createDom = function() { // Create the div for the block option. - var blockOptContainer = goog.dom.createDom('div', { - 'id': this.blockType, - 'class': 'blockOption' - }, ''); // Empty quotes for empty div. + var blockOptContainer = document.createElement('div'); + blockOptContainer.id = this.blockType; + blockOptContainer.classList.add('blockOption'); // Create and append div in which to inject the workspace for viewing the // block option. - var blockOptionPreview = goog.dom.createDom('div', { - 'id' : this.blockType + '_workspace', - 'class': 'blockOption_preview' - }, ''); + var blockOptionPreview = document.createElement('div'); + blockOptionPreview.id = this.blockType + '_workspace'; + blockOptionPreview.classList.add('blockOption_preview'); blockOptContainer.appendChild(blockOptionPreview); // Create and append container to hold checkbox and label. - var checkLabelContainer = goog.dom.createDom('div', { - 'class': 'blockOption_checkLabel' - }, ''); + var checkLabelContainer = document.createElement('div'); + checkLabelContainer.classList.add('blockOption_checkLabel'); blockOptContainer.appendChild(checkLabelContainer); // Create and append container for checkbox. - var checkContainer = goog.dom.createDom('div', { - 'class': 'blockOption_check' - }, ''); + var checkContainer = document.createElement('div'); + checkContainer.classList.add('blockOption_check'); checkLabelContainer.appendChild(checkContainer); // Create and append checkbox. - this.checkbox = goog.dom.createDom('input', { - 'type': 'checkbox', - 'id': this.blockType + '_check' - }, ''); + this.checkbox = document.createElement('input'); + this.checkbox.id = this.blockType + '_check'; + this.checkbox.setAttribute('type', 'checkbox'); checkContainer.appendChild(this.checkbox); // Create and append container for block label. - var labelContainer = goog.dom.createDom('div', { - 'class': 'blockOption_label' - }, ''); + var labelContainer = document.createElement('div'); + labelContainer.classList.add('blockOption_label'); checkLabelContainer.appendChild(labelContainer); // Create and append text node for the label. - var labelText = goog.dom.createDom('p', { - 'id': this.blockType + '_text' - }, this.blockType); + var labelText = document.createElement('p'); + labelText.id = this.blockType + '_text'; + labelText.textContent = this.blockType; labelContainer.appendChild(labelText); this.dom = blockOptContainer; @@ -126,9 +101,9 @@ BlockOption.prototype.showPreviewBlock = function() { var blockOptPreviewID = this.dom.id + '_workspace'; // Inject preview block. - var workspace = Blockly.inject(blockOptPreviewID, {readOnly:true}); - Blockly.Xml.domToWorkspace(this.previewBlockXml, workspace); - this.previewWorkspace = workspace; + var demoWorkspace = Blockly.inject(blockOptPreviewID, {readOnly:true}); + Blockly.Xml.domToWorkspace(this.previewBlockXml, demoWorkspace); + this.previewWorkspace = demoWorkspace; // Center the preview block in the workspace. this.centerBlock(); diff --git a/demos/blockfactory/blocks.js b/demos/blockfactory/blocks.js index 6dcd0061709..9a983460f5f 100644 --- a/demos/blockfactory/blocks.js +++ b/demos/blockfactory/blocks.js @@ -1,25 +1,11 @@ /** - * Blockly Demos: Block Factory Blocks - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Blocks for Blockly's Block Factory application. - * @author fraser@google.com (Neil Fraser) */ 'use strict'; @@ -46,9 +32,9 @@ Blockly.Blocks['factory_base'] = { ['↑ top connection', 'TOP'], ['↓ bottom connection', 'BOTTOM']], function(option) { - this.sourceBlock_.updateShape_(option); + this.getSourceBlock().updateShape_(option); // Connect a shadow block to this new input. - this.sourceBlock_.spawnOutputShadow_(option); + this.getSourceBlock().spawnOutputShadow_(option); }); this.appendDummyInput() .appendField(dropdown, 'CONNECTIONS'); @@ -67,7 +53,7 @@ Blockly.Blocks['factory_base'] = { 'https://developers.google.com/blockly/guides/create-custom-blocks/block-factory'); }, mutationToDom: function() { - var container = document.createElement('mutation'); + var container = Blockly.utils.xml.createElement('mutation'); container.setAttribute('connections', this.getFieldValue('CONNECTIONS')); return container; }, @@ -77,7 +63,7 @@ Blockly.Blocks['factory_base'] = { }, spawnOutputShadow_: function(option) { // Helper method for deciding which type of outputs this block needs - // to attach shaddow blocks to. + // to attach shadow blocks to. switch (option) { case 'LEFT': this.connectOutputShadow_('OUTPUTTYPE'); @@ -99,28 +85,30 @@ Blockly.Blocks['factory_base'] = { var type = this.workspace.newBlock('type_null'); type.setShadow(true); type.outputConnection.connect(this.getInput(outputType).connection); - type.initSvg(); - type.render(); + if (this.rendered) { + type.initSvg(); + type.render(); + } }, updateShape_: function(option) { var outputExists = this.getInput('OUTPUTTYPE'); var topExists = this.getInput('TOPTYPE'); var bottomExists = this.getInput('BOTTOMTYPE'); - if (option == 'LEFT') { + if (option === 'LEFT') { if (!outputExists) { this.addTypeInput_('OUTPUTTYPE', 'output type'); } } else if (outputExists) { this.removeInput('OUTPUTTYPE'); } - if (option == 'TOP' || option == 'BOTH') { + if (option === 'TOP' || option === 'BOTH') { if (!topExists) { this.addTypeInput_('TOPTYPE', 'top type'); } } else if (topExists) { this.removeInput('TOPTYPE'); } - if (option == 'BOTTOM' || option == 'BOTH') { + if (option === 'BOTTOM' || option === 'BOTH') { if (!bottomExists) { this.addTypeInput_('BOTTOMTYPE', 'bottom type'); } @@ -232,25 +220,64 @@ Blockly.Blocks['input_dummy'] = { "previousStatement": "Input", "nextStatement": "Input", "colour": 210, - "tooltip": "For adding fields on a separate row with no " + - "connections. Alignment options (left, right, centre) " + - "apply only to multi-line fields.", + "tooltip": "For adding fields without any block connections." + + "Alignment options (left, right, centre) only affect " + + "multi-row blocks.", "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=293" }); } }; +Blockly.Blocks['input_end_row'] = { + // End-row input. + init: function() { + this.jsonInit({ + "message0": "end-row input", + "message1": FIELD_MESSAGE, + "args1": FIELD_ARGS, + "previousStatement": "Input", + "nextStatement": "Input", + "colour": 210, + "tooltip": "For adding fields without any block connections that will " + + "be rendered on a separate row from any following inputs. " + + "Alignment options (left, right, centre) only affect " + + "multi-row blocks.", + "helpUrl": "https://developers.google.com/blockly/guides/create-custom-blocks/define-blocks#block_inputs" + }); + } +}; + Blockly.Blocks['field_static'] = { // Text value. init: function() { this.setColour(160); - this.appendDummyInput() + this.appendDummyInput('FIRST') .appendField('text') .appendField(new Blockly.FieldTextInput(''), 'TEXT'); this.setPreviousStatement(true, 'Field'); this.setNextStatement(true, 'Field'); this.setTooltip('Static text that serves as a label.'); this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=88'); + }, +}; + +Blockly.Blocks['field_label_serializable'] = { + // Text value that is saved to XML. + init: function() { + this.setColour(160); + this.appendDummyInput('FIRST') + .appendField('text') + .appendField(new Blockly.FieldTextInput(''), 'TEXT') + .appendField(',') + .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); + this.setPreviousStatement(true, 'Field'); + this.setNextStatement(true, 'Field'); + this.setTooltip('Static text that serves as a label, and is saved to' + + ' XML. Use only if you want to modify this label at runtime.'); + this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=88'); + }, + onchange: function() { + fieldNameCheck(this); } }; @@ -328,22 +355,22 @@ Blockly.Blocks['field_dropdown'] = { this.updateShape_(); this.setPreviousStatement(true, 'Field'); this.setNextStatement(true, 'Field'); - this.setMutator(new Blockly.Mutator(['field_dropdown_option_text', - 'field_dropdown_option_image'])); + this.setMutator(new Blockly.icons.MutatorIcon( + ['field_dropdown_option_text', 'field_dropdown_option_image'], this)); this.setColour(160); this.setTooltip('Dropdown menu with a list of options.'); this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386'); }, mutationToDom: function(workspace) { // Create XML to represent menu options. - var container = document.createElement('mutation'); + var container = Blockly.utils.xml.createElement('mutation'); container.setAttribute('options', JSON.stringify(this.optionList_)); return container; }, domToMutation: function(container) { // Parse XML to restore the menu options. var value = JSON.parse(container.getAttribute('options')); - if (typeof value == 'number') { + if (typeof value === 'number') { // Old format from before images were added. November 2016. this.optionList_ = []; for (var i = 0; i < value; i++) { @@ -375,9 +402,9 @@ Blockly.Blocks['field_dropdown'] = { this.optionList_.length = 0; var data = []; while (optionBlock) { - if (optionBlock.type == 'field_dropdown_option_text') { + if (optionBlock.type === 'field_dropdown_option_text') { this.optionList_.push('text'); - } else if (optionBlock.type == 'field_dropdown_option_image') { + } else if (optionBlock.type === 'field_dropdown_option_image') { this.optionList_.push('image'); } data.push([optionBlock.userData_, optionBlock.cpuData_]); @@ -389,7 +416,7 @@ Blockly.Blocks['field_dropdown'] = { for (var i = 0; i < this.optionList_.length; i++) { var userData = data[i][0]; if (userData !== undefined) { - if (typeof userData == 'string') { + if (typeof userData === 'string') { this.setFieldValue(userData || 'option', 'USER' + i); } else { this.setFieldValue(userData.src, 'SRC' + i); @@ -425,13 +452,13 @@ Blockly.Blocks['field_dropdown'] = { var src = 'https://www.gstatic.com/codesite/ph/images/star_on.gif'; for (var i = 0; i <= this.optionList_.length; i++) { var type = this.optionList_[i]; - if (type == 'text') { + if (type === 'text') { this.appendDummyInput('OPTION' + i) .appendField('•') .appendField(new Blockly.FieldTextInput('option'), 'USER' + i) .appendField(',') .appendField(new Blockly.FieldTextInput('OPTIONNAME'), 'CPU' + i); - } else if (type == 'image') { + } else if (type === 'image') { this.appendDummyInput('OPTION' + i) .appendField('•') .appendField('image') @@ -457,10 +484,10 @@ Blockly.Blocks['field_dropdown'] = { } }, getUserData: function(n) { - if (this.optionList_[n] == 'text') { + if (this.optionList_[n] === 'text') { return this.getFieldValue('USER' + n); } - if (this.optionList_[n] == 'image') { + if (this.optionList_[n] === 'image') { return { src: this.getFieldValue('SRC' + n), width: Number(this.getFieldValue('WIDTH' + n)), @@ -552,24 +579,6 @@ Blockly.Blocks['field_colour'] = { } }; -Blockly.Blocks['field_date'] = { - // Date input. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('date') - .appendField(new Blockly.FieldDate(), 'DATE') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Date input field.'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - Blockly.Blocks['field_variable'] = { // Dropdown for variables. init: function() { @@ -603,7 +612,9 @@ Blockly.Blocks['field_image'] = { .appendField('height') .appendField(new Blockly.FieldNumber('15', 0, NaN, 1), 'HEIGHT') .appendField('alt text') - .appendField(new Blockly.FieldTextInput('*'), 'ALT'); + .appendField(new Blockly.FieldTextInput('*'), 'ALT') + .appendField('flip RTL') + .appendField(new Blockly.FieldCheckbox('false'), 'FLIP_RTL'); this.setPreviousStatement(true, 'Field'); this.setNextStatement(true, 'Field'); this.setTooltip('Static image (JPEG, PNG, GIF, SVG, BMP).\n' + @@ -619,14 +630,14 @@ Blockly.Blocks['type_group'] = { this.typeCount_ = 2; this.updateShape_(); this.setOutput(true, 'Type'); - this.setMutator(new Blockly.Mutator(['type_group_item'])); + this.setMutator(new Blockly.icons.MutatorIcon(['type_group_item'], this)); this.setColour(230); this.setTooltip('Allows more than one type to be accepted.'); this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677'); }, mutationToDom: function(workspace) { // Create XML to represent a group of types. - var container = document.createElement('mutation'); + var container = Blockly.utils.xml.createElement('mutation'); container.setAttribute('types', this.typeCount_); return container; }, @@ -640,7 +651,7 @@ Blockly.Blocks['type_group'] = { for (var i = 0; i < this.typeCount_; i++) { var input = this.appendValueInput('TYPE' + i) .setCheck('Type'); - if (i == 0) { + if (i === 0) { input.appendField('any of'); } } @@ -671,7 +682,7 @@ Blockly.Blocks['type_group'] = { // Disconnect any children that don't belong. for (var i = 0; i < this.typeCount_; i++) { var connection = this.getInput('TYPE' + i).connection.targetConnection; - if (connection && connections.indexOf(connection) == -1) { + if (connection && !connections.includes(connection)) { connection.disconnect(); } } @@ -679,7 +690,7 @@ Blockly.Blocks['type_group'] = { this.updateShape_(); // Reconnect any child blocks. for (var i = 0; i < this.typeCount_; i++) { - Blockly.Mutator.reconnect(connections[i], this, 'TYPE' + i); + connections[i]?.reconnect(this, 'TYPE' + i); } }, saveConnections: function(containerBlock) { @@ -700,7 +711,7 @@ Blockly.Blocks['type_group'] = { for (var i = 0; i < this.typeCount_; i++) { if (!this.getInput('TYPE' + i)) { var input = this.appendValueInput('TYPE' + i); - if (i == 0) { + if (i === 0) { input.appendField('any of'); } } @@ -841,11 +852,11 @@ Blockly.Blocks['colour_hue'] = { // Update the current block's colour to match. var hue = parseInt(text, 10); if (!isNaN(hue)) { - this.sourceBlock_.setColour(hue); + this.getSourceBlock().setColour(hue); } }, mutationToDom: function(workspace) { - var container = document.createElement('mutation'); + var container = Blockly.utils.xml.createElement('mutation'); container.setAttribute('colour', this.getColour()); return container; }, @@ -866,11 +877,11 @@ function fieldNameCheck(referenceBlock) { } var name = referenceBlock.getFieldValue('FIELDNAME').toLowerCase(); var count = 0; - var blocks = referenceBlock.workspace.getAllBlocks(); + var blocks = referenceBlock.workspace.getAllBlocks(false); for (var i = 0, block; block = blocks[i]; i++) { var otherName = block.getFieldValue('FIELDNAME'); - if (!block.disabled && !block.getInheritedDisabled() && - otherName && otherName.toLowerCase() == name) { + if (block.isEnabled() && !block.getInheritedDisabled() && + otherName && otherName.toLowerCase() === name) { count++; } } @@ -891,11 +902,11 @@ function inputNameCheck(referenceBlock) { } var name = referenceBlock.getFieldValue('INPUTNAME').toLowerCase(); var count = 0; - var blocks = referenceBlock.workspace.getAllBlocks(); + var blocks = referenceBlock.workspace.getAllBlocks(false); for (var i = 0, block; block = blocks[i]; i++) { var otherName = block.getFieldValue('INPUTNAME'); - if (!block.disabled && !block.getInheritedDisabled() && - otherName && otherName.toLowerCase() == name) { + if (block.isEnabled() && !block.getInheritedDisabled() && + otherName && otherName.toLowerCase() === name) { count++; } } @@ -903,3 +914,7 @@ function inputNameCheck(referenceBlock) { 'There are ' + count + ' input blocks\n with this name.' : null; referenceBlock.setWarningText(msg); } + +// Make a set of all of block types that are required for the block factory. +var reservedBlockFactoryBlocks = + new Set(Object.getOwnPropertyNames(Blockly.Blocks)); diff --git a/demos/blockfactory/cp.css b/demos/blockfactory/cp.css new file mode 100644 index 00000000000..508533e1627 --- /dev/null +++ b/demos/blockfactory/cp.css @@ -0,0 +1,46 @@ +.cp_swatch { + border: outset 3px #888; + display: inline-block; + font-family: sans-serif; + height: 20px; + line-height: 1.4; + margin: 1px; + text-align: center; + width: 30px; + vertical-align: bottom; +} + +#cp_popup { + cursor: default; + font-family: sans-serif; + left: 0; + position: absolute; + text-align: center; + top: 0; + user-select: none; +} + +#cp_popup>table { + border: 2px solid #808080; + background-color: #808080; + border-collapse: collapse; +} + +#cp_popup>table>tbody>tr>td { + border: 1px solid #808080; + background-color: #fff; + width: 20px; + padding: 0; +} + +#cp_popup>table>tbody>tr>td>div { + border: 1px solid #808080; +} + +#cp_popup>table>tbody>tr>td>div:hover { + border-color: #fff; +} + +#cp_popup>table>tbody>tr>td>div.cp_current { + border: 1px solid #000; +} diff --git a/demos/blockfactory/cp.js b/demos/blockfactory/cp.js new file mode 100644 index 00000000000..96248176173 --- /dev/null +++ b/demos/blockfactory/cp.js @@ -0,0 +1,179 @@ +/** + * Colour Picker v2.0 + * + * Copyright 2006 Neil Fraser + * https://neil.fraser.name/software/colourpicker/ + * SPDX-License-Identifier: Apache-2.0 + */ + +// Include at the top of your page: +// +// +// Call with: +// +// + +var cp_grid = [ + ['ffffff', 'ffcccc', 'ffcc99', 'ffff99', 'ffffcc', '99ff99', '99ffff', 'ccffff', 'ccccff', 'ffccff'], + ['cccccc', 'ff6666', 'ff9966', 'ffff66', 'ffff33', '66ff99', '33ffff', '66ffff', '9999ff', 'ff99ff'], + ['c0c0c0', 'ff0000', 'ff9900', 'ffcc66', 'ffff00', '33ff33', '66cccc', '33ccff', '6666cc', 'cc66cc'], + ['999999', 'cc0000', 'ff6600', 'ffcc33', 'ffcc00', '33cc00', '00cccc', '3366ff', '6633ff', 'cc33cc'], + ['666666', '990000', 'cc6600', 'cc9933', '999900', '009900', '339999', '3333ff', '6600cc', '993399'], + ['333333', '660000', '993300', '996633', '666600', '006600', '336666', '000099', '333399', '663366'], + ['000000', '330000', '663300', '663333', '333300', '003300', '003333', '000066', '330099', '330033'], + [''] +]; + +var cp_popupDom = null; +var cp_activeSwatch = null; +var cp_closePid = null; + +function cp_init(id) { + var input = document.getElementById(id); + if (!input) { + throw Error('Colour picker can\'t find "' + id + '"'); + } + if (!input.cp_swatch) { + // Hide the input. + input.type = 'hidden'; + // + var swatch = document.createElement('span'); + swatch.className = 'cp_swatch'; + swatch.addEventListener('click', cp_open); + swatch.addEventListener('mouseover', cp_cancelclose); + swatch.addEventListener('mouseout', cp_closesoon); + input.parentNode.insertBefore(swatch, input); + // Cross-link the swatch and input. + swatch.cp_input = input; + input.cp_swatch = swatch; + } + cp_updateSwatch(input.cp_swatch); +} + +function cp_updateSwatch(swatch) { + var colour = swatch.cp_input.value; + if (colour) { + swatch.style.backgroundColor = '#' + colour; + swatch.textContent = '\xa0'; + } else { + swatch.style.backgroundColor = '#fff'; + swatch.innerHTML = 'X'; + } +} + +function cp_open(e) { + // Create a table of colours. + if (cp_popupDom) { + cp_close(); + return; + } + cp_activeSwatch = e.currentTarget; + var currentColour = cp_activeSwatch.cp_input.value.toLowerCase(); + var element = cp_activeSwatch; + var posX = 0; + var posY = element.offsetHeight; + while (element) { + posX += element.offsetLeft; + posY += element.offsetTop; + element = element.offsetParent; + } + cp_popupDom = document.createElement('div'); + cp_popupDom.id = 'cp_popup'; + var table = document.createElement('table'); + table.addEventListener('mouseover', cp_cancelclose); + table.addEventListener('mouseout', cp_closesoon); + table.addEventListener('click', cp_onclick); + var tbody = document.createElement('tbody'); + var row, cell, div; + for (var y = 0; y < cp_grid.length; y++) { + row = document.createElement('tr'); + tbody.appendChild(row); + for (var x = 0; x < cp_grid[y].length; x++) { + var colour = cp_grid[y][x]; + if (colour === undefined) continue; + cell = document.createElement('td'); + row.appendChild(cell); + div = document.createElement('div'); + cell.appendChild(div); + cell.cp_colour = colour; + if (colour) { + div.style.backgroundColor = '#' + colour; + div.innerHTML = '\xa0'; + } else { + div.innerHTML = 'X'; + } + if (currentColour === colour.toLowerCase()) { + div.className = 'cp_current' + } + } + } + table.appendChild(tbody); + cp_popupDom.appendChild(table); + + document.body.appendChild(cp_popupDom); + // Don't widen the screen. + var rightOverhang = (posX + cp_popupDom.offsetWidth) - + (window.innerWidth + window.scrollX) + 15; // Scrollbar is 15px. + if (rightOverhang > 0) { + posX -= rightOverhang; + } + // Flip to above swatch if no room below. + if (posY + cp_popupDom.offsetHeight >= window.innerHeight + window.scrollY) { + posY -= cp_popupDom.offsetHeight + cp_activeSwatch.offsetHeight; + if (posY < window.scrollY) { + posY = window.scrollY; + } + } + cp_popupDom.style.left = posX + 'px'; + cp_popupDom.style.top = posY + 'px'; +} + +function cp_close() { + // Close the table now. + cp_cancelclose(); + if (cp_popupDom) { + document.body.removeChild(cp_popupDom) + } + cp_popupDom = null; + cp_activeSwatch = null; +} + +function cp_closesoon() { + // Close the table a split-second from now. + cp_closePid = setTimeout(cp_close, 250); +} + +function cp_cancelclose() { + // Don't close the colour table after all. + if (cp_closePid) { + clearTimeout(cp_closePid); + } +} + +function cp_onclick(e) { + // Clicked on a colour. + var element = e.target; + var colour; + // Walk up the DOM, looking for a colour. + while (element) { + colour = element.cp_colour; + if (colour !== undefined) { + break; + } + element = element.parentNode; + } + if (colour !== undefined) { + // Set the colour. + cp_activeSwatch.cp_input.value = colour; + cp_updateSwatch(cp_activeSwatch); + // Fire a change event. + var evt = document.createEvent('HTMLEvents'); + evt.initEvent('change', false, true); + cp_activeSwatch.cp_input.dispatchEvent(evt); + } + // Close the table. + cp_close(); +} diff --git a/demos/blockfactory/factory.css b/demos/blockfactory/factory.css index 6661c139c87..ded1f6cda50 100644 --- a/demos/blockfactory/factory.css +++ b/demos/blockfactory/factory.css @@ -1,21 +1,7 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ html, body { @@ -56,11 +42,11 @@ td { p { display: block; - -webkit-margin-before: 0em; - -webkit-margin-after: 0em; - -webkit-margin-start: 0px; - -webkit-margin-end: 0px; - padding: 5px 0px; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + -webkit-margin-start: 0; + -webkit-margin-end: 0; + padding: 5px 0; } #factoryHeader { @@ -141,6 +127,13 @@ button, .buttonStyle { float: right; } +#legacyBanner { + border: #ccc 1px solid; + background-color: #FFCDD2; + margin: 4px; + padding: 4px; +} + #blockFactoryContent { height: 85%; width: 100%; @@ -233,7 +226,7 @@ button, .buttonStyle { } .subsettings { - margin: 0px 25px; + margin: 0 25px; } #exporterHiddenWorkspace { @@ -335,7 +328,7 @@ button, .buttonStyle { padding: 5px 19px; } -.tab:hover:not(.tabon){ +.tab:hover:not(.tabon) { background-color: #e8e8e8; } @@ -511,53 +504,10 @@ td.taboff:hover { right: 0; bottom: 0; left: 0; - background: rgba(0, 0, 0, 0.05); + background: rgba(0, 0, 0, 0.1); z-index: 100; } -/* Rules for Closure popup color picker */ -.goog-palette { - outline: none; - cursor: default; -} - -.goog-palette-cell { - height: 13px; - width: 15px; - margin: 0; - border: 0; - text-align: center; - vertical-align: middle; - border-right: 1px solid #000; - font-size: 1px; -} - -.goog-palette-colorswatch { - border: 1px solid #000; - height: 13px; - position: relative; - width: 15px; -} - -.goog-palette-cell-hover .goog-palette-colorswatch { - border: 1px solid #fff; -} - -.goog-palette-cell-selected .goog-palette-colorswatch { - border: 1px solid #000; - color: #fff; -} - -.goog-palette-table { - border: 1px solid #000; - border-collapse: collapse; -} - -.goog-popupcolorpicker { - position: absolute; - z-index: 101; /* On top of the modal Shadow. */ -} - /* The container
    - needed to position the dropdown content */ .dropdown { display: inline-block; @@ -566,7 +516,7 @@ td.taboff:hover { /* Dropdown Content (Hidden by Default) */ .dropdown-content { background-color: #fff; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,.2); + box-shadow: 0 8px 16px 0 rgba(0,0,0,.2); display: none; min-width: 170px; opacity: 1; @@ -583,12 +533,12 @@ td.taboff:hover { text-decoration: none; } -/* Change color of dropdown links on hover. */ +/* Change colour of dropdown links on hover. */ .dropdown-content a:hover, .dropdown-content label:hover { background-color: #EEE; } -/* Change color of dropdown links on selected. */ +/* Change colour of dropdown links on selected. */ .dropdown-content-selected { background-color: #DDD; } @@ -598,6 +548,22 @@ td.taboff:hover { display: block; } +#dropdownDiv_editCategory { + padding: 0 1ex; +} + +#dropdownDiv_editCategory>img { + vertical-align: middle; +} + +.cp_swatch { + vertical-align: middle !important; +} + +#cp_popup { + z-index: 999; +} + .shadowBlock>.blocklyPath { fill-opacity: .5; stroke-opacity: .5; @@ -607,3 +573,14 @@ td.taboff:hover { .shadowBlock>.blocklyPathDark { display: none; } + +/* Privacy link */ +.privacyLink { + font-family: Roboto, Arial, Helvetica, sans-serif; + font-size: small; + text-decoration: none; +} + +.privacyButton { + float: right; +} diff --git a/demos/blockfactory/factory.js b/demos/blockfactory/factory.js index 974cd8a7ec5..2e6ebc924e1 100644 --- a/demos/blockfactory/factory.js +++ b/demos/blockfactory/factory.js @@ -1,21 +1,7 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** @@ -24,19 +10,13 @@ * generate a preview block and starter code for the block (block definition and * generator stub. Uses the Block Factory namespace. Depends on the FactoryUtils * for its code generation functions. - * - * @author fraser@google.com (Neil Fraser), quachtina96 (Tina Quach) */ 'use strict'; /** * Namespace for Block Factory. */ -goog.provide('BlockFactory'); - -goog.require('FactoryUtils'); -goog.require('StandardCategories'); - +var BlockFactory = BlockFactory || Object.create(null); /** * Workspace for user to build block. @@ -52,31 +32,57 @@ BlockFactory.previewWorkspace = null; /** * Name of block if not named. + * @type string */ BlockFactory.UNNAMED = 'unnamed'; /** * Existing direction ('ltr' vs 'rtl') of preview. + * @type string */ BlockFactory.oldDir = null; -/* +/** + * Flag to signal that an update came from a manual update to the JSON or JavaScript. + * definition manually. + * @type boolean + */ +// TODO: Replace global state with parameter passed to functions. +BlockFactory.updateBlocksFlag = false; + +/** + * Delayed flag to avoid infinite update after updating the JSON or JavaScript. + * definition manually. + * @type boolean + */ +// TODO: Replace global state with parameter passed to functions. +BlockFactory.updateBlocksFlagDelayed = false; + +/** * The starting XML for the Block Factory main workspace. Contains the * unmovable, undeletable factory_base block. */ -BlockFactory.STARTER_BLOCK_XML_TEXT = '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '230' + - ''; +BlockFactory.STARTER_BLOCK_XML_TEXT = + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '230' + + '' + + '' + + '' + + ''; /** * Change the language code format. @@ -85,8 +91,9 @@ BlockFactory.formatChange = function() { var mask = document.getElementById('blocklyMask'); var languagePre = document.getElementById('languagePre'); var languageTA = document.getElementById('languageTA'); - if (document.getElementById('format').value == 'Manual') { - Blockly.hideChaff(); + if (document.getElementById('format').value === 'Manual-JSON' || + document.getElementById('format').value === 'Manual-JS') { + Blockly.common.getMainWorkspace().hideChaff(); mask.style.display = 'block'; languagePre.style.display = 'none'; languageTA.style.display = 'block'; @@ -98,6 +105,9 @@ BlockFactory.formatChange = function() { mask.style.display = 'none'; languageTA.style.display = 'none'; languagePre.style.display = 'block'; + var code = languagePre.textContent.trim(); + languageTA.value = code; + BlockFactory.updateLanguage(); } BlockFactory.disableEnableLink(); @@ -115,10 +125,26 @@ BlockFactory.updateLanguage = function() { if (!blockType) { blockType = BlockFactory.UNNAMED; } - var format = document.getElementById('format').value; - var code = FactoryUtils.getBlockDefinition(blockType, rootBlock, format, - BlockFactory.mainWorkspace); - FactoryUtils.injectCode(code, 'languagePre'); + + if (!BlockFactory.updateBlocksFlag) { + var format = document.getElementById('format').value; + if (format === 'Manual-JSON') { + format = 'JSON'; + } else if (format === 'Manual-JS') { + format = 'JavaScript'; + } + + var code = FactoryUtils.getBlockDefinition(blockType, rootBlock, format, + BlockFactory.mainWorkspace); + FactoryUtils.injectCode(code, 'languagePre'); + if (!BlockFactory.updateBlocksFlagDelayed) { + var languagePre = document.getElementById('languagePre'); + var languageTA = document.getElementById('languageTA'); + code = languagePre.innerText.trim(); + languageTA.value = code; + } + } + BlockFactory.updatePreview(); }; @@ -138,11 +164,11 @@ BlockFactory.updateGenerator = function(block) { BlockFactory.updatePreview = function() { // Toggle between LTR/RTL if needed (also used in first display). var newDir = document.getElementById('direction').value; - if (BlockFactory.oldDir != newDir) { + if (BlockFactory.oldDir !== newDir) { if (BlockFactory.previewWorkspace) { BlockFactory.previewWorkspace.dispose(); } - var rtl = newDir == 'rtl'; + var rtl = newDir === 'rtl'; BlockFactory.previewWorkspace = Blockly.inject('preview', {rtl: rtl, media: '../../media/', @@ -151,60 +177,54 @@ BlockFactory.updatePreview = function() { } BlockFactory.previewWorkspace.clear(); - // Fetch the code and determine its format (JSON or JavaScript). - var format = document.getElementById('format').value; - if (format == 'Manual') { - var code = document.getElementById('languageTA').value; - // If the code is JSON, it will parse, otherwise treat as JS. - try { - JSON.parse(code); - format = 'JSON'; - } catch (e) { - format = 'JavaScript'; - } - } else { - var code = document.getElementById('languagePre').textContent; - } + var format = BlockFactory.getBlockDefinitionFormat(); + var code = document.getElementById('languageTA').value; if (!code.trim()) { // Nothing to render. Happens while cloud storage is loading. return; } - // Backup Blockly.Blocks object so that main workspace and preview don't - // collide if user creates a 'factory_base' block, for instance. - var backupBlocks = Blockly.Blocks; - try { - // Make a shallow copy. - Blockly.Blocks = Object.create(null); - for (var prop in backupBlocks) { - Blockly.Blocks[prop] = backupBlocks[prop]; + // Don't let the user create a block type that already exists, + // because it doesn't work. + var warnExistingBlock = function(blockType) { + if (reservedBlockFactoryBlocks.has(blockType)) { + var text = `You can't make a block called ${blockType} in this tool ` + + `because that name is reserved.`; + FactoryUtils.getRootBlock(BlockFactory.mainWorkspace).setWarningText(text); + console.error(text); + return true; } + return false; + } - if (format == 'JSON') { + var blockType = 'block_type'; + var blockCreated = false; + try { + if (format === 'JSON') { var json = JSON.parse(code); - Blockly.Blocks[json.type || BlockFactory.UNNAMED] = { + blockType = json.type || BlockFactory.UNNAMED; + if (warnExistingBlock(blockType)) { + return; + } + Blockly.Blocks[blockType] = { init: function() { this.jsonInit(json); } }; - } else if (format == 'JavaScript') { - eval(code); - } else { - throw 'Unknown format: ' + format; - } - - // Look for a block on Blockly.Blocks that does not match the backup. - var blockType = null; - for (var type in Blockly.Blocks) { - if (typeof Blockly.Blocks[type].init == 'function' && - Blockly.Blocks[type] != backupBlocks[type]) { - blockType = type; - break; + } else if (format === 'JavaScript') { + try { + blockType = FactoryUtils.getBlockTypeFromJsDefinition(code); + if (warnExistingBlock(blockType)) { + return; + } + eval(code); + } catch (e) { + // TODO: Display error in the UI + console.error("Error while evaluating JavaScript formatted block definition", e); + return; } } - if (!blockType) { - return; - } + blockCreated = true; // Create the preview block. var previewBlock = BlockFactory.previewWorkspace.newBlock(blockType); @@ -219,11 +239,11 @@ BlockFactory.updatePreview = function() { // Warn user only if their block type is already exists in Blockly's // standard library. var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); - if (StandardCategories.coreBlockTypes.indexOf(blockType) != -1) { + if (StandardCategories.coreBlockTypes.includes(blockType)) { rootBlock.setWarningText('A core Blockly block already exists ' + 'under this name.'); - } else if (blockType == 'block_type') { + } else if (blockType === 'block_type') { // Warn user to let them know they can't save a block under the default // name 'block_type' rootBlock.setWarningText('You cannot save a block with the default ' + @@ -232,12 +252,41 @@ BlockFactory.updatePreview = function() { } else { rootBlock.setWarningText(null); } - + } catch(err) { + // TODO: Show error on the UI + console.log(err); + BlockFactory.updateBlocksFlag = false + BlockFactory.updateBlocksFlagDelayed = false } finally { - Blockly.Blocks = backupBlocks; + // Remove the newly-created block. + // We have to check if the block was actually created so that we don't remove + // one of the built-in blocks, like factory_base. + if (blockCreated) { + delete Blockly.Blocks[blockType]; + } } }; +/** + * Gets the format from the Block Definitions' format selector/drop-down. + * @return Either 'JavaScript' or 'JSON'. + * @throws If selector value is not recognized. + */ +BlockFactory.getBlockDefinitionFormat = function() { + switch (document.getElementById('format').value) { + case 'JSON': + case 'Manual-JSON': + return 'JSON'; + + case 'JavaScript': + case 'Manual-JS': + return 'JavaScript'; + + default: + throw 'Unknown format: ' + format; + } +} + /** * Disable link and save buttons if the format is 'Manual', enable otherwise. */ @@ -245,7 +294,7 @@ BlockFactory.disableEnableLink = function() { var linkButton = document.getElementById('linkButton'); var saveBlockButton = document.getElementById('localSaveButton'); var saveToLibButton = document.getElementById('saveToBlockLibraryButton'); - var disabled = document.getElementById('format').value == 'Manual'; + var disabled = document.getElementById('format').value.substr(0, 6) === 'Manual'; linkButton.disabled = disabled; saveBlockButton.disabled = disabled; saveToLibButton.disabled = disabled; @@ -256,7 +305,7 @@ BlockFactory.disableEnableLink = function() { */ BlockFactory.showStarterBlock = function() { BlockFactory.mainWorkspace.clear(); - var xml = Blockly.Xml.textToDom(BlockFactory.STARTER_BLOCK_XML_TEXT); + var xml = Blockly.utils.xml.textToDom(BlockFactory.STARTER_BLOCK_XML_TEXT); Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace); }; @@ -265,12 +314,25 @@ BlockFactory.showStarterBlock = function() { */ BlockFactory.isStarterBlock = function() { var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); - // The starter block does not have blocks nested into the factory_base block. - return !(rootBlock.getChildren().length > 0 || + return rootBlock && !( + // The starter block does not have blocks nested into the factory_base block. + rootBlock.getChildren().length > 0 || // The starter block's name is the default, 'block_type'. - rootBlock.getFieldValue('NAME').trim().toLowerCase() != 'block_type' || + rootBlock.getFieldValue('NAME').trim().toLowerCase() !== 'block_type' || // The starter block has no connections. - rootBlock.getFieldValue('CONNECTIONS') != 'NONE' || + rootBlock.getFieldValue('CONNECTIONS') !== 'NONE' || // The starter block has automatic inputs. - rootBlock.getFieldValue('INLINE') != 'AUTO'); + rootBlock.getFieldValue('INLINE') !== 'AUTO' + ); +}; + +/** + * Updates blocks from the manually edited js or json from their text area. + */ +BlockFactory.manualEdit = function() { + // TODO(#1267): Replace these global state flags with parameters passed to + // the right functions. + BlockFactory.updateBlocksFlag = true; + BlockFactory.updateBlocksFlagDelayed = true; + BlockFactory.updateLanguage(); }; diff --git a/demos/blockfactory/factory_utils.js b/demos/blockfactory/factory_utils.js index d66225c3adc..4731d1ce9e7 100644 --- a/demos/blockfactory/factory_utils.js +++ b/demos/blockfactory/factory_utils.js @@ -1,21 +1,7 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** @@ -24,15 +10,14 @@ * Exporter applications within Blockly Factory. Holds functions to generate * block definitions and generator stubs and to create and download files. * - * @author fraser@google.com (Neil Fraser), quachtina96 (Tina Quach) + * (Juan Carlos Orozco) */ - 'use strict'; +'use strict'; /** * Namespace for FactoryUtils. */ -goog.provide('FactoryUtils'); - +var FactoryUtils = FactoryUtils || Object.create(null); /** * Get block definition code for the current block. @@ -73,19 +58,32 @@ FactoryUtils.cleanBlockType = function(blockType) { * Get the generator code for a given block. * @param {!Blockly.Block} block Rendered block in preview workspace. * @param {string} generatorLanguage 'JavaScript', 'Python', 'PHP', 'Lua', - * 'Dart'. + * or 'Dart'. * @return {string} Generator code for multiple blocks. */ FactoryUtils.getGeneratorStub = function(block, generatorLanguage) { + // Build factory blocks from block + if (BlockFactory.updateBlocksFlag) { // TODO: Move this to updatePreview() + BlockFactory.mainWorkspace.clear(); + var xml = BlockDefinitionExtractor.buildBlockFactoryWorkspace(block); + Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace); + // Calculate timer to avoid infinite update loops + // TODO(#1267): Remove the global variables and any infinite loops. + BlockFactory.updateBlocksFlag = false; + setTimeout( + function() {BlockFactory.updateBlocksFlagDelayed = false;}, 3000); + } + BlockFactory.lastUpdatedBlock = block; // Variable to share the block value. + function makeVar(root, name) { name = name.toLowerCase().replace(/\W/g, '_'); return ' var ' + root + '_' + name; } // The makevar function lives in the original update generator. - var language = generatorLanguage; + var language = generatorLanguage.toLowerCase(); var code = []; - code.push("Blockly." + language + "['" + block.type + - "'] = function(block) {"); + code.push(`${language}.${language}Generator.forBlock['${block.type}'] = ` + + 'function(block, generator) {'); // Generate getters for any fields or inputs. for (var i = 0, input; input = block.inputList[i]; i++) { @@ -95,54 +93,43 @@ FactoryUtils.getGeneratorStub = function(block, generatorLanguage) { continue; } if (field instanceof Blockly.FieldVariable) { - // Subclass of Blockly.FieldDropdown, must test first. - code.push(makeVar('variable', name) + - " = Blockly." + language + - ".variableDB_.getName(block.getFieldValue('" + name + - "'), Blockly.Variables.NAME_TYPE);"); - } else if (field instanceof Blockly.FieldAngle) { - // Subclass of Blockly.FieldTextInput, must test first. - code.push(makeVar('angle', name) + - " = block.getFieldValue('" + name + "');"); - } else if (Blockly.FieldDate && field instanceof Blockly.FieldDate) { - // Blockly.FieldDate may not be compiled into Blockly. - code.push(makeVar('date', name) + - " = block.getFieldValue('" + name + "');"); - } else if (field instanceof Blockly.FieldColour) { - code.push(makeVar('colour', name) + - " = block.getFieldValue('" + name + "');"); + // FieldVariable is subclass of FieldDropdown; must test first. + code.push(`${makeVar('variable', name)} = ` + + `generator.nameDB_.getName(block.getFieldValue('${name}'), ` + + `Blockly.Variables.NAME_TYPE);`); } else if (field instanceof Blockly.FieldCheckbox) { - code.push(makeVar('checkbox', name) + - " = block.getFieldValue('" + name + "') == 'TRUE';"); - } else if (field instanceof Blockly.FieldDropdown) { - code.push(makeVar('dropdown', name) + - " = block.getFieldValue('" + name + "');"); - } else if (field instanceof Blockly.FieldNumber) { - code.push(makeVar('number', name) + - " = block.getFieldValue('" + name + "');"); - } else if (field instanceof Blockly.FieldTextInput) { - code.push(makeVar('text', name) + - " = block.getFieldValue('" + name + "');"); + code.push(`${makeVar('checkbox', name)} = ` + + `block.getFieldValue('${name}') === 'TRUE';`); + } else { + let prefix = + // Angle is subclass of FieldTextInput; must test first. + field instanceof Blockly.FieldAngle ? 'angle' : + field instanceof Blockly.FieldColour ? 'colour' : + field instanceof Blockly.FieldDropdown ? 'dropdown' : + field instanceof Blockly.FieldNumber ? 'number' : + field instanceof Blockly.FieldTextInput ? 'text' : + 'field'; // Default if subclass not found. + code.push(`${makeVar(prefix, name)} = block.getFieldValue('${name}');`); } } var name = input.name; if (name) { - if (input.type == Blockly.INPUT_VALUE) { - code.push(makeVar('value', name) + - " = Blockly." + language + ".valueToCode(block, '" + name + - "', Blockly." + language + ".ORDER_ATOMIC);"); - } else if (input.type == Blockly.NEXT_STATEMENT) { - code.push(makeVar('statements', name) + - " = Blockly." + language + ".statementToCode(block, '" + - name + "');"); + if (input.type === Blockly.INPUT_VALUE) { + code.push(`${makeVar('value', name)} = ` + + `generator.valueToCode(block, '${name}', ` + + `${language}.Order.ATOMIC);`); + } else if (input.type === Blockly.NEXT_STATEMENT) { + code.push(`${makeVar('statements', name)} = ` + + `generator.statementToCode(block, '${name}');`); } } } - // Most languages end lines with a semicolon. Python does not. + // Most languages end lines with a semicolon. Python & Lua do not. var lineEnd = { 'JavaScript': ';', 'Python': '', 'PHP': ';', + 'Lua': '', 'Dart': ';' }; code.push(" // TODO: Assemble " + language + " into code variable."); @@ -163,7 +150,7 @@ FactoryUtils.getGeneratorStub = function(block, generatorLanguage) { * Update the language code as JSON. * @param {string} blockType Name of block. * @param {!Blockly.Block} rootBlock Factory_base block. - * @return {string} Generanted language code. + * @return {string} Generated language code. * @private */ FactoryUtils.formatJson_ = function(blockType, rootBlock) { @@ -176,11 +163,11 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) { var contentsBlock = rootBlock.getInputTargetBlock('INPUTS'); var lastInput = null; while (contentsBlock) { - if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { + if (contentsBlock.isEnabled() && !contentsBlock.getInheritedDisabled()) { var fields = FactoryUtils.getFieldsJson_( contentsBlock.getInputTargetBlock('FIELDS')); for (var i = 0; i < fields.length; i++) { - if (typeof fields[i] == 'string') { + if (typeof fields[i] === 'string') { message.push(fields[i].replace(/%/g, '%%')); } else { args.push(fields[i]); @@ -190,7 +177,8 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) { var input = {type: contentsBlock.type}; // Dummy inputs don't have names. Other inputs do. - if (contentsBlock.type != 'input_dummy') { + if (contentsBlock.type !== 'input_dummy' && + contentsBlock.type !== 'input_end_row') { input.name = contentsBlock.getFieldValue('INPUTNAME'); } var check = JSON.parse( @@ -199,7 +187,7 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) { input.check = check; } var align = contentsBlock.getFieldValue('ALIGN'); - if (align != 'LEFT') { + if (align !== 'LEFT') { input.align = align; } args.push(input); @@ -210,12 +198,12 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) { contentsBlock.nextConnection.targetBlock(); } // Remove last input if dummy and not empty. - if (lastInput && lastInput.type == 'input_dummy') { + if (lastInput && lastInput.type === 'input_dummy') { var fields = lastInput.getInputTargetBlock('FIELDS'); - if (fields && FactoryUtils.getFieldsJson_(fields).join('').trim() != '') { + if (fields && FactoryUtils.getFieldsJson_(fields).join('').trim() !== '') { var align = lastInput.getFieldValue('ALIGN'); - if (align != 'LEFT') { - JS.lastDummyAlign0 = align; + if (align !== 'LEFT') { + JS.implicitAlign0 = align; } args.pop(); message.pop(); @@ -226,9 +214,9 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) { JS.args0 = args; } // Generate inline/external switch. - if (rootBlock.getFieldValue('INLINE') == 'EXT') { + if (rootBlock.getFieldValue('INLINE') === 'EXT') { JS.inputsInline = false; - } else if (rootBlock.getFieldValue('INLINE') == 'INT') { + } else if (rootBlock.getFieldValue('INLINE') === 'INT') { JS.inputsInline = true; } // Generate output, or next/previous connections. @@ -259,7 +247,7 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) { } // Generate colour. var colourBlock = rootBlock.getInputTargetBlock('COLOUR'); - if (colourBlock && !colourBlock.disabled) { + if (colourBlock && colourBlock.isEnabled()) { var hue = parseInt(colourBlock.getFieldValue('HUE'), 10); JS.colour = hue; } @@ -285,13 +273,15 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) { // Generate inputs. var TYPES = {'input_value': 'appendValueInput', 'input_statement': 'appendStatementInput', - 'input_dummy': 'appendDummyInput'}; + 'input_dummy': 'appendDummyInput', + 'input_end_row': 'appendEndRowInput'}; var contentsBlock = rootBlock.getInputTargetBlock('INPUTS'); while (contentsBlock) { - if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { + if (contentsBlock.isEnabled() && !contentsBlock.getInheritedDisabled()) { var name = ''; // Dummy inputs don't have names. Other inputs do. - if (contentsBlock.type != 'input_dummy') { + if (contentsBlock.type !== 'input_dummy' && + contentsBlock.type !== 'input_end_row') { name = JSON.stringify(contentsBlock.getFieldValue('INPUTNAME')); } @@ -301,7 +291,7 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) { code.push(' .setCheck(' + check + ')'); } var align = contentsBlock.getFieldValue('ALIGN'); - if (align != 'LEFT') { + if (align !== 'LEFT') { code.push(' .setAlign(Blockly.ALIGN_' + align + ')'); } var fields = FactoryUtils.getFieldsJs_( @@ -316,9 +306,9 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) { contentsBlock.nextConnection.targetBlock(); } // Generate inline/external switch. - if (rootBlock.getFieldValue('INLINE') == 'EXT') { + if (rootBlock.getFieldValue('INLINE') === 'EXT') { code.push(' this.setInputsInline(false);'); - } else if (rootBlock.getFieldValue('INLINE') == 'INT') { + } else if (rootBlock.getFieldValue('INLINE') === 'INT') { code.push(' this.setInputsInline(true);'); } // Generate output, or next/previous connections. @@ -343,7 +333,7 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) { } // Generate colour. var colourBlock = rootBlock.getInputTargetBlock('COLOUR'); - if (colourBlock && !colourBlock.disabled) { + if (colourBlock && colourBlock.isEnabled()) { var hue = parseInt(colourBlock.getFieldValue('HUE'), 10); if (!isNaN(hue)) { code.push(' this.setColour(' + hue + ');'); @@ -381,18 +371,24 @@ FactoryUtils.connectionLineJs_ = function(functionName, typeName, workspace) { /** * Returns field strings and any config. * @param {!Blockly.Block} block Input block. - * @return {!Array.} Field strings. + * @return {!Array} Field strings. * @private */ FactoryUtils.getFieldsJs_ = function(block) { var fields = []; while (block) { - if (!block.disabled && !block.getInheritedDisabled()) { + if (block.isEnabled() && !block.getInheritedDisabled()) { switch (block.type) { case 'field_static': // Result: 'hello' fields.push(JSON.stringify(block.getFieldValue('TEXT'))); break; + case 'field_label_serializable': + // Result: new Blockly.FieldLabelSerializable('Hello'), 'GREET' + fields.push('new Blockly.FieldLabelSerializable(' + + JSON.stringify(block.getFieldValue('TEXT')) + '), ' + + JSON.stringify(block.getFieldValue('FIELDNAME'))); + break; case 'field_input': // Result: new Blockly.FieldTextInput('Hello'), 'GREET' fields.push('new Blockly.FieldTextInput(' + @@ -408,11 +404,11 @@ FactoryUtils.getFieldsJs_ = function(block) { Number(block.getFieldValue('PRECISION')) ]; // Remove any trailing arguments that aren't needed. - if (args[3] == 0) { + if (args[3] === 0) { args.pop(); - if (args[2] == Infinity) { + if (args[2] === Infinity) { args.pop(); - if (args[1] == -Infinity) { + if (args[1] === -Infinity) { args.pop(); } } @@ -423,7 +419,7 @@ FactoryUtils.getFieldsJs_ = function(block) { case 'field_angle': // Result: new Blockly.FieldAngle(90), 'ANGLE' fields.push('new Blockly.FieldAngle(' + - parseFloat(block.getFieldValue('ANGLE')) + '), ' + + Number(block.getFieldValue('ANGLE')) + '), ' + JSON.stringify(block.getFieldValue('FIELDNAME'))); break; case 'field_checkbox': @@ -440,12 +436,6 @@ FactoryUtils.getFieldsJs_ = function(block) { '), ' + JSON.stringify(block.getFieldValue('FIELDNAME'))); break; - case 'field_date': - // Result: new Blockly.FieldDate('2015-02-04'), 'DATE' - fields.push('new Blockly.FieldDate(' + - JSON.stringify(block.getFieldValue('DATE')) + '), ' + - JSON.stringify(block.getFieldValue('FIELDNAME'))); - break; case 'field_variable': // Result: new Blockly.FieldVariable('item'), 'VAR' var varname @@ -473,8 +463,10 @@ FactoryUtils.getFieldsJs_ = function(block) { var width = Number(block.getFieldValue('WIDTH')); var height = Number(block.getFieldValue('HEIGHT')); var alt = JSON.stringify(block.getFieldValue('ALT')); + var flipRtl = JSON.stringify(block.getFieldValue('FLIP_RTL')); fields.push('new Blockly.FieldImage(' + - src + ', ' + width + ', ' + height + ', ' + alt + ')'); + src + ', ' + width + ', ' + height + + ', { alt: ' + alt + ', flipRtl: ' + flipRtl + ' })'); break; } } @@ -486,18 +478,25 @@ FactoryUtils.getFieldsJs_ = function(block) { /** * Returns field strings and any config. * @param {!Blockly.Block} block Input block. - * @return {!Array.} Array of static text and field configs. + * @return {!Array} Array of static text and field configs. * @private */ FactoryUtils.getFieldsJson_ = function(block) { var fields = []; while (block) { - if (!block.disabled && !block.getInheritedDisabled()) { + if (block.isEnabled() && !block.getInheritedDisabled()) { switch (block.type) { case 'field_static': // Result: 'hello' fields.push(block.getFieldValue('TEXT')); break; + case 'field_label_serializable': + fields.push({ + type: block.type, + name: block.getFieldValue('FIELDNAME'), + text: block.getFieldValue('TEXT') + }); + break; case 'field_input': fields.push({ type: block.type, @@ -509,17 +508,17 @@ FactoryUtils.getFieldsJson_ = function(block) { var obj = { type: block.type, name: block.getFieldValue('FIELDNAME'), - value: parseFloat(block.getFieldValue('VALUE')) + value: Number(block.getFieldValue('VALUE')) }; - var min = parseFloat(block.getFieldValue('MIN')); + var min = Number(block.getFieldValue('MIN')); if (min > -Infinity) { obj.min = min; } - var max = parseFloat(block.getFieldValue('MAX')); + var max = Number(block.getFieldValue('MAX')); if (max < Infinity) { obj.max = max; } - var precision = parseFloat(block.getFieldValue('PRECISION')); + var precision = Number(block.getFieldValue('PRECISION')); if (precision) { obj.precision = precision; } @@ -536,7 +535,7 @@ FactoryUtils.getFieldsJson_ = function(block) { fields.push({ type: block.type, name: block.getFieldValue('FIELDNAME'), - checked: block.getFieldValue('CHECKED') == 'TRUE' + checked: block.getFieldValue('CHECKED') === 'TRUE' }); break; case 'field_colour': @@ -546,13 +545,6 @@ FactoryUtils.getFieldsJson_ = function(block) { colour: block.getFieldValue('COLOUR') }); break; - case 'field_date': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - date: block.getFieldValue('DATE') - }); - break; case 'field_variable': fields.push({ type: block.type, @@ -580,7 +572,8 @@ FactoryUtils.getFieldsJson_ = function(block) { src: block.getFieldValue('SRC'), width: Number(block.getFieldValue('WIDTH')), height: Number(block.getFieldValue('HEIGHT')), - alt: block.getFieldValue('ALT') + alt: block.getFieldValue('ALT'), + flipRtl: block.getFieldValue('FLIP_RTL') === 'TRUE' }); break; } @@ -599,11 +592,11 @@ FactoryUtils.getFieldsJson_ = function(block) { */ FactoryUtils.getOptTypesFrom = function(block, name) { var types = FactoryUtils.getTypesFrom_(block, name); - if (types.length == 0) { + if (types.length === 0) { return undefined; - } else if (types.indexOf('null') != -1) { + } else if (types.includes('null')) { return 'null'; - } else if (types.length == 1) { + } else if (types.length === 1) { return types[0]; } else { return '[' + types.join(', ') + ']'; @@ -615,17 +608,17 @@ FactoryUtils.getOptTypesFrom = function(block, name) { * Fetch the type(s) defined in the given input. * @param {!Blockly.Block} block Block with input. * @param {string} name Name of the input. - * @return {!Array.} List of types. + * @return {!Array} List of types. * @private */ FactoryUtils.getTypesFrom_ = function(block, name) { var typeBlock = block.getInputTargetBlock(name); var types; - if (!typeBlock || typeBlock.disabled) { + if (!typeBlock || !typeBlock.isEnabled()) { types = []; - } else if (typeBlock.type == 'type_other') { + } else if (typeBlock.type === 'type_other') { types = [JSON.stringify(typeBlock.getFieldValue('TYPE'))]; - } else if (typeBlock.type == 'type_group') { + } else if (typeBlock.type === 'type_group') { types = []; for (var n = 0; n < typeBlock.typeCount_; n++) { types = types.concat(FactoryUtils.getTypesFrom_(typeBlock, 'TYPE' + n)); @@ -653,7 +646,7 @@ FactoryUtils.getTypesFrom_ = function(block, name) { FactoryUtils.getRootBlock = function(workspace) { var blocks = workspace.getTopBlocks(false); for (var i = 0, block; block = blocks[i]; i++) { - if (block.type == 'factory_base') { + if (block.type === 'factory_base') { return block; } } @@ -736,23 +729,23 @@ FactoryUtils.getDefinedBlock = function(blockType, workspace) { FactoryUtils.getBlockTypeFromJsDefinition = function(blockDef) { var indexOfStartBracket = blockDef.indexOf('[\''); var indexOfEndBracket = blockDef.indexOf('\']'); - if (indexOfStartBracket != -1 && indexOfEndBracket != -1) { + if (indexOfStartBracket !== -1 && indexOfEndBracket !== -1) { return blockDef.substring(indexOfStartBracket + 2, indexOfEndBracket); } else { - throw new Error ('Could not parse block type out of JavaScript block ' + + throw Error('Could not parse block type out of JavaScript block ' + 'definition. Brackets normally enclosing block type not found.'); } }; /** * Generates a category containing blocks of the specified block types. - * @param {!Array.} blocks Blocks to include in the category. + * @param {!Array} blocks Blocks to include in the category. * @param {string} categoryName Name to use for the generated category. * @return {!Element} Category XML containing the given block types. */ FactoryUtils.generateCategoryXml = function(blocks, categoryName) { // Create category DOM element. - var categoryElement = goog.dom.createDom('category'); + var categoryElement = Blockly.utils.xml.createElement('category'); categoryElement.setAttribute('name', categoryName); // For each block, add block element to category. @@ -772,15 +765,15 @@ FactoryUtils.generateCategoryXml = function(blocks, categoryName) { * Parses a string containing JavaScript block definition(s) to create an array * in which each element is a single block definition. * @param {string} blockDefsString JavaScript block definition(s). - * @return {!Array.} Array of block definitions. + * @return {!Array} Array of block definitions. */ FactoryUtils.parseJsBlockDefinitions = function(blockDefsString) { var blockDefArray = []; var defStart = blockDefsString.indexOf('Blockly.Blocks'); - while (blockDefsString.indexOf('Blockly.Blocks', defStart) != -1) { + while (blockDefsString.includes('Blockly.Blocks', defStart)) { var nextStart = blockDefsString.indexOf('Blockly.Blocks', defStart + 1); - if (nextStart == -1) { + if (nextStart === -1) { // This is the last block definition. nextStart = blockDefsString.length; } @@ -798,7 +791,7 @@ FactoryUtils.parseJsBlockDefinitions = function(blockDefsString) { * JSON objects. * @param {string} blockDefsString String containing JSON block * definition(s). - * @return {!Array.} Array of block definitions. + * @return {!Array} Array of block definitions. */ FactoryUtils.parseJsonBlockDefinitions = function(blockDefsString) { var blockDefArray = []; @@ -808,13 +801,13 @@ FactoryUtils.parseJsonBlockDefinitions = function(blockDefsString) { // are balanced. for (var i = 0; i < blockDefsString.length; i++) { var currentChar = blockDefsString[i]; - if (currentChar == '{') { + if (currentChar === '{') { unbalancedBracketCount++; } - else if (currentChar == '}') { + else if (currentChar === '}') { unbalancedBracketCount--; - if (unbalancedBracketCount == 0 && i > 0) { - // The brackets are balanced. We've got a complete block defintion. + if (unbalancedBracketCount === 0 && i > 0) { + // The brackets are balanced. We've got a complete block definition. var blockDef = blockDefsString.substring(defStart, i + 1); blockDefArray.push(blockDef); defStart = i + 1; @@ -828,13 +821,13 @@ FactoryUtils.parseJsonBlockDefinitions = function(blockDefsString) { * Define blocks from imported block definitions. * @param {string} blockDefsString Block definition(s). * @param {string} format Block definition format ('JSON' or 'JavaScript'). - * @return {!Array.} Array of block types defined. + * @return {!Array} Array of block types defined. */ FactoryUtils.defineAndGetBlockTypes = function(blockDefsString, format) { var blockTypes = []; // Define blocks and get block types. - if (format == 'JSON') { + if (format === 'JSON') { var blockDefArray = FactoryUtils.parseJsonBlockDefinitions(blockDefsString); // Populate array of blocktypes and define each block. @@ -849,7 +842,7 @@ FactoryUtils.defineAndGetBlockTypes = function(blockDefsString, format) { } }; } - } else if (format == 'JavaScript') { + } else if (format === 'JavaScript') { var blockDefArray = FactoryUtils.parseJsBlockDefinitions(blockDefsString); // Populate array of block types. @@ -874,9 +867,9 @@ FactoryUtils.defineAndGetBlockTypes = function(blockDefsString, format) { FactoryUtils.injectCode = function(code, id) { var pre = document.getElementById(id); pre.textContent = code; - code = pre.textContent; - code = PR.prettyPrintOne(code, 'js'); - pre.innerHTML = code; + // Remove the 'prettyprinted' class, so that Prettify will recalculate. + pre.className = pre.className.replace('prettyprinted', ''); + PR.prettyPrint(); }; /** @@ -891,33 +884,77 @@ FactoryUtils.injectCode = function(code, id) { */ FactoryUtils.sameBlockXml = function(blockXml1, blockXml2) { // Each XML element should contain a single child element with a 'block' tag - if (blockXml1.tagName.toLowerCase() != 'xml' || - blockXml2.tagName.toLowerCase() != 'xml') { - throw new Error('Expected two XML elements, recieved elements with tag ' + + if (blockXml1.tagName.toLowerCase() !== 'xml' || + blockXml2.tagName.toLowerCase() !== 'xml') { + throw Error('Expected two XML elements, received elements with tag ' + 'names: ' + blockXml1.tagName + ' and ' + blockXml2.tagName + '.'); } // Compare the block elements directly. The XML tags may include other meta - // information we want to igrore. + // information we want to ignore. var blockElement1 = blockXml1.getElementsByTagName('block')[0]; var blockElement2 = blockXml2.getElementsByTagName('block')[0]; if (!(blockElement1 && blockElement2)) { - throw new Error('Could not get find block element in XML.'); + throw Error('Could not get find block element in XML.'); } - var blockXmlText1 = Blockly.Xml.domToText(blockElement1); - var blockXmlText2 = Blockly.Xml.domToText(blockElement2); + var cleanBlockXml1 = FactoryUtils.cleanXml(blockElement1); + var cleanBlockXml2 = FactoryUtils.cleanXml(blockElement2); + + var blockXmlText1 = Blockly.Xml.domToText(cleanBlockXml1); + var blockXmlText2 = Blockly.Xml.domToText(cleanBlockXml2); // Strip white space. blockXmlText1 = blockXmlText1.replace(/\s+/g, ''); blockXmlText2 = blockXmlText2.replace(/\s+/g, ''); // Return whether or not changes have been saved. - return blockXmlText1 == blockXmlText2; + return blockXmlText1 === blockXmlText2; +}; + +/** + * Strips the provided xml of any attributes that don't describe the + * 'structure' of the blocks (i.e. block order, field values, etc). + * @param {Node} xml The xml to clean. + * @return {Node} + */ +FactoryUtils.cleanXml = function(xml) { + var newXml = xml.cloneNode(true); + var node = newXml; + while (node) { + // Things like text inside tags are still treated as nodes, but they + // don't have attributes (or the removeAttribute function) so we can + // skip removing attributes from them. + if (node.removeAttribute) { + node.removeAttribute('xmlns'); + node.removeAttribute('x'); + node.removeAttribute('y'); + node.removeAttribute('id'); + } + + // Try to go down the tree + var nextNode = node.firstChild || node.nextSibling; + // If we can't go down, try to go back up the tree. + if (!nextNode) { + nextNode = node.parentNode; + while (nextNode) { + // We are valid again! + if (nextNode.nextSibling) { + nextNode = nextNode.nextSibling; + break; + } + // Try going up again. If parentNode is null that means we have + // reached the top, and we will break out of both loops. + nextNode = nextNode.parentNode; + } + } + node = nextNode; + } + return newXml; }; -/* +/** * Checks if a block has a variable field. Blocks with variable fields cannot * be shadow blocks. * @param {Blockly.Block} block The block to check if a variable field exists. @@ -939,11 +976,11 @@ FactoryUtils.hasVariableField = function(block) { */ FactoryUtils.isProcedureBlock = function(block) { return block && - (block.type == 'procedures_defnoreturn' || - block.type == 'procedures_defreturn' || - block.type == 'procedures_callnoreturn' || - block.type == 'procedures_callreturn' || - block.type == 'procedures_ifreturn'); + (block.type === 'procedures_defnoreturn' || + block.type === 'procedures_defreturn' || + block.type === 'procedures_callnoreturn' || + block.type === 'procedures_callreturn' || + block.type === 'procedures_ifreturn'); }; /** @@ -978,7 +1015,7 @@ FactoryUtils.savedBlockChanges = function(blockLibraryController) { */ FactoryUtils.getTooltipFromRootBlock_ = function(rootBlock) { var tooltipBlock = rootBlock.getInputTargetBlock('TOOLTIP'); - if (tooltipBlock && !tooltipBlock.disabled) { + if (tooltipBlock && tooltipBlock.isEnabled()) { return tooltipBlock.getFieldValue('TEXT'); } return ''; @@ -992,7 +1029,7 @@ FactoryUtils.getTooltipFromRootBlock_ = function(rootBlock) { */ FactoryUtils.getHelpUrlFromRootBlock_ = function(rootBlock) { var helpUrlBlock = rootBlock.getInputTargetBlock('HELPURL'); - if (helpUrlBlock && !helpUrlBlock.disabled) { + if (helpUrlBlock && helpUrlBlock.isEnabled()) { return helpUrlBlock.getFieldValue('TEXT'); } return ''; diff --git a/demos/blockfactory/index.html b/demos/blockfactory/index.html index 2b49f7ef26c..3e532bc49a6 100644 --- a/demos/blockfactory/index.html +++ b/demos/blockfactory/index.html @@ -4,14 +4,16 @@ Blockly Demo: Blockly Developer Tools - - - - - + + + + + + + @@ -28,10 +30,13 @@ - + + -
    
    +              
    
                   
                 
               
    @@ -380,7 +405,7 @@ 

    Generator stub: -
    
    +              
    
                 
               
             
    @@ -390,7 +415,7 @@ 

    Generator stub:
    - + @@ -403,20 +428,17 @@

    Generator stub: + + - @@ -442,7 +464,7 @@

    Generator stub: - + diff --git a/demos/blockfactory/standard_categories.js b/demos/blockfactory/standard_categories.js index 6b4072680c5..c0e82d50357 100644 --- a/demos/blockfactory/standard_categories.js +++ b/demos/blockfactory/standard_categories.js @@ -1,21 +1,7 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** @@ -24,15 +10,13 @@ * the lower case name of the category, and contains the Category object for * that particular category. Also has a list of core block types provided * by Blockly. - * - * @author Emma Dauterman (evd2014) */ 'use strict'; /** * Namespace for StandardCategories */ -goog.provide('StandardCategories'); +var StandardCategories = StandardCategories || Object.create(null); // Map of standard category information necessary to add a standard category @@ -42,340 +26,345 @@ StandardCategories.categoryMap = Object.create(null); StandardCategories.categoryMap['logic'] = new ListElement(ListElement.TYPE_CATEGORY, 'Logic'); StandardCategories.categoryMap['logic'].xml = - Blockly.Xml.textToDom( - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + + Blockly.utils.xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + ''); -StandardCategories.categoryMap['logic'].color ='#5C81A6'; +StandardCategories.categoryMap['logic'].hue = 210; StandardCategories.categoryMap['loops'] = new ListElement(ListElement.TYPE_CATEGORY, 'Loops'); StandardCategories.categoryMap['loops'].xml = - Blockly.Xml.textToDom( - '' + - '' + - '' + - '' + - '10' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '1' + - '' + - '' + - '' + - '' + - '10' + - '' + - '' + - '' + - '' + - '1' + - '' + - '' + - '' + - '' + - '' + + Blockly.utils.xml.textToDom( + '' + + '' + + '' + + '' + + '10' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '10' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '' + ''); -StandardCategories.categoryMap['loops'].color = '#5CA65C'; +StandardCategories.categoryMap['loops'].hue = 120; StandardCategories.categoryMap['math'] = new ListElement(ListElement.TYPE_CATEGORY, 'Math'); StandardCategories.categoryMap['math'].xml = - Blockly.Xml.textToDom( - '' + - '' + - '' + - '' + - '' + - '1' + - '' + - '' + - '' + - '' + - '1' + - '' + - '' + - '' + - '' + - '' + - '' + - '9' + - '' + - '' + - '' + - '' + - '' + - '' + - '45' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '0' + - '' + - '' + - '' + - '' + - '' + - '' + - '3.1' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '64' + - '' + - '' + - '' + - '' + - '10'+ - '' + - '' + - '' + - '' + - '' + - '' + - '50' + - '' + - '' + - '' + - '' + - '1' + - '' + - '' + - '' + - '' + - '100' + - '' + - '' + - '' + - '' + - '' + - '' + - '1' + - '' + - '' + - '' + - '' + - '100' + - '' + - '' + - '' + - '' + + Blockly.utils.xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '' + + '' + + '9' + + '' + + '' + + '' + + '' + + '' + + '' + + '45' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '0' + + '' + + '' + + '' + + '' + + '' + + '' + + '3.1' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '64' + + '' + + '' + + '' + + '' + + '10'+ + '' + + '' + + '' + + '' + + '' + + '' + + '50' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '100' + + '' + + '' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '100' + + '' + + '' + + '' + + '' + ''); -StandardCategories.categoryMap['math'].color = '#5C68A6'; +StandardCategories.categoryMap['math'].hue = 230; StandardCategories.categoryMap['text'] = new ListElement(ListElement.TYPE_CATEGORY, 'Text'); StandardCategories.categoryMap['text'].xml = - Blockly.Xml.textToDom( - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - 'abc' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - 'text' + - '' + - '' + - '' + - '' + - 'abc' + - '' + - '' + - '' + - '' + - '' + - '' + - 'text' + - '' + - '' + - '' + - '' + - '' + - '' + - 'text' + - '' + - '' + - '' + - '' + - '' + - '' + - 'abc' + - '' + - '' + - '' + - '' + - '' + - '' + - 'abc' + - '' + - '' + - '' + - '' + - '' + - '' + - 'abc' + - '' + - '' + - '' + - '' + - '' + - '' + - 'abc' + - '' + - '' + - '' + + Blockly.utils.xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + 'text' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'text' + + '' + + '' + + '' + + '' + + '' + + '' + + 'text' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + ''); -StandardCategories.categoryMap['text'].color = '#5CA68D'; +StandardCategories.categoryMap['text'].hue = 160; StandardCategories.categoryMap['lists'] = new ListElement(ListElement.TYPE_CATEGORY, 'Lists'); StandardCategories.categoryMap['lists'].xml = - Blockly.Xml.textToDom( - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '5' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - 'list' + - '' + - '' + - '' + - '' + - '' + - '' + - 'list' + - '' + - '' + - '' + - '' + - '' + - '' + - 'list' + - '' + - '' + - '' + - '' + - '' + - '' + - 'list' + - '' + - '' + - '' + - '' + - '' + - '' + - ',' + - '' + - '' + - '' + - '' + + Blockly.utils.xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '5' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + ',' + + '' + + '' + + '' + + '' + ''); -StandardCategories.categoryMap['lists'].color = '#745CA6'; +StandardCategories.categoryMap['lists'].hue = 260; StandardCategories.categoryMap['colour'] = new ListElement(ListElement.TYPE_CATEGORY, 'Colour'); StandardCategories.categoryMap['colour'].xml = - Blockly.Xml.textToDom( - '' + - '' + - '' + - '' + - '' + - '' + - '100' + - '' + - '' + - '' + - '' + - '50' + - '' + - '' + - '' + - '' + - '0' + + Blockly.utils.xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '100' + + '' + + '' + + '' + + '' + + '50' + + '' + + '' + + '' + + '' + + '0' + + '' + + '' + + '' + + '' + + '' + + '' + + '#ff0000' + + '' + + '' + + '' + + '' + + '#3333ff' + + '' + + '' + + '' + + '' + + '0.5' + '' + - '' + - '' + - '' + - '' + - '' + - '#ff0000' + - '' + - '' + - '' + - '' + - '#3333ff' + - '' + - '' + - '' + - '' + - '0.5' + - '' + - '' + - '' + + '' + + '' + ''); -StandardCategories.categoryMap['colour'].color = '#A6745C'; +StandardCategories.categoryMap['colour'].hue = 20; StandardCategories.categoryMap['functions'] = new ListElement(ListElement.TYPE_CATEGORY, 'Functions'); -StandardCategories.categoryMap['functions'].color = '#9A5CA6' +StandardCategories.categoryMap['functions'].hue = 290; StandardCategories.categoryMap['functions'].custom = 'PROCEDURE'; StandardCategories.categoryMap['variables'] = new ListElement(ListElement.TYPE_CATEGORY, 'Variables'); -StandardCategories.categoryMap['variables'].color = '#A65C81'; +StandardCategories.categoryMap['variables'].hue = 330; StandardCategories.categoryMap['variables'].custom = 'VARIABLE'; +StandardCategories.categoryMap['typedvariables'] = + new ListElement(ListElement.TYPE_CATEGORY, 'TypedVariables'); +StandardCategories.categoryMap['typedvariables'].custom = 'VARIABLE_DYNAMIC'; +StandardCategories.categoryMap['typedvariables'].hue = 290; + // All standard block types in provided in Blockly core. StandardCategories.coreBlockTypes = ["controls_if", "logic_compare", "logic_operation", "logic_negate", "logic_boolean", "logic_null", diff --git a/demos/blockfactory/workspacefactory/wfactory_controller.js b/demos/blockfactory/workspacefactory/wfactory_controller.js index 5d80f485858..385feede8ec 100644 --- a/demos/blockfactory/workspacefactory/wfactory_controller.js +++ b/demos/blockfactory/workspacefactory/wfactory_controller.js @@ -1,21 +1,7 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** @@ -30,14 +16,8 @@ * - updating the preview workspace * - changing a category name * - moving the position of a category. - * - * @author Emma Dauterman (evd2014) */ - goog.require('FactoryUtils'); - goog.require('StandardCategories'); - - /** * Class for a WorkspaceFactoryController * @param {string} toolboxName Name of workspace toolbox XML. @@ -68,7 +48,7 @@ WorkspaceFactoryController = function(toolboxName, toolboxDiv, previewDiv) { colour: '#ccc', snap: true}, media: '../../media/', - toolbox: '', + toolbox: '', zoom: {controls: true, wheel: true} @@ -94,7 +74,7 @@ WorkspaceFactoryController = function(toolboxName, toolboxDiv, previewDiv) { // toolbox. WorkspaceFactoryController.MODE_TOOLBOX = 'toolbox'; // Pre-loaded workspace editing mode. Changes the user makes to the workspace -// udpates the pre-loaded blocks. +// updates the pre-loaded blocks. WorkspaceFactoryController.MODE_PRELOAD = 'preload'; /** @@ -171,7 +151,7 @@ WorkspaceFactoryController.prototype.transferFlyoutBlocksToCategory = // Saves the user's blocks from the flyout in a category if there is no // toolbox and the user has dragged in blocks. if (!this.model.hasElements() && - this.toolboxWorkspace.getAllBlocks().length > 0) { + this.toolboxWorkspace.getAllBlocks(false).length > 0) { // Create the new category. this.createCategory('Category 1', true); // Set the new category as selected. @@ -265,7 +245,7 @@ WorkspaceFactoryController.prototype.switchElement = function(id) { Blockly.Events.disable(); // Caches information to reload or generate XML if switching to/from element. // Only saves if a category is selected. - if (this.model.getSelectedId() != null && id != null) { + if (this.model.getSelectedId() !== null && id !== null) { this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace); } // Load element. @@ -281,13 +261,13 @@ WorkspaceFactoryController.prototype.switchElement = function(id) { */ WorkspaceFactoryController.prototype.clearAndLoadElement = function(id) { // Unselect current tab if switching to and from an element. - if (this.model.getSelectedId() != null && id != null) { + if (this.model.getSelectedId() !== null && id !== null) { this.view.setCategoryTabSelection(this.model.getSelectedId(), false); } // If switching to another category, set category selection in the model and // view. - if (id != null) { + if (id !== null) { // Set next category. this.model.setSelectedById(id); @@ -319,7 +299,7 @@ WorkspaceFactoryController.prototype.clearAndLoadElement = function(id) { */ WorkspaceFactoryController.prototype.exportXmlFile = function(exportMode) { // Get file name. - if (exportMode == WorkspaceFactoryController.MODE_TOOLBOX) { + if (exportMode === WorkspaceFactoryController.MODE_TOOLBOX) { var fileName = prompt('File Name for toolbox XML:', 'toolbox.xml'); } else { var fileName = prompt('File Name for pre-loaded workspace XML:', @@ -330,25 +310,37 @@ WorkspaceFactoryController.prototype.exportXmlFile = function(exportMode) { } // Generate XML. - if (exportMode == WorkspaceFactoryController.MODE_TOOLBOX) { + if (exportMode === WorkspaceFactoryController.MODE_TOOLBOX) { // Export the toolbox XML. - var configXml = Blockly.Xml.domToPrettyText - (this.generator.generateToolboxXml()); + var configXml = Blockly.Xml.domToPrettyText( + this.generator.generateToolboxXml()); this.hasUnsavedToolboxChanges = false; - } else if (exportMode == WorkspaceFactoryController.MODE_PRELOAD) { + } else if (exportMode === WorkspaceFactoryController.MODE_PRELOAD) { // Export the pre-loaded block XML. - var configXml = Blockly.Xml.domToPrettyText - (this.generator.generateWorkspaceXml()); + var configXml = Blockly.Xml.domToPrettyText( + this.generator.generateWorkspaceXml()); this.hasUnsavedPreloadChanges = false; } else { // Unknown mode. Throw error. - throw new Error ("Unknown export mode: " + exportMode); + var msg = 'Unknown export mode: ' + exportMode; + BlocklyDevTools.Analytics.onError(msg); + throw Error(msg); } // Download file. var data = new Blob([configXml], {type: 'text/xml'}); this.view.createAndDownloadFile(fileName, data); - }; + + if (exportMode === WorkspaceFactoryController.MODE_TOOLBOX) { + BlocklyDevTools.Analytics.onExport( + BlocklyDevTools.Analytics.TOOLBOX, + { format: BlocklyDevTools.Analytics.FORMAT_XML }); + } else if (exportMode === WorkspaceFactoryController.MODE_PRELOAD) { + BlocklyDevTools.Analytics.onExport( + BlocklyDevTools.Analytics.WORKSPACE_CONTENTS, + { format: BlocklyDevTools.Analytics.FORMAT_XML }); + } +}; /** * Export the options object to be used for the Blockly inject call. Gets a @@ -366,6 +358,13 @@ WorkspaceFactoryController.prototype.exportInjectFile = function() { var printableOptions = this.generator.generateInjectString() var data = new Blob([printableOptions], {type: 'text/javascript'}); this.view.createAndDownloadFile(fileName, data); + + BlocklyDevTools.Analytics.onExport( + BlocklyDevTools.Analytics.STARTER_CODE, + { + format: BlocklyDevTools.Analytics.FORMAT_JS, + platform: BlocklyDevTools.Analytics.PLATFORM_WEB + }); }; /** @@ -376,8 +375,7 @@ WorkspaceFactoryController.prototype.printConfig = function() { // Capture any changes made by user before generating XML. this.saveStateFromWorkspace(); // Print XML. - window.console.log(Blockly.Xml.domToPrettyText - (this.generator.generateToolboxXml())); + console.log(Blockly.Xml.domToPrettyText(this.generator.generateToolboxXml())); }; /** @@ -398,11 +396,11 @@ WorkspaceFactoryController.prototype.updatePreview = function() { // Only update the toolbox if not in read only mode. if (!this.model.options['readOnly']) { // Get toolbox XML. - var tree = Blockly.Options.parseToolboxTree( + var tree = Blockly.utils.toolbox.parseToolboxTree( this.generator.generateToolboxXml()); // No categories, creates a simple flyout. - if (tree.getElementsByTagName('category').length == 0) { + if (tree.getElementsByTagName('category').length === 0) { // No categories, creates a simple flyout. if (this.previewWorkspace.toolbox_) { this.reinjectPreview(tree); // Switch to simple flyout, expensive. @@ -436,20 +434,20 @@ WorkspaceFactoryController.prototype.updatePreview = function() { * be called after making changes to the workspace. */ WorkspaceFactoryController.prototype.saveStateFromWorkspace = function() { - if (this.selectedMode == WorkspaceFactoryController.MODE_TOOLBOX) { + if (this.selectedMode === WorkspaceFactoryController.MODE_TOOLBOX) { // If currently editing the toolbox. // Update flags if toolbox has been changed. - if (this.model.getSelectedXml() != + if (this.model.getSelectedXml() !== Blockly.Xml.workspaceToDom(this.toolboxWorkspace)) { this.hasUnsavedToolboxChanges = true; } this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace); - } else if (this.selectedMode == WorkspaceFactoryController.MODE_PRELOAD) { + } else if (this.selectedMode === WorkspaceFactoryController.MODE_PRELOAD) { // If currently editing the pre-loaded workspace. // Update flags if preloaded blocks have been changed. - if (this.model.getPreloadXml() != + if (this.model.getPreloadXml() !== Blockly.Xml.workspaceToDom(this.toolboxWorkspace)) { this.hasUnsavedPreloadChanges = true; } @@ -476,26 +474,25 @@ WorkspaceFactoryController.prototype.reinjectPreview = function(tree) { }; /** - * Tied to "change name" button. Changes the name of the selected category. - * Continues prompting the user until they input a category name that is not - * currently in use, exits if user presses cancel. + * Changes the name and colour of the selected category. + * Return if selected element is a separator. + * @param {string} name New name for selected category. + * @param {?string} colour New colour for selected category, or null if none. + * Must be a valid CSS string, or '' for none. */ -WorkspaceFactoryController.prototype.changeCategoryName = function() { +WorkspaceFactoryController.prototype.changeSelectedCategory = function(name, + colour) { var selected = this.model.getSelected(); // Return if a category is not selected. - if (selected.type != ListElement.TYPE_CATEGORY) { - return; - } - // Get new name from user. - window.foo = selected; - var newName = this.promptForNewCategoryName('What do you want to change this' - + ' category\'s name to?', selected.name); - if (!newName) { // If cancelled. + if (selected.type !== ListElement.TYPE_CATEGORY) { return; } + // Change colour of selected category. + selected.changeColour(colour); + this.view.setBorderColour(this.model.getSelectedId(), colour); // Change category name. - selected.changeName(newName); - this.view.updateCategoryName(newName, this.model.getSelectedId()); + selected.changeName(name); + this.view.updateCategoryName(name, this.model.getSelectedId()); // Update preview. this.updatePreview(); }; @@ -542,24 +539,6 @@ WorkspaceFactoryController.prototype.moveElementToIndex = function(element, this.view.moveTabToIndex(element.id, newIndex, oldIndex); }; -/** - * Changes the color of the selected category. Return if selected element is - * a separator. - * @param {string} color The color to change the selected category. Must be - * a valid CSS string. - */ -WorkspaceFactoryController.prototype.changeSelectedCategoryColor = - function(color) { - // Return if category is not selected. - if (this.model.getSelected().type != ListElement.TYPE_CATEGORY) { - return; - } - // Change color of selected category. - this.model.getSelected().changeColor(color); - this.view.setBorderColor(this.model.getSelectedId(), color); - this.updatePreview(); -}; - /** * Tied to the "Standard Category" dropdown option, this function prompts * the user for a name of a standard Blockly category (case insensitive) and @@ -569,7 +548,8 @@ WorkspaceFactoryController.prototype.loadCategory = function() { // Prompt user for the name of the standard category to load. do { var name = prompt('Enter the name of the category you would like to import ' - + '(Logic, Loops, Math, Text, Lists, Colour, Variables, or Functions)'); + + '(Logic, Loops, Math, Text, Lists, Colour, Variables, TypedVariables ' + + 'or Functions)'); if (!name) { return; // Exit if cancelled. } @@ -589,12 +569,12 @@ WorkspaceFactoryController.prototype.loadCategoryByName = function(name) { if (!this.isStandardCategoryName(name)) { return; } - if (this.model.hasVariables() && name.toLowerCase() == 'variables') { + if (this.model.hasVariables() && name.toLowerCase() === 'variables') { alert('A Variables category already exists. You cannot create multiple' + ' variables categories.'); return; } - if (this.model.hasProcedures() && name.toLowerCase() == 'functions') { + if (this.model.hasProcedures() && name.toLowerCase() === 'functions') { alert('A Functions category already exists. You cannot create multiple' + ' functions categories.'); return; @@ -606,6 +586,11 @@ WorkspaceFactoryController.prototype.loadCategoryByName = function(name) { + '. Rename your category and try again.'); return; } + if (!standardCategory.colour && standardCategory.hue !== undefined) { + // Calculate the hex colour based on the hue. + standardCategory.colour = Blockly.utils.colour.hueToHex( + standardCategory.hue); + } // Transfers current flyout blocks to a category if it's the first category // created. this.transferFlyoutBlocksToCategory(); @@ -620,9 +605,9 @@ WorkspaceFactoryController.prototype.loadCategoryByName = function(name) { // Update the copy in the view. var tab = this.view.addCategoryRow(copy.name, copy.id); this.addClickToSwitch(tab, copy.id); - // Color the category tab in the view. - if (copy.color) { - this.view.setBorderColor(copy.id, copy.color); + // Colour the category tab in the view. + if (copy.colour) { + this.view.setBorderColour(copy.id, copy.colour); } // Switch to loaded category. this.switchElement(copy.id); @@ -664,12 +649,7 @@ WorkspaceFactoryController.prototype.loadStandardToolbox = function() { * @return {boolean} True if name is a standard category name, false otherwise. */ WorkspaceFactoryController.prototype.isStandardCategoryName = function(name) { - for (var category in StandardCategories.categoryMap) { - if (name.toLowerCase() == category) { - return true; - } - } - return false; + return !!StandardCategories.categoryMap[name.toLowerCase()]; }; /** @@ -721,41 +701,53 @@ WorkspaceFactoryController.prototype.importFile = function(file, importMode) { // Try to parse XML from file and load it into toolbox editing area. // Print error message if fail. try { - var tree = Blockly.Xml.textToDom(reader.result); - if (importMode == WorkspaceFactoryController.MODE_TOOLBOX) { + var tree = Blockly.utils.xml.textToDom(reader.result); + if (importMode === WorkspaceFactoryController.MODE_TOOLBOX) { // Switch mode. controller.setMode(WorkspaceFactoryController.MODE_TOOLBOX); // Confirm that the user wants to override their current toolbox. var hasToolboxElements = controller.model.hasElements() || - controller.toolboxWorkspace.getAllBlocks().length > 0; - if (hasToolboxElements && - !confirm('Are you sure you want to import? You will lose your ' + - 'current toolbox.')) { - return; + controller.toolboxWorkspace.getAllBlocks(false).length > 0; + if (hasToolboxElements) { + var msg = 'Are you sure you want to import? You will lose your ' + + 'current toolbox.'; + BlocklyDevTools.Analytics.onWarning(msg); + var continueAnyway = confirm(); + if (!continueAnyway) { + return; + } } // Import toolbox XML. controller.importToolboxFromTree_(tree); + BlocklyDevTools.Analytics.onImport('Toolbox.xml'); - } else if (importMode == WorkspaceFactoryController.MODE_PRELOAD) { + } else if (importMode === WorkspaceFactoryController.MODE_PRELOAD) { // Switch mode. controller.setMode(WorkspaceFactoryController.MODE_PRELOAD); // Confirm that the user wants to override their current blocks. - if (controller.toolboxWorkspace.getAllBlocks().length > 0 && - !confirm('Are you sure you want to import? You will lose your ' + - 'current workspace blocks.')) { + if (controller.toolboxWorkspace.getAllBlocks(false).length > 0) { + var msg = 'Are you sure you want to import? You will lose your ' + + 'current workspace blocks.'; + var continueAnyway = confirm(msg); + BlocklyDevTools.Analytics.onWarning(msg); + if (!continueAnyway) { return; + } } // Import pre-loaded workspace XML. controller.importPreloadFromTree_(tree); + BlocklyDevTools.Analytics.onImport('WorkspaceContents.xml'); } else { // Throw error if invalid mode. - throw new Error("Unknown import mode: " + importMode); + throw Error('Unknown import mode: ' + importMode); } } catch(e) { - alert('Cannot load XML from file.'); + var msg = 'Cannot load XML from file.'; + alert(msg); + BlocklyDevTools.Analytics.onError(msg); console.log(e); } finally { Blockly.Events.enable(); @@ -778,7 +770,7 @@ WorkspaceFactoryController.prototype.importToolboxFromTree_ = function(tree) { this.model.clearToolboxList(); this.view.clearToolboxTabs(); - if (tree.getElementsByTagName('category').length == 0) { + if (tree.getElementsByTagName('category').length === 0) { // No categories present. // Load all the blocks into a single category evenly spaced. Blockly.Xml.domToWorkspace(tree, this.toolboxWorkspace); @@ -794,7 +786,7 @@ WorkspaceFactoryController.prototype.importToolboxFromTree_ = function(tree) { // Categories/separators present. for (var i = 0, item; item = tree.children[i]; i++) { - if (item.tagName == 'category') { + if (item.tagName === 'category') { // If the element is a category, create a new category and switch to it. this.createCategory(item.getAttribute('name'), false); var category = this.model.getElementByIndex(i); @@ -812,10 +804,10 @@ WorkspaceFactoryController.prototype.importToolboxFromTree_ = function(tree) { // Convert actual shadow blocks to user-generated shadow blocks. this.convertShadowBlocks(); - // Set category color. + // Set category colour. if (item.getAttribute('colour')) { - category.changeColor(item.getAttribute('colour')); - this.view.setBorderColor(category.id, category.color); + category.changeColour(item.getAttribute('colour')); + this.view.setBorderColour(category.id, category.colour); } // Set any custom tags. if (item.getAttribute('custom')) { @@ -888,14 +880,15 @@ WorkspaceFactoryController.prototype.importPreloadFromTree_ = function(tree) { * "Clear" button. */ WorkspaceFactoryController.prototype.clearAll = function() { - if (!confirm('Are you sure you want to clear all of your work in Workspace' + - ' Factory?')) { + var msg = 'Are you sure you want to clear all of your work in Workspace' + + ' Factory?'; + BlocklyDevTools.Analytics.onWarning(msg); + if (!confirm(msg)) { return; } - var hasCategories = this.model.hasElements(); this.model.clearToolboxList(); this.view.clearToolboxTabs(); - this.model.savePreloadXml(Blockly.Xml.textToDom('')); + this.model.savePreloadXml(Blockly.utils.xml.createElement('xml')); this.view.addEmptyCategoryMessage(); this.view.updateState(-1, null); this.toolboxWorkspace.clear(); @@ -908,7 +901,7 @@ WorkspaceFactoryController.prototype.clearAll = function() { this.updatePreview(); }; -/* +/** * Makes the currently selected block a user-generated shadow block. These * blocks are not made into real shadow blocks, but recorded in the model * and visually marked as shadow blocks, allowing the user to move and edit @@ -917,14 +910,14 @@ WorkspaceFactoryController.prototype.clearAll = function() { */ WorkspaceFactoryController.prototype.addShadow = function() { // No block selected to make a shadow block. - if (!Blockly.selected) { + if (!Blockly.common.getSelected()) { return; } // Clear any previous warnings on the block (would only have warnings on // a non-shadow block if it was nested inside another shadow block). - Blockly.selected.setWarningText(null); + Blockly.common.getSelected().setWarningText(null); // Set selected block and all children as shadow blocks. - this.addShadowForBlockAndChildren_(Blockly.selected); + this.addShadowForBlockAndChildren_(Blockly.common.getSelected()); // Save and update the preview. this.saveStateFromWorkspace(); @@ -962,14 +955,14 @@ WorkspaceFactoryController.prototype.addShadowForBlockAndChildren_ = */ WorkspaceFactoryController.prototype.removeShadow = function() { // No block selected to modify. - if (!Blockly.selected) { + if (!Blockly.common.getSelected()) { return; } - this.model.removeShadowBlock(Blockly.selected.id); - this.view.unmarkShadowBlock(Blockly.selected); + this.model.removeShadowBlock(Blockly.common.getSelected().id); + this.view.unmarkShadowBlock(Blockly.common.getSelected()); // If turning invalid shadow block back to normal block, remove warning. - Blockly.selected.setWarningText(null); + Blockly.common.getSelected().setWarningText(null); this.saveStateFromWorkspace(); this.updatePreview(); @@ -993,7 +986,7 @@ WorkspaceFactoryController.prototype.isUserGenShadowBlock = function(blockId) { * shadow blocks in the view but are still editable and movable. */ WorkspaceFactoryController.prototype.convertShadowBlocks = function() { - var blocks = this.toolboxWorkspace.getAllBlocks(); + var blocks = this.toolboxWorkspace.getAllBlocks(false); for (var i = 0, block; block = blocks[i]; i++) { if (block.isShadow()) { block.setShadow(false); @@ -1021,12 +1014,12 @@ WorkspaceFactoryController.prototype.convertShadowBlocks = function() { */ WorkspaceFactoryController.prototype.setMode = function(mode) { // No work to change mode that's currently set. - if (this.selectedMode == mode) { + if (this.selectedMode === mode) { return; } // No work to change mode that's currently set. - if (this.selectedMode == mode) { + if (this.selectedMode === mode) { return; } @@ -1039,7 +1032,7 @@ WorkspaceFactoryController.prototype.setMode = function(mode) { // Update help text above workspace. this.view.updateHelpText(mode); - if (mode == WorkspaceFactoryController.MODE_TOOLBOX) { + if (mode === WorkspaceFactoryController.MODE_TOOLBOX) { // Open the toolbox editing space. this.model.savePreloadXml (Blockly.Xml.workspaceToDom(this.toolboxWorkspace)); @@ -1067,7 +1060,7 @@ WorkspaceFactoryController.prototype.clearAndLoadXml_ = function(xml) { this.toolboxWorkspace.clearUndo(); Blockly.Xml.domToWorkspace(xml, this.toolboxWorkspace); this.view.markShadowBlocks(this.model.getShadowBlocksInWorkspace - (this.toolboxWorkspace.getAllBlocks())); + (this.toolboxWorkspace.getAllBlocks(false))); this.warnForUndefinedBlocks_(); }; @@ -1090,8 +1083,8 @@ WorkspaceFactoryController.prototype.setStandardOptionsAndUpdate = function() { WorkspaceFactoryController.prototype.generateNewOptions = function() { this.model.setOptions(this.readOptions_()); - this.reinjectPreview(Blockly.Options.parseToolboxTree - (this.generator.generateToolboxXml())); + this.reinjectPreview(Blockly.utils.toolbox.parseToolboxTree( + this.generator.generateToolboxXml())); }; /** @@ -1120,7 +1113,7 @@ WorkspaceFactoryController.prototype.readOptions_ = function() { } else { var maxBlocksValue = document.getElementById('option_maxBlocks_number').value; - optionsObj['maxBlocks'] = typeof maxBlocksValue == 'string' ? + optionsObj['maxBlocks'] = typeof maxBlocksValue === 'string' ? parseInt(maxBlocksValue) : maxBlocksValue; } optionsObj['trashcan'] = @@ -1147,10 +1140,10 @@ WorkspaceFactoryController.prototype.readOptions_ = function() { var grid = Object.create(null); var spacingValue = document.getElementById('gridOption_spacing_number').value; - grid['spacing'] = typeof spacingValue == 'string' ? + grid['spacing'] = typeof spacingValue === 'string' ? parseInt(spacingValue) : spacingValue; var lengthValue = document.getElementById('gridOption_length_number').value; - grid['length'] = typeof lengthValue == 'string' ? + grid['length'] = typeof lengthValue === 'string' ? parseInt(lengthValue) : lengthValue; grid['colour'] = document.getElementById('gridOption_colour_text').value; if (!readonly) { @@ -1169,20 +1162,20 @@ WorkspaceFactoryController.prototype.readOptions_ = function() { document.getElementById('zoomOption_wheel_checkbox').checked; var startScaleValue = document.getElementById('zoomOption_startScale_number').value; - zoom['startScale'] = typeof startScaleValue == 'string' ? - parseFloat(startScaleValue) : startScaleValue; + zoom['startScale'] = typeof startScaleValue === 'string' ? + Number(startScaleValue) : startScaleValue; var maxScaleValue = document.getElementById('zoomOption_maxScale_number').value; - zoom['maxScale'] = typeof maxScaleValue == 'string' ? - parseFloat(maxScaleValue) : maxScaleValue; + zoom['maxScale'] = typeof maxScaleValue === 'string' ? + Number(maxScaleValue) : maxScaleValue; var minScaleValue = document.getElementById('zoomOption_minScale_number').value; - zoom['minScale'] = typeof minScaleValue == 'string' ? - parseFloat(minScaleValue) : minScaleValue; + zoom['minScale'] = typeof minScaleValue === 'string' ? + Number(minScaleValue) : minScaleValue; var scaleSpeedValue = document.getElementById('zoomOption_scaleSpeed_number').value; - zoom['scaleSpeed'] = typeof scaleSpeedValue == 'string' ? - parseFloat(scaleSpeedValue) : scaleSpeedValue; + zoom['scaleSpeed'] = typeof scaleSpeedValue === 'string' ? + Number(scaleSpeedValue) : scaleSpeedValue; optionsObj['zoom'] = zoom; } @@ -1213,18 +1206,22 @@ WorkspaceFactoryController.prototype.importBlocks = function(file, format) { // If an imported block type is already defined, check if the user wants // to override the current block definition. - if (controller.model.hasDefinedBlockTypes(blockTypes) && - !confirm('An imported block uses the same name as a block ' - + 'already in your toolbox. Are you sure you want to override the ' - + 'currently defined block?')) { + if (controller.model.hasDefinedBlockTypes(blockTypes)) { + var msg = 'An imported block uses the same name as a block ' + + 'already in your toolbox. Are you sure you want to override the ' + + 'currently defined block?'; + var continueAnyway = confirm(msg); + BlocklyDevTools.Analytics.onWarning(msg); + if (!continueAnyway) { return; + } } var blocks = controller.generator.getDefinedBlocks(blockTypes); // Generate category XML and append to toolbox. var categoryXml = FactoryUtils.generateCategoryXml(blocks, categoryName); - // Get random color for category between 0 and 360. Gives each imported - // category a different color. + // Get random colour for category between 0 and 360. Gives each imported + // category a different colour. var randomColor = Math.floor(Math.random() * 360); categoryXml.setAttribute('colour', randomColor); controller.toolbox.appendChild(categoryXml); @@ -1234,8 +1231,13 @@ WorkspaceFactoryController.prototype.importBlocks = function(file, format) { // Reload current category to possibly reflect any newly defined blocks. controller.clearAndLoadXml_ (Blockly.Xml.workspaceToDom(controller.toolboxWorkspace)); + + BlocklyDevTools.Analytics.onImport('BlockDefinitions' + + (format === 'JSON' ? '.json' : '.js')); } catch (e) { - alert('Cannot read blocks from file.'); + msg = 'Cannot read blocks from file.'; + alert(msg); + BlocklyDevTools.Analytics.onError(msg); window.console.log(e); } } @@ -1244,10 +1246,10 @@ WorkspaceFactoryController.prototype.importBlocks = function(file, format) { reader.readAsText(file); }; -/* +/** * Updates the block library category in the toolbox workspace toolbox. * @param {!Element} categoryXml XML for the block library category. - * @param {!Array.} libBlockTypes Array of block types from the block + * @param {!Array} libBlockTypes Array of block types from the block * library. */ WorkspaceFactoryController.prototype.setBlockLibCategory = @@ -1255,8 +1257,8 @@ WorkspaceFactoryController.prototype.setBlockLibCategory = var blockLibCategory = document.getElementById('blockLibCategory'); // Set category ID so that it can be easily replaced, and set a standard, - // arbitrary block library color. - categoryXml.setAttribute('id', 'blockLibCategory'); + // arbitrary block library colour. + categoryXml.id = 'blockLibCategory'; categoryXml.setAttribute('colour', 260); // Update the toolbox and toolboxWorkspace. @@ -1274,7 +1276,7 @@ WorkspaceFactoryController.prototype.setBlockLibCategory = /** * Return the block types used in the custom toolbox and pre-loaded workspace. - * @return {!Array.} Block types used in the custom toolbox and + * @return {!Array} Block types used in the custom toolbox and * pre-loaded workspace. */ WorkspaceFactoryController.prototype.getAllUsedBlockTypes = function() { @@ -1296,16 +1298,16 @@ WorkspaceFactoryController.prototype.isDefinedBlock = function(block) { * @private */ WorkspaceFactoryController.prototype.warnForUndefinedBlocks_ = function() { - var blocks = this.toolboxWorkspace.getAllBlocks(); + var blocks = this.toolboxWorkspace.getAllBlocks(false); for (var i = 0, block; block = blocks[i]; i++) { if (!this.isDefinedBlock(block)) { - block.setWarningText(block.type + ' is not defined (it is not a standard ' - + 'block, \nin your block library, or an imported block)'); + block.setWarningText(block.type + ' is not defined (it is not a ' + + 'standard block,\nin your block library, or an imported block)'); } } }; -/* +/** * Determines if a standard variable category is in the custom toolbox. * @return {boolean} True if a variables category is in use, false otherwise. */ diff --git a/demos/blockfactory/workspacefactory/wfactory_generator.js b/demos/blockfactory/workspacefactory/wfactory_generator.js index 3b06ac282e1..ca1a47b090e 100644 --- a/demos/blockfactory/workspacefactory/wfactory_generator.js +++ b/demos/blockfactory/workspacefactory/wfactory_generator.js @@ -1,21 +1,7 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** @@ -24,12 +10,8 @@ * Blockly.Xml and depends on information in the model (holds a reference). * Depends on a hidden workspace created in the generator to load saved XML in * order to generate toolbox XML. - * - * @author Emma Dauterman (evd2014) */ -goog.require('FactoryUtils'); - /** * Class for a WorkspaceFactoryGenerator @@ -42,7 +24,7 @@ WorkspaceFactoryGenerator = function(model) { var hiddenBlocks = document.createElement('div'); // Generate a globally unique ID for the hidden div element to avoid // collisions. - var hiddenBlocksId = Blockly.utils.genUid(); + var hiddenBlocksId = Blockly.utils.idGenerator.genUid(); hiddenBlocks.id = hiddenBlocksId; hiddenBlocks.style.display = 'none'; document.body.appendChild(hiddenBlocks); @@ -61,20 +43,19 @@ WorkspaceFactoryGenerator = function(model) { */ WorkspaceFactoryGenerator.prototype.generateToolboxXml = function() { // Create DOM for XML. - var xmlDom = goog.dom.createDom('xml', - { - 'id' : 'toolbox', - 'style' : 'display:none' - }); + var xmlDom = Blockly.utils.xml.createElement('xml'); + xmlDom.id = 'toolbox'; + xmlDom.setAttribute('style', 'display: none'); + if (!this.model.hasElements()) { // Toolbox has no categories. Use XML directly from workspace. this.loadToHiddenWorkspace_(this.model.getSelectedXml()); this.appendHiddenWorkspaceToDom_(xmlDom); } else { // Toolbox has categories. - // Assert that selected != null + // Assert that selected !== null if (!this.model.getSelected()) { - throw new Error('Selected is null when the toolbox is empty.'); + throw Error('Selected is null when the toolbox is empty.'); } var xml = this.model.getSelectedXml(); @@ -86,19 +67,19 @@ WorkspaceFactoryGenerator.prototype.generateToolboxXml = function() { // groups in the flyout. for (var i = 0; i < toolboxList.length; i++) { var element = toolboxList[i]; - if (element.type == ListElement.TYPE_SEPARATOR) { + if (element.type === ListElement.TYPE_SEPARATOR) { // If the next element is a separator. - var nextElement = goog.dom.createDom('sep'); - } else if (element.type == ListElement.TYPE_CATEGORY) { + var nextElement = Blockly.utils.xml.createElement('sep'); + } else if (element.type === ListElement.TYPE_CATEGORY) { // If the next element is a category. - var nextElement = goog.dom.createDom('category'); + var nextElement = Blockly.utils.xml.createElement('category'); nextElement.setAttribute('name', element.name); // Add a colour attribute if one exists. - if (element.color != null) { - nextElement.setAttribute('colour', element.color); + if (element.colour !== null) { + nextElement.setAttribute('colour', element.colour); } // Add a custom attribute if one exists. - if (element.custom != null) { + if (element.custom !== null) { nextElement.setAttribute('custom', element.custom); } // Load that category to hidden workspace, setting user-generated shadow @@ -128,10 +109,10 @@ WorkspaceFactoryGenerator.prototype.generateWorkspaceXml = function() { this.setShadowBlocksInHiddenWorkspace_(); // Generate XML and set attributes. - var generatedXml = Blockly.Xml.workspaceToDom(this.hiddenWorkspace); - generatedXml.setAttribute('id', 'workspaceBlocks'); - generatedXml.setAttribute('style', 'display:none'); - return generatedXml; + var xmlDom = Blockly.Xml.workspaceToDom(this.hiddenWorkspace); + xmlDom.id = 'workspaceBlocks'; + xmlDom.setAttribute('style', 'display: none'); + return xmlDom; }; /** @@ -146,10 +127,10 @@ WorkspaceFactoryGenerator.prototype.generateInjectString = function() { } var str = ''; for (var key in obj) { - if (key == 'grid' || key == 'zoom') { + if (key === 'grid' || key === 'zoom') { var temp = tabChar + key + ' : {\n' + addAttributes(obj[key], tabChar + '\t') + tabChar + '}, \n'; - } else if (typeof obj[key] == 'string') { + } else if (typeof obj[key] === 'string') { var temp = tabChar + key + ' : \'' + obj[key] + '\', \n'; } else { var temp = tabChar + key + ' : ' + obj[key] + ', \n'; @@ -178,7 +159,7 @@ WorkspaceFactoryGenerator.prototype.generateInjectString = function() { ' workspace blocks XML from Workspace Factory. */\n' + 'var workspaceBlocks = document.getElementById("workspaceBlocks"); \n\n' + '/* Load blocks to workspace. */\n' + - 'Blockly.Xml.domToWorkspace(workspace, workspaceBlocks);'; + 'Blockly.Xml.domToWorkspace(workspaceBlocks, workspace);'; return finalStr; }; @@ -218,7 +199,7 @@ WorkspaceFactoryGenerator.prototype.appendHiddenWorkspaceToDom_ = */ WorkspaceFactoryGenerator.prototype.setShadowBlocksInHiddenWorkspace_ = function() { - var blocks = this.hiddenWorkspace.getAllBlocks(); + var blocks = this.hiddenWorkspace.getAllBlocks(false); for (var i = 0; i < blocks.length; i++) { if (this.model.isShadowBlock(blocks[i].id)) { blocks[i].setShadow(true); @@ -229,8 +210,8 @@ WorkspaceFactoryGenerator.prototype.setShadowBlocksInHiddenWorkspace_ = /** * Given a set of block types, gets the Blockly.Block objects for each block * type. - * @param {!Array.} blockTypes Array of blocks that have been defined. - * @return {!Array.} Array of Blockly.Block objects corresponding + * @param {!Array} blockTypes Array of blocks that have been defined. + * @return {!Array} Array of Blockly.Block objects corresponding * to the array of blockTypes. */ WorkspaceFactoryGenerator.prototype.getDefinedBlocks = function(blockTypes) { diff --git a/demos/blockfactory/workspacefactory/wfactory_init.js b/demos/blockfactory/workspacefactory/wfactory_init.js index 6b619a76f8c..352c1d220d7 100644 --- a/demos/blockfactory/workspacefactory/wfactory_init.js +++ b/demos/blockfactory/workspacefactory/wfactory_init.js @@ -1,21 +1,7 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** @@ -23,12 +9,8 @@ * Adds click handlers to buttons and dropdowns, adds event listeners for * keydown events and Blockly events, and configures the initial setup of * the page. - * - * @author Emma Dauterman (evd2014) */ - goog.require('FactoryUtils'); - /** * Namespace for workspace factory initialization methods. * @namespace @@ -47,7 +29,7 @@ WorkspaceFactoryInit.initWorkspaceFactory = function(controller) { document.getElementById('button_down').disabled = true; document.getElementById('button_editCategory').disabled = true; - this.initColorPicker_(controller); + this.initColourPicker_(controller); this.addWorkspaceFactoryEventListeners_(controller); this.assignWorkspaceFactoryClickHandlers_(controller); this.addWorkspaceFactoryOptionsListeners_(controller); @@ -57,98 +39,37 @@ WorkspaceFactoryInit.initWorkspaceFactory = function(controller) { }; /** - * Initialize the color picker in workspace factory. + * Initialize the colour picker in workspace factory. * @param {!FactoryController} controller The controller for the workspace * factory tab. * @private */ -WorkspaceFactoryInit.initColorPicker_ = function(controller) { - // Array of Blockly category colours, consitent with the 15 degree default - // of the block factory's colour wheel. - var colours = []; - for (var hue = 0; hue < 360; hue += 15) { - colours.push(WorkspaceFactoryInit.hsvToHex_(hue, - Blockly.HSV_SATURATION, Blockly.HSV_VALUE)); +WorkspaceFactoryInit.initColourPicker_ = function(controller) { + // Array of Blockly category colours, consistent with the colour defaults. + var colours = [20, 65, 120, 160, 210, 230, 260, 290, 330, '']; + // Convert hue numbers to RRGGBB strings. + for (var i = 0; i < colours.length; i++) { + if (colours[i] !== '') { + colours[i] = Blockly.utils.colour.hueToHex(colours[i]).substring(1); + } } - - // Create color picker with specific set of Blockly colours. - var colourPicker = new goog.ui.ColorPicker(); - colourPicker.setSize(6); - colourPicker.setColors(colours); - - // Create and render the popup colour picker and attach to button. - var popupPicker = new goog.ui.PopupColorPicker(null, colourPicker); - popupPicker.render(); - popupPicker.attach(document.getElementById('dropdown_color')); - popupPicker.setFocusable(true); - goog.events.listen(popupPicker, 'change', function(e) { - controller.changeSelectedCategoryColor(popupPicker.getSelectedColor()); - blocklyFactory.closeModal(); - }); -}; - -/** - * Converts from h,s,v values to a hex string - * @param {number} h Hue, in [0, 360]. - * @param {number} s Saturation, in [0, 1]. - * @param {number} v Value, in [0, 1]. - * @return {string} hex representation of the color. - * @private - */ -WorkspaceFactoryInit.hsvToHex_ = function(h, s, v) { - var brightness = v * 255; - var red = 0; - var green = 0; - var blue = 0; - if (s == 0) { - red = brightness; - green = brightness; - blue = brightness; - } else { - var sextant = Math.floor(h / 60); - var remainder = (h / 60) - sextant; - var val1 = brightness * (1 - s); - var val2 = brightness * (1 - (s * remainder)); - var val3 = brightness * (1 - (s * (1 - remainder))); - switch (sextant) { - case 1: - red = val2; - green = brightness; - blue = val1; - break; - case 2: - red = val1; - green = brightness; - blue = val3; - break; - case 3: - red = val1; - green = val2; - blue = brightness; - break; - case 4: - red = val3; - green = val1; - blue = brightness; - break; - case 5: - red = brightness; - green = val1; - blue = val2; - break; - case 6: - case 0: - red = brightness; - green = val3; - blue = val1; - break; + // Convert to 2D array. + var maxCols = Math.ceil(Math.sqrt(colours.length)); + var grid = []; + var row = []; + for (var i = 0; i < colours.length; i++) { + row.push(colours[i]); + if (row.length === maxCols) { + grid.push(row); + row = []; } } + if (row.length) { + grid.push(row); + } - var hexR = ('0' + Math.floor(red).toString(16)).slice(-2); - var hexG = ('0' + Math.floor(green).toString(16)).slice(-2); - var hexB = ('0' + Math.floor(blue).toString(16)).slice(-2); - return '#' + hexR + hexG + hexB; + // Override the default colours. + cp_grid = grid; }; /** @@ -310,12 +231,27 @@ WorkspaceFactoryInit.assignWorkspaceFactoryClickHandlers_ = document.getElementById('button_editCategory').addEventListener ('click', function() { + var selected = controller.model.getSelected(); + // Return if a category is not selected. + if (selected.type !== ListElement.TYPE_CATEGORY) { + return; + } + document.getElementById('categoryName').value = selected.name; + document.getElementById('categoryColour').value = selected.colour ? + selected.colour.substring(1).toLowerCase() : ''; + console.log(document.getElementById('categoryColour').value); + // Link the colour picker to the field. + cp_init('categoryColour'); blocklyFactory.openModal('dropdownDiv_editCategory'); }); - document.getElementById('dropdown_name').addEventListener + + document.getElementById('categorySave').addEventListener ('click', function() { - controller.changeCategoryName(); + var name = document.getElementById('categoryName').value.trim(); + var colour = document.getElementById('categoryColour').value; + colour = colour ? '#' + colour : null; + controller.changeSelectedCategory(name, colour); blocklyFactory.closeModal(); }); @@ -336,7 +272,7 @@ WorkspaceFactoryInit.assignWorkspaceFactoryClickHandlers_ = // Disable shadow editing button if turning invalid shadow block back // to normal block. - if (!Blockly.selected.getSurroundParent()) { + if (!Blockly.common.getSelected().getSurroundParent()) { document.getElementById('button_addShadow').disabled = true; } }); @@ -366,14 +302,14 @@ WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) { // Don't let arrow keys have any effect if not in Workspace Factory // editing the toolbox. if (!(controller.keyEventsEnabled && controller.selectedMode - == WorkspaceFactoryController.MODE_TOOLBOX)) { + === WorkspaceFactoryController.MODE_TOOLBOX)) { return; } - if (e.keyCode == 38) { + if (e.keyCode === 38) { // Arrow up. controller.moveElement(-1); - } else if (e.keyCode == 40) { + } else if (e.keyCode === 40) { // Arrow down. controller.moveElement(1); } @@ -396,9 +332,9 @@ WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) { // Not listening for Blockly create events because causes the user to drop // blocks when dragging them into workspace. Could cause problems if ever // load blocks into workspace directly without calling updatePreview. - if (e.type == Blockly.Events.BLOCK_MOVE || - e.type == Blockly.Events.BLOCK_DELETE || - e.type == Blockly.Events.BLOCK_CHANGE) { + if (e.type === Blockly.Events.BLOCK_MOVE || + e.type === Blockly.Events.BLOCK_DELETE || + e.type === Blockly.Events.BLOCK_CHANGE) { controller.saveStateFromWorkspace(); controller.updatePreview(); } @@ -407,9 +343,9 @@ WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) { // Only enable "Edit Block" when a block is selected and it has a // surrounding parent, meaning it is nested in another block (blocks that // are not nested in parents cannot be shadow blocks). - if (e.type == Blockly.Events.BLOCK_MOVE || (e.type == Blockly.Events.UI && - e.element == 'selected')) { - var selected = Blockly.selected; + if (e.type === Blockly.Events.BLOCK_MOVE || + e.type === Blockly.Events.SELECTED) { + var selected = Blockly.common.getSelected(); // Show shadow button if a block is selected. Show "Add Shadow" if // a block is not a shadow block, show "Remove Shadow" if it is a @@ -423,7 +359,7 @@ WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) { WorkspaceFactoryInit.displayRemoveShadow_(false); } - if (selected != null && selected.getSurroundParent() != null && + if (selected !== null && selected.getSurroundParent() !== null && !controller.isUserGenShadowBlock(selected.getSurroundParent().id)) { // Selected block is a valid shadow block or could be a valid shadow // block. @@ -440,7 +376,7 @@ WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) { } else { // Selected block cannot be a valid shadow block. - if (selected != null && isInvalidBlockPlacement(selected)) { + if (selected !== null && isInvalidBlockPlacement(selected)) { // Selected block breaks shadow block rules. // Invalid shadow block if (1) a shadow block no longer has a valid // parent, or (2) a normal block is inside of a shadow block. @@ -465,7 +401,7 @@ WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) { // be a shadow block. // Remove possible 'invalid shadow block placement' warning. - if (selected != null && controller.isDefinedBlock(selected) && + if (selected !== null && controller.isDefinedBlock(selected) && (!FactoryUtils.hasVariableField(selected) || !controller.isUserGenShadowBlock(selected.id))) { selected.setWarningText(null); @@ -481,7 +417,7 @@ WorkspaceFactoryInit.addWorkspaceFactoryEventListeners_ = function(controller) { // Convert actual shadow blocks added from the toolbox to user-generated // shadow blocks. - if (e.type == Blockly.Events.BLOCK_CREATE) { + if (e.type === Blockly.Events.BLOCK_CREATE) { controller.convertShadowBlocks(); // Let the user create a Variables or Functions category if they use diff --git a/demos/blockfactory/workspacefactory/wfactory_model.js b/demos/blockfactory/workspacefactory/wfactory_model.js index 7b60810d38a..b0e1ab9d4f3 100644 --- a/demos/blockfactory/workspacefactory/wfactory_model.js +++ b/demos/blockfactory/workspacefactory/wfactory_model.js @@ -1,33 +1,17 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Stores and updates information about state and categories * in workspace factory. Each list element is either a separator or a category, - * and each category stores its name, XML to load that category, color, + * and each category stores its name, XML to load that category, colour, * custom tags, and a unique ID making it possible to change category names and * move categories easily. Keeps track of the currently selected list * element. Also keeps track of all the user-created shadow blocks and * manipulates them as necessary. - * - * @author Emma Dauterman (evd2014) */ /** @@ -50,7 +34,7 @@ WorkspaceFactoryModel = function() { // Boolean for if a Procedure category has been added. this.hasProcedureCategory = false; // XML to be pre-loaded to workspace. Empty on default; - this.preloadXml = Blockly.Xml.textToDom(''); + this.preloadXml = Blockly.utils.xml.createElement('xml'); // Options object to be configured for Blockly inject call. this.options = new Object(null); // Block Library block types. @@ -68,8 +52,8 @@ WorkspaceFactoryModel = function() { */ WorkspaceFactoryModel.prototype.hasCategoryByName = function(name) { for (var i = 0; i < this.toolboxList.length; i++) { - if (this.toolboxList[i].type == ListElement.TYPE_CATEGORY && - this.toolboxList[i].name == name) { + if (this.toolboxList[i].type === ListElement.TYPE_CATEGORY && + this.toolboxList[i].name === name) { return true; } } @@ -109,9 +93,9 @@ WorkspaceFactoryModel.prototype.hasElements = function() { */ WorkspaceFactoryModel.prototype.addElementToList = function(element) { // Update state if the copied category has a custom tag. - this.hasVariableCategory = element.custom == 'VARIABLE' ? true : + this.hasVariableCategory = element.custom === 'VARIABLE' ? true : this.hasVariableCategory; - this.hasProcedureCategory = element.custom == 'PROCEDURE' ? true : + this.hasProcedureCategory = element.custom === 'PROCEDURE' ? true : this.hasProcedureCategory; // Add element to toolboxList. this.toolboxList.push(element); @@ -129,9 +113,9 @@ WorkspaceFactoryModel.prototype.deleteElementFromList = function(index) { return; // No entry to delete. } // Check if need to update flags. - this.hasVariableCategory = this.toolboxList[index].custom == 'VARIABLE' ? + this.hasVariableCategory = this.toolboxList[index].custom === 'VARIABLE' ? false : this.hasVariableCategory; - this.hasProcedureCategory = this.toolboxList[index].custom == 'PROCEDURE' ? + this.hasProcedureCategory = this.toolboxList[index].custom === 'PROCEDURE' ? false : this.hasProcedureCategory; // Remove element. this.toolboxList.splice(index, 1); @@ -144,7 +128,7 @@ WorkspaceFactoryModel.prototype.deleteElementFromList = function(index) { * of blocks displayed. */ WorkspaceFactoryModel.prototype.createDefaultSelectedIfEmpty = function() { - if (this.toolboxList.length == 0) { + if (this.toolboxList.length === 0) { this.flyout = new ListElement(ListElement.TYPE_FLYOUT); this.selected = this.flyout; } @@ -164,7 +148,7 @@ WorkspaceFactoryModel.prototype.moveElementToIndex = function(element, newIndex, // Check that indexes are in bounds. if (newIndex < 0 || newIndex >= this.toolboxList.length || oldIndex < 0 || oldIndex >= this.toolboxList.length) { - throw new Error('Index out of bounds when moving element in the model.'); + throw Error('Index out of bounds when moving element in the model.'); } this.deleteElementFromList(oldIndex); this.toolboxList.splice(newIndex, 0, element); @@ -172,7 +156,7 @@ WorkspaceFactoryModel.prototype.moveElementToIndex = function(element, newIndex, /** * Returns the ID of the currently selected element. Returns null if there are - * no categories (if selected == null). + * no categories (if selected === null). * @return {string} The ID of the element currently selected. */ WorkspaceFactoryModel.prototype.getSelectedId = function() { @@ -181,7 +165,7 @@ WorkspaceFactoryModel.prototype.getSelectedId = function() { /** * Returns the name of the currently selected category. Returns null if there - * are no categories (if selected == null) or the selected element is not + * are no categories (if selected === null) or the selected element is not * a category (in which case its name is null). * @return {string} The name of the category currently selected. */ @@ -214,7 +198,7 @@ WorkspaceFactoryModel.prototype.setSelectedById = function(id) { */ WorkspaceFactoryModel.prototype.getIndexByElementId = function(id) { for (var i = 0; i < this.toolboxList.length; i++) { - if (this.toolboxList[i].id == id) { + if (this.toolboxList[i].id === id) { return i; } } @@ -229,7 +213,7 @@ WorkspaceFactoryModel.prototype.getIndexByElementId = function(id) { */ WorkspaceFactoryModel.prototype.getElementById = function(id) { for (var i = 0; i < this.toolboxList.length; i++) { - if (this.toolboxList[i].id == id) { + if (this.toolboxList[i].id === id) { return this.toolboxList[i]; } } @@ -260,7 +244,7 @@ WorkspaceFactoryModel.prototype.getSelectedXml = function() { /** * Return ordered list of ListElement objects. - * @return {!Array.} ordered list of ListElement objects + * @return {!Array} ordered list of ListElement objects */ WorkspaceFactoryModel.prototype.getToolboxList = function() { return this.toolboxList; @@ -273,7 +257,7 @@ WorkspaceFactoryModel.prototype.getToolboxList = function() { */ WorkspaceFactoryModel.prototype.getCategoryIdByName = function(name) { for (var i = 0; i < this.toolboxList.length; i++) { - if (this.toolboxList[i].name == name) { + if (this.toolboxList[i].name === name) { return this.toolboxList[i].id; } } @@ -288,7 +272,7 @@ WorkspaceFactoryModel.prototype.clearToolboxList = function() { this.hasVariableCategory = false; this.hasProcedureCategory = false; this.shadowBlocks = []; - this.selected.xml = Blockly.Xml.textToDom(''); + this.selected.xml = Blockly.utils.xml.createElement('xml'); }; /** @@ -307,7 +291,7 @@ WorkspaceFactoryModel.prototype.addShadowBlock = function(blockId) { */ WorkspaceFactoryModel.prototype.removeShadowBlock = function(blockId) { for (var i = 0; i < this.shadowBlocks.length; i++) { - if (this.shadowBlocks[i] == blockId) { + if (this.shadowBlocks[i] === blockId) { this.shadowBlocks.splice(i, 1); return; } @@ -322,7 +306,7 @@ WorkspaceFactoryModel.prototype.removeShadowBlock = function(blockId) { */ WorkspaceFactoryModel.prototype.isShadowBlock = function(blockId) { for (var i = 0; i < this.shadowBlocks.length; i++) { - if (this.shadowBlocks[i] == blockId) { + if (this.shadowBlocks[i] === blockId) { return true; } } @@ -355,14 +339,14 @@ WorkspaceFactoryModel.prototype.getShadowBlocksInWorkspace = */ WorkspaceFactoryModel.prototype.addCustomTag = function(category, tag) { // Only update list elements that are categories. - if (category.type != ListElement.TYPE_CATEGORY) { + if (category.type !== ListElement.TYPE_CATEGORY) { return; } // Only update the tag to be 'VARIABLE' or 'PROCEDURE'. - if (tag == 'VARIABLE') { + if (tag === 'VARIABLE') { this.hasVariableCategory = true; category.custom = 'VARIABLE'; - } else if (tag == 'PROCEDURE') { + } else if (tag === 'PROCEDURE') { this.hasProcedureCategory = true; category.custom = 'PROCEDURE'; } @@ -374,7 +358,7 @@ WorkspaceFactoryModel.prototype.addCustomTag = function(category, tag) { * @param {!Element} xml The XML to be saved. */ WorkspaceFactoryModel.prototype.savePreloadXml = function(xml) { - this.preloadXml = xml + this.preloadXml = xml; }; /** @@ -393,11 +377,11 @@ WorkspaceFactoryModel.prototype.setOptions = function(options) { this.options = options; }; -/* +/** * Returns an array of all the block types currently being used in the toolbox * and the pre-loaded blocks. No duplicates. * TODO(evd2014): Move pushBlockTypesToList to FactoryUtils. - * @return {!Array.} Array of block types currently being used. + * @return {!Array} Array of block types currently being used. */ WorkspaceFactoryModel.prototype.getAllUsedBlockTypes = function() { var blockTypeList = []; @@ -411,7 +395,7 @@ WorkspaceFactoryModel.prototype.getAllUsedBlockTypes = function() { // Add block types if not already in list. for (var i = 0; i < blocks.length; i++) { var type = blocks[i].getAttribute('type'); - if (list.indexOf(type) == -1) { + if (!list.includes(type)) { list.push(type); } } @@ -424,7 +408,7 @@ WorkspaceFactoryModel.prototype.getAllUsedBlockTypes = function() { // If has categories, add block types for each category. for (var i = 0, category; category = this.toolboxList[i]; i++) { - if (category.type == ListElement.TYPE_CATEGORY) { + if (category.type === ListElement.TYPE_CATEGORY) { pushBlockTypesToList(category.xml, blockTypeList); } } @@ -438,7 +422,7 @@ WorkspaceFactoryModel.prototype.getAllUsedBlockTypes = function() { /** * Adds new imported block types to the list of current imported block types. - * @param {!Array.} blockTypes Array of block types imported. + * @param {!Array} blockTypes Array of block types imported. */ WorkspaceFactoryModel.prototype.addImportedBlockTypes = function(blockTypes) { this.importedBlockTypes = this.importedBlockTypes.concat(blockTypes); @@ -446,7 +430,7 @@ WorkspaceFactoryModel.prototype.addImportedBlockTypes = function(blockTypes) { /** * Updates block types in block library. - * @param {!Array.} blockTypes Array of block types in block library. + * @param {!Array} blockTypes Array of block types in block library. */ WorkspaceFactoryModel.prototype.updateLibBlockTypes = function(blockTypes) { this.libBlockTypes = blockTypes; @@ -459,16 +443,16 @@ WorkspaceFactoryModel.prototype.updateLibBlockTypes = function(blockTypes) { * @return {boolean} True if blockType is defined, false otherwise. */ WorkspaceFactoryModel.prototype.isDefinedBlockType = function(blockType) { - var isStandardBlock = StandardCategories.coreBlockTypes.indexOf(blockType) - != -1; - var isLibBlock = this.libBlockTypes.indexOf(blockType) != -1; - var isImportedBlock = this.importedBlockTypes.indexOf(blockType) != -1; + var isStandardBlock = + StandardCategories.coreBlockTypes.includes(blockType); + var isLibBlock = this.libBlockTypes.includes(blockType); + var isImportedBlock = this.importedBlockTypes.includes(blockType); return (isStandardBlock || isLibBlock || isImportedBlock); }; /** * Checks if any of the block types are already defined. - * @param {!Array.} blockTypes Array of block types. + * @param {!Array} blockTypes Array of block types. * @return {boolean} True if a block type in the array is already defined, * false if none of the blocks are already defined. */ @@ -488,13 +472,13 @@ WorkspaceFactoryModel.prototype.hasDefinedBlockTypes = function(blockTypes) { ListElement = function(type, opt_name) { this.type = type; // XML DOM element to load the element. - this.xml = Blockly.Xml.textToDom(''); + this.xml = Blockly.utils.xml.createElement('xml'); // Name of category. Can be changed by user. Null if separator. this.name = opt_name ? opt_name : null; // Unique ID of element. Does not change. - this.id = Blockly.utils.genUid(); - // Color of category. Default is no color. Null if separator. - this.color = null; + this.id = Blockly.utils.idGenerator.genUid(); + // Colour of category. Default is no colour. Null if separator. + this.colour = null; // Stores a custom tag, if necessary. Null if no custom tag or separator. this.custom = null; }; @@ -512,8 +496,8 @@ ListElement.TYPE_FLYOUT = 'flyout'; */ ListElement.prototype.saveFromWorkspace = function(workspace) { // Only save XML for categories and flyouts. - if (this.type == ListElement.TYPE_FLYOUT || - this.type == ListElement.TYPE_CATEGORY) { + if (this.type === ListElement.TYPE_FLYOUT || + this.type === ListElement.TYPE_CATEGORY) { this.xml = Blockly.Xml.workspaceToDom(workspace); } }; @@ -524,24 +508,25 @@ ListElement.prototype.saveFromWorkspace = function(workspace) { * not a category. * @param {string} name New name of category. */ -ListElement.prototype.changeName = function (name) { +ListElement.prototype.changeName = function(name) { // Only update list elements that are categories. - if (this.type != ListElement.TYPE_CATEGORY) { + if (this.type !== ListElement.TYPE_CATEGORY) { return; } this.name = name; }; /** - * Sets the color of a category. If tries to set the color of something other + * Sets the colour of a category. If tries to set the colour of something other * than a category, returns. - * @param {string} color The color that should be used for that category. + * @param {?string} colour The colour that should be used for that category, + * or null if none. */ -ListElement.prototype.changeColor = function (color) { - if (this.type != ListElement.TYPE_CATEGORY) { +ListElement.prototype.changeColour = function(colour) { + if (this.type !== ListElement.TYPE_CATEGORY) { return; } - this.color = color; + this.colour = colour; }; /** @@ -552,11 +537,11 @@ ListElement.prototype.changeColor = function (color) { ListElement.prototype.copy = function() { copy = new ListElement(this.type); // Generate a unique ID for the element. - copy.id = Blockly.utils.genUid(); + copy.id = Blockly.utils.idGenerator.genUid(); // Copy all attributes except ID. copy.name = this.name; copy.xml = this.xml; - copy.color = this.color; + copy.colour = this.colour; copy.custom = this.custom; // Return copy. return copy; diff --git a/demos/blockfactory/workspacefactory/wfactory_view.js b/demos/blockfactory/workspacefactory/wfactory_view.js index bf7463eb98f..f98b1353049 100644 --- a/demos/blockfactory/workspacefactory/wfactory_view.js +++ b/demos/blockfactory/workspacefactory/wfactory_view.js @@ -1,21 +1,7 @@ /** * @license - * Blockly Demos: Block Factory - * - * Copyright 2016 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** @@ -24,11 +10,8 @@ * Depends on WorkspaceFactoryController (for adding mouse listeners). Tabs for * each category are stored in tab map, which associates a unique ID for a * category with a particular tab. - * - * @author Emma Dauterman (edauterman) */ -goog.require('FactoryUtils'); /** * Class for a WorkspaceFactoryView @@ -50,7 +33,7 @@ WorkspaceFactoryView.prototype.addCategoryRow = function(name, id) { var count = table.rows.length; // Delete help label and enable category buttons if it's the first category. - if (count == 0) { + if (count === 0) { document.getElementById('categoryHeader').textContent = 'Your categories:'; } @@ -110,7 +93,7 @@ WorkspaceFactoryView.prototype.addEmptyCategoryMessage = function() { WorkspaceFactoryView.prototype.updateState = function(selectedIndex, selected) { // Disable/enable editing buttons as necessary. document.getElementById('button_editCategory').disabled = selectedIndex < 0 || - selected.type != ListElement.TYPE_CATEGORY; + selected.type !== ListElement.TYPE_CATEGORY; document.getElementById('button_remove').disabled = selectedIndex < 0; document.getElementById('button_up').disabled = selectedIndex <= 0; var table = document.getElementById('categoryTable'); @@ -138,7 +121,7 @@ WorkspaceFactoryView.prototype.createCategoryIdName = function(name) { WorkspaceFactoryView.prototype.setCategoryTabSelection = function(id, selected) { if (!this.tabMap[id]) { - return; // Exit if tab does not exist. + return; // Exit if tab does not exist. } this.tabMap[id].className = selected ? 'tabon' : 'taboff'; }; @@ -150,7 +133,7 @@ WorkspaceFactoryView.prototype.setCategoryTabSelection = * @param {!Function} func Function to be executed on click. */ WorkspaceFactoryView.prototype.bindClick = function(el, func) { - if (typeof el == 'string') { + if (typeof el === 'string') { el = document.getElementById(el); } el.addEventListener('click', func, true); @@ -160,8 +143,8 @@ WorkspaceFactoryView.prototype.bindClick = function(el, func) { /** * Creates a file and downloads it. In some browsers downloads, and in other * browsers, opens new tab with contents. - * @param {string} filename Name of file - * @param {!Blob} data Blob containing contents to download + * @param {string} filename Name of file. + * @param {!Blob} data Blob containing contents to download. */ WorkspaceFactoryView.prototype.createAndDownloadFile = function(filename, data) { @@ -180,8 +163,8 @@ WorkspaceFactoryView.prototype.createAndDownloadFile = /** * Given the ID of a certain category, updates the corresponding tab in * the DOM to show a new name. - * @param {string} newName Name of string to be displayed on tab - * @param {string} id ID of category to be updated + * @param {string} newName Name of string to be displayed on tab. + * @param {string} id ID of category to be updated. */ WorkspaceFactoryView.prototype.updateCategoryName = function(newName, id) { this.tabMap[id].textContent = newName; @@ -202,7 +185,7 @@ WorkspaceFactoryView.prototype.moveTabToIndex = // Check that indexes are in bounds. if (newIndex < 0 || newIndex >= table.rows.length || oldIndex < 0 || oldIndex >= table.rows.length) { - throw new Error('Index out of bounds when moving tab in the view.'); + throw Error('Index out of bounds when moving tab in the view.'); } if (newIndex < oldIndex) { @@ -219,17 +202,23 @@ WorkspaceFactoryView.prototype.moveTabToIndex = }; /** - * Given a category ID and color, use that color to color the left border of the - * tab for that category. - * @param {string} id The ID of the category to color. - * @param {string} color The color for to be used for the border of the tab. - * Must be a valid CSS string. + * Given a category ID and colour, use that colour to colour the left border of + * the tab for that category. + * @param {string} id The ID of the category to colour. + * @param {?string} colour The colour for to be used for the border of the tab, + * or null if none. Must be a valid CSS string. */ -WorkspaceFactoryView.prototype.setBorderColor = function(id, color) { - var tab = this.tabMap[id]; - tab.style.borderLeftWidth = '8px'; - tab.style.borderLeftStyle = 'solid'; - tab.style.borderColor = color; +WorkspaceFactoryView.prototype.setBorderColour = function(id, colour) { + var style = this.tabMap[id].style; + if (colour) { + style.borderLeftWidth = '8px'; + style.borderLeftStyle = 'solid'; + style.borderColor = colour; + } else { + style.borderLeftWidth = ''; + style.borderLeftStyle = ''; + style.borderColor = ''; + } }; /** @@ -242,7 +231,7 @@ WorkspaceFactoryView.prototype.addSeparatorTab = function(id) { var table = document.getElementById('categoryTable'); var count = table.rows.length; - if (count == 0) { + if (count === 0) { document.getElementById('categoryHeader').textContent = 'Your categories:'; } // Create separator. @@ -280,9 +269,9 @@ WorkspaceFactoryView.prototype.disableWorkspace = function(disable) { * @return {boolean} True if the workspace should be disabled, false otherwise. */ WorkspaceFactoryView.prototype.shouldDisableWorkspace = function(category) { - return category != null && category.type != ListElement.TYPE_FLYOUT && - (category.type == ListElement.TYPE_SEPARATOR || - category.custom == 'VARIABLE' || category.custom == 'PROCEDURE'); + return category !== null && category.type !== ListElement.TYPE_FLYOUT && + (category.type === ListElement.TYPE_SEPARATOR || + category.custom === 'VARIABLE' || category.custom === 'PROCEDURE'); }; /** @@ -303,7 +292,7 @@ WorkspaceFactoryView.prototype.clearToolboxTabs = function() { * Given a set of blocks currently loaded user-generated shadow blocks, visually * marks them without making them actual shadow blocks (allowing them to still * be editable and movable). - * @param {!Array.} blocks Array of user-generated shadow blocks + * @param {!Array} blocks Array of user-generated shadow blocks * currently loaded. */ WorkspaceFactoryView.prototype.markShadowBlocks = function(blocks) { @@ -321,7 +310,7 @@ WorkspaceFactoryView.prototype.markShadowBlocks = function(blocks) { */ WorkspaceFactoryView.prototype.markShadowBlock = function(block) { // Add Blockly CSS for user-generated shadow blocks. - Blockly.utils.addClass(block.svgGroup_, 'shadowBlock'); + block.getSvgRoot().classList.add('shadowBlock'); // If not a valid shadow block, add a warning message. if (!block.getSurroundParent()) { block.setWarningText('Shadow blocks must be nested inside' + @@ -339,23 +328,23 @@ WorkspaceFactoryView.prototype.markShadowBlock = function(block) { */ WorkspaceFactoryView.prototype.unmarkShadowBlock = function(block) { // Remove Blockly CSS for user-generated shadow blocks. - Blockly.utils.removeClass(block.svgGroup_, 'shadowBlock'); + block.getSvgRoot().classList.remove('shadowBlock'); }; /** - * Sets the tabs for modes according to which mode the user is currenly + * Sets the tabs for modes according to which mode the user is currently * editing in. * @param {string} mode The mode being switched to * (WorkspaceFactoryController.MODE_TOOLBOX or WorkspaceFactoryController.MODE_PRELOAD). */ WorkspaceFactoryView.prototype.setModeSelection = function(mode) { - document.getElementById('tab_preload').className = mode == + document.getElementById('tab_preload').className = mode === WorkspaceFactoryController.MODE_PRELOAD ? 'tabon' : 'taboff'; - document.getElementById('preload_div').style.display = mode == + document.getElementById('preload_div').style.display = mode === WorkspaceFactoryController.MODE_PRELOAD ? 'block' : 'none'; - document.getElementById('tab_toolbox').className = mode == + document.getElementById('tab_toolbox').className = mode === WorkspaceFactoryController.MODE_TOOLBOX ? 'tabon' : 'taboff'; - document.getElementById('toolbox_div').style.display = mode == + document.getElementById('toolbox_div').style.display = mode === WorkspaceFactoryController.MODE_TOOLBOX ? 'block' : 'none'; }; @@ -365,7 +354,7 @@ WorkspaceFactoryView.prototype.setModeSelection = function(mode) { * WorkspaceFactoryController.MODE_PRELOAD). */ WorkspaceFactoryView.prototype.updateHelpText = function(mode) { - if (mode == WorkspaceFactoryController.MODE_TOOLBOX) { + if (mode === WorkspaceFactoryController.MODE_TOOLBOX) { var helpText = 'Drag blocks into the workspace to configure the toolbox ' + 'in your custom workspace.'; } else { @@ -397,8 +386,7 @@ WorkspaceFactoryView.prototype.setBaseOptions = function() { // Check infinite blocks and hide suboption. document.getElementById('option_infiniteBlocks_checkbox').checked = true; - document.getElementById('maxBlockNumber_option').style.display = - 'none'; + document.getElementById('maxBlockNumber_option').style.display = 'none'; // Uncheck grid and zoom options and hide suboptions. document.getElementById('option_grid_checkbox').checked = false; diff --git a/demos/blockfactory_old/blocks.js b/demos/blockfactory_old/blocks.js deleted file mode 100644 index 856780a526a..00000000000 --- a/demos/blockfactory_old/blocks.js +++ /dev/null @@ -1,826 +0,0 @@ -/** - * Blockly Demos: Block Factory Blocks - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 Blocks for Blockly's Block Factory application. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -Blockly.Blocks['factory_base'] = { - // Base of new block. - init: function() { - this.setColour(120); - this.appendDummyInput() - .appendField('name') - .appendField(new Blockly.FieldTextInput('block_type'), 'NAME'); - this.appendStatementInput('INPUTS') - .setCheck('Input') - .appendField('inputs'); - var dropdown = new Blockly.FieldDropdown([ - ['automatic inputs', 'AUTO'], - ['external inputs', 'EXT'], - ['inline inputs', 'INT']]); - this.appendDummyInput() - .appendField(dropdown, 'INLINE'); - dropdown = new Blockly.FieldDropdown([ - ['no connections', 'NONE'], - ['← left output', 'LEFT'], - ['↕ top+bottom connections', 'BOTH'], - ['↑ top connection', 'TOP'], - ['↓ bottom connection', 'BOTTOM']], - function(option) { - this.sourceBlock_.updateShape_(option); - // Connect a shadow block to this new input. - this.sourceBlock_.spawnOutputShadow_(option); - }); - this.appendDummyInput() - .appendField(dropdown, 'CONNECTIONS'); - this.appendValueInput('COLOUR') - .setCheck('Colour') - .appendField('colour'); - this.setTooltip('Build a custom block by plugging\n' + - 'fields, inputs and other blocks here.'); - this.setHelpUrl( - 'https://developers.google.com/blockly/guides/create-custom-blocks/block-factory'); - }, - mutationToDom: function() { - var container = document.createElement('mutation'); - container.setAttribute('connections', this.getFieldValue('CONNECTIONS')); - return container; - }, - domToMutation: function(xmlElement) { - var connections = xmlElement.getAttribute('connections'); - this.updateShape_(connections); - }, - spawnOutputShadow_: function(option) { - // Helper method for deciding which type of outputs this block needs - // to attach shaddow blocks to. - switch (option) { - case 'LEFT': - this.connectOutputShadow_('OUTPUTTYPE'); - break; - case 'TOP': - this.connectOutputShadow_('TOPTYPE'); - break; - case 'BOTTOM': - this.connectOutputShadow_('BOTTOMTYPE'); - break; - case 'BOTH': - this.connectOutputShadow_('TOPTYPE'); - this.connectOutputShadow_('BOTTOMTYPE'); - break; - } - }, - connectOutputShadow_: function(outputType) { - // Helper method to create & connect shadow block. - var type = this.workspace.newBlock('type_null'); - type.setShadow(true); - type.outputConnection.connect(this.getInput(outputType).connection); - type.initSvg(); - type.render(); - }, - updateShape_: function(option) { - var outputExists = this.getInput('OUTPUTTYPE'); - var topExists = this.getInput('TOPTYPE'); - var bottomExists = this.getInput('BOTTOMTYPE'); - if (option == 'LEFT') { - if (!outputExists) { - this.addTypeInput_('OUTPUTTYPE', 'output type'); - } - } else if (outputExists) { - this.removeInput('OUTPUTTYPE'); - } - if (option == 'TOP' || option == 'BOTH') { - if (!topExists) { - this.addTypeInput_('TOPTYPE', 'top type'); - } - } else if (topExists) { - this.removeInput('TOPTYPE'); - } - if (option == 'BOTTOM' || option == 'BOTH') { - if (!bottomExists) { - this.addTypeInput_('BOTTOMTYPE', 'bottom type'); - } - } else if (bottomExists) { - this.removeInput('BOTTOMTYPE'); - } - }, - addTypeInput_: function(name, label) { - this.appendValueInput(name) - .setCheck('Type') - .appendField(label); - this.moveInputBefore(name, 'COLOUR'); - } -}; - -var FIELD_MESSAGE = 'fields %1 %2'; -var FIELD_ARGS = [ - { - "type": "field_dropdown", - "name": "ALIGN", - "options": [['left', 'LEFT'], ['right', 'RIGHT'], ['centre', 'CENTRE']], - }, - { - "type": "input_statement", - "name": "FIELDS", - "check": "Field" - } -]; - -var TYPE_MESSAGE = 'type %1'; -var TYPE_ARGS = [ - { - "type": "input_value", - "name": "TYPE", - "check": "Type", - "align": "RIGHT" - } -]; - -Blockly.Blocks['input_value'] = { - // Value input. - init: function() { - this.jsonInit({ - "message0": "value input %1 %2", - "args0": [ - { - "type": "field_input", - "name": "INPUTNAME", - "text": "NAME" - }, - { - "type": "input_dummy" - } - ], - "message1": FIELD_MESSAGE, - "args1": FIELD_ARGS, - "message2": TYPE_MESSAGE, - "args2": TYPE_ARGS, - "previousStatement": "Input", - "nextStatement": "Input", - "colour": 210, - "tooltip": "A value socket for horizontal connections.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=71" - }); - }, - onchange: function() { - inputNameCheck(this); - } -}; - -Blockly.Blocks['input_statement'] = { - // Statement input. - init: function() { - this.jsonInit({ - "message0": "statement input %1 %2", - "args0": [ - { - "type": "field_input", - "name": "INPUTNAME", - "text": "NAME" - }, - { - "type": "input_dummy" - }, - ], - "message1": FIELD_MESSAGE, - "args1": FIELD_ARGS, - "message2": TYPE_MESSAGE, - "args2": TYPE_ARGS, - "previousStatement": "Input", - "nextStatement": "Input", - "colour": 210, - "tooltip": "A statement socket for enclosed vertical stacks.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=246" - }); - }, - onchange: function() { - inputNameCheck(this); - } -}; - -Blockly.Blocks['input_dummy'] = { - // Dummy input. - init: function() { - this.jsonInit({ - "message0": "dummy input", - "message1": FIELD_MESSAGE, - "args1": FIELD_ARGS, - "previousStatement": "Input", - "nextStatement": "Input", - "colour": 210, - "tooltip": "For adding fields on a separate row with no " + - "connections. Alignment options (left, right, centre) " + - "apply only to multi-line fields.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=293" - }); - } -}; - -Blockly.Blocks['field_static'] = { - // Text value. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('text') - .appendField(new Blockly.FieldTextInput(''), 'TEXT'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Static text that serves as a label.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=88'); - } -}; - -Blockly.Blocks['field_input'] = { - // Text input. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('text input') - .appendField(new Blockly.FieldTextInput('default'), 'TEXT') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('An input field for the user to enter text.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=319'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_number'] = { - // Numeric input. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('numeric input') - .appendField(new Blockly.FieldNumber(0), 'VALUE') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.appendDummyInput() - .appendField('min') - .appendField(new Blockly.FieldNumber(-Infinity), 'MIN') - .appendField('max') - .appendField(new Blockly.FieldNumber(Infinity), 'MAX') - .appendField('precision') - .appendField(new Blockly.FieldNumber(0, 0), 'PRECISION'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('An input field for the user to enter a number.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=319'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_angle'] = { - // Angle input. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('angle input') - .appendField(new Blockly.FieldAngle('90'), 'ANGLE') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('An input field for the user to enter an angle.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=372'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_dropdown'] = { - // Dropdown menu. - init: function() { - this.appendDummyInput() - .appendField('dropdown') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.optionCount_ = 3; - this.updateShape_(); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setMutator(new Blockly.Mutator(['field_dropdown_option'])); - this.setColour(160); - this.setTooltip('Dropdown menu with a list of options.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386'); - }, - mutationToDom: function(workspace) { - // Create XML to represent menu options. - var container = document.createElement('mutation'); - container.setAttribute('options', this.optionCount_); - return container; - }, - domToMutation: function(container) { - // Parse XML to restore the menu options. - this.optionCount_ = parseInt(container.getAttribute('options'), 10); - this.updateShape_(); - }, - decompose: function(workspace) { - // Populate the mutator's dialog with this block's components. - var containerBlock = workspace.newBlock('field_dropdown_container'); - containerBlock.initSvg(); - var connection = containerBlock.getInput('STACK').connection; - for (var i = 0; i < this.optionCount_; i++) { - var optionBlock = workspace.newBlock('field_dropdown_option'); - optionBlock.initSvg(); - connection.connect(optionBlock.previousConnection); - connection = optionBlock.nextConnection; - } - return containerBlock; - }, - compose: function(containerBlock) { - // Reconfigure this block based on the mutator dialog's components. - var optionBlock = containerBlock.getInputTargetBlock('STACK'); - // Count number of inputs. - var data = []; - while (optionBlock) { - data.push([optionBlock.userData_, optionBlock.cpuData_]); - optionBlock = optionBlock.nextConnection && - optionBlock.nextConnection.targetBlock(); - } - this.optionCount_ = data.length; - this.updateShape_(); - // Restore any data. - for (var i = 0; i < this.optionCount_; i++) { - this.setFieldValue(data[i][0] || 'option', 'USER' + i); - this.setFieldValue(data[i][1] || 'OPTIONNAME', 'CPU' + i); - } - }, - saveConnections: function(containerBlock) { - // Store names and values for each option. - var optionBlock = containerBlock.getInputTargetBlock('STACK'); - var i = 0; - while (optionBlock) { - optionBlock.userData_ = this.getFieldValue('USER' + i); - optionBlock.cpuData_ = this.getFieldValue('CPU' + i); - i++; - optionBlock = optionBlock.nextConnection && - optionBlock.nextConnection.targetBlock(); - } - }, - updateShape_: function() { - // Modify this block to have the correct number of options. - // Add new options. - for (var i = 0; i < this.optionCount_; i++) { - if (!this.getInput('OPTION' + i)) { - this.appendDummyInput('OPTION' + i) - .appendField(new Blockly.FieldTextInput('option'), 'USER' + i) - .appendField(',') - .appendField(new Blockly.FieldTextInput('OPTIONNAME'), 'CPU' + i); - } - } - // Remove deleted options. - while (this.getInput('OPTION' + i)) { - this.removeInput('OPTION' + i); - i++; - } - }, - onchange: function() { - if (this.workspace && this.optionCount_ < 1) { - this.setWarningText('Drop down menu must\nhave at least one option.'); - } else { - fieldNameCheck(this); - } - } -}; - -Blockly.Blocks['field_dropdown_container'] = { - // Container. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('add options'); - this.appendStatementInput('STACK'); - this.setTooltip('Add, remove, or reorder options\n' + - 'to reconfigure this dropdown menu.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386'); - this.contextMenu = false; - } -}; - -Blockly.Blocks['field_dropdown_option'] = { - // Add option. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('option'); - this.setPreviousStatement(true); - this.setNextStatement(true); - this.setTooltip('Add a new option to the dropdown menu.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386'); - this.contextMenu = false; - } -}; - -Blockly.Blocks['field_checkbox'] = { - // Checkbox. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('checkbox') - .appendField(new Blockly.FieldCheckbox('TRUE'), 'CHECKED') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Checkbox field.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=485'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_colour'] = { - // Colour input. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('colour') - .appendField(new Blockly.FieldColour('#ff0000'), 'COLOUR') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Colour input field.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=495'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_date'] = { - // Date input. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('date') - .appendField(new Blockly.FieldDate(), 'DATE') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Date input field.'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_variable'] = { - // Dropdown for variables. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('variable') - .appendField(new Blockly.FieldTextInput('item'), 'TEXT') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Dropdown menu for variable names.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=510'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_image'] = { - // Image. - init: function() { - this.setColour(160); - var src = 'https://www.gstatic.com/codesite/ph/images/star_on.gif'; - this.appendDummyInput() - .appendField('image') - .appendField(new Blockly.FieldTextInput(src), 'SRC'); - this.appendDummyInput() - .appendField('width') - .appendField(new Blockly.FieldNumber('15', 0, NaN, 1), 'WIDTH') - .appendField('height') - .appendField(new Blockly.FieldNumber('15', 0, NaN, 1), 'HEIGHT') - .appendField('alt text') - .appendField(new Blockly.FieldTextInput('*'), 'ALT'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Static image (JPEG, PNG, GIF, SVG, BMP).\n' + - 'Retains aspect ratio regardless of height and width.\n' + - 'Alt text is for when collapsed.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=567'); - } -}; - -Blockly.Blocks['type_group'] = { - // Group of types. - init: function() { - this.typeCount_ = 2; - this.updateShape_(); - this.setOutput(true, 'Type'); - this.setMutator(new Blockly.Mutator(['type_group_item'])); - this.setColour(230); - this.setTooltip('Allows more than one type to be accepted.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677'); - }, - mutationToDom: function(workspace) { - // Create XML to represent a group of types. - var container = document.createElement('mutation'); - container.setAttribute('types', this.typeCount_); - return container; - }, - domToMutation: function(container) { - // Parse XML to restore the group of types. - this.typeCount_ = parseInt(container.getAttribute('types'), 10); - this.updateShape_(); - for (var i = 0; i < this.typeCount_; i++) { - this.removeInput('TYPE' + i); - } - for (var i = 0; i < this.typeCount_; i++) { - var input = this.appendValueInput('TYPE' + i) - .setCheck('Type'); - if (i == 0) { - input.appendField('any of'); - } - } - }, - decompose: function(workspace) { - // Populate the mutator's dialog with this block's components. - var containerBlock = workspace.newBlock('type_group_container'); - containerBlock.initSvg(); - var connection = containerBlock.getInput('STACK').connection; - for (var i = 0; i < this.typeCount_; i++) { - var typeBlock = workspace.newBlock('type_group_item'); - typeBlock.initSvg(); - connection.connect(typeBlock.previousConnection); - connection = typeBlock.nextConnection; - } - return containerBlock; - }, - compose: function(containerBlock) { - // Reconfigure this block based on the mutator dialog's components. - var typeBlock = containerBlock.getInputTargetBlock('STACK'); - // Count number of inputs. - var connections = []; - while (typeBlock) { - connections.push(typeBlock.valueConnection_); - typeBlock = typeBlock.nextConnection && - typeBlock.nextConnection.targetBlock(); - } - // Disconnect any children that don't belong. - for (var i = 0; i < this.typeCount_; i++) { - var connection = this.getInput('TYPE' + i).connection.targetConnection; - if (connection && connections.indexOf(connection) == -1) { - connection.disconnect(); - } - } - this.typeCount_ = connections.length; - this.updateShape_(); - // Reconnect any child blocks. - for (var i = 0; i < this.typeCount_; i++) { - Blockly.Mutator.reconnect(connections[i], this, 'TYPE' + i); - } - }, - saveConnections: function(containerBlock) { - // Store a pointer to any connected child blocks. - var typeBlock = containerBlock.getInputTargetBlock('STACK'); - var i = 0; - while (typeBlock) { - var input = this.getInput('TYPE' + i); - typeBlock.valueConnection_ = input && input.connection.targetConnection; - i++; - typeBlock = typeBlock.nextConnection && - typeBlock.nextConnection.targetBlock(); - } - }, - updateShape_: function() { - // Modify this block to have the correct number of inputs. - // Add new inputs. - for (var i = 0; i < this.typeCount_; i++) { - if (!this.getInput('TYPE' + i)) { - var input = this.appendValueInput('TYPE' + i); - if (i == 0) { - input.appendField('any of'); - } - } - } - // Remove deleted inputs. - while (this.getInput('TYPE' + i)) { - this.removeInput('TYPE' + i); - i++; - } - } -}; - -Blockly.Blocks['type_group_container'] = { - // Container. - init: function() { - this.jsonInit({ - "message0": "add types %1 %2", - "args0": [ - {"type": "input_dummy"}, - {"type": "input_statement", "name": "STACK"} - ], - "colour": 230, - "tooltip": "Add, or remove allowed type.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677" - }); - } -}; - -Blockly.Blocks['type_group_item'] = { - // Add type. - init: function() { - this.jsonInit({ - "message0": "type", - "previousStatement": null, - "nextStatement": null, - "colour": 230, - "tooltip": "Add a new allowed type.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677" - }); - } -}; - -Blockly.Blocks['type_null'] = { - // Null type. - valueType: null, - init: function() { - this.jsonInit({ - "message0": "any", - "output": "Type", - "colour": 230, - "tooltip": "Any type is allowed.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602" - }); - } -}; - -Blockly.Blocks['type_boolean'] = { - // Boolean type. - valueType: 'Boolean', - init: function() { - this.jsonInit({ - "message0": "Boolean", - "output": "Type", - "colour": 230, - "tooltip": "Booleans (true/false) are allowed.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602" - }); - } -}; - -Blockly.Blocks['type_number'] = { - // Number type. - valueType: 'Number', - init: function() { - this.jsonInit({ - "message0": "Number", - "output": "Type", - "colour": 230, - "tooltip": "Numbers (int/float) are allowed.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602" - }); - } -}; - -Blockly.Blocks['type_string'] = { - // String type. - valueType: 'String', - init: function() { - this.jsonInit({ - "message0": "String", - "output": "Type", - "colour": 230, - "tooltip": "Strings (text) are allowed.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602" - }); - } -}; - -Blockly.Blocks['type_list'] = { - // List type. - valueType: 'Array', - init: function() { - this.jsonInit({ - "message0": "Array", - "output": "Type", - "colour": 230, - "tooltip": "Arrays (lists) are allowed.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602" - }); - } -}; - -Blockly.Blocks['type_other'] = { - // Other type. - init: function() { - this.jsonInit({ - "message0": "other %1", - "args0": [{"type": "field_input", "name": "TYPE", "text": ""}], - "output": "Type", - "colour": 230, - "tooltip": "Custom type to allow.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=702" - }); - } -}; - -Blockly.Blocks['colour_hue'] = { - // Set the colour of the block. - init: function() { - this.appendDummyInput() - .appendField('hue:') - .appendField(new Blockly.FieldAngle('0', this.validator), 'HUE'); - this.setOutput(true, 'Colour'); - this.setTooltip('Paint the block with this colour.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=55'); - }, - validator: function(text) { - // Update the current block's colour to match. - var hue = parseInt(text, 10); - if (!isNaN(hue)) { - this.sourceBlock_.setColour(hue); - } - }, - mutationToDom: function(workspace) { - var container = document.createElement('mutation'); - container.setAttribute('colour', this.getColour()); - return container; - }, - domToMutation: function(container) { - this.setColour(container.getAttribute('colour')); - } -}; - -/** - * Check to see if more than one field has this name. - * Highly inefficient (On^2), but n is small. - * @param {!Blockly.Block} referenceBlock Block to check. - */ -function fieldNameCheck(referenceBlock) { - if (!referenceBlock.workspace) { - // Block has been deleted. - return; - } - var name = referenceBlock.getFieldValue('FIELDNAME').toLowerCase(); - var count = 0; - var blocks = referenceBlock.workspace.getAllBlocks(); - for (var i = 0, block; block = blocks[i]; i++) { - var otherName = block.getFieldValue('FIELDNAME'); - if (!block.disabled && !block.getInheritedDisabled() && - otherName && otherName.toLowerCase() == name) { - count++; - } - } - var msg = (count > 1) ? - 'There are ' + count + ' field blocks\n with this name.' : null; - referenceBlock.setWarningText(msg); -} - -/** - * Check to see if more than one input has this name. - * Highly inefficient (On^2), but n is small. - * @param {!Blockly.Block} referenceBlock Block to check. - */ -function inputNameCheck(referenceBlock) { - if (!referenceBlock.workspace) { - // Block has been deleted. - return; - } - var name = referenceBlock.getFieldValue('INPUTNAME').toLowerCase(); - var count = 0; - var blocks = referenceBlock.workspace.getAllBlocks(); - for (var i = 0, block; block = blocks[i]; i++) { - var otherName = block.getFieldValue('INPUTNAME'); - if (!block.disabled && !block.getInheritedDisabled() && - otherName && otherName.toLowerCase() == name) { - count++; - } - } - var msg = (count > 1) ? - 'There are ' + count + ' input blocks\n with this name.' : null; - referenceBlock.setWarningText(msg); -} diff --git a/demos/blockfactory_old/factory.js b/demos/blockfactory_old/factory.js deleted file mode 100644 index 9db58bd6dbe..00000000000 --- a/demos/blockfactory_old/factory.js +++ /dev/null @@ -1,850 +0,0 @@ -/** - * Blockly Demos: Block Factory - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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 JavaScript for Blockly's Block Factory application. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -/** - * Workspace for user to build block. - * @type {Blockly.Workspace} - */ -var mainWorkspace = null; - -/** - * Workspace for preview of block. - * @type {Blockly.Workspace} - */ -var previewWorkspace = null; - -/** - * Name of block if not named. - */ -var UNNAMED = 'unnamed'; - -/** - * Change the language code format. - */ -function formatChange() { - var mask = document.getElementById('blocklyMask'); - var languagePre = document.getElementById('languagePre'); - var languageTA = document.getElementById('languageTA'); - if (document.getElementById('format').value == 'Manual') { - Blockly.hideChaff(); - mask.style.display = 'block'; - languagePre.style.display = 'none'; - languageTA.style.display = 'block'; - var code = languagePre.textContent.trim(); - languageTA.value = code; - languageTA.focus(); - updatePreview(); - } else { - mask.style.display = 'none'; - languageTA.style.display = 'none'; - languagePre.style.display = 'block'; - updateLanguage(); - } - disableEnableLink(); -} - -/** - * Update the language code based on constructs made in Blockly. - */ -function updateLanguage() { - var rootBlock = getRootBlock(); - if (!rootBlock) { - return; - } - var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase(); - if (!blockType) { - blockType = UNNAMED; - } - blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1'); - switch (document.getElementById('format').value) { - case 'JSON': - var code = formatJson_(blockType, rootBlock); - break; - case 'JavaScript': - var code = formatJavaScript_(blockType, rootBlock); - break; - } - injectCode(code, 'languagePre'); - updatePreview(); -} - -/** - * Update the language code as JSON. - * @param {string} blockType Name of block. - * @param {!Blockly.Block} rootBlock Factory_base block. - * @return {string} Generanted language code. - * @private - */ -function formatJson_(blockType, rootBlock) { - var JS = {}; - // Type is not used by Blockly, but may be used by a loader. - JS.type = blockType; - // Generate inputs. - var message = []; - var args = []; - var contentsBlock = rootBlock.getInputTargetBlock('INPUTS'); - var lastInput = null; - while (contentsBlock) { - if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { - var fields = getFieldsJson_(contentsBlock.getInputTargetBlock('FIELDS')); - for (var i = 0; i < fields.length; i++) { - if (typeof fields[i] == 'string') { - message.push(fields[i].replace(/%/g, '%%')); - } else { - args.push(fields[i]); - message.push('%' + args.length); - } - } - - var input = {type: contentsBlock.type}; - // Dummy inputs don't have names. Other inputs do. - if (contentsBlock.type != 'input_dummy') { - input.name = contentsBlock.getFieldValue('INPUTNAME'); - } - var check = JSON.parse(getOptTypesFrom(contentsBlock, 'TYPE') || 'null'); - if (check) { - input.check = check; - } - var align = contentsBlock.getFieldValue('ALIGN'); - if (align != 'LEFT') { - input.align = align; - } - args.push(input); - message.push('%' + args.length); - lastInput = contentsBlock; - } - contentsBlock = contentsBlock.nextConnection && - contentsBlock.nextConnection.targetBlock(); - } - // Remove last input if dummy and not empty. - if (lastInput && lastInput.type == 'input_dummy') { - var fields = lastInput.getInputTargetBlock('FIELDS'); - if (fields && getFieldsJson_(fields).join('').trim() != '') { - var align = lastInput.getFieldValue('ALIGN'); - if (align != 'LEFT') { - JS.lastDummyAlign0 = align; - } - args.pop(); - message.pop(); - } - } - JS.message0 = message.join(' '); - if (args.length) { - JS.args0 = args; - } - // Generate inline/external switch. - if (rootBlock.getFieldValue('INLINE') == 'EXT') { - JS.inputsInline = false; - } else if (rootBlock.getFieldValue('INLINE') == 'INT') { - JS.inputsInline = true; - } - // Generate output, or next/previous connections. - switch (rootBlock.getFieldValue('CONNECTIONS')) { - case 'LEFT': - JS.output = - JSON.parse(getOptTypesFrom(rootBlock, 'OUTPUTTYPE') || 'null'); - break; - case 'BOTH': - JS.previousStatement = - JSON.parse(getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null'); - JS.nextStatement = - JSON.parse(getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null'); - break; - case 'TOP': - JS.previousStatement = - JSON.parse(getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null'); - break; - case 'BOTTOM': - JS.nextStatement = - JSON.parse(getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null'); - break; - } - // Generate colour. - var colourBlock = rootBlock.getInputTargetBlock('COLOUR'); - if (colourBlock && !colourBlock.disabled) { - var hue = parseInt(colourBlock.getFieldValue('HUE'), 10); - JS.colour = hue; - } - JS.tooltip = ''; - JS.helpUrl = 'http://www.example.com/'; - return JSON.stringify(JS, null, ' '); -} - -/** - * Update the language code as JavaScript. - * @param {string} blockType Name of block. - * @param {!Blockly.Block} rootBlock Factory_base block. - * @return {string} Generanted language code. - * @private - */ -function formatJavaScript_(blockType, rootBlock) { - var code = []; - code.push("Blockly.Blocks['" + blockType + "'] = {"); - code.push(" init: function() {"); - // Generate inputs. - var TYPES = {'input_value': 'appendValueInput', - 'input_statement': 'appendStatementInput', - 'input_dummy': 'appendDummyInput'}; - var contentsBlock = rootBlock.getInputTargetBlock('INPUTS'); - while (contentsBlock) { - if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { - var name = ''; - // Dummy inputs don't have names. Other inputs do. - if (contentsBlock.type != 'input_dummy') { - name = escapeString(contentsBlock.getFieldValue('INPUTNAME')); - } - code.push(' this.' + TYPES[contentsBlock.type] + '(' + name + ')'); - var check = getOptTypesFrom(contentsBlock, 'TYPE'); - if (check) { - code.push(' .setCheck(' + check + ')'); - } - var align = contentsBlock.getFieldValue('ALIGN'); - if (align != 'LEFT') { - code.push(' .setAlign(Blockly.ALIGN_' + align + ')'); - } - var fields = getFieldsJs_(contentsBlock.getInputTargetBlock('FIELDS')); - for (var i = 0; i < fields.length; i++) { - code.push(' .appendField(' + fields[i] + ')'); - } - // Add semicolon to last line to finish the statement. - code[code.length - 1] += ';'; - } - contentsBlock = contentsBlock.nextConnection && - contentsBlock.nextConnection.targetBlock(); - } - // Generate inline/external switch. - if (rootBlock.getFieldValue('INLINE') == 'EXT') { - code.push(' this.setInputsInline(false);'); - } else if (rootBlock.getFieldValue('INLINE') == 'INT') { - code.push(' this.setInputsInline(true);'); - } - // Generate output, or next/previous connections. - switch (rootBlock.getFieldValue('CONNECTIONS')) { - case 'LEFT': - code.push(connectionLineJs_('setOutput', 'OUTPUTTYPE')); - break; - case 'BOTH': - code.push(connectionLineJs_('setPreviousStatement', 'TOPTYPE')); - code.push(connectionLineJs_('setNextStatement', 'BOTTOMTYPE')); - break; - case 'TOP': - code.push(connectionLineJs_('setPreviousStatement', 'TOPTYPE')); - break; - case 'BOTTOM': - code.push(connectionLineJs_('setNextStatement', 'BOTTOMTYPE')); - break; - } - // Generate colour. - var colourBlock = rootBlock.getInputTargetBlock('COLOUR'); - if (colourBlock && !colourBlock.disabled) { - var hue = parseInt(colourBlock.getFieldValue('HUE'), 10); - if (!isNaN(hue)) { - code.push(' this.setColour(' + hue + ');'); - } - } - code.push(" this.setTooltip('');"); - code.push(" this.setHelpUrl('http://www.example.com/');"); - code.push(' }'); - code.push('};'); - return code.join('\n'); -} - -/** - * Create JS code required to create a top, bottom, or value connection. - * @param {string} functionName JavaScript function name. - * @param {string} typeName Name of type input. - * @return {string} Line of JavaScript code to create connection. - * @private - */ -function connectionLineJs_(functionName, typeName) { - var type = getOptTypesFrom(getRootBlock(), typeName); - if (type) { - type = ', ' + type; - } else { - type = ''; - } - return ' this.' + functionName + '(true' + type + ');'; -} - -/** - * Returns field strings and any config. - * @param {!Blockly.Block} block Input block. - * @return {!Array.} Field strings. - * @private - */ -function getFieldsJs_(block) { - var fields = []; - while (block) { - if (!block.disabled && !block.getInheritedDisabled()) { - switch (block.type) { - case 'field_static': - // Result: 'hello' - fields.push(escapeString(block.getFieldValue('TEXT'))); - break; - case 'field_input': - // Result: new Blockly.FieldTextInput('Hello'), 'GREET' - fields.push('new Blockly.FieldTextInput(' + - escapeString(block.getFieldValue('TEXT')) + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_number': - // Result: new Blockly.FieldNumber(10, 0, 100, 1), 'NUMBER' - var args = [ - Number(block.getFieldValue('VALUE')), - Number(block.getFieldValue('MIN')), - Number(block.getFieldValue('MAX')), - Number(block.getFieldValue('PRECISION')) - ]; - // Remove any trailing arguments that aren't needed. - if (args[3] == 0) { - args.pop(); - if (args[2] == Infinity) { - args.pop(); - if (args[1] == -Infinity) { - args.pop(); - } - } - } - fields.push('new Blockly.FieldNumber(' + args.join(', ') + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_angle': - // Result: new Blockly.FieldAngle(90), 'ANGLE' - fields.push('new Blockly.FieldAngle(' + - Number(block.getFieldValue('ANGLE')) + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_checkbox': - // Result: new Blockly.FieldCheckbox('TRUE'), 'CHECK' - fields.push('new Blockly.FieldCheckbox(' + - escapeString(block.getFieldValue('CHECKED')) + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_colour': - // Result: new Blockly.FieldColour('#ff0000'), 'COLOUR' - fields.push('new Blockly.FieldColour(' + - escapeString(block.getFieldValue('COLOUR')) + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_date': - // Result: new Blockly.FieldDate('2015-02-04'), 'DATE' - fields.push('new Blockly.FieldDate(' + - escapeString(block.getFieldValue('DATE')) + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_variable': - // Result: new Blockly.FieldVariable('item'), 'VAR' - var varname = escapeString(block.getFieldValue('TEXT') || null); - fields.push('new Blockly.FieldVariable(' + varname + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_dropdown': - // Result: - // new Blockly.FieldDropdown([['yes', '1'], ['no', '0']]), 'TOGGLE' - var options = []; - for (var i = 0; i < block.optionCount_; i++) { - options[i] = '[' + escapeString(block.getFieldValue('USER' + i)) + - ', ' + escapeString(block.getFieldValue('CPU' + i)) + ']'; - } - if (options.length) { - fields.push('new Blockly.FieldDropdown([' + - options.join(', ') + ']), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - } - break; - case 'field_image': - // Result: new Blockly.FieldImage('http://...', 80, 60, '*') - var src = escapeString(block.getFieldValue('SRC')); - var width = Number(block.getFieldValue('WIDTH')); - var height = Number(block.getFieldValue('HEIGHT')); - var alt = escapeString(block.getFieldValue('ALT')); - fields.push('new Blockly.FieldImage(' + - src + ', ' + width + ', ' + height + ', ' + alt + ')'); - break; - } - } - block = block.nextConnection && block.nextConnection.targetBlock(); - } - return fields; -} - -/** - * Returns field strings and any config. - * @param {!Blockly.Block} block Input block. - * @return {!Array.} Array of static text and field configs. - * @private - */ -function getFieldsJson_(block) { - var fields = []; - while (block) { - if (!block.disabled && !block.getInheritedDisabled()) { - switch (block.type) { - case 'field_static': - // Result: 'hello' - fields.push(block.getFieldValue('TEXT')); - break; - case 'field_input': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - text: block.getFieldValue('TEXT') - }); - break; - case 'field_number': - var obj = { - type: block.type, - name: block.getFieldValue('FIELDNAME'), - value: parseFloat(block.getFieldValue('VALUE')) - }; - var min = parseFloat(block.getFieldValue('MIN')); - if (min > -Infinity) { - obj.min = min; - } - var max = parseFloat(block.getFieldValue('MAX')); - if (max < Infinity) { - obj.max = max; - } - var precision = parseFloat(block.getFieldValue('PRECISION')); - if (precision) { - obj.precision = precision; - } - fields.push(obj); - break; - case 'field_angle': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - angle: Number(block.getFieldValue('ANGLE')) - }); - break; - case 'field_checkbox': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - checked: block.getFieldValue('CHECKED') == 'TRUE' - }); - break; - case 'field_colour': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - colour: block.getFieldValue('COLOUR') - }); - break; - case 'field_date': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - date: block.getFieldValue('DATE') - }); - break; - case 'field_variable': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - variable: block.getFieldValue('TEXT') || null - }); - break; - case 'field_dropdown': - var options = []; - for (var i = 0; i < block.optionCount_; i++) { - options[i] = [block.getFieldValue('USER' + i), - block.getFieldValue('CPU' + i)]; - } - if (options.length) { - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - options: options - }); - } - break; - case 'field_image': - fields.push({ - type: block.type, - src: block.getFieldValue('SRC'), - width: Number(block.getFieldValue('WIDTH')), - height: Number(block.getFieldValue('HEIGHT')), - alt: block.getFieldValue('ALT') - }); - break; - } - } - block = block.nextConnection && block.nextConnection.targetBlock(); - } - return fields; -} - -/** - * Escape a string. - * @param {string} string String to escape. - * @return {string} Escaped string surrouned by quotes. - */ -function escapeString(string) { - return JSON.stringify(string); -} - -/** - * Fetch the type(s) defined in the given input. - * Format as a string for appending to the generated code. - * @param {!Blockly.Block} block Block with input. - * @param {string} name Name of the input. - * @return {?string} String defining the types. - */ -function getOptTypesFrom(block, name) { - var types = getTypesFrom_(block, name); - if (types.length == 0) { - return undefined; - } else if (types.indexOf('null') != -1) { - return 'null'; - } else if (types.length == 1) { - return types[0]; - } else { - return '[' + types.join(', ') + ']'; - } -} - -/** - * Fetch the type(s) defined in the given input. - * @param {!Blockly.Block} block Block with input. - * @param {string} name Name of the input. - * @return {!Array.} List of types. - * @private - */ -function getTypesFrom_(block, name) { - var typeBlock = block.getInputTargetBlock(name); - var types; - if (!typeBlock || typeBlock.disabled) { - types = []; - } else if (typeBlock.type == 'type_other') { - types = [escapeString(typeBlock.getFieldValue('TYPE'))]; - } else if (typeBlock.type == 'type_group') { - types = []; - for (var i = 0; i < typeBlock.typeCount_; i++) { - types = types.concat(getTypesFrom_(typeBlock, 'TYPE' + i)); - } - // Remove duplicates. - var hash = Object.create(null); - for (var n = types.length - 1; n >= 0; n--) { - if (hash[types[n]]) { - types.splice(n, 1); - } - hash[types[n]] = true; - } - } else { - types = [escapeString(typeBlock.valueType)]; - } - return types; -} - -/** - * Update the generator code. - * @param {!Blockly.Block} block Rendered block in preview workspace. - */ -function updateGenerator(block) { - function makeVar(root, name) { - name = name.toLowerCase().replace(/\W/g, '_'); - return ' var ' + root + '_' + name; - } - var language = document.getElementById('language').value; - var code = []; - code.push("Blockly." + language + "['" + block.type + - "'] = function(block) {"); - - // Generate getters for any fields or inputs. - for (var i = 0, input; input = block.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { - var name = field.name; - if (!name) { - continue; - } - if (field instanceof Blockly.FieldVariable) { - // Subclass of Blockly.FieldDropdown, must test first. - code.push(makeVar('variable', name) + - " = Blockly." + language + - ".variableDB_.getName(block.getFieldValue('" + name + - "'), Blockly.Variables.NAME_TYPE);"); - } else if (field instanceof Blockly.FieldAngle) { - // Subclass of Blockly.FieldTextInput, must test first. - code.push(makeVar('angle', name) + - " = block.getFieldValue('" + name + "');"); - } else if (Blockly.FieldDate && field instanceof Blockly.FieldDate) { - // Blockly.FieldDate may not be compiled into Blockly. - code.push(makeVar('date', name) + - " = block.getFieldValue('" + name + "');"); - } else if (field instanceof Blockly.FieldColour) { - code.push(makeVar('colour', name) + - " = block.getFieldValue('" + name + "');"); - } else if (field instanceof Blockly.FieldCheckbox) { - code.push(makeVar('checkbox', name) + - " = block.getFieldValue('" + name + "') == 'TRUE';"); - } else if (field instanceof Blockly.FieldDropdown) { - code.push(makeVar('dropdown', name) + - " = block.getFieldValue('" + name + "');"); - } else if (field instanceof Blockly.FieldNumber) { - code.push(makeVar('number', name) + - " = block.getFieldValue('" + name + "');"); - } else if (field instanceof Blockly.FieldTextInput) { - code.push(makeVar('text', name) + - " = block.getFieldValue('" + name + "');"); - } - } - var name = input.name; - if (name) { - if (input.type == Blockly.INPUT_VALUE) { - code.push(makeVar('value', name) + - " = Blockly." + language + ".valueToCode(block, '" + name + - "', Blockly." + language + ".ORDER_ATOMIC);"); - } else if (input.type == Blockly.NEXT_STATEMENT) { - code.push(makeVar('statements', name) + - " = Blockly." + language + ".statementToCode(block, '" + - name + "');"); - } - } - } - // Most languages end lines with a semicolon. Python does not. - var lineEnd = { - 'JavaScript': ';', - 'Python': '', - 'PHP': ';', - 'Dart': ';' - }; - code.push(" // TODO: Assemble " + language + " into code variable."); - if (block.outputConnection) { - code.push(" var code = '...';"); - code.push(" // TODO: Change ORDER_NONE to the correct strength."); - code.push(" return [code, Blockly." + language + ".ORDER_NONE];"); - } else { - code.push(" var code = '..." + (lineEnd[language] || '') + "\\n';"); - code.push(" return code;"); - } - code.push("};"); - - injectCode(code.join('\n'), 'generatorPre'); -} - -/** - * Existing direction ('ltr' vs 'rtl') of preview. - */ -var oldDir = null; - -/** - * Update the preview display. - */ -function updatePreview() { - // Toggle between LTR/RTL if needed (also used in first display). - var newDir = document.getElementById('direction').value; - if (oldDir != newDir) { - if (previewWorkspace) { - previewWorkspace.dispose(); - } - var rtl = newDir == 'rtl'; - previewWorkspace = Blockly.inject('preview', - {rtl: rtl, - media: '../../media/', - scrollbars: true}); - oldDir = newDir; - } - previewWorkspace.clear(); - - // Fetch the code and determine its format (JSON or JavaScript). - var format = document.getElementById('format').value; - if (format == 'Manual') { - var code = document.getElementById('languageTA').value; - // If the code is JSON, it will parse, otherwise treat as JS. - try { - JSON.parse(code); - format = 'JSON'; - } catch (e) { - format = 'JavaScript'; - } - } else { - var code = document.getElementById('languagePre').textContent; - } - if (!code.trim()) { - // Nothing to render. Happens while cloud storage is loading. - return; - } - - // Backup Blockly.Blocks object so that main workspace and preview don't - // collide if user creates a 'factory_base' block, for instance. - var backupBlocks = Blockly.Blocks; - try { - // Make a shallow copy. - Blockly.Blocks = {}; - for (var prop in backupBlocks) { - Blockly.Blocks[prop] = backupBlocks[prop]; - } - - if (format == 'JSON') { - var json = JSON.parse(code); - Blockly.Blocks[json.type || UNNAMED] = { - init: function() { - this.jsonInit(json); - } - }; - } else if (format == 'JavaScript') { - eval(code); - } else { - throw 'Unknown format: ' + format; - } - - // Look for a block on Blockly.Blocks that does not match the backup. - var blockType = null; - for (var type in Blockly.Blocks) { - if (typeof Blockly.Blocks[type].init == 'function' && - Blockly.Blocks[type] != backupBlocks[type]) { - blockType = type; - break; - } - } - if (!blockType) { - return; - } - - // Create the preview block. - var previewBlock = previewWorkspace.newBlock(blockType); - previewBlock.initSvg(); - previewBlock.render(); - previewBlock.setMovable(false); - previewBlock.setDeletable(false); - previewBlock.moveBy(15, 10); - previewWorkspace.clearUndo(); - - updateGenerator(previewBlock); - } finally { - Blockly.Blocks = backupBlocks; - } -} - -/** - * Inject code into a pre tag, with syntax highlighting. - * Safe from HTML/script injection. - * @param {string} code Lines of code. - * @param {string} id ID of
     element to inject into.
    - */
    -function injectCode(code, id) {
    -  var pre = document.getElementById(id);
    -  pre.textContent = code;
    -  code = pre.textContent;
    -  code = PR.prettyPrintOne(code, 'js');
    -  pre.innerHTML = code;
    -}
    -
    -/**
    - * Return the uneditable container block that everything else attaches to.
    - * @return {Blockly.Block}
    - */
    -function getRootBlock() {
    -  var blocks = mainWorkspace.getTopBlocks(false);
    -  for (var i = 0, block; block = blocks[i]; i++) {
    -    if (block.type == 'factory_base') {
    -      return block;
    -    }
    -  }
    -  return null;
    -}
    -
    -/**
    - * Disable the link button if the format is 'Manual', enable otherwise.
    - */
    -function disableEnableLink() {
    -  var linkButton = document.getElementById('linkButton');
    -  linkButton.disabled = document.getElementById('format').value == 'Manual';
    -}
    -
    -/**
    - * Initialize Blockly and layout.  Called on page load.
    - */
    -function init() {
    -  if ('BlocklyStorage' in window) {
    -    BlocklyStorage.HTTPREQUEST_ERROR =
    -        'There was a problem with the request.\n';
    -    BlocklyStorage.LINK_ALERT =
    -        'Share your blocks with this link:\n\n%1';
    -    BlocklyStorage.HASH_ERROR =
    -        'Sorry, "%1" doesn\'t correspond with any saved Blockly file.';
    -    BlocklyStorage.XML_ERROR = 'Could not load your saved file.\n'+
    -        'Perhaps it was created with a different version of Blockly?';
    -    var linkButton = document.getElementById('linkButton');
    -    linkButton.style.display = 'inline-block';
    -    linkButton.addEventListener('click',
    -        function() {BlocklyStorage.link(mainWorkspace);});
    -    disableEnableLink();
    -  }
    -
    -  document.getElementById('helpButton').addEventListener('click',
    -    function() {
    -      open('https://developers.google.com/blockly/guides/create-custom-blocks/block-factory',
    -           'BlockFactoryHelp');
    -    });
    -
    -  var expandList = [
    -    document.getElementById('blockly'),
    -    document.getElementById('blocklyMask'),
    -    document.getElementById('preview'),
    -    document.getElementById('languagePre'),
    -    document.getElementById('languageTA'),
    -    document.getElementById('generatorPre')
    -  ];
    -  var onresize = function(e) {
    -    for (var i = 0, expand; expand = expandList[i]; i++) {
    -      expand.style.width = (expand.parentNode.offsetWidth - 2) + 'px';
    -      expand.style.height = (expand.parentNode.offsetHeight - 2) + 'px';
    -    }
    -  };
    -  onresize();
    -  window.addEventListener('resize', onresize);
    -
    -  var toolbox = document.getElementById('toolbox');
    -  mainWorkspace = Blockly.inject('blockly',
    -      {collapse: false,
    -       toolbox: toolbox,
    -       media: '../../media/'});
    -
    -  // Create the root block.
    -  if ('BlocklyStorage' in window && window.location.hash.length > 1) {
    -    BlocklyStorage.retrieveXml(window.location.hash.substring(1),
    -                               mainWorkspace);
    -  } else {
    -    var xml = '';
    -    Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(xml), mainWorkspace);
    -  }
    -  mainWorkspace.clearUndo();
    -
    -  mainWorkspace.addChangeListener(Blockly.Events.disableOrphans);
    -  mainWorkspace.addChangeListener(updateLanguage);
    -  document.getElementById('direction')
    -      .addEventListener('change', updatePreview);
    -  document.getElementById('languageTA')
    -      .addEventListener('change', updatePreview);
    -  document.getElementById('languageTA')
    -      .addEventListener('keyup', updatePreview);
    -  document.getElementById('format')
    -      .addEventListener('change', formatChange);
    -  document.getElementById('language')
    -      .addEventListener('change', updatePreview);
    -}
    -window.addEventListener('load', init);
    diff --git a/demos/blockfactory_old/icon.png b/demos/blockfactory_old/icon.png
    deleted file mode 100644
    index d4d19b45768..00000000000
    Binary files a/demos/blockfactory_old/icon.png and /dev/null differ
    diff --git a/demos/blockfactory_old/index.html b/demos/blockfactory_old/index.html
    deleted file mode 100644
    index f4fd4f6c788..00000000000
    --- a/demos/blockfactory_old/index.html
    +++ /dev/null
    @@ -1,229 +0,0 @@
    -
    -
    -
    -  
    -  
    -  Blockly Demo: Block Factory
    -  
    -  
    -  
    -  
    -  
    -  
    -  
    -
    -
    -  
    -    
    -      
    -      
    -    
    -    
    -      
    -      
    -    
    -  
    -

    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 8ad4d166736..c264ff2dc9d 100644 --- a/demos/code/code.js +++ b/demos/code/code.js @@ -1,25 +1,11 @@ /** - * Blockly Demos: Code - * - * Copyright 2012 Google Inc. - * https://developers.google.com/blockly/ - * - * 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. + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview JavaScript for Blockly's Code demo. - * @author fraser@google.com (Neil Fraser) */ 'use strict'; @@ -46,12 +32,14 @@ Code.LANGUAGE_NAME = { 'fa': 'فارسی', 'fr': 'Français', 'he': 'עברית', + 'hr': 'Hrvatski', 'hrx': 'Hunsrik', 'hu': 'Magyar', 'ia': 'Interlingua', 'is': 'Íslenska', 'it': 'Italiano', 'ja': '日本語', + 'kab': 'Kabyle', 'ko': '한국어', 'mk': 'Македонски', 'ms': 'Bahasa Melayu', @@ -92,7 +80,7 @@ Code.workspace = null; * Extracts a parameter from the URL. * If the parameter is absent default_value is returned. * @param {string} name The name of the parameter. - * @param {string} defaultValue Value to return if paramater not found. + * @param {string} defaultValue Value to return if parameter not found. * @return {string} The parameter value or the default value if not found. */ Code.getStringParamFromUrl = function(name, defaultValue) { @@ -118,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); }; /** @@ -139,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 @@ -157,10 +145,8 @@ Code.loadBlocks = function(defaultXml) { */ Code.changeLanguage = function() { // Store the blocks for the duration of the reload. - // This should be skipped for the index page, which has no blocks and does - // not load Blockly. // MSIE 11 does not support sessionStorage on file:// URLs. - if (typeof Blockly != 'undefined' && window.sessionStorage) { + if (window.sessionStorage) { var xml = Blockly.Xml.workspaceToDom(Code.workspace); var text = Blockly.Xml.domToText(xml); window.sessionStorage.loadOnceBlocks = text; @@ -182,6 +168,15 @@ Code.changeLanguage = function() { window.location.host + window.location.pathname + search; }; +/** + * Changes the output language by clicking the tab matching + * the selected language in the codeMenu. + */ +Code.changeCodingLanguage = function() { + var codeMenu = document.getElementById('code_menu'); + Code.tabClick(codeMenu.options[codeMenu.selectedIndex].value); +} + /** * Bind a function to a button's click event. * On touch enabled browsers, ontouchend is treated as equivalent to onclick. @@ -189,11 +184,16 @@ Code.changeLanguage = 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); }; /** @@ -201,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); }; @@ -239,7 +239,17 @@ 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', 'JSON' +]; Code.selected = 'blocks'; @@ -249,15 +259,15 @@ Code.selected = 'blocks'; */ Code.tabClick = function(clickedName) { // If the XML tab was open, save and render the content. - if (document.getElementById('tab_xml').className == 'tabon') { + if (document.getElementById('tab_xml').classList.contains('tabon')) { var xmlTextarea = document.getElementById('content_xml'); 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; @@ -269,25 +279,61 @@ Code.tabClick = function(clickedName) { } } - if (document.getElementById('tab_blocks').className == 'tabon') { + 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); } // Deselect all tabs and hide all panes. for (var i = 0; i < Code.TABS_.length; i++) { var name = Code.TABS_[i]; - document.getElementById('tab_' + name).className = 'taboff'; + var tab = document.getElementById('tab_' + name); + tab.classList.add('taboff'); + tab.classList.remove('tabon'); document.getElementById('content_' + name).style.visibility = 'hidden'; } // Select the active tab. Code.selected = clickedName; - document.getElementById('tab_' + clickedName).className = 'tabon'; + var selectedTab = document.getElementById('tab_' + clickedName); + selectedTab.classList.remove('taboff'); + selectedTab.classList.add('tabon'); // Show the selected pane. document.getElementById('content_' + clickedName).style.visibility = 'visible'; Code.renderContent(); - if (clickedName == 'blocks') { + // The code menu tab is on if the blocks tab is off. + var codeMenuTab = document.getElementById('tab_code'); + if (clickedName === 'blocks') { Code.workspace.setVisible(true); + codeMenuTab.className = 'taboff'; + } else { + codeMenuTab.className = 'tabon'; + } + // 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) { + codeMenu.selectedIndex = i; + break; + } } Blockly.svgResize(Code.workspace); }; @@ -298,53 +344,71 @@ 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') { - var code = Blockly.JavaScript.workspaceToCode(Code.workspace); - content.textContent = code; - if (typeof PR.prettyPrintOne == 'function') { - code = content.textContent; - code = PR.prettyPrintOne(code, 'js'); - content.innerHTML = code; - } - } else if (content.id == 'content_python') { - code = Blockly.Python.workspaceToCode(Code.workspace); - content.textContent = code; - if (typeof PR.prettyPrintOne == 'function') { - code = content.textContent; - code = PR.prettyPrintOne(code, 'py'); - content.innerHTML = code; - } - } else if (content.id == 'content_php') { - code = Blockly.PHP.workspaceToCode(Code.workspace); - content.textContent = code; - if (typeof PR.prettyPrintOne == 'function') { - code = content.textContent; - code = PR.prettyPrintOne(code, 'php'); - content.innerHTML = code; - } - } else if (content.id == 'content_dart') { - code = Blockly.Dart.workspaceToCode(Code.workspace); - content.textContent = code; - if (typeof PR.prettyPrintOne == 'function') { - code = content.textContent; - code = PR.prettyPrintOne(code, 'dart'); - content.innerHTML = code; - } - } else if (content.id == 'content_lua') { - code = Blockly.Lua.workspaceToCode(Code.workspace); + } 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') { + PR.prettyPrint(); + } +}; + +/** + * Attempt to generate the code and display it in the UI, pretty printed. + * @param generator {!Blockly.CodeGenerator} The generator to use. + */ +Code.attemptCodeGeneration = function(generator) { + var content = document.getElementById('content_' + Code.selected); + content.textContent = ''; + if (Code.checkAllGeneratorFunctionsDefined(generator)) { + var code = generator.workspaceToCode(Code.workspace); content.textContent = code; - if (typeof PR.prettyPrintOne == 'function') { - code = content.textContent; - code = PR.prettyPrintOne(code, 'lua'); - content.innerHTML = code; + // Remove the 'prettyprinted' class, so that Prettify will recalculate. + content.className = content.className.replace('prettyprinted', ''); + } +}; + +/** + * Check whether all blocks in use have generator functions. + * @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.forBlock[blockType]) { + if (!missingBlockGenerators.includes(blockType)) { + missingBlockGenerators.push(blockType); + } } } + + 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.dialog.alert(msg); // Assuming synchronous. No callback. + } + return valid; }; /** @@ -369,9 +433,9 @@ Code.init = function() { el.style.width = (2 * bBox.width - el.offsetWidth) + 'px'; } // Make the 'Blocks' tab line up with the toolbox. - if (Code.workspace && Code.workspace.toolbox_.width) { + if (Code.workspace && Code.workspace.getToolbox().width) { document.getElementById('tab_blocks').style.minWidth = - (Code.workspace.toolbox_.width - 38) + 'px'; + (Code.workspace.getToolbox().width - 38) + 'px'; // Account for the 19 pixel margin and on each side. } }; @@ -386,14 +450,16 @@ 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 (goog.string.startsWith(messageKey, 'cat')) { + if (messageKey.startsWith('cat')) { Blockly.Msg[messageKey.toUpperCase()] = MSG[messageKey]; } } - // Construct the toolbox XML. + // Construct the toolbox XML, replacing translated variable names. var toolboxText = document.getElementById('toolbox').outerHTML; - var toolboxXml = Blockly.Xml.textToDom(toolboxText); + toolboxText = toolboxText.replace(/(^|[^%]){(\w+)}/g, + function(m, p1, p2) {return p1 + MSG[p2];}); + var toolboxXml = Blockly.utils.xml.textToDom(toolboxText); Code.workspace = Blockly.inject('content_blocks', {grid: @@ -411,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(''); @@ -431,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) { @@ -443,6 +509,14 @@ Code.init = function() { Code.bindClick('tab_' + name, function(name_) {return function() {Code.tabClick(name_);};}(name)); } + Code.bindClick('tab_code', function(e) { + if (e.target !== document.getElementById('tab_code')) { + // Prevent clicks on child codeMenu from triggering a tab click. + return; + } + Code.changeCodingLanguage(); + }); + onresize(); Blockly.svgResize(Code.workspace); @@ -478,13 +552,21 @@ 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); } languageMenu.addEventListener('change', Code.changeLanguage, true); + // Populate the coding language selection menu. + var codeMenu = document.getElementById('code_menu'); + codeMenu.options.length = 0; + for (var i = 1; i < Code.TABS_.length; i++) { + codeMenu.options.add(new Option(Code.TABS_DISPLAY_[i], Code.TABS_[i])); + } + codeMenu.addEventListener('change', Code.changeCodingLanguage); + // Inject language strings. document.title += ' ' + MSG['title']; document.getElementById('title').textContent = MSG['title']; @@ -498,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) { @@ -520,9 +608,9 @@ Code.runJS = function() { * Discard all blocks from the workspace. */ Code.discard = function() { - var count = Code.workspace.getAllBlocks().length; + var count = Code.workspace.getAllBlocks(false).length; if (count < 2 || - window.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.replace('%1', count))) { + window.confirm(Blockly.Msg['DELETE_ALL_BLOCKS'].replace('%1', count))) { Code.workspace.clear(); if (window.location.hash) { window.location.hash = ''; @@ -533,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/icon.png b/demos/code/icon.png index e2f23bd8304..feaa92996a4 100644 Binary files a/demos/code/icon.png and b/demos/code/icon.png differ diff --git a/demos/code/index.html b/demos/code/index.html index 84a35ab50c2..d8f894607f7 100644 --- a/demos/code/index.html +++ b/demos/code/index.html @@ -2,17 +2,18 @@ + Blockly Demo: - - - - - - - + + + + + + + @@ -26,6 +27,7 @@

    Blockly‏ > + Privacy @@ -33,18 +35,24 @@

    Blockly‏ > + + + + + + + + + + + + + + - - - - - - - - - - - +
    ... JavaScript Python PHP Lua Dart XML JSON  JavaScript Python PHP Lua Dart XML + +
    -
    
    -  
    
    -  
    
    -  
    
    -  
    
    +  
    
    +  
    
    +  
    
    +  
    
    +  
    
       
    +  
     
    -